diff --git a/HISTORY.rst b/HISTORY.rst index 0d817d305..038e261b2 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -30,6 +30,7 @@ Full Change List - Improving Clinvar record aggregation (#640). - Fixing Docker builds (#660). - Fixing ClinVar submission XML generation (#677). +- Adding regular task to sync ClinVar submission ``Individual`` sex from the one from the ``Case``. ----------------- v1.2.2 (anthenea) diff --git a/clinvar_export/management/commands/refresh_individuals.py b/clinvar_export/management/commands/refresh_individuals.py new file mode 100644 index 000000000..1bc05ce3c --- /dev/null +++ b/clinvar_export/management/commands/refresh_individuals.py @@ -0,0 +1,16 @@ +"""Django management command for triggering clinvar_export individual update.""" + +from django.core.management.base import BaseCommand + +from clinvar_export.models import refresh_individual_sex_affected + + +class Command(BaseCommand): + #: Help message displayed on the command line. + help = "Refresh ClinVar export individual sex/affected attributes." + + def handle(self, *args, **options): + """The actual implementation is in ``_handle()``, splitting to get commit times.""" + self.stderr.write(self.style.NOTICE("Refreshing all clinvar_xml individuals")) + refresh_individual_sex_affected() + self.stderr.write(self.style.NOTICE("All done. Have a nice day!")) diff --git a/clinvar_export/models.py b/clinvar_export/models.py index f828cadce..d48033a4a 100644 --- a/clinvar_export/models.py +++ b/clinvar_export/models.py @@ -17,6 +17,20 @@ #: Django user model. AUTH_USER_MODEL = getattr(settings, "AUTH_USER_MODEL", "auth.User") +#: Map pedigree value for "sex" to value for models. +SEX_MAP = { + 0: "unknown", + 1: "male", + 2: "female", +} + +#: Map pedigree value for "affected" to value for models. +AFFECTED_MAP = { + 0: "unknown", + 1: "no", + 2: "yes", +} + class FamilyManager(models.Manager): """Custom ``Manager`` that allows to easily get or create a ``Family`` from a project with a given name.""" @@ -123,16 +137,6 @@ class Meta: def create_families_and_individuals(project: Project) -> None: """Create missing families and individuals for the given ``project``.""" - sex_map = { - 0: "unknown", - 1: "male", - 2: "female", - } - affected_map = { - 0: "unknown", - 1: "no", - 2: "yes", - } with transaction.atomic(): for case in Case.objects.filter(project=project): family = Family.objects.get_or_create_in_project(project, case=case) @@ -141,8 +145,8 @@ def create_families_and_individuals(project: Project) -> None: project, entry["patient"], family=family, - sex=sex_map.get(entry.get("sex", 0), "unknown"), - affected=affected_map.get(entry.get("affected", 0), "unknown"), + sex=SEX_MAP.get(entry.get("sex", 0), "unknown"), + affected=AFFECTED_MAP.get(entry.get("affected", 0), "unknown"), ) @@ -388,3 +392,17 @@ def get_project(self): class Meta: ordering = ("sort_order",) + + +def refresh_individual_sex_affected(): + """Update the ``Individual.sex`` field from the upstream case. + + This is done regularly in a related task. + """ + for family in Family.objects.select_related("case").prefetch_related("individual_set").all(): + ped_entries = {entry["patient"]: entry for entry in family.case.pedigree} + for individual in family.individual_set.all(): + related_ped_entry = ped_entries[individual.name] + individual.sex = SEX_MAP.get(related_ped_entry["sex"], "unknown") + individual.affected = AFFECTED_MAP.get(related_ped_entry["affected"], "unknown") + individual.save() diff --git a/clinvar_export/tasks.py b/clinvar_export/tasks.py new file mode 100644 index 000000000..a64414f24 --- /dev/null +++ b/clinvar_export/tasks.py @@ -0,0 +1,15 @@ +from celery.schedules import crontab +from config.celery import app + +from clinvar_export import models + + +@app.task(bind=True) +def refresh_individual_sex_affected(_self): + models.refresh_individual_sex_affected() + + +@app.on_after_finalize.connect +def setup_periodic_tasks(sender, **_kwargs): + """Register periodic tasks""" + sender.add_periodic_task(schedule=crontab(minute=11), sig=refresh_individual_sex_affected.s()) diff --git a/clinvar_export/tests/test_models.py b/clinvar_export/tests/test_models.py index e1a31832a..351c5b86c 100644 --- a/clinvar_export/tests/test_models.py +++ b/clinvar_export/tests/test_models.py @@ -2,28 +2,30 @@ from test_plus.test import TestCase -from ..models import ( +from variants.models import Case +from variants.tests.factories import CaseFactory + +from clinvar_export.models import ( + AssertionMethod, Family, Individual, - AssertionMethod, - Submitter, Organisation, + Submission, SubmissionSet, + Submitter, SubmittingOrg, - Submission, + refresh_individual_sex_affected, ) -from .factories import ( +from clinvar_export.tests.factories import ( + AssertionMethodFactory, FamilyFactory, IndividualFactory, - AssertionMethodFactory, - SubmitterFactory, OrganisationFactory, + SubmissionFactory, SubmissionSetFactory, + SubmitterFactory, SubmittingOrgFactory, - SubmissionFactory, ) -from variants.models import Case -from variants.tests.factories import CaseFactory class TestFamily(TestCase): @@ -168,3 +170,19 @@ def testCreate(self): self.assertEquals(Submission.objects.count(), 0) SubmissionFactory() self.assertEquals(Submission.objects.count(), 1) + + +class TestUpdateIndividualSexAffectedTask(TestCase): + """Test the task that updates the sex/affected of ``Individual`` records from upstream case.""" + + def testRun(self): + individual = IndividualFactory(sex="male", affected="yes") + individual.sex = "unknown" + individual.affected = "unknown" + individual.save() + self.assertEquals(individual.sex, "unknown") + self.assertEquals(individual.sex, "unknown") + refresh_individual_sex_affected() + individual.refresh_from_db() + self.assertEquals(individual.sex, "male") + self.assertEquals(individual.affected, "yes")