From 37767885d053b55b04a46d9d3b40bfa9a0e60975 Mon Sep 17 00:00:00 2001 From: "D. Gopal Krishna" Date: Thu, 4 Apr 2024 10:47:12 +0530 Subject: [PATCH 1/6] #279 admin changes in AEMeta and AEIntersection for Synonyms --- backend/composer/admin.py | 73 ++++++++++++++++--- ...051_alter_anatomicalentity_region_layer.py | 24 ++++++ backend/composer/models.py | 5 +- backend/composer/signals.py | 23 ++++-- 4 files changed, 109 insertions(+), 16 deletions(-) create mode 100644 backend/composer/migrations/0051_alter_anatomicalentity_region_layer.py diff --git a/backend/composer/admin.py b/backend/composer/admin.py index e6c09687..cc4d1201 100644 --- a/backend/composer/admin.py +++ b/backend/composer/admin.py @@ -4,6 +4,7 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.models import User from fsm_admin.mixins import FSMTransitionMixin +from django import forms from composer.models import ( Phenotype, @@ -89,12 +90,6 @@ class SentenceAdmin( NoteSentenceInline, ) - -class SynonymInline(admin.TabularInline): - model = Synonym - extra = 1 - - class AnatomicalEntityAdmin(admin.ModelAdmin): search_fields = ('simple_entity__name', 'region_layer__layer__name', 'region_layer__region__name') @@ -105,10 +100,42 @@ def get_model_perms(self, request): return {} +class AnatomicalEntityMetaForm(forms.ModelForm): + synonyms = forms.CharField( + max_length=255, required=False, validators=[lambda x: x.split(",")] + ) + + class Meta: + model = AnatomicalEntityMeta + fields = '__all__' + class AnatomicalEntityMetaAdmin(admin.ModelAdmin): - list_display = ("name", "ontology_uri") + list_display = ("name", "ontology_uri", "synonyms") list_display_links = ("name", "ontology_uri") - search_fields = ("name",) + search_fields = ("name", "ontology_uri") + form = AnatomicalEntityMetaForm + + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + if obj and getattr(obj, 'anatomicalentity', None): ## only for simple anatomical entity - check this by - if obj has anatomicalentity attribute + form.base_fields['synonyms'].initial = ", ".join([synonym.name for synonym in obj.anatomicalentity.synonyms.all()]) + else: + # show synonyms only for simple anatomical entity (not for Layer or Region) + del form.base_fields['synonyms'] + return form + + def save_model(self, request, obj, form, change): + obj.synonyms = form.cleaned_data.get('synonyms') + super().save_model(request, obj, form, change) + + + @admin.display(description="Synonyms") + def synonyms(self, obj): + """ + ONLY FOR SIMPLE ANATOMICAL ENTITY + """ + synonyms = obj.anatomicalentity.synonyms.all() + return ", ".join([synonym.name for synonym in synonyms]) class LayerAdmin(admin.ModelAdmin): @@ -122,10 +149,36 @@ class RegionAdmin(admin.ModelAdmin): filter_horizontal = ('layers',) -class AnatomicalEntityIntersectionAdmin(admin.ModelAdmin): - list_display = ('layer', 'region',) +class AnatomicalEntityIntersectionForm(forms.ModelForm): + synonyms = forms.CharField( + max_length=255, required=False, validators=[lambda x: x.split(",")] + ) + + class Meta: + model = AnatomicalEntityIntersection + fields = '__all__' + + +class AnatomicalEntityIntersectionAdmin(nested_admin.NestedModelAdmin, admin.ModelAdmin): + list_display = ('layer', 'region', "synonyms") list_filter = ('layer', 'region',) raw_id_fields = ('layer', 'region',) + form = AnatomicalEntityIntersectionForm + + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + if obj and getattr(obj, 'anatomicalentity', None): + form.base_fields['synonyms'].initial = ", ".join([synonym.name for synonym in obj.anatomicalentity.synonyms.all()]) + return form + + def save_model(self, request, obj, form, change): + obj.synonyms = form.cleaned_data.get('synonyms') + super().save_model(request, obj, form, change) + + @admin.display(description="Synonyms") + def synonyms(self, obj): + synonyms = obj.anatomicalentity.synonyms.all() + return ", ".join([synonym.name for synonym in synonyms]) class ViaInline(SortableStackedInline): diff --git a/backend/composer/migrations/0051_alter_anatomicalentity_region_layer.py b/backend/composer/migrations/0051_alter_anatomicalentity_region_layer.py new file mode 100644 index 00000000..b4bccd18 --- /dev/null +++ b/backend/composer/migrations/0051_alter_anatomicalentity_region_layer.py @@ -0,0 +1,24 @@ +# Generated by Django 4.1.4 on 2024-04-03 13:57 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("composer", "0050_alter_anatomicalentityintersection_options_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="anatomicalentity", + name="region_layer", + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="composer.anatomicalentityintersection", + ), + ), + ] diff --git a/backend/composer/models.py b/backend/composer/models.py index f6f08520..41c1d88b 100644 --- a/backend/composer/models.py +++ b/backend/composer/models.py @@ -254,10 +254,13 @@ class Meta: verbose_name = "Region/Layer Combination" verbose_name_plural = "Region/Layer Combinations" + def __str__(self): + return f'{self.region.name} - {self.layer.name}' + class AnatomicalEntity(models.Model): simple_entity = models.OneToOneField(AnatomicalEntityMeta, on_delete=models.CASCADE, null=True, blank=True) - region_layer = models.ForeignKey(AnatomicalEntityIntersection, on_delete=models.CASCADE, null=True, blank=True) + region_layer = models.OneToOneField(AnatomicalEntityIntersection, on_delete=models.CASCADE, null=True, blank=True) @property def name(self): diff --git a/backend/composer/signals.py b/backend/composer/signals.py index 70ae94a2..5360a3a3 100644 --- a/backend/composer/signals.py +++ b/backend/composer/signals.py @@ -6,7 +6,7 @@ from .enums import CSState, NoteType from .models import ConnectivityStatement, ExportBatch, Note, Sentence, AnatomicalEntity, AnatomicalEntityIntersection, \ - AnatomicalEntityMeta + AnatomicalEntityMeta, Synonym from .services.export_services import compute_metrics, ConnectivityStatementStateService @@ -49,13 +49,26 @@ def post_transition_cs(sender, instance, name, source, target, **kwargs): instance = ConnectivityStatementStateService.add_important_tag(instance) +def create_synonyms_on_save(instance, ae): + if instance.synonyms: + synonyms = [synonym.strip() for synonym in instance.synonyms.split(",")] + synonyms = [ + Synonym.objects.create(name=synonym) if (not Synonym.objects.filter(name=synonym).exists()) \ + else Synonym.objects.get(name=synonym) \ + for synonym in synonyms + ] + ae.synonyms.set(synonyms) + + @receiver(post_save, sender=AnatomicalEntityIntersection) def create_region_layer_anatomical_entity(sender, instance=None, created=False, **kwargs): - if created and instance: - AnatomicalEntity.objects.create(region_layer=instance) + if instance: + ae = AnatomicalEntity.objects.create(region_layer=instance) if created else instance.anatomicalentity + create_synonyms_on_save(instance, ae) @receiver(post_save, sender=AnatomicalEntityMeta) def create_simple_anatomical_entity(sender, instance=None, created=False, **kwargs): - if created and instance: - AnatomicalEntity.objects.create(simple_entity=instance) + if instance: + ae = AnatomicalEntity.objects.create(simple_entity=instance) if created else instance.anatomicalentity + create_synonyms_on_save(instance, ae) From dad080dd8f737ab00376c52e09a30363d246470d Mon Sep 17 00:00:00 2001 From: "D. Gopal Krishna" Date: Thu, 4 Apr 2024 10:55:21 +0530 Subject: [PATCH 2/6] #279 comment for helper function --- backend/composer/signals.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/composer/signals.py b/backend/composer/signals.py index 5360a3a3..0f150342 100644 --- a/backend/composer/signals.py +++ b/backend/composer/signals.py @@ -50,6 +50,10 @@ def post_transition_cs(sender, instance, name, source, target, **kwargs): def create_synonyms_on_save(instance, ae): + """ + ONLY allowed through the admin interface. + F.E. - check AnatomicalEntityMetaAdmin -> save_model() + """ if instance.synonyms: synonyms = [synonym.strip() for synonym in instance.synonyms.split(",")] synonyms = [ From 1b229bd435526571707c6136cf397988cab71bb3 Mon Sep 17 00:00:00 2001 From: "D. Gopal Krishna" Date: Thu, 4 Apr 2024 12:18:00 +0530 Subject: [PATCH 3/6] #279 efficient synonyms in AEMeta and Intersection with prefetch_related --- backend/composer/admin.py | 11 ++++++++++- backend/composer/signals.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/backend/composer/admin.py b/backend/composer/admin.py index cc4d1201..60c76021 100644 --- a/backend/composer/admin.py +++ b/backend/composer/admin.py @@ -115,6 +115,11 @@ class AnatomicalEntityMetaAdmin(admin.ModelAdmin): search_fields = ("name", "ontology_uri") form = AnatomicalEntityMetaForm + def get_queryset(self, request): + qs = super().get_queryset(request) + return qs.prefetch_related('anatomicalentity__synonyms') + + def get_form(self, request, obj=None, **kwargs): form = super().get_form(request, obj, **kwargs) if obj and getattr(obj, 'anatomicalentity', None): ## only for simple anatomical entity - check this by - if obj has anatomicalentity attribute @@ -136,7 +141,7 @@ def synonyms(self, obj): """ synonyms = obj.anatomicalentity.synonyms.all() return ", ".join([synonym.name for synonym in synonyms]) - + class LayerAdmin(admin.ModelAdmin): list_display = ('name', 'ontology_uri',) @@ -165,6 +170,10 @@ class AnatomicalEntityIntersectionAdmin(nested_admin.NestedModelAdmin, admin.Mod raw_id_fields = ('layer', 'region',) form = AnatomicalEntityIntersectionForm + def get_queryset(self, request): + qs = super().get_queryset(request) + return qs.prefetch_related('anatomicalentity__synonyms') + def get_form(self, request, obj=None, **kwargs): form = super().get_form(request, obj, **kwargs) if obj and getattr(obj, 'anatomicalentity', None): diff --git a/backend/composer/signals.py b/backend/composer/signals.py index 0f150342..9bc34dba 100644 --- a/backend/composer/signals.py +++ b/backend/composer/signals.py @@ -54,7 +54,7 @@ def create_synonyms_on_save(instance, ae): ONLY allowed through the admin interface. F.E. - check AnatomicalEntityMetaAdmin -> save_model() """ - if instance.synonyms: + if getattr(instance, 'synonyms', None): synonyms = [synonym.strip() for synonym in instance.synonyms.split(",")] synonyms = [ Synonym.objects.create(name=synonym) if (not Synonym.objects.filter(name=synonym).exists()) \ From 2b17bca0e65a85f82409ffda1fc15f7ceea8eaff Mon Sep 17 00:00:00 2001 From: Zoran Sinnema Date: Thu, 4 Apr 2024 12:06:05 +0200 Subject: [PATCH 4/6] feat(admin): started changing AEMeta admin to AE admin --- backend/composer/admin.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/backend/composer/admin.py b/backend/composer/admin.py index 60c76021..f1ecdfc7 100644 --- a/backend/composer/admin.py +++ b/backend/composer/admin.py @@ -39,7 +39,9 @@ class ProvenanceInline(admin.StackedInline): model = Provenance extra = 1 - +class SynonymeInline(admin.StackedInline): + model = Synonym + extra = 3 class ProvenanceNestedInline(nested_admin.NestedStackedInline): model = Provenance extra = 1 @@ -92,12 +94,14 @@ class SentenceAdmin( class AnatomicalEntityAdmin(admin.ModelAdmin): search_fields = ('simple_entity__name', 'region_layer__layer__name', 'region_layer__region__name') + autocomplete_fields = ('simple_entity', 'region_layer') + inlines = (SynonymeInline,) - def get_model_perms(self, request): - """ - Return empty dict to hide the model from admin index. - """ - return {} + # def get_model_perms(self, request): + # """ + # Return empty dict to hide the model from admin index. + # """ + # return {} class AnatomicalEntityMetaForm(forms.ModelForm): @@ -166,6 +170,7 @@ class Meta: class AnatomicalEntityIntersectionAdmin(nested_admin.NestedModelAdmin, admin.ModelAdmin): list_display = ('layer', 'region', "synonyms") + search_fields = ("region__name", "layer__name") list_filter = ('layer', 'region',) raw_id_fields = ('layer', 'region',) form = AnatomicalEntityIntersectionForm From 22bc3f07a3516fa688de330873d757ac6f0609c8 Mon Sep 17 00:00:00 2001 From: "D. Gopal Krishna" Date: Thu, 4 Apr 2024 18:58:14 +0530 Subject: [PATCH 5/6] #279 remove signals, shift synonyms to AE Admin --- backend/composer/admin.py | 96 +++++++++---------------------------- backend/composer/models.py | 17 +++++-- backend/composer/signals.py | 13 ----- 3 files changed, 34 insertions(+), 92 deletions(-) diff --git a/backend/composer/admin.py b/backend/composer/admin.py index f1ecdfc7..04e10497 100644 --- a/backend/composer/admin.py +++ b/backend/composer/admin.py @@ -1,3 +1,6 @@ +from typing import Any +from django.db.models.query import QuerySet +from django.http import HttpRequest import nested_admin from adminsortable2.admin import SortableAdminBase, SortableStackedInline from django.contrib import admin @@ -41,7 +44,9 @@ class ProvenanceInline(admin.StackedInline): class SynonymeInline(admin.StackedInline): model = Synonym - extra = 3 + extra = 1 + + class ProvenanceNestedInline(nested_admin.NestedStackedInline): model = Provenance extra = 1 @@ -95,57 +100,30 @@ class SentenceAdmin( class AnatomicalEntityAdmin(admin.ModelAdmin): search_fields = ('simple_entity__name', 'region_layer__layer__name', 'region_layer__region__name') autocomplete_fields = ('simple_entity', 'region_layer') + list_display = ('simple_entity', 'region_layer', "synonyms") + list_display_links = ('simple_entity', 'region_layer') inlines = (SynonymeInline,) - # def get_model_perms(self, request): - # """ - # Return empty dict to hide the model from admin index. - # """ - # return {} - + # we need to make efficient queries to the database to get the list of anatomical entities + def get_queryset(self, request: HttpRequest) -> QuerySet[Any]: + return super().get_queryset(request) \ + .select_related('simple_entity', 'region_layer__layer', 'region_layer__region') \ + .prefetch_related('synonyms') -class AnatomicalEntityMetaForm(forms.ModelForm): - synonyms = forms.CharField( - max_length=255, required=False, validators=[lambda x: x.split(",")] - ) + @admin.display(description="Synonyms") + def synonyms(self, obj): + synonyms = obj.synonyms.all() + return ', '.join([synonym.name for synonym in synonyms]) - class Meta: - model = AnatomicalEntityMeta - fields = '__all__' class AnatomicalEntityMetaAdmin(admin.ModelAdmin): - list_display = ("name", "ontology_uri", "synonyms") + list_display = ("name", "ontology_uri") list_display_links = ("name", "ontology_uri") search_fields = ("name", "ontology_uri") - form = AnatomicalEntityMetaForm - - def get_queryset(self, request): - qs = super().get_queryset(request) - return qs.prefetch_related('anatomicalentity__synonyms') - - - def get_form(self, request, obj=None, **kwargs): - form = super().get_form(request, obj, **kwargs) - if obj and getattr(obj, 'anatomicalentity', None): ## only for simple anatomical entity - check this by - if obj has anatomicalentity attribute - form.base_fields['synonyms'].initial = ", ".join([synonym.name for synonym in obj.anatomicalentity.synonyms.all()]) - else: - # show synonyms only for simple anatomical entity (not for Layer or Region) - del form.base_fields['synonyms'] - return form - - def save_model(self, request, obj, form, change): - obj.synonyms = form.cleaned_data.get('synonyms') - super().save_model(request, obj, form, change) - - @admin.display(description="Synonyms") - def synonyms(self, obj): - """ - ONLY FOR SIMPLE ANATOMICAL ENTITY - """ - synonyms = obj.anatomicalentity.synonyms.all() - return ", ".join([synonym.name for synonym in synonyms]) - + def get_model_perms(self, request): + return {} + class LayerAdmin(admin.ModelAdmin): list_display = ('name', 'ontology_uri',) @@ -158,41 +136,11 @@ class RegionAdmin(admin.ModelAdmin): filter_horizontal = ('layers',) -class AnatomicalEntityIntersectionForm(forms.ModelForm): - synonyms = forms.CharField( - max_length=255, required=False, validators=[lambda x: x.split(",")] - ) - - class Meta: - model = AnatomicalEntityIntersection - fields = '__all__' - - class AnatomicalEntityIntersectionAdmin(nested_admin.NestedModelAdmin, admin.ModelAdmin): - list_display = ('layer', 'region', "synonyms") + list_display = ('layer', 'region') search_fields = ("region__name", "layer__name") list_filter = ('layer', 'region',) raw_id_fields = ('layer', 'region',) - form = AnatomicalEntityIntersectionForm - - def get_queryset(self, request): - qs = super().get_queryset(request) - return qs.prefetch_related('anatomicalentity__synonyms') - - def get_form(self, request, obj=None, **kwargs): - form = super().get_form(request, obj, **kwargs) - if obj and getattr(obj, 'anatomicalentity', None): - form.base_fields['synonyms'].initial = ", ".join([synonym.name for synonym in obj.anatomicalentity.synonyms.all()]) - return form - - def save_model(self, request, obj, form, change): - obj.synonyms = form.cleaned_data.get('synonyms') - super().save_model(request, obj, form, change) - - @admin.display(description="Synonyms") - def synonyms(self, obj): - synonyms = obj.anatomicalentity.synonyms.all() - return ", ".join([synonym.name for synonym in synonyms]) class ViaInline(SortableStackedInline): diff --git a/backend/composer/models.py b/backend/composer/models.py index 41c1d88b..f3ab62d0 100644 --- a/backend/composer/models.py +++ b/backend/composer/models.py @@ -264,19 +264,26 @@ class AnatomicalEntity(models.Model): @property def name(self): - return self.simple_entity.name if self.simple_entity \ - else f'{self.region_layer.region.name},{self.region_layer.layer.name}' + if self.simple_entity: + return self.simple_entity.name + elif self.region_layer: + return f'{self.region_layer.region.name},{self.region_layer.layer.name}' + return 'Unknown Anatomical Entity' @property def ontology_uri(self): - return self.simple_entity.ontology_uri if self.simple_entity \ - else f'{self.region_layer.region.ontology_uri},{self.region_layer.layer.ontology_uri}' - + if self.simple_entity: + return self.simple_entity.ontology_uri + elif self.region_layer: + return f'{self.region_layer.region.ontology_uri},{self.region_layer.layer.ontology_uri}' + return 'Unknown URI' def __str__(self): return self.name class Meta: + verbose_name = "Anatomical Entity" + verbose_name_plural = "Anatomical Entities" constraints = [ CheckConstraint( check=( diff --git a/backend/composer/signals.py b/backend/composer/signals.py index 9bc34dba..ae338961 100644 --- a/backend/composer/signals.py +++ b/backend/composer/signals.py @@ -63,16 +63,3 @@ def create_synonyms_on_save(instance, ae): ] ae.synonyms.set(synonyms) - -@receiver(post_save, sender=AnatomicalEntityIntersection) -def create_region_layer_anatomical_entity(sender, instance=None, created=False, **kwargs): - if instance: - ae = AnatomicalEntity.objects.create(region_layer=instance) if created else instance.anatomicalentity - create_synonyms_on_save(instance, ae) - - -@receiver(post_save, sender=AnatomicalEntityMeta) -def create_simple_anatomical_entity(sender, instance=None, created=False, **kwargs): - if instance: - ae = AnatomicalEntity.objects.create(simple_entity=instance) if created else instance.anatomicalentity - create_synonyms_on_save(instance, ae) From 97d400a4966e68355988db19f4d5621281080a33 Mon Sep 17 00:00:00 2001 From: "D. Gopal Krishna" Date: Fri, 5 Apr 2024 13:59:25 +0530 Subject: [PATCH 6/6] #279 add signals to Layer and Region --- backend/composer/admin.py | 3 +++ backend/composer/signals.py | 26 ++++++++++---------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/backend/composer/admin.py b/backend/composer/admin.py index 04e10497..c57bdd9e 100644 --- a/backend/composer/admin.py +++ b/backend/composer/admin.py @@ -142,6 +142,9 @@ class AnatomicalEntityIntersectionAdmin(nested_admin.NestedModelAdmin, admin.Mod list_filter = ('layer', 'region',) raw_id_fields = ('layer', 'region',) + def get_model_perms(self, request): + return {} + class ViaInline(SortableStackedInline): model = Via diff --git a/backend/composer/signals.py b/backend/composer/signals.py index ae338961..a8690f8f 100644 --- a/backend/composer/signals.py +++ b/backend/composer/signals.py @@ -5,8 +5,8 @@ from django_fsm.signals import post_transition from .enums import CSState, NoteType -from .models import ConnectivityStatement, ExportBatch, Note, Sentence, AnatomicalEntity, AnatomicalEntityIntersection, \ - AnatomicalEntityMeta, Synonym +from .models import ConnectivityStatement, ExportBatch, Note, Sentence, Synonym, \ + AnatomicalEntity, Layer, Region from .services.export_services import compute_metrics, ConnectivityStatementStateService @@ -48,18 +48,12 @@ def post_transition_cs(sender, instance, name, source, target, **kwargs): # add important tag to CS when transition to COMPOSE_NOW from NPO Approved or Exported instance = ConnectivityStatementStateService.add_important_tag(instance) +@receiver(post_save, sender=Layer) +def create_layer_anatomical_entity(sender, instance=None, created=False, **kwargs): + if created and instance: + AnatomicalEntity.objects.create(simple_entity=instance) -def create_synonyms_on_save(instance, ae): - """ - ONLY allowed through the admin interface. - F.E. - check AnatomicalEntityMetaAdmin -> save_model() - """ - if getattr(instance, 'synonyms', None): - synonyms = [synonym.strip() for synonym in instance.synonyms.split(",")] - synonyms = [ - Synonym.objects.create(name=synonym) if (not Synonym.objects.filter(name=synonym).exists()) \ - else Synonym.objects.get(name=synonym) \ - for synonym in synonyms - ] - ae.synonyms.set(synonyms) - +@receiver(post_save, sender=Region) +def create_region_anatomical_entity(sender, instance=None, created=False, **kwargs): + if created and instance: + AnatomicalEntity.objects.create(simple_entity=instance)