Skip to content

Commit 99b0a4b

Browse files
authored
Recommendations changes (#1203)
1 parent b79249d commit 99b0a4b

32 files changed

Lines changed: 551 additions & 59 deletions

File tree

app/airflow/dags/libs/enums.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ class JobStageType(StrEnum):
1717
BUILD_CONCEPTS_FROM_DICT = "Build concepts from OMOP Data dictionary"
1818
REUSE_CONCEPTS = "Reuse concepts from other scan reports"
1919
GENERATE_RULES = "Generate mapping rules from available concepts"
20+
GENERATE_RECOMMENDATIONS = "Generate mapping recommendations"
2021
DOWNLOAD_RULES = "Generate and download mapping rules JSON"

app/api/api/serializers.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
VisibilityChoices,
2424
UploadStatus,
2525
MappingStatus,
26+
MappingRecommendation,
2627
)
2728
from users.serializers import UserSerializer
2829
from mapping.permissions import has_editorship, is_admin, is_az_function_user
@@ -793,8 +794,26 @@ class Meta:
793794
fields = ["id", "object_id", "creation_type", "content_type", "concept"]
794795

795796

797+
class MappingRecommendationSerializerV3(serializers.ModelSerializer):
798+
concept = ConceptSerializerV2()
799+
800+
class Meta:
801+
model = MappingRecommendation
802+
fields = [
803+
"id",
804+
"concept",
805+
"score",
806+
"tool_name",
807+
"tool_version",
808+
"created_at",
809+
]
810+
811+
796812
class ScanReportValueViewSerializerV3(serializers.ModelSerializer):
797813
concepts = ScanReportConceptSerializerV2(many=True, read_only=True)
814+
mapping_recommendations = MappingRecommendationSerializerV3(
815+
many=True, read_only=True
816+
)
798817

799818
class Meta:
800819
model = ScanReportValue
@@ -805,6 +824,7 @@ class Meta:
805824
"value_description",
806825
"scan_report_field",
807826
"concepts",
827+
"mapping_recommendations",
808828
]
809829

810830

app/api/api/views.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -933,7 +933,12 @@ def get_queryset(self):
933933
ScanReportValue.objects.filter(scan_report_field=self.field)
934934
.order_by("id")
935935
.select_related("scan_report_field")
936-
.prefetch_related("concepts", "concepts__concept")
936+
.prefetch_related(
937+
"concepts",
938+
"concepts__concept",
939+
"mapping_recommendations",
940+
"mapping_recommendations__concept",
941+
)
937942
)
938943

939944
def list(self, request, *args, **kwargs):

app/api/test/test_serializers.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@
1111
ScanReport,
1212
VisibilityChoices,
1313
)
14+
from django.contrib.contenttypes.models import ContentType
15+
from data.models import Concept
16+
from mapping.models import (
17+
ScanReport,
18+
ScanReportTable,
19+
ScanReportField,
20+
ScanReportValue,
21+
MappingRecommendation,
22+
)
23+
from api.serializers import (
24+
MappingRecommendationSerializerV3,
25+
ScanReportValueViewSerializerV3,
26+
)
1427

1528

1629
class TestScanReportEditSerializer(TestCase):
@@ -370,3 +383,87 @@ def test_validate_admin(self):
370383
# check admin can alter admins
371384
request.user = self.admin_user
372385
self.assertEqual(serializer.validate_admins(new_admin), new_admin)
386+
387+
388+
class TestMappingRecommendationSerializerV3(TestCase):
389+
def setUp(self):
390+
# Create test data
391+
self.scan_report = ScanReport.objects.create(
392+
dataset="Test Dataset",
393+
visibility="PUBLIC",
394+
)
395+
396+
self.table = ScanReportTable.objects.create(
397+
scan_report=self.scan_report,
398+
name="Test Table",
399+
)
400+
401+
self.field = ScanReportField.objects.create(
402+
scan_report_table=self.table,
403+
name="Test Field",
404+
description_column="Test Description",
405+
type_column="string",
406+
)
407+
408+
self.value = ScanReportValue.objects.create(
409+
scan_report_field=self.field,
410+
value="test_value",
411+
frequency=5,
412+
)
413+
414+
# Create a concept for the recommendation
415+
self.concept = Concept.objects.create(
416+
concept_id=12345,
417+
concept_name="Test Concept",
418+
concept_code="TEST123",
419+
domain_id="Test",
420+
vocabulary_id="Test",
421+
concept_class_id="Test",
422+
standard_concept="S",
423+
valid_start_date="2020-01-01",
424+
valid_end_date="2099-12-31",
425+
)
426+
427+
# Create a mapping recommendation
428+
content_type = ContentType.objects.get_for_model(ScanReportValue)
429+
self.recommendation = MappingRecommendation.objects.create(
430+
content_type=content_type,
431+
object_id=self.value.id,
432+
concept=self.concept,
433+
score=0.85,
434+
tool_name="test-tool",
435+
tool_version="1.0.0",
436+
)
437+
438+
def test_mapping_recommendation_serializer(self):
439+
"""Test that MappingRecommendationSerializerV3 serializes correctly."""
440+
serializer = MappingRecommendationSerializerV3(self.recommendation)
441+
data = serializer.data
442+
443+
self.assertEqual(data["id"], self.recommendation.id)
444+
self.assertEqual(data["score"], 0.85)
445+
self.assertEqual(data["tool_name"], "test-tool")
446+
self.assertEqual(data["tool_version"], "1.0.0")
447+
self.assertEqual(data["concept"]["concept_id"], self.concept.concept_id)
448+
self.assertEqual(data["concept"]["concept_name"], self.concept.concept_name)
449+
450+
def test_scan_report_value_v3_serializer_includes_recommendations(self):
451+
"""Test that ScanReportValueViewSerializerV3 includes mapping recommendations."""
452+
serializer = ScanReportValueViewSerializerV3(self.value)
453+
data = serializer.data
454+
455+
# Check that mapping recommendations are included
456+
self.assertIn("mapping_recommendations", data)
457+
self.assertEqual(len(data["mapping_recommendations"]), 1)
458+
459+
recommendation_data = data["mapping_recommendations"][0]
460+
self.assertEqual(recommendation_data["id"], self.recommendation.id)
461+
self.assertEqual(recommendation_data["score"], 0.85)
462+
self.assertEqual(recommendation_data["tool_name"], "test-tool")
463+
self.assertEqual(recommendation_data["tool_version"], "1.0.0")
464+
self.assertEqual(
465+
recommendation_data["concept"]["concept_id"], self.concept.concept_id
466+
)
467+
self.assertEqual(
468+
recommendation_data["concept"]["concept_name"], self.concept.concept_name
469+
)

app/api/test/test_views.py

Lines changed: 80 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
ScanReportTable,
2121
ScanReportValue,
2222
VisibilityChoices,
23+
MappingRecommendation,
2324
)
2425

2526

@@ -673,13 +674,11 @@ def setUp(self):
673674
dataset="The Heights of Hobbits",
674675
visibility=VisibilityChoices.PUBLIC,
675676
parent_dataset=self.public_dataset,
676-
status="COMPLET",
677677
)
678678
self.scanreport2 = ScanReport.objects.create(
679679
dataset="The Kinds of Orcs",
680680
visibility=VisibilityChoices.RESTRICTED,
681681
parent_dataset=self.restricted_dataset,
682-
status="COMPLET",
683682
)
684683

685684
# Set up projects
@@ -701,12 +700,6 @@ def setUp(self):
701700
name="Field1",
702701
description_column="",
703702
type_column="",
704-
max_length=32,
705-
nrows=0,
706-
nrows_checked=0,
707-
fraction_empty=0.0,
708-
nunique_values=0,
709-
fraction_unique=0.0,
710703
)
711704
self.scanreportvalue1 = ScanReportValue.objects.create(
712705
scan_report_field=self.scanreportfield1,
@@ -745,12 +738,6 @@ def setUp(self):
745738
name="Field2",
746739
description_column="",
747740
type_column="",
748-
max_length=32,
749-
nrows=0,
750-
nrows_checked=0,
751-
fraction_empty=0.0,
752-
nunique_values=0,
753-
fraction_unique=0.0,
754741
)
755742
self.scanreportvalue2 = ScanReportValue.objects.create(
756743
scan_report_field=self.scanreportfield2,
@@ -805,3 +792,82 @@ def test_az_function_user_get(self):
805792
az_response_ids = [item["id"] for item in az_response.data]
806793
self.assertTrue(self.scanreportconcept2.id in az_response_ids)
807794
self.assertTrue(self.scanreportconcept4.id in az_response_ids)
795+
796+
def test_scan_report_value_list_v3_includes_recommendations(self):
797+
"""Test that ScanReportValueListV3 includes mapping recommendations."""
798+
# Create test data
799+
scan_report = ScanReport.objects.create(
800+
dataset="Test Dataset",
801+
visibility=VisibilityChoices.PUBLIC,
802+
parent_dataset=self.public_dataset,
803+
)
804+
805+
table = ScanReportTable.objects.create(
806+
scan_report=scan_report,
807+
name="Test Table",
808+
)
809+
810+
field = ScanReportField.objects.create(
811+
scan_report_table=table,
812+
name="Test Field",
813+
description_column="Test Description",
814+
type_column="string",
815+
)
816+
817+
value = ScanReportValue.objects.create(
818+
scan_report_field=field,
819+
value="test_value",
820+
frequency=5,
821+
)
822+
823+
# Create a concept for the recommendation
824+
concept = Concept.objects.create(
825+
concept_id=12345,
826+
concept_name="Test Concept",
827+
concept_code="TEST123",
828+
domain_id="Test",
829+
vocabulary_id="Test",
830+
concept_class_id="Test",
831+
standard_concept="S",
832+
valid_start_date="2020-01-01",
833+
valid_end_date="2099-12-31",
834+
)
835+
836+
# Create a mapping recommendation
837+
content_type = ContentType.objects.get_for_model(ScanReportValue)
838+
recommendation = MappingRecommendation.objects.create(
839+
content_type=content_type,
840+
object_id=value.id,
841+
concept=concept,
842+
score=0.85,
843+
tool_name="test-tool",
844+
tool_version="1.0.0",
845+
)
846+
847+
# Make request
848+
url = f"/api/v3/scanreports/{scan_report.id}/tables/{table.id}/fields/{field.id}/values/"
849+
response = self.client.get(url)
850+
851+
# Verify response
852+
self.assertEqual(response.status_code, 200)
853+
data = response.json()
854+
855+
# Check that the value is returned
856+
self.assertEqual(len(data["results"]), 1)
857+
value_data = data["results"][0]
858+
859+
# Check that mapping recommendations are included
860+
self.assertIn("mapping_recommendations", value_data)
861+
self.assertEqual(len(value_data["mapping_recommendations"]), 1)
862+
863+
recommendation_data = value_data["mapping_recommendations"][0]
864+
self.assertEqual(recommendation_data["id"], recommendation.id)
865+
self.assertEqual(recommendation_data["score"], 0.85)
866+
self.assertEqual(recommendation_data["tool_name"], "test-tool")
867+
self.assertEqual(recommendation_data["tool_version"], "1.0.0")
868+
self.assertEqual(
869+
recommendation_data["concept"]["concept_id"], concept.concept_id
870+
)
871+
self.assertEqual(
872+
recommendation_data["concept"]["concept_name"], concept.concept_name
873+
)

app/next-client-app/api/recommendations.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
unisonApiKey,
77
recommendationService,
88
} from "@/constants";
9-
import { UnisonConceptResponse } from "@/types/recommendation";
109

1110
export const getConceptRecommendationsUnison = async (
1211
// Unison can query by concept name, or concept code (exact match).

app/next-client-app/app/(protected)/datasets/[id]/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { DataTable } from "@/components/data-table";
44
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
55
import { objToQuery } from "@/lib/client-utils";
66
import { ScanReportsTableFilter } from "@/components/scanreports/ScanReportsTableFilter";
7-
import { FilterParameters } from "@/types/filter";
87
import { VisibilityState } from "@tanstack/react-table";
98
import { EmptyState } from "@/components/ui/empty-state";
109

app/next-client-app/app/(protected)/datasets/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { getDataPartners, getDataSets } from "@/api/datasets";
44
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
55
import { objToQuery } from "@/lib/client-utils";
66
import { DataTableFilter } from "@/components/data-table/DataTableFilter";
7-
import { FilterParameters } from "@/types/filter";
87
import { CreateDatasetDialog } from "@/components/datasets/CreateDatasetDialog";
98
import { Database } from "lucide-react";
109
import { getAllProjects } from "@/api/projects";

app/next-client-app/app/(protected)/projects/[id]/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { DataTable } from "@/components/data-table";
22
import { objToQuery } from "@/lib/client-utils";
3-
import { FilterParameters } from "@/types/filter";
43
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
54
import { DataTableFilter } from "@/components/data-table/DataTableFilter";
65
import { getDataSets } from "@/api/datasets";

app/next-client-app/app/(protected)/projects/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { DataTable } from "@/components/data-table";
22
import { columns } from "./columns";
33
import { objToQuery } from "@/lib/client-utils";
44
import { DataTableFilter } from "@/components/data-table/DataTableFilter";
5-
import { FilterParameters } from "@/types/filter";
65
import { Folders } from "lucide-react";
76
import { getProjectsList } from "@/api/projects";
87
import { Metadata } from "next";

0 commit comments

Comments
 (0)