diff --git a/.github/workflows/update-schema.yaml b/.github/workflows/update-schema.yaml index 8ee36d3f9..780dfd167 100644 --- a/.github/workflows/update-schema.yaml +++ b/.github/workflows/update-schema.yaml @@ -34,6 +34,7 @@ jobs: run: | export DJANGO_SETTINGS_MODULE=config.settings.base echo "CACHE_DURATION = 0" >> config/settings/base.py + echo "AGGREGATE_COUNT_THRESHOLD = 5" >> config/settings/base.py python manage.py export_openapi_schema --api chord_metadata_service.mohpackets.apis.core.api | python -m json.tool > chord_metadata_service/mohpackets/docs/schema.json - name: Commit new schema.json diff --git a/chord_metadata_service/mohpackets/apis/discovery.py b/chord_metadata_service/mohpackets/apis/discovery.py index 85e1bd7ff..c7bb80327 100644 --- a/chord_metadata_service/mohpackets/apis/discovery.py +++ b/chord_metadata_service/mohpackets/apis/discovery.py @@ -1,13 +1,20 @@ -from collections import Counter -from typing import Any, Dict, List, Type +from typing import Any, Dict, List from django.conf import settings from django.db.models import ( + Case, + CharField, Count, - Model, + F, + Func, + IntegerField, + Q, + Value, + When, ) +from django.db.models.functions import Abs, Cast, Coalesce +from ninja import Router from django.views.decorators.cache import cache_page -from ninja import Query, Router from ninja.decorators import decorate_view from chord_metadata_service.mohpackets.models import ( @@ -23,16 +30,19 @@ TREATMENT_TYPE, ) from chord_metadata_service.mohpackets.schemas.discovery import ( - DiscoverySchema, + DiagnosisAgeCountSchema, + DiscoveryDonorSchema, + GenderCountSchema, + PatientPerCohortSchema, + PrimarySiteCountSchema, ProgramDiscoverySchema, -) -from chord_metadata_service.mohpackets.schemas.filter import ( - DonorFilterSchema, + TreatmentTypeCountSchema, ) """ Module with overview APIs for the summary page and discovery APIs. These APIs do not require authorization but return only donor counts. +It also masks the value if the data is too small. Author: Son Chau """ @@ -41,43 +51,9 @@ overview_router = Router() discovery_router.add_router("/overview/", overview_router, tags=["overview"]) - -########################################## -# # -# HELPER FUNCTIONS # -# # -########################################## -def count_terms(terms): - """ - Return a dictionary of counts for every term in a list, used in overview endpoints - for fields with lists as entries. - """ - # Unnest list if nested - if terms and isinstance(terms[0], list): - terms = sum(terms, []) - - # Convert None values to "null" - terms = ["null" if term is None else term for term in terms] - return Counter(terms) - - -def count_donors(model: Type[Model], filters=None) -> Dict[str, int]: - queryset = model.objects.all() - if model == Donor: - count_field = "uuid" - else: - count_field = "donor_uuid" - - if filters is not None: - queryset = filters.filter(queryset) - - item_counts = ( - queryset.values("program_id") - .annotate(donor_count=Count(count_field, distinct=True)) - .order_by("program_id") - ) - - return {f"{item['program_id']}": item["donor_count"] for item in item_counts} +# To protect privacy, numbers below a certain threshold will be censored, e.g., <5 +SMALL_NUMBER_THRESHOLD = int(settings.AGGREGATE_COUNT_THRESHOLD) +SMALL_NUMBER_DISPLAY = "<" + str(SMALL_NUMBER_THRESHOLD) ############################################### @@ -90,21 +66,42 @@ def count_donors(model: Type[Model], filters=None) -> Dict[str, int]: @discovery_router.get("/programs/", response=List[ProgramDiscoverySchema]) @decorate_view(cache_page(CACHE_DURATION)) def discover_programs(request): + """ + Return all the programs in the database. + """ return Program.objects.only("program_id", "metadata") -@discovery_router.get("/donors/", response=DiscoverySchema) -def discover_donors(request, filters: DonorFilterSchema = Query(...)): - donors = count_donors(Donor, filters) - return DiscoverySchema(donors_by_cohort=donors) +@discovery_router.get("/donors/", response=List[DiscoveryDonorSchema]) +def discover_donors(request): + """ + Return the number of donors per cohort in the database. + Note: This function is identical to `discover_patients_per_cohort` + and is here because the frontend ingest uses it. It's probably best + to clean up later. + """ + result = ( + Donor.objects.values("program_id") + .annotate( + count=Count("uuid"), + donors_count=Case( + When( + count__lt=SMALL_NUMBER_THRESHOLD, + then=Value(SMALL_NUMBER_DISPLAY), + ), + default=Cast(F("count"), output_field=CharField()), + ), + ) + .values("program_id", "donors_count") + ) + return result @discovery_router.get("/sidebar_list/", response=Dict[str, Any]) @decorate_view(cache_page(CACHE_DURATION)) def discover_sidebar_list(request): """ - Retrieve the list of available values for all fields (including for - datasets that the user is not authorized to view) + Retrieve the list of drug names and treatment for frontend usage """ # Drugs queryable for chemotherapy chemotherapy_drug_names = list( @@ -129,8 +126,7 @@ def discover_sidebar_list(request): .distinct() ) - # Create a dictionary of results - results = { + result = { "treatment_types": TREATMENT_TYPE, "tumour_primary_sites": PRIMARY_SITE, "chemotherapy_drug_names": chemotherapy_drug_names, @@ -138,7 +134,7 @@ def discover_sidebar_list(request): "hormone_therapy_drug_names": hormone_therapy_drug_names, } - return results + return result ############################################### @@ -157,111 +153,166 @@ def discover_cohort_count(request): return {"cohort_count": Program.objects.count()} -@overview_router.get("/patients_per_cohort/", response=Dict[str, int]) +@overview_router.get("/patients_per_cohort/", response=List[PatientPerCohortSchema]) @decorate_view(cache_page(CACHE_DURATION)) def discover_patients_per_cohort(request): """ Return the number of patients per cohort in the database. """ - cohorts = Donor.objects.values_list("program_id", flat=True) - return count_terms(cohorts) + result = ( + Donor.objects.values("program_id") + .annotate(count=Count("uuid")) + .annotate( + patients_count=Case( + When( + count__lt=SMALL_NUMBER_THRESHOLD, + then=Value(SMALL_NUMBER_DISPLAY), + ), + default=Cast(F("count"), output_field=CharField()), + ) + ) + .values("program_id", "patients_count") + ) + return result -@overview_router.get("/individual_count/", response=Dict[str, int]) +@overview_router.get("/individual_count/", response=Dict[str, str]) @decorate_view(cache_page(CACHE_DURATION)) def discover_individual_count(request): """ Return the number of individuals in the database. """ - return {"individual_count": Donor.objects.count()} + donor_count = Donor.objects.count() + + if donor_count == 0: + result = "0" + elif donor_count < SMALL_NUMBER_THRESHOLD: + result = SMALL_NUMBER_DISPLAY + else: + result = str(donor_count) + + return {"individual_count": result} -@overview_router.get("/gender_count/", response=Dict[str, int]) +@overview_router.get("/gender_count/", response=List[GenderCountSchema]) @decorate_view(cache_page(CACHE_DURATION)) def discover_gender_count(request): """ Return the count for every gender in the database. """ - genders = Donor.objects.values_list("gender", flat=True) - return count_terms(genders) + result = ( + Donor.objects.values("gender") + .annotate(count=Count("uuid")) + .annotate( + gender_count=Case( + When( + count__lt=SMALL_NUMBER_THRESHOLD, + then=Value(SMALL_NUMBER_DISPLAY), + ), + default=Cast(F("count"), output_field=CharField()), + ) + ) + .values("gender", "gender_count") + ) + return result -@overview_router.get("/cancer_type_count/", response=Dict[str, int]) +@overview_router.get("/primary_site_count/", response=List[PrimarySiteCountSchema]) @decorate_view(cache_page(CACHE_DURATION)) -def discover_cancer_type_count(request): +def discover_primary_site_count(request): """ Return the count for every cancer type in the database. """ - cancer_types = list(Donor.objects.values_list("primary_site", flat=True)) - - # Handle missing values as empty arrays - for i in range(len(cancer_types)): - if cancer_types[i] is None: - cancer_types[i] = [None] - - return count_terms(cancer_types) + result = ( + Donor.objects.annotate( + primary_site_name=Func( + Coalesce(F("primary_site"), Value(["None"])), function="unnest" + ) + ) + .values("primary_site_name") + .annotate(count=Count("uuid")) + .annotate( + primary_site_count=Case( + When( + count__lt=SMALL_NUMBER_THRESHOLD, + then=Value(SMALL_NUMBER_DISPLAY), + ), + default=Cast(F("count"), output_field=CharField()), + ) + ) + .values("primary_site_name", "primary_site_count") + ) + return result -@overview_router.get("/treatment_type_count/", response=Dict[str, int]) +@overview_router.get("/treatment_type_count/", response=List[TreatmentTypeCountSchema]) @decorate_view(cache_page(CACHE_DURATION)) def discover_treatment_type_count(request): """ Return the count for every treatment type in the database. """ - treatment_types = list(Treatment.objects.values_list("treatment_type", flat=True)) - # Handle missing values as empty arrays - for i in range(len(treatment_types)): - if treatment_types[i] is None: - treatment_types[i] = [None] + result = ( + Treatment.objects.annotate( + treatment_type_name=Func( + Coalesce(F("treatment_type"), Value(["None"])), function="unnest" + ) + ) + .values("treatment_type_name") + .annotate(count=Count("uuid")) + .annotate( + treatment_type_count=Case( + When( + count__lt=SMALL_NUMBER_THRESHOLD, + then=Value(SMALL_NUMBER_DISPLAY), + ), + default=Cast(F("count"), output_field=CharField()), + ) + ) + .values("treatment_type_name", "treatment_type_count") + ) - return count_terms(treatment_types) + return result -@overview_router.get("/diagnosis_age_count/", response=Dict[str, int]) +@overview_router.get("/diagnosis_age_count/", response=List[DiagnosisAgeCountSchema]) @decorate_view(cache_page(CACHE_DURATION)) def discover_diagnosis_age_count(request): """ Return the count for age of diagnosis by calculating the date of birth interval. """ - months_in_year = 12 - - age_counts = { - "null": 0, - "0-19": 0, - "20-29": 0, - "30-39": 0, - "40-49": 0, - "50-59": 0, - "60-69": 0, - "70-79": 0, - "80+": 0, - } + result = ( + Donor.objects.annotate( + abs_month_interval=Abs( + Cast("date_of_birth__month_interval", output_field=IntegerField()) + ), + age_at_diagnosis=Case( + When(Q(date_of_birth__isnull=True), then=Value("null")), + When(abs_month_interval__lt=240, then=Value("0-19")), + When(abs_month_interval__lt=360, then=Value("20-29")), + When(abs_month_interval__lt=480, then=Value("30-39")), + When(abs_month_interval__lt=600, then=Value("40-49")), + When(abs_month_interval__lt=720, then=Value("50-59")), + When(abs_month_interval__lt=840, then=Value("60-69")), + When(abs_month_interval__lt=960, then=Value("70-79")), + default=Value("80+"), + output_field=CharField(), + ), + ) + .values( + "age_at_diagnosis", + ) + .annotate(count=Count("uuid")) + .annotate( + age_count=Case( + When( + count__lt=SMALL_NUMBER_THRESHOLD, + then=Value(SMALL_NUMBER_DISPLAY), + ), + default=Cast("count", output_field=CharField()), + ) + ) + .values("age_at_diagnosis", "age_count") + ) - donors = Donor.objects.values("date_of_birth") - - for donor in donors: - age = -1 - if donor["date_of_birth"] and donor["date_of_birth"].get("month_interval"): - age = abs(donor["date_of_birth"]["month_interval"]) // months_in_year - - if age < 0: - age_counts["null"] += 1 - elif age <= 19: - age_counts["0-19"] += 1 - elif age <= 29: - age_counts["20-29"] += 1 - elif age <= 39: - age_counts["30-39"] += 1 - elif age <= 49: - age_counts["40-49"] += 1 - elif age <= 59: - age_counts["50-59"] += 1 - elif age <= 69: - age_counts["60-69"] += 1 - elif age <= 79: - age_counts["70-79"] += 1 - else: - age_counts["80+"] += 1 - - return age_counts + return result diff --git a/chord_metadata_service/mohpackets/docs/schema.json b/chord_metadata_service/mohpackets/docs/schema.json index aefdb3bf4..69649d8ef 100644 --- a/chord_metadata_service/mohpackets/docs/schema.json +++ b/chord_metadata_service/mohpackets/docs/schema.json @@ -3,7 +3,7 @@ "info": { "title": "MoH Service API", "version": "4.2.1", - "description": "This is the RESTful API for the MoH Service. Based on https://raw.githubusercontent.com/CanDIG/katsu/a5656164c539273637fe7ae7709f41d61bf86ced/chord_metadata_service/mohpackets/docs/schema.json" + "description": "This is the RESTful API for the MoH Service." }, "paths": { "/v2/service-info": { @@ -3943,6 +3943,7 @@ } } }, + "description": "Return all the programs in the database.", "tags": [ "discovery" ] @@ -3952,259 +3953,6 @@ "get": { "operationId": "chord_metadata_service_mohpackets_apis_discovery_discover_donors", "summary": "Discover Donors", - "parameters": [ - { - "in": "query", - "name": "submitter_donor_id", - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Submitter Donor Id" - }, - "required": false - }, - { - "in": "query", - "name": "program_id", - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Program Id" - }, - "required": false - }, - { - "in": "query", - "name": "gender", - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "q": "gender__icontains", - "title": "Gender" - }, - "required": false - }, - { - "in": "query", - "name": "sex_at_birth", - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Sex At Birth" - }, - "required": false - }, - { - "in": "query", - "name": "is_deceased", - "schema": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "title": "Is Deceased" - }, - "required": false - }, - { - "in": "query", - "name": "lost_to_followup_after_clinical_event_identifier", - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Lost To Followup After Clinical Event Identifier" - }, - "required": false - }, - { - "in": "query", - "name": "lost_to_followup_reason", - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Lost To Followup Reason" - }, - "required": false - }, - { - "in": "query", - "name": "cause_of_death", - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Cause Of Death" - }, - "required": false - }, - { - "in": "query", - "name": "primary_site", - "schema": { - "items": { - "type": "string" - }, - "q": "primary_site__overlap", - "title": "Primary Site", - "type": "array" - }, - "required": false - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DiscoverySchema" - } - } - } - } - }, - "tags": [ - "discovery" - ] - } - }, - "/v2/discovery/specimen/": { - "get": { - "operationId": "chord_metadata_service_mohpackets_apis_discovery_discover_specimens", - "summary": "Discover Specimens", - "parameters": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DiscoverySchema" - } - } - } - } - }, - "tags": [ - "discovery" - ] - } - }, - "/v2/discovery/sample_registrations/": { - "get": { - "operationId": "chord_metadata_service_mohpackets_apis_discovery_discover_sample_registrations", - "summary": "Discover Sample Registrations", - "parameters": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DiscoverySchema" - } - } - } - } - }, - "tags": [ - "discovery" - ] - } - }, - "/v2/discovery/primary_diagnoses/": { - "get": { - "operationId": "chord_metadata_service_mohpackets_apis_discovery_discover_primary_diagnoses", - "summary": "Discover Primary Diagnoses", - "parameters": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DiscoverySchema" - } - } - } - } - }, - "tags": [ - "discovery" - ] - } - }, - "/v2/discovery/treatments/": { - "get": { - "operationId": "chord_metadata_service_mohpackets_apis_discovery_discover_treatments", - "summary": "Discover Treatments", - "parameters": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DiscoverySchema" - } - } - } - } - }, - "tags": [ - "discovery" - ] - } - }, - "/v2/discovery/chemotherapies/": { - "get": { - "operationId": "chord_metadata_service_mohpackets_apis_discovery_discover_chemotherapies", - "summary": "Discover Chemotherapies", "parameters": [], "responses": { "200": { @@ -4212,188 +3960,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DiscoverySchema" - } - } - } - } - }, - "tags": [ - "discovery" - ] - } - }, - "/v2/discovery/hormone_therapies/": { - "get": { - "operationId": "chord_metadata_service_mohpackets_apis_discovery_discover_hormone_therapies", - "summary": "Discover Hormone Therapies", - "parameters": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DiscoverySchema" - } - } - } - } - }, - "tags": [ - "discovery" - ] - } - }, - "/v2/discovery/radiations/": { - "get": { - "operationId": "chord_metadata_service_mohpackets_apis_discovery_discover_radiations", - "summary": "Discover Radiations", - "parameters": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DiscoverySchema" - } - } - } - } - }, - "tags": [ - "discovery" - ] - } - }, - "/v2/discovery/immunotherapies/": { - "get": { - "operationId": "chord_metadata_service_mohpackets_apis_discovery_discover_immunotherapies", - "summary": "Discover Immunotherapies", - "parameters": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DiscoverySchema" - } - } - } - } - }, - "tags": [ - "discovery" - ] - } - }, - "/v2/discovery/surgeries/": { - "get": { - "operationId": "chord_metadata_service_mohpackets_apis_discovery_discover_surgeries", - "summary": "Discover Surgeries", - "parameters": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DiscoverySchema" - } - } - } - } - }, - "tags": [ - "discovery" - ] - } - }, - "/v2/discovery/follow_ups/": { - "get": { - "operationId": "chord_metadata_service_mohpackets_apis_discovery_discover_follow_ups", - "summary": "Discover Follow Ups", - "parameters": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DiscoverySchema" - } - } - } - } - }, - "tags": [ - "discovery" - ] - } - }, - "/v2/discovery/biomarkers/": { - "get": { - "operationId": "chord_metadata_service_mohpackets_apis_discovery_discover_biomarkers", - "summary": "Discover Biomarkers", - "parameters": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DiscoverySchema" - } - } - } - } - }, - "tags": [ - "discovery" - ] - } - }, - "/v2/discovery/comorbidities/": { - "get": { - "operationId": "chord_metadata_service_mohpackets_apis_discovery_discover_comorbidities", - "summary": "Discover Comorbidities", - "parameters": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DiscoverySchema" - } - } - } - } - }, - "tags": [ - "discovery" - ] - } - }, - "/v2/discovery/exposures/": { - "get": { - "operationId": "chord_metadata_service_mohpackets_apis_discovery_discover_exposures", - "summary": "Discover Exposures", - "parameters": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DiscoverySchema" + "items": { + "$ref": "#/components/schemas/DiscoveryDonorSchema" + }, + "title": "Response", + "type": "array" } } } } }, + "description": "Return the number of donors per cohort in the database.", "tags": [ "discovery" ] @@ -4461,11 +4038,11 @@ "content": { "application/json": { "schema": { - "additionalProperties": { - "type": "integer" + "items": { + "$ref": "#/components/schemas/PatientPerCohortSchema" }, "title": "Response", - "type": "object" + "type": "array" } } } @@ -4489,7 +4066,7 @@ "application/json": { "schema": { "additionalProperties": { - "type": "integer" + "type": "string" }, "title": "Response", "type": "object" @@ -4515,11 +4092,11 @@ "content": { "application/json": { "schema": { - "additionalProperties": { - "type": "integer" + "items": { + "$ref": "#/components/schemas/GenderCountSchema" }, "title": "Response", - "type": "object" + "type": "array" } } } @@ -4531,10 +4108,10 @@ ] } }, - "/v2/discovery/overview/cancer_type_count/": { + "/v2/discovery/overview/primary_site_count/": { "get": { - "operationId": "chord_metadata_service_mohpackets_apis_discovery_discover_cancer_type_count", - "summary": "Discover Cancer Type Count", + "operationId": "chord_metadata_service_mohpackets_apis_discovery_discover_primary_site_count", + "summary": "Discover Primary Site Count", "parameters": [], "responses": { "200": { @@ -4542,11 +4119,11 @@ "content": { "application/json": { "schema": { - "additionalProperties": { - "type": "integer" + "items": { + "$ref": "#/components/schemas/PrimarySiteCountSchema" }, "title": "Response", - "type": "object" + "type": "array" } } } @@ -4569,11 +4146,11 @@ "content": { "application/json": { "schema": { - "additionalProperties": { - "type": "integer" + "items": { + "$ref": "#/components/schemas/TreatmentTypeCountSchema" }, "title": "Response", - "type": "object" + "type": "array" } } } @@ -4596,11 +4173,11 @@ "content": { "application/json": { "schema": { - "additionalProperties": { - "type": "integer" + "items": { + "$ref": "#/components/schemas/DiagnosisAgeCountSchema" }, "title": "Response", - "type": "object" + "type": "array" } } } @@ -14219,20 +13796,119 @@ "title": "ProgramDiscoverySchema", "type": "object" }, - "DiscoverySchema": { + "DiscoveryDonorSchema": { "properties": { - "donors_by_cohort": { - "additionalProperties": { - "type": "integer" - }, - "title": "Donors By Cohort", - "type": "object" + "program_id": { + "title": "Program Id", + "type": "string" + }, + "donors_count": { + "title": "Donors Count", + "type": "string" + } + }, + "required": [ + "program_id", + "donors_count" + ], + "title": "DiscoveryDonorSchema", + "type": "object" + }, + "PatientPerCohortSchema": { + "properties": { + "program_id": { + "title": "Program Id", + "type": "string" + }, + "patients_count": { + "title": "Patients Count", + "type": "string" + } + }, + "required": [ + "program_id", + "patients_count" + ], + "title": "PatientPerCohortSchema", + "type": "object" + }, + "GenderCountSchema": { + "properties": { + "gender": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Gender" + }, + "gender_count": { + "title": "Gender Count", + "type": "string" + } + }, + "required": [ + "gender", + "gender_count" + ], + "title": "GenderCountSchema", + "type": "object" + }, + "PrimarySiteCountSchema": { + "properties": { + "primary_site_name": { + "title": "Primary Site Name", + "type": "string" + }, + "primary_site_count": { + "title": "Primary Site Count", + "type": "string" + } + }, + "required": [ + "primary_site_name", + "primary_site_count" + ], + "title": "PrimarySiteCountSchema", + "type": "object" + }, + "TreatmentTypeCountSchema": { + "properties": { + "treatment_type_name": { + "title": "Treatment Type Name", + "type": "string" + }, + "treatment_type_count": { + "title": "Treatment Type Count", + "type": "string" + } + }, + "required": [ + "treatment_type_name", + "treatment_type_count" + ], + "title": "TreatmentTypeCountSchema", + "type": "object" + }, + "DiagnosisAgeCountSchema": { + "properties": { + "age_at_diagnosis": { + "title": "Age At Diagnosis", + "type": "string" + }, + "age_count": { + "title": "Age Count", + "type": "string" } }, "required": [ - "donors_by_cohort" + "age_at_diagnosis", + "age_count" ], - "title": "DiscoverySchema", + "title": "DiagnosisAgeCountSchema", "type": "object" }, "DonorExplorerFilterSchema": { diff --git a/chord_metadata_service/mohpackets/docs/schema.md b/chord_metadata_service/mohpackets/docs/schema.md index 6750a3635..3bda3c72d 100644 --- a/chord_metadata_service/mohpackets/docs/schema.md +++ b/chord_metadata_service/mohpackets/docs/schema.md @@ -1,7 +1,7 @@

MoH Service API v4.2.1

-This is the RESTful API for the MoH Service. Based on https://raw.githubusercontent.com/CanDIG/katsu/a5656164c539273637fe7ae7709f41d61bf86ced/chord_metadata_service/mohpackets/docs/schema.json +This is the RESTful API for the MoH Service. Base URLs: @@ -1440,6 +1440,8 @@ Base URLs: *Discover Programs* +Return all the programs in the database. + > Example responses > 200 Response @@ -1461,304 +1463,19 @@ Base URLs: *Discover Donors* -

Parameters

- -|Name|In|Type|Required|Description| -|---|---|---|---|---| -|submitter_donor_id|query|any|false|none| -|program_id|query|any|false|none| -|gender|query|any|false|none| -|sex_at_birth|query|any|false|none| -|is_deceased|query|any|false|none| -|lost_to_followup_after_clinical_event_identifier|query|any|false|none| -|lost_to_followup_reason|query|any|false|none| -|cause_of_death|query|any|false|none| -|primary_site|query|array[string]|false|none| - -> Example responses - -> 200 Response - -```json -{ - "donors_by_cohort": { - "property1": 0, - "property2": 0 - } -} -``` - -## chord_metadata_service_mohpackets_apis_discovery_discover_specimens - - - -`GET /v2/discovery/specimen/` - -*Discover Specimens* - -> Example responses - -> 200 Response - -```json -{ - "donors_by_cohort": { - "property1": 0, - "property2": 0 - } -} -``` - -## chord_metadata_service_mohpackets_apis_discovery_discover_sample_registrations - - - -`GET /v2/discovery/sample_registrations/` - -*Discover Sample Registrations* - -> Example responses - -> 200 Response - -```json -{ - "donors_by_cohort": { - "property1": 0, - "property2": 0 - } -} -``` - -## chord_metadata_service_mohpackets_apis_discovery_discover_primary_diagnoses - - - -`GET /v2/discovery/primary_diagnoses/` - -*Discover Primary Diagnoses* - -> Example responses - -> 200 Response - -```json -{ - "donors_by_cohort": { - "property1": 0, - "property2": 0 - } -} -``` - -## chord_metadata_service_mohpackets_apis_discovery_discover_treatments - - - -`GET /v2/discovery/treatments/` - -*Discover Treatments* - -> Example responses - -> 200 Response - -```json -{ - "donors_by_cohort": { - "property1": 0, - "property2": 0 - } -} -``` - -## chord_metadata_service_mohpackets_apis_discovery_discover_chemotherapies - - - -`GET /v2/discovery/chemotherapies/` - -*Discover Chemotherapies* - -> Example responses - -> 200 Response - -```json -{ - "donors_by_cohort": { - "property1": 0, - "property2": 0 - } -} -``` - -## chord_metadata_service_mohpackets_apis_discovery_discover_hormone_therapies - - - -`GET /v2/discovery/hormone_therapies/` - -*Discover Hormone Therapies* +Return the number of donors per cohort in the database. > Example responses > 200 Response ```json -{ - "donors_by_cohort": { - "property1": 0, - "property2": 0 - } -} -``` - -## chord_metadata_service_mohpackets_apis_discovery_discover_radiations - - - -`GET /v2/discovery/radiations/` - -*Discover Radiations* - -> Example responses - -> 200 Response - -```json -{ - "donors_by_cohort": { - "property1": 0, - "property2": 0 - } -} -``` - -## chord_metadata_service_mohpackets_apis_discovery_discover_immunotherapies - - - -`GET /v2/discovery/immunotherapies/` - -*Discover Immunotherapies* - -> Example responses - -> 200 Response - -```json -{ - "donors_by_cohort": { - "property1": 0, - "property2": 0 - } -} -``` - -## chord_metadata_service_mohpackets_apis_discovery_discover_surgeries - - - -`GET /v2/discovery/surgeries/` - -*Discover Surgeries* - -> Example responses - -> 200 Response - -```json -{ - "donors_by_cohort": { - "property1": 0, - "property2": 0 - } -} -``` - -## chord_metadata_service_mohpackets_apis_discovery_discover_follow_ups - - - -`GET /v2/discovery/follow_ups/` - -*Discover Follow Ups* - -> Example responses - -> 200 Response - -```json -{ - "donors_by_cohort": { - "property1": 0, - "property2": 0 - } -} -``` - -## chord_metadata_service_mohpackets_apis_discovery_discover_biomarkers - - - -`GET /v2/discovery/biomarkers/` - -*Discover Biomarkers* - -> Example responses - -> 200 Response - -```json -{ - "donors_by_cohort": { - "property1": 0, - "property2": 0 - } -} -``` - -## chord_metadata_service_mohpackets_apis_discovery_discover_comorbidities - - - -`GET /v2/discovery/comorbidities/` - -*Discover Comorbidities* - -> Example responses - -> 200 Response - -```json -{ - "donors_by_cohort": { - "property1": 0, - "property2": 0 - } -} -``` - -## chord_metadata_service_mohpackets_apis_discovery_discover_exposures - - - -`GET /v2/discovery/exposures/` - -*Discover Exposures* - -> Example responses - -> 200 Response - -```json -{ - "donors_by_cohort": { - "property1": 0, - "property2": 0 +[ + { + "program_id": "string", + "donors_count": "string" } -} +] ``` ## chord_metadata_service_mohpackets_apis_discovery_discover_sidebar_list @@ -1818,10 +1535,12 @@ Return the number of patients per cohort in the database. > 200 Response ```json -{ - "property1": 0, - "property2": 0 -} +[ + { + "program_id": "string", + "patients_count": "string" + } +] ``` ## chord_metadata_service_mohpackets_apis_discovery_discover_individual_count @@ -1840,8 +1559,8 @@ Return the number of individuals in the database. ```json { - "property1": 0, - "property2": 0 + "property1": "string", + "property2": "string" } ``` @@ -1860,19 +1579,21 @@ Return the count for every gender in the database. > 200 Response ```json -{ - "property1": 0, - "property2": 0 -} +[ + { + "gender": "string", + "gender_count": "string" + } +] ``` -## chord_metadata_service_mohpackets_apis_discovery_discover_cancer_type_count +## chord_metadata_service_mohpackets_apis_discovery_discover_primary_site_count - + -`GET /v2/discovery/overview/cancer_type_count/` +`GET /v2/discovery/overview/primary_site_count/` -*Discover Cancer Type Count* +*Discover Primary Site Count* Return the count for every cancer type in the database. @@ -1881,10 +1602,12 @@ Return the count for every cancer type in the database. > 200 Response ```json -{ - "property1": 0, - "property2": 0 -} +[ + { + "primary_site_name": "string", + "primary_site_count": "string" + } +] ``` ## chord_metadata_service_mohpackets_apis_discovery_discover_treatment_type_count @@ -1902,10 +1625,12 @@ Return the count for every treatment type in the database. > 200 Response ```json -{ - "property1": 0, - "property2": 0 -} +[ + { + "treatment_type_name": "string", + "treatment_type_count": "string" + } +] ``` ## chord_metadata_service_mohpackets_apis_discovery_discover_diagnosis_age_count @@ -1923,10 +1648,12 @@ Return the count for age of diagnosis by calculating the date of birth interval. > 200 Response ```json -{ - "property1": 0, - "property2": 0 -} +[ + { + "age_at_diagnosis": "string", + "age_count": "string" + } +] ```

explorer

@@ -18627,31 +18354,166 @@ ProgramDiscoverySchema |program_id|string|true|none|none| |metadata|any|true|none|none| -

DiscoverySchema

+

DiscoveryDonorSchema

- - - - + + + + ```json { - "donors_by_cohort": { - "property1": 0, - "property2": 0 - } + "program_id": "string", + "donors_count": "string" +} + +``` + +DiscoveryDonorSchema + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|program_id|string|true|none|none| +|donors_count|string|true|none|none| + +

PatientPerCohortSchema

+ + + + + + +```json +{ + "program_id": "string", + "patients_count": "string" +} + +``` + +PatientPerCohortSchema + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|program_id|string|true|none|none| +|patients_count|string|true|none|none| + +

GenderCountSchema

+ + + + + + +```json +{ + "gender": "string", + "gender_count": "string" +} + +``` + +GenderCountSchema + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|gender|any|true|none|none| + +anyOf + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|» *anonymous*|string|false|none|none| + +or + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|» *anonymous*|null|false|none|none| + +continued + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|gender_count|string|true|none|none| + +

PrimarySiteCountSchema

+ + + + + + +```json +{ + "primary_site_name": "string", + "primary_site_count": "string" +} + +``` + +PrimarySiteCountSchema + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|primary_site_name|string|true|none|none| +|primary_site_count|string|true|none|none| + +

TreatmentTypeCountSchema

+ + + + + + +```json +{ + "treatment_type_name": "string", + "treatment_type_count": "string" +} + +``` + +TreatmentTypeCountSchema + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|treatment_type_name|string|true|none|none| +|treatment_type_count|string|true|none|none| + +

DiagnosisAgeCountSchema

+ + + + + + +```json +{ + "age_at_diagnosis": "string", + "age_count": "string" } ``` -DiscoverySchema +DiagnosisAgeCountSchema ### Properties |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| -|donors_by_cohort|object|true|none|none| -|» **additionalProperties**|integer|false|none|none| +|age_at_diagnosis|string|true|none|none| +|age_count|string|true|none|none|

DonorExplorerFilterSchema

diff --git a/chord_metadata_service/mohpackets/docs/schema.yml b/chord_metadata_service/mohpackets/docs/schema.yml index 8ca7c38c4..dd3d86032 100644 --- a/chord_metadata_service/mohpackets/docs/schema.yml +++ b/chord_metadata_service/mohpackets/docs/schema.yml @@ -628,16 +628,31 @@ components: - month_interval title: DateInterval type: object - DiscoverySchema: + DiagnosisAgeCountSchema: properties: - donors_by_cohort: - additionalProperties: - type: integer - title: Donors By Cohort - type: object + age_at_diagnosis: + title: Age At Diagnosis + type: string + age_count: + title: Age Count + type: string required: - - donors_by_cohort - title: DiscoverySchema + - age_at_diagnosis + - age_count + title: DiagnosisAgeCountSchema + type: object + DiscoveryDonorSchema: + properties: + donors_count: + title: Donors Count + type: string + program_id: + title: Program Id + type: string + required: + - program_id + - donors_count + title: DiscoveryDonorSchema type: object DiseaseStatusFollowupEnum: enum: @@ -1372,6 +1387,21 @@ components: - submitter_donor_id title: FollowUpModelSchema type: object + GenderCountSchema: + properties: + gender: + anyOf: + - type: string + - type: 'null' + title: Gender + gender_count: + title: Gender Count + type: string + required: + - gender + - gender_count + title: GenderCountSchema + type: object GenderEnum: enum: - Man @@ -2994,6 +3024,19 @@ components: - previous_page title: PagedTreatmentModelSchema type: object + PatientPerCohortSchema: + properties: + patients_count: + title: Patients Count + type: string + program_id: + title: Program Id + type: string + required: + - program_id + - patients_count + title: PatientPerCohortSchema + type: object PercentCellsRangeEnum: enum: - 0-19% @@ -3240,6 +3283,19 @@ components: - submitter_donor_id title: PrimaryDiagnosisModelSchema type: object + PrimarySiteCountSchema: + properties: + primary_site_count: + title: Primary Site Count + type: string + primary_site_name: + title: Primary Site Name + type: string + required: + - primary_site_name + - primary_site_count + title: PrimarySiteCountSchema + type: object PrimarySiteEnum: enum: - Accessory sinuses @@ -5228,6 +5284,19 @@ components: - Unknown title: TreatmentStatusEnum type: string + TreatmentTypeCountSchema: + properties: + treatment_type_count: + title: Treatment Type Count + type: string + treatment_type_name: + title: Treatment Type Name + type: string + required: + - treatment_type_name + - treatment_type_count + title: TreatmentTypeCountSchema + type: object TreatmentTypeEnum: enum: - Bone marrow transplant @@ -5344,7 +5413,7 @@ components: name: X-Service-Token type: apiKey info: - description: This is the RESTful API for the MoH Service. Based on https://raw.githubusercontent.com/CanDIG/katsu/a5656164c539273637fe7ae7709f41d61bf86ced/chord_metadata_service/mohpackets/docs/schema.json + description: This is the RESTful API for the MoH Service. title: MoH Service API version: 4.2.1 openapi: 3.1.0 @@ -7201,210 +7270,24 @@ paths: summary: List Treatments tags: - authorized - /v2/discovery/biomarkers/: - get: - operationId: chord_metadata_service_mohpackets_apis_discovery_discover_biomarkers - parameters: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/DiscoverySchema' - description: OK - summary: Discover Biomarkers - tags: - - discovery - /v2/discovery/chemotherapies/: - get: - operationId: chord_metadata_service_mohpackets_apis_discovery_discover_chemotherapies - parameters: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/DiscoverySchema' - description: OK - summary: Discover Chemotherapies - tags: - - discovery - /v2/discovery/comorbidities/: - get: - operationId: chord_metadata_service_mohpackets_apis_discovery_discover_comorbidities - parameters: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/DiscoverySchema' - description: OK - summary: Discover Comorbidities - tags: - - discovery /v2/discovery/donors/: get: + description: Return the number of donors per cohort in the database. operationId: chord_metadata_service_mohpackets_apis_discovery_discover_donors - parameters: - - in: query - name: submitter_donor_id - required: false - schema: - anyOf: - - type: string - - type: 'null' - title: Submitter Donor Id - - in: query - name: program_id - required: false - schema: - anyOf: - - type: string - - type: 'null' - title: Program Id - - in: query - name: gender - required: false - schema: - anyOf: - - type: string - - type: 'null' - q: gender__icontains - title: Gender - - in: query - name: sex_at_birth - required: false - schema: - anyOf: - - type: string - - type: 'null' - title: Sex At Birth - - in: query - name: is_deceased - required: false - schema: - anyOf: - - type: boolean - - type: 'null' - title: Is Deceased - - in: query - name: lost_to_followup_after_clinical_event_identifier - required: false - schema: - anyOf: - - type: string - - type: 'null' - title: Lost To Followup After Clinical Event Identifier - - in: query - name: lost_to_followup_reason - required: false - schema: - anyOf: - - type: string - - type: 'null' - title: Lost To Followup Reason - - in: query - name: cause_of_death - required: false - schema: - anyOf: - - type: string - - type: 'null' - title: Cause Of Death - - in: query - name: primary_site - required: false - schema: - items: - type: string - q: primary_site__overlap - title: Primary Site - type: array - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/DiscoverySchema' - description: OK - summary: Discover Donors - tags: - - discovery - /v2/discovery/exposures/: - get: - operationId: chord_metadata_service_mohpackets_apis_discovery_discover_exposures - parameters: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/DiscoverySchema' - description: OK - summary: Discover Exposures - tags: - - discovery - /v2/discovery/follow_ups/: - get: - operationId: chord_metadata_service_mohpackets_apis_discovery_discover_follow_ups - parameters: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/DiscoverySchema' - description: OK - summary: Discover Follow Ups - tags: - - discovery - /v2/discovery/hormone_therapies/: - get: - operationId: chord_metadata_service_mohpackets_apis_discovery_discover_hormone_therapies - parameters: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/DiscoverySchema' - description: OK - summary: Discover Hormone Therapies - tags: - - discovery - /v2/discovery/immunotherapies/: - get: - operationId: chord_metadata_service_mohpackets_apis_discovery_discover_immunotherapies - parameters: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/DiscoverySchema' - description: OK - summary: Discover Immunotherapies - tags: - - discovery - /v2/discovery/overview/cancer_type_count/: - get: - description: Return the count for every cancer type in the database. - operationId: chord_metadata_service_mohpackets_apis_discovery_discover_cancer_type_count parameters: [] responses: '200': content: application/json: schema: - additionalProperties: - type: integer + items: + $ref: '#/components/schemas/DiscoveryDonorSchema' title: Response - type: object + type: array description: OK - summary: Discover Cancer Type Count + summary: Discover Donors tags: - - overview + - discovery /v2/discovery/overview/cohort_count/: get: description: Return the number of cohorts in the database. @@ -7434,10 +7317,10 @@ paths: content: application/json: schema: - additionalProperties: - type: integer + items: + $ref: '#/components/schemas/DiagnosisAgeCountSchema' title: Response - type: object + type: array description: OK summary: Discover Diagnosis Age Count tags: @@ -7452,10 +7335,10 @@ paths: content: application/json: schema: - additionalProperties: - type: integer + items: + $ref: '#/components/schemas/GenderCountSchema' title: Response - type: object + type: array description: OK summary: Discover Gender Count tags: @@ -7471,7 +7354,7 @@ paths: application/json: schema: additionalProperties: - type: integer + type: string title: Response type: object description: OK @@ -7488,48 +7371,53 @@ paths: content: application/json: schema: - additionalProperties: - type: integer + items: + $ref: '#/components/schemas/PatientPerCohortSchema' title: Response - type: object + type: array description: OK summary: Discover Patients Per Cohort tags: - overview - /v2/discovery/overview/treatment_type_count/: + /v2/discovery/overview/primary_site_count/: get: - description: Return the count for every treatment type in the database. - operationId: chord_metadata_service_mohpackets_apis_discovery_discover_treatment_type_count + description: Return the count for every cancer type in the database. + operationId: chord_metadata_service_mohpackets_apis_discovery_discover_primary_site_count parameters: [] responses: '200': content: application/json: schema: - additionalProperties: - type: integer + items: + $ref: '#/components/schemas/PrimarySiteCountSchema' title: Response - type: object + type: array description: OK - summary: Discover Treatment Type Count + summary: Discover Primary Site Count tags: - overview - /v2/discovery/primary_diagnoses/: + /v2/discovery/overview/treatment_type_count/: get: - operationId: chord_metadata_service_mohpackets_apis_discovery_discover_primary_diagnoses + description: Return the count for every treatment type in the database. + operationId: chord_metadata_service_mohpackets_apis_discovery_discover_treatment_type_count parameters: [] responses: '200': content: application/json: schema: - $ref: '#/components/schemas/DiscoverySchema' + items: + $ref: '#/components/schemas/TreatmentTypeCountSchema' + title: Response + type: array description: OK - summary: Discover Primary Diagnoses + summary: Discover Treatment Type Count tags: - - discovery + - overview /v2/discovery/programs/: get: + description: Return all the programs in the database. operationId: chord_metadata_service_mohpackets_apis_discovery_discover_programs parameters: [] responses: @@ -7545,34 +7433,6 @@ paths: summary: Discover Programs tags: - discovery - /v2/discovery/radiations/: - get: - operationId: chord_metadata_service_mohpackets_apis_discovery_discover_radiations - parameters: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/DiscoverySchema' - description: OK - summary: Discover Radiations - tags: - - discovery - /v2/discovery/sample_registrations/: - get: - operationId: chord_metadata_service_mohpackets_apis_discovery_discover_sample_registrations - parameters: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/DiscoverySchema' - description: OK - summary: Discover Sample Registrations - tags: - - discovery /v2/discovery/sidebar_list/: get: description: 'Retrieve the list of available values for all fields (including @@ -7592,48 +7452,6 @@ paths: summary: Discover Sidebar List tags: - discovery - /v2/discovery/specimen/: - get: - operationId: chord_metadata_service_mohpackets_apis_discovery_discover_specimens - parameters: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/DiscoverySchema' - description: OK - summary: Discover Specimens - tags: - - discovery - /v2/discovery/surgeries/: - get: - operationId: chord_metadata_service_mohpackets_apis_discovery_discover_surgeries - parameters: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/DiscoverySchema' - description: OK - summary: Discover Surgeries - tags: - - discovery - /v2/discovery/treatments/: - get: - operationId: chord_metadata_service_mohpackets_apis_discovery_discover_treatments - parameters: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/DiscoverySchema' - description: OK - summary: Discover Treatments - tags: - - discovery /v2/explorer/donors/: get: operationId: chord_metadata_service_mohpackets_apis_explorer_explorer_donor diff --git a/chord_metadata_service/mohpackets/schemas/discovery.py b/chord_metadata_service/mohpackets/schemas/discovery.py index 9076b8b43..0fa9424eb 100644 --- a/chord_metadata_service/mohpackets/schemas/discovery.py +++ b/chord_metadata_service/mohpackets/schemas/discovery.py @@ -1,6 +1,6 @@ -from typing import Dict -from ninja import Schema +from typing import Optional +from ninja import Schema """ Module with schema used for discovery response @@ -14,5 +14,35 @@ class ProgramDiscoverySchema(Schema): metadata: object -class DiscoverySchema(Schema): - donors_by_cohort: Dict[str, int] +# class DiscoverySchema(Schema): +# donors_by_cohort: Dict[str, int] + + +class DiscoveryDonorSchema(Schema): + program_id: str + donors_count: str + + +class PatientPerCohortSchema(Schema): + program_id: str + patients_count: str + + +class GenderCountSchema(Schema): + gender: Optional[str] + gender_count: str + + +class PrimarySiteCountSchema(Schema): + primary_site_name: str + primary_site_count: str + + +class TreatmentTypeCountSchema(Schema): + treatment_type_name: str + treatment_type_count: str + + +class DiagnosisAgeCountSchema(Schema): + age_at_diagnosis: str + age_count: str diff --git a/chord_metadata_service/mohpackets/tests/endpoints/factories.py b/chord_metadata_service/mohpackets/tests/endpoints/factories.py index 96b9f359a..dd44d7234 100644 --- a/chord_metadata_service/mohpackets/tests/endpoints/factories.py +++ b/chord_metadata_service/mohpackets/tests/endpoints/factories.py @@ -39,6 +39,8 @@ Note: These factories use the Factory Boy library (https://factoryboy.readthedocs.io/) to generate test data. + Some business logic is not strictly enforced. For example, + date_of_birth could have mismatched month_interval and day_interval. Author: Son Chau """ @@ -73,16 +75,20 @@ class Meta: ), no_declaration=None, ) - date_of_birth = { - "month_interval": random.randint(0, 100), - "day_interval": random.randint(0, 300), - } + date_of_birth = factory.LazyFunction( + lambda: { + "month_interval": random.randint(0, 1000), + "day_interval": random.randint(0, 3000), + } + ) date_of_death = factory.Maybe( "is_deceased", - yes_declaration={ - "month_interval": random.randint(0, 100), - "day_interval": random.randint(0, 300), - }, + yes_declaration=factory.LazyFunction( + lambda: { + "month_interval": random.randint(0, 1000), + "day_interval": random.randint(0, 3000), + } + ), no_declaration=None, ) primary_site = factory.Faker( @@ -103,10 +109,12 @@ class Meta: # Default values submitter_primary_diagnosis_id = factory.Sequence(lambda n: "DIAG_%d" % n) - date_of_diagnosis = { - "month_interval": random.randint(0, 100), - "day_interval": random.randint(0, 300), - } + date_of_diagnosis = factory.LazyFunction( + lambda: { + "month_interval": random.randint(0, 1000), + "day_interval": random.randint(0, 3000), + } + ) cancer_type_code = factory.Faker("uuid4") basis_of_diagnosis = factory.Faker( "random_element", elements=PERM_VAL.BASIS_OF_DIAGNOSIS @@ -147,8 +155,8 @@ def set_clinical_event_identifier(self, create, extracted, **kwargs): PERM_VAL.LOST_TO_FOLLOWUP_REASON ) donor.date_alive_after_lost_to_followup = { - "month_interval": random.randint(0, 100), - "day_interval": random.randint(0, 300), + "month_interval": random.randint(0, 1000), + "day_interval": random.randint(0, 3000), } donor.save() diff --git a/chord_metadata_service/mohpackets/tests/endpoints/test_overview.py b/chord_metadata_service/mohpackets/tests/endpoints/test_overview.py new file mode 100644 index 000000000..ed01ebc13 --- /dev/null +++ b/chord_metadata_service/mohpackets/tests/endpoints/test_overview.py @@ -0,0 +1,203 @@ +from http import HTTPStatus + +import chord_metadata_service.mohpackets.permissible_values as PERM_VAL +from chord_metadata_service.mohpackets.models import Donor, Treatment +from chord_metadata_service.mohpackets.tests.endpoints.base import BaseTestCase +from chord_metadata_service.mohpackets.tests.endpoints.factories import ( + DonorFactory, + TreatmentFactory, +) + +""" + This module contains API tests related to overview endpoints. + + It includes tests for: + - Displaying actual number if data >5 + - Censoring if data <5 + + Author: Son Chau +""" + + +class OverviewTestCase(BaseTestCase): + def setUp(self): + super().setUp() + self.patients_per_cohort_url = "/v2/discovery/overview/patients_per_cohort/" + self.individual_count_url = "/v2/discovery/overview/individual_count/" + self.gender_count_url = "/v2/discovery/overview/gender_count/" + self.primary_site_count_url = "/v2/discovery/overview/primary_site_count/" + self.treatment_type_count_url = "/v2/discovery/overview/treatment_type_count/" + self.diagnosis_age_count_url = "/v2/discovery/overview/diagnosis_age_count/" + self.discover_donors_url = "/v2/discovery/donors/" + # The default dataset are <5 so we need to add 5 more donors + # and 5 more treatments to display data >5 + self.gender_man = PERM_VAL.GENDER[0] # Man + self.primary_site_sinus = PERM_VAL.PRIMARY_SITE[0] # Accessory sinuses + self.treatment_type_bone = PERM_VAL.TREATMENT_TYPE[0] # Bone marrow transplant + self.donors.extend( + DonorFactory.create_batch( + 5, + program_id=self.programs[0], + gender=self.gender_man, + primary_site=[self.primary_site_sinus], + date_of_birth={"month_interval": 840}, # 70-79 age + ) + ) + self.treatments.extend( + TreatmentFactory.create_batch( + 5, + primary_diagnosis_uuid=self.primary_diagnoses[0], + treatment_type=[self.treatment_type_bone], + ) + ) + + def test_individual_count_api_no_censoring(self): + """ + Verify individual_count endpoint does not censor number >5. + + Testing Strategy: + - Total number of donors is 9 + - Send a request to individual_count endpoint + - Ensure that the response show the full number + """ + response = self.client.get(self.individual_count_url) + self.assertEqual(response.status_code, HTTPStatus.OK) + individual_count_value = response.json()["individual_count"] + self.assertGreaterEqual(len(self.donors), 5) + self.assertEqual(individual_count_value, str(len(self.donors))) + + def test_individual_count_api_censoring(self): + """ + Verify individual_count endpoint censoring for small datasets. + + Testing Strategy: + - Remove some donors (total count is 2 now) + - Send a request to individual_count endpoint. + - Ensure that the response does not reveal the number when it is less than 5. + """ + Donor.objects.filter(program_id=self.programs[0]).delete() + donors = Donor.objects.all() + response = self.client.get(self.individual_count_url) + self.assertEqual(response.status_code, HTTPStatus.OK) + individual_count_value = response.json()["individual_count"] + self.assertLess(len(donors), 5) + self.assertEqual(individual_count_value, "<5") + + def test_patients_per_cohort_api_censoring(self): + """ + Verify the censoring of patient counts per cohort endpoint. + + Testing Strategy: + - Program 0 has 7, Program 1 has 2 donors + - Send a request to patients_per_cohort endpoint. + - Ensure that the response does not reveal the number when it is less than 5 donors. + """ + response = self.client.get(self.patients_per_cohort_url) + self.assertEqual(response.status_code, HTTPStatus.OK) + for item in response.json(): + patients_count_value = item["patients_count"] + patients_count = Donor.objects.filter(program_id=item["program_id"]).count() + if patients_count < 5: + self.assertEqual(patients_count_value, "<5") + else: + self.assertEqual(patients_count_value, str(patients_count)) + + def test_gender_count_api_censoring(self): + """ + Test gender count API censoring for small datasets. + + Testing Strategy: + - "Man" gender is greater or equal to 5 + - Other genders is less than 5 + - Send a request to gender_count endpoint. + - Ensure that the response does not reveal the count when it is less than 5. + """ + + response = self.client.get(self.gender_count_url) + self.assertEqual(response.status_code, HTTPStatus.OK) + for item in response.json(): + gender_count_value = item["gender_count"] + gender_count = Donor.objects.filter(gender=item["gender"]).count() + if gender_count < 5: + self.assertEqual(gender_count_value, "<5") + else: + self.assertEqual(gender_count_value, str(gender_count)) + + def test_primary_site_count_api_censoring(self): + """ + Test primary site count API censoring for small datasets. + + Testing Strategy: + - "Accessory sinuses" primary site is greater or equal to 5 + - Other primary site is less than 5 + - Send a request to primary_site_count endpoint. + - Ensure that the response does not reveal the count when it is less than 5. + """ + response = self.client.get(self.primary_site_count_url) + self.assertEqual(response.status_code, HTTPStatus.OK) + for item in response.json(): + primary_site_count_value = item["primary_site_count"] + primary_site_count = Donor.objects.filter( + primary_site__contains=[item["primary_site_name"]] + ).count() + if primary_site_count < 5: + self.assertEqual(primary_site_count_value, "<5") + else: + self.assertEqual(primary_site_count_value, str(primary_site_count)) + + def test_treatment_type_count_api_censoring(self): + """ + Test treatment type count count API censoring for small datasets. + + Testing Strategy: + - "Bone marrow transplant" treatment type is greater or equal to 5 + - Other treatment type should be less than 5 (but might not since randomized) + - Send a request to treatment_type_count endpoint. + - Ensure that the response does not reveal the count when it is less than 5. + """ + response = self.client.get(self.treatment_type_count_url) + self.assertEqual(response.status_code, HTTPStatus.OK) + for item in response.json(): + treatment_type_count_value = item["treatment_type_count"] + treatment_type_count = Treatment.objects.filter( + treatment_type__contains=[item["treatment_type_name"]] + ).count() + if treatment_type_count < 5: + self.assertEqual(treatment_type_count_value, "<5") + else: + self.assertEqual(treatment_type_count_value, str(treatment_type_count)) + + def test_diagnosis_age_count_api_censoring(self): + """ + Test diagnosis age count count API censoring for small datasets. + + Testing Strategy: + - "70-79" age bracket is greater or equal to 5 + - Other age bracket is less than 5 + - Send a request to diagnosis_age_count endpoint. + - Ensure that the response does not reveal the count when it is less than 5. + """ + response = self.client.get(self.diagnosis_age_count_url) + self.assertEqual(response.status_code, HTTPStatus.OK) + for item in response.json(): + if item["age_at_diagnosis"] != "70-79": + self.assertEqual(item["age_count"], "<5") + + def test_discover_donors_api_censoring(self): + """ + Test discovery donor API censoring for small datasets. + + Testing Strategy: + - Program 0 has 7, Program 1 has 2 donors + - Send a request to discovery donors endpoint. + - Ensure that the response does not reveal the count when it is less than 5. + """ + response = self.client.get(self.discover_donors_url) + self.assertEqual(response.status_code, HTTPStatus.OK) + for item in response.json(): + donors_count_value = item["donors_count"] + donors_count = Donor.objects.filter(program_id=item["program_id"]).count() + if donors_count < 5: + self.assertEqual(donors_count_value, "<5") + else: + self.assertEqual(donors_count_value, str(donors_count)) diff --git a/config/settings/base.py b/config/settings/base.py index fc0ffe931..6ab612b14 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -147,4 +147,4 @@ # CURRENT KATSU VERSION ACCORDING TO MODEL CHANGES # ------------------------------------------------ -KATSU_VERSION = "4.2.1" +KATSU_VERSION = "4.3.0" diff --git a/config/settings/dev.py b/config/settings/dev.py index 67b03c04c..474ab0276 100644 --- a/config/settings/dev.py +++ b/config/settings/dev.py @@ -14,15 +14,35 @@ from .base import * +required_env_vars = [ + "HOST_CONTAINER_NAME", + "EXTERNAL_URL", + "AGGREGATE_COUNT_THRESHOLD", + "OPA_URL", + "POSTGRES_DATABASE", + "POSTGRES_USER", + "POSTGRES_PASSWORD_FILE", + "POSTGRES_HOST", + "POSTGRES_PORT", + "REDIS_PASSWORD_FILE", +] + +missing_vars = [var for var in required_env_vars if not os.getenv(var)] +if missing_vars: + raise EnvironmentError( + f"Missing required environment variables: {', '.join(missing_vars)}" + ) + DEBUG = True ALLOWED_HOSTS = [ "localhost", "127.0.0.1", - os.environ.get("HOST_CONTAINER_NAME"), - os.environ.get("EXTERNAL_URL"), - "query" + os.environ["HOST_CONTAINER_NAME"], + os.environ["EXTERNAL_URL"], + "query", ] +AGGREGATE_COUNT_THRESHOLD = os.environ["AGGREGATE_COUNT_THRESHOLD"] # Debug toolbar settings # ---------------------- @@ -30,8 +50,8 @@ MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") INTERNAL_IPS = type("c", (), {"__contains__": lambda *a: True})() DEBUG_TOOLBAR_CONFIG = { - 'RENDER_PANELS': False, - 'RESULTS_CACHE_SIZE': 100, + "RENDER_PANELS": False, + "RESULTS_CACHE_SIZE": 100, } # Whitenoise @@ -41,7 +61,7 @@ # CANDIG SETTINGS # --------------- -CANDIG_OPA_URL = os.getenv("OPA_URL") +CANDIG_OPA_URL = os.environ["OPA_URL"] CACHE_DURATION = int(os.getenv("CACHE_DURATION", 86400)) # default to 1 day CONN_MAX_AGE = int(os.getenv("CONN_MAX_AGE", 0)) if exists("/run/secrets/opa-service-token"): @@ -65,11 +85,11 @@ def get_secret(path): DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", - "NAME": os.environ.get("POSTGRES_DATABASE"), - "USER": os.environ.get("POSTGRES_USER"), - "PASSWORD": get_secret(os.environ.get("POSTGRES_PASSWORD_FILE")), - "HOST": os.environ.get("POSTGRES_HOST"), - "PORT": os.environ.get("POSTGRES_PORT"), + "NAME": os.environ["POSTGRES_DATABASE"], + "USER": os.environ["POSTGRES_USER"], + "PASSWORD": get_secret(os.environ["POSTGRES_PASSWORD_FILE"]), + "HOST": os.environ["POSTGRES_HOST"], + "PORT": os.environ["POSTGRES_PORT"], } } @@ -78,7 +98,7 @@ def get_secret(path): CACHES = { "default": { "BACKEND": "django.core.cache.backends.redis.RedisCache", - "LOCATION": f"redis://:{get_secret(os.environ.get('REDIS_PASSWORD_FILE'))}@{os.environ.get('EXTERNAL_URL')}:6379/1", + "LOCATION": f"redis://:{get_secret(os.environ['REDIS_PASSWORD_FILE'])}@{os.environ['EXTERNAL_URL']}:6379/1", "TIMEOUT": CACHE_DURATION, } } diff --git a/config/settings/local.py b/config/settings/local.py index 0c32dc338..9aa8b2918 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -17,6 +17,7 @@ DEBUG = True ALLOWED_HOSTS = ["localhost", "127.0.0.1", "0.0.0.0"] +AGGREGATE_COUNT_THRESHOLD = 5 # Debug toolbar settings # ---------------------- diff --git a/config/settings/prod.py b/config/settings/prod.py index 6495b1bb9..a75da7c93 100644 --- a/config/settings/prod.py +++ b/config/settings/prod.py @@ -10,13 +10,34 @@ from .base import * +required_env_vars = [ + "HOST_CONTAINER_NAME", + "EXTERNAL_URL", + "CANDIG_INTERNAL_DOMAIN", + "AGGREGATE_COUNT_THRESHOLD", + "OPA_URL", + "POSTGRES_DATABASE", + "POSTGRES_USER", + "POSTGRES_PASSWORD_FILE", + "POSTGRES_HOST", + "POSTGRES_PORT", + "REDIS_PASSWORD_FILE", +] + +missing_vars = [var for var in required_env_vars if not os.getenv(var)] +if missing_vars: + raise EnvironmentError( + f"Missing required environment variables: {', '.join(missing_vars)}" + ) + ALLOWED_HOSTS = [ - os.environ.get("HOST_CONTAINER_NAME"), - os.environ.get("EXTERNAL_URL"), - os.environ.get("CANDIG_INTERNAL_DOMAIN"), + os.environ["HOST_CONTAINER_NAME"], + os.environ["EXTERNAL_URL"], + os.environ["CANDIG_INTERNAL_DOMAIN"], "127.0.0.1", - "query" + "query", ] +AGGREGATE_COUNT_THRESHOLD = os.environ["AGGREGATE_COUNT_THRESHOLD"] # Whitenoise # ---------- @@ -25,13 +46,14 @@ # CANDIG SETTINGS # --------------- -CANDIG_OPA_URL = os.getenv("OPA_URL") +CANDIG_OPA_URL = os.environ["OPA_URL"] CACHE_DURATION = int(os.getenv("CACHE_DURATION", 86400)) # default to 1 day CONN_MAX_AGE = int(os.getenv("CONN_MAX_AGE", 0)) if exists("/run/secrets/opa-service-token"): with open("/run/secrets/opa-service-token", "r") as f: CANDIG_OPA_SECRET = f.read() + # function to read docker secret password file def get_secret(path): try: @@ -49,11 +71,11 @@ def get_secret(path): DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", - "NAME": os.environ.get("POSTGRES_DATABASE"), - "USER": os.environ.get("POSTGRES_USER"), - "PASSWORD": get_secret(os.environ.get("POSTGRES_PASSWORD_FILE")), - "HOST": os.environ.get("POSTGRES_HOST"), - "PORT": os.environ.get("POSTGRES_PORT"), + "NAME": os.environ["POSTGRES_DATABASE"], + "USER": os.environ["POSTGRES_USER"], + "PASSWORD": get_secret(os.environ["POSTGRES_PASSWORD_FILE"]), + "HOST": os.environ["POSTGRES_HOST"], + "PORT": os.environ["POSTGRES_PORT"], } } @@ -62,7 +84,7 @@ def get_secret(path): CACHES = { "default": { "BACKEND": "django.core.cache.backends.redis.RedisCache", - "LOCATION": f"redis://:{get_secret(os.environ.get('REDIS_PASSWORD_FILE'))}@{os.environ.get('EXTERNAL_URL')}:6379/1", + "LOCATION": f"redis://:{get_secret(os.environ['REDIS_PASSWORD_FILE'])}@{os.environ['EXTERNAL_URL']}:6379/1", "TIMEOUT": CACHE_DURATION, } }