diff --git a/backend/composer/admin.py b/backend/composer/admin.py index 108e27b1..97b2b6f6 100644 --- a/backend/composer/admin.py +++ b/backend/composer/admin.py @@ -98,11 +98,15 @@ 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", "ontology_uri") + list_display = ('simple_entity', 'region_layer', 'synonyms', 'ontology_uri' ) list_display_links = ('simple_entity', 'region_layer') + search_fields = ( + 'simple_entity__name', 'simple_entity__ontology_uri', + 'region_layer__layer__name', 'region_layer__region__name', + 'region_layer__layer__ontology_uri', 'region_layer__region__ontology_uri', + ) inlines = (SynonymInline,) + autocomplete_fields = ('simple_entity', 'region_layer') # we need to make efficient queries to the database to get the list of anatomical entities def get_queryset(self, request: HttpRequest) -> QuerySet[Any]: @@ -130,22 +134,66 @@ def get_model_perms(self, request): return {} +class LayerAdminForm(forms.ModelForm): + class Meta: + model = Layer + fields = '__all__' + labels = { + 'ae_meta': 'layer', + } + + +class RegionAdminForm(forms.ModelForm): + class Meta: + model = Region + fields = '__all__' + labels = { + 'ae_meta': 'region', + } + + class LayerAdmin(admin.ModelAdmin): - list_display = ('name', 'ontology_uri',) - search_fields = ('name',) + form = LayerAdminForm + list_display = ('layer_name', 'ontology_uri') + list_display_links = ('layer_name', 'ontology_uri') + search_fields = ('ae_meta__name', 'ae_meta__ontology_uri') + autocomplete_fields = ('ae_meta',) + + @admin.display(description="Layer Name") + def layer_name(self, obj): + return obj.ae_meta.name + + @admin.display(description="Ontology URI") + def ontology_uri(self, obj): + return obj.ae_meta.ontology_uri + class RegionAdmin(admin.ModelAdmin): - list_display = ('name', 'ontology_uri',) - search_fields = ('name',) - filter_horizontal = ('layers',) + form = RegionAdminForm + list_display = ('region_name', 'ontology_uri') + list_display_links = ('region_name', 'ontology_uri') + search_fields = ('ae_meta__name', 'ae_meta__ontology_uri') + autocomplete_fields = ('ae_meta',) + + @admin.display(description="Region Name") + def region_name(self, obj): + return obj.ae_meta.name + + @admin.display(description="Ontology URI") + def ontology_uri(self, obj): + return obj.ae_meta.ontology_uri + class AnatomicalEntityIntersectionAdmin(nested_admin.NestedModelAdmin, admin.ModelAdmin): list_display = ('layer', 'region') - search_fields = ("region__name", "layer__name") - list_filter = ('layer', 'region',) - raw_id_fields = ('layer', 'region',) + raw_id_fields = ('layer', 'region') + list_filter = ('layer', 'region') + search_fields = ( + 'layer__ae_meta__name', 'region__ae_meta__name', + 'layer__ae_meta__ontology_uri', 'region__ae_meta__ontology_uri' + ) def get_model_perms(self, request): return {} diff --git a/backend/composer/api/serializers.py b/backend/composer/api/serializers.py index d475476a..1fab89b6 100644 --- a/backend/composer/api/serializers.py +++ b/backend/composer/api/serializers.py @@ -81,29 +81,6 @@ class Meta: dept = 2 -class LayerSerializer(serializers.ModelSerializer): - class Meta: - model = Layer - fields = ( - "id", - "name", - "ontology_uri", - ) - - -class RegionSerializer(serializers.ModelSerializer): - layers = LayerSerializer(many=True, read_only=True) - - class Meta: - model = Region - fields = ( - "id", - "name", - "ontology_uri", - "layers", - ) - - class AnatomicalEntityMetaSerializer(serializers.ModelSerializer): class Meta: model = AnatomicalEntityMeta @@ -115,8 +92,8 @@ class Meta: class AnatomicalEntityIntersectionSerializer(serializers.ModelSerializer): - layer = LayerSerializer(read_only=True) - region = RegionSerializer(read_only=True) + layer = AnatomicalEntityMetaSerializer(read_only=True) + region = AnatomicalEntityMetaSerializer(read_only=True) class Meta: model = AnatomicalEntityIntersection diff --git a/backend/composer/migrations/0053_alter_anatomicalentity_options_and_more.py b/backend/composer/migrations/0053_alter_anatomicalentity_options_and_more.py new file mode 100644 index 00000000..2683c607 --- /dev/null +++ b/backend/composer/migrations/0053_alter_anatomicalentity_options_and_more.py @@ -0,0 +1,116 @@ +# Generated by Django 4.1.4 on 2024-07-08 13:18 + +from django.db import migrations, models +import django.db.models.deletion + + +def populate_intersection_anatomical_entity_meta_id_from_layer_region(apps, schema_editor): + """ + populating the new fields from AnatomicalEntityIntersection + layer_id -> layer_meta_id + region_id -> region_meta_id + """ + AnatomicalEntityIntersection = apps.get_model("composer", "AnatomicalEntityIntersection") + for intersection in AnatomicalEntityIntersection.objects.all(): + intersection.layer_meta_id = intersection.layer_id + intersection.region_meta_id = intersection.region_id + intersection.save() + + +def populate_layer_region_ae_meta_ids_from_anatomicalentitymeta_ptr_id(apps, schema_editor): + """ + populating the new fields from Layer and Region + anatomicalentitymeta_ptr_id -> layer_ae_meta_id + anatomicalentitymeta_ptr_id -> region_ae_meta_id + """ + Layer = apps.get_model("composer", "Layer") + Region = apps.get_model("composer", "Region") + + for layer in Layer.objects.all(): + layer.layer_ae_meta_id = layer.anatomicalentitymeta_ptr_id + layer.save() + + for region in Region.objects.all(): + region.region_ae_meta_id = region.anatomicalentitymeta_ptr_id + region.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("composer", "0052_alter_anatomicalentity_options_and_more"), + ] + + # Steps taken in migration: + # 1. create new fields for layer and region - layer_ae_meta, region_ae_meta + # 2. create new fields for AnatomicalEntityIntersection - layer_meta, region_meta + # 3. populate the new fields in AnatomicalEntityIntersection from the old fields - layer, region + # 4. populate the new fields in Layer and Region from the old fields - anatomicalentitymeta_ptr_id + operations = [ + migrations.AlterModelOptions( + name="anatomicalentity", + options={ + "verbose_name": "Anatomical Entity", + "verbose_name_plural": "Anatomical Entities", + }, + ), + + migrations.AddField( + model_name="layer", + name="layer_ae_meta", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="layer_meta", + to="composer.anatomicalentitymeta", + ), + ), + migrations.AddField( + model_name="region", + name="region_ae_meta", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="region_meta", + to="composer.anatomicalentitymeta", + ), + ), + + # ------------------------ + migrations.AddField( + model_name="anatomicalentityintersection", + name="layer_meta", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="layer_intersection", + to="composer.anatomicalentitymeta", + ), + ), + migrations.AddField( + model_name="anatomicalentityintersection", + name="region_meta", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="region_intersection", + to="composer.anatomicalentitymeta", + ), + ), + + # ------------------------ + + migrations.RunPython( + code=populate_intersection_anatomical_entity_meta_id_from_layer_region, + reverse_code=migrations.RunPython.noop, + ), + + # ------------------------ + + migrations.RunPython( + code=populate_layer_region_ae_meta_ids_from_anatomicalentitymeta_ptr_id, + reverse_code=migrations.RunPython.noop, + ), + + ] + + diff --git a/backend/composer/migrations/0054_auto_20240709_0909.py b/backend/composer/migrations/0054_auto_20240709_0909.py new file mode 100644 index 00000000..d115b998 --- /dev/null +++ b/backend/composer/migrations/0054_auto_20240709_0909.py @@ -0,0 +1,104 @@ +# Generated by Django 4.1.4 on 2024-07-09 07:09 + +from django.db import migrations, models +import django.db.models.deletion + + + + + +class Migration(migrations.Migration): + + dependencies = [ + ("composer", "0053_alter_anatomicalentity_options_and_more"), + ] + + # Steps taken in migration: + # 1. create new models for layer and region - NewLayer, NewRegion + # 2. copy the necessary columns from the old models to the new models - layer_ae_meta, region_ae_meta + # 3. remove the old models - Layer, Region and it's fk from AnatomicalEntityIntersection + # 4. rename the new models to the old models - Layer, Region + + operations = [ + migrations.CreateModel( + name="NewLayer", + fields=[ + ( + "layer_id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + ), + ), + ( + "layer_ae_meta", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="layer_meta", + to="composer.anatomicalentitymeta", + ), + ) + ] + ), + migrations.RunSQL( + sql="INSERT INTO composer_newlayer (layer_ae_meta_id) SELECT anatomicalentitymeta_ptr_id FROM composer_layer", + reverse_sql="", + ), + migrations.RemoveField( + model_name="anatomicalentityintersection", + name="layer", + ), + migrations.DeleteModel( + name="Layer", + ), + migrations.RenameModel( + old_name="NewLayer", + new_name="Layer", + ), + + + # ---------------------------- + migrations.CreateModel( + name="NewRegion", + fields=[ + ( + "region_id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + ), + ), + ( + "region_ae_meta", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="region_meta", + to="composer.anatomicalentitymeta", + ), + ) + ] + ), + migrations.RunSQL( + sql="INSERT INTO composer_newregion (region_ae_meta_id) SELECT anatomicalentitymeta_ptr_id FROM composer_region", + reverse_sql="", + ), + migrations.RemoveField( + model_name="anatomicalentityintersection", + name="region", + ), + migrations.DeleteModel( + name="Region", + ), + migrations.RenameModel( + old_name="NewRegion", + new_name="Region", + ), + + + + + ] diff --git a/backend/composer/migrations/0055_auto_20240708_1540.py b/backend/composer/migrations/0055_auto_20240708_1540.py new file mode 100644 index 00000000..be004df8 --- /dev/null +++ b/backend/composer/migrations/0055_auto_20240708_1540.py @@ -0,0 +1,31 @@ +# Generated by Django 4.1.4 on 2024-07-08 13:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("composer", "0054_auto_20240709_0909"), + ] + + operations = [ + migrations.AlterModelOptions( + name="layer", + options={"verbose_name": "Layer", "verbose_name_plural": "Layers"}, + ), + migrations.AlterModelOptions( + name="region", + options={"verbose_name": "Region", "verbose_name_plural": "Regions"}, + ), + migrations.RenameField( + model_name="anatomicalentityintersection", + old_name="layer_meta", + new_name="layer", + ), + migrations.RenameField( + model_name="anatomicalentityintersection", + old_name="region_meta", + new_name="region", + ), + ] diff --git a/backend/composer/migrations/0056_rename_layer_ae_meta_layer_ae_meta_and_more.py b/backend/composer/migrations/0056_rename_layer_ae_meta_layer_ae_meta_and_more.py new file mode 100644 index 00000000..702b27f3 --- /dev/null +++ b/backend/composer/migrations/0056_rename_layer_ae_meta_layer_ae_meta_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.4 on 2024-07-09 19:01 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("composer", "0055_auto_20240708_1540"), + ] + + operations = [ + migrations.RenameField( + model_name="layer", + old_name="layer_ae_meta", + new_name="ae_meta", + ), + migrations.RenameField( + model_name="region", + old_name="region_ae_meta", + new_name="ae_meta", + ), + ] diff --git a/backend/composer/migrations/0057_alter_layer_ae_meta_alter_region_ae_meta.py b/backend/composer/migrations/0057_alter_layer_ae_meta_alter_region_ae_meta.py new file mode 100644 index 00000000..453a4264 --- /dev/null +++ b/backend/composer/migrations/0057_alter_layer_ae_meta_alter_region_ae_meta.py @@ -0,0 +1,31 @@ +# Generated by Django 4.1.4 on 2024-07-10 14:46 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("composer", "0056_rename_layer_ae_meta_layer_ae_meta_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="layer", + name="ae_meta", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="layer_meta", + to="composer.anatomicalentitymeta", + ), + ), + migrations.AlterField( + model_name="region", + name="ae_meta", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="region_meta", + to="composer.anatomicalentitymeta", + ), + ), + ] diff --git a/backend/composer/migrations/0058_remove_connectivitystatement_projection_valid_and_more.py b/backend/composer/migrations/0058_remove_connectivitystatement_projection_valid_and_more.py new file mode 100644 index 00000000..1ee3ed7a --- /dev/null +++ b/backend/composer/migrations/0058_remove_connectivitystatement_projection_valid_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 4.1.4 on 2024-07-10 18:12 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("composer", "0057_alter_layer_ae_meta_alter_region_ae_meta"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="connectivitystatement", + name="projection_valid", + ), + migrations.AlterField( + model_name="anatomicalentityintersection", + name="layer", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="layer_intersection", + to="composer.anatomicalentitymeta", + ), + ), + migrations.AlterField( + model_name="anatomicalentityintersection", + name="region", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="region_intersection", + to="composer.anatomicalentitymeta", + ), + ), + ] diff --git a/backend/composer/migrations/0059_auto_20240717_1550.py b/backend/composer/migrations/0059_auto_20240717_1550.py new file mode 100644 index 00000000..3a68cbc2 --- /dev/null +++ b/backend/composer/migrations/0059_auto_20240717_1550.py @@ -0,0 +1,67 @@ +# Generated by Django 4.1.4 on 2024-07-17 13:50 + +from django.db import migrations +from django.db.models import Count + + +def remove_duplicate_layers(apps, schema_editor): + Layer = apps.get_model('composer', 'Layer') + duplicates = ( + Layer.objects + .values('ae_meta_id') + .annotate(count_id=Count('layer_id')) + .filter(count_id__gt=1) + ) + + for duplicate in duplicates: + layers = Layer.objects.filter(ae_meta_id=duplicate['ae_meta_id']).order_by('layer_id') + layers_to_delete = layers[1:] # Keep the first occurrence, delete the rest + for layer in layers_to_delete: + layer.delete() + + +def remove_duplicate_regions(apps, schema_editor): + Region = apps.get_model('composer', 'Region') + duplicates = ( + Region.objects + .values('ae_meta_id') + .annotate(count_id=Count('region_id')) + .filter(count_id__gt=1) + ) + + for duplicate in duplicates: + regions = Region.objects.filter(ae_meta_id=duplicate['ae_meta_id']).order_by('region_id') + regions_to_delete = regions[1:] # Keep the first occurrence, delete the rest + for region in regions_to_delete: + region.delete() + + +def remove_duplicate_intersections(apps, schema_editor): + AnatomicalEntityIntersection = apps.get_model('composer', 'AnatomicalEntityIntersection') + duplicates = ( + AnatomicalEntityIntersection.objects + .values('layer_id', 'region_id') + .annotate(count_id=Count('id')) + .filter(count_id__gt=1) + ) + + for duplicate in duplicates: + intersections = AnatomicalEntityIntersection.objects.filter( + layer_id=duplicate['layer_id'], + region_id=duplicate['region_id'] + ).order_by('id') + intersections_to_delete = intersections[1:] # Keep the first occurrence, delete the rest + for intersection in intersections_to_delete: + intersection.delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("composer", "0058_remove_connectivitystatement_projection_valid_and_more"), + ] + + operations = [ + migrations.RunPython(remove_duplicate_layers), + migrations.RunPython(remove_duplicate_regions), + migrations.RunPython(remove_duplicate_intersections), + ] diff --git a/backend/composer/migrations/0060_anatomicalentityintersection_unique_layer_region_combination_and_more.py b/backend/composer/migrations/0060_anatomicalentityintersection_unique_layer_region_combination_and_more.py new file mode 100644 index 00000000..ca0b9145 --- /dev/null +++ b/backend/composer/migrations/0060_anatomicalentityintersection_unique_layer_region_combination_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.1.4 on 2024-07-17 14:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("composer", "0059_auto_20240717_1550"), + ] + + operations = [ + migrations.AddConstraint( + model_name="anatomicalentityintersection", + constraint=models.UniqueConstraint( + fields=("layer", "region"), name="unique_layer_region_combination" + ), + ), + migrations.AddConstraint( + model_name="layer", + constraint=models.UniqueConstraint( + fields=("ae_meta",), name="unique_layer_ae_meta" + ), + ), + migrations.AddConstraint( + model_name="region", + constraint=models.UniqueConstraint( + fields=("ae_meta",), name="unique_region_ae_meta" + ), + ), + ] diff --git a/backend/composer/models.py b/backend/composer/models.py index 58796a28..3c30424c 100644 --- a/backend/composer/models.py +++ b/backend/composer/models.py @@ -4,6 +4,7 @@ from django.db.models.expressions import F from django.forms.widgets import Input as InputWidget from django_fsm import FSMField, transition +from django.core.exceptions import ValidationError from composer.services.state_services import ( ConnectivityStatementStateService, @@ -97,7 +98,7 @@ def get_queryset(self): def excluding_draft(self): return self.get_queryset().exclude(state=CSState.DRAFT) - + def exported(self): return self.get_queryset().filter(state=CSState.EXPORTED) @@ -240,22 +241,55 @@ class Meta: verbose_name_plural = "Anatomical Entities" -class Layer(AnatomicalEntityMeta): - ... +class Layer(models.Model): + layer_id = models.BigAutoField(primary_key=True, auto_created=True) + ae_meta = models.ForeignKey(AnatomicalEntityMeta, on_delete=models.CASCADE, related_name='layer_meta', null=False) + def __str__(self): + return self.ae_meta.name -class Region(AnatomicalEntityMeta): - ... - layers = models.ManyToManyField(Layer, through='AnatomicalEntityIntersection') + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + + class Meta: + verbose_name = "Layer" + verbose_name_plural = "Layers" + constraints = [ + models.UniqueConstraint(fields=['ae_meta'], name='unique_layer_ae_meta') + ] + + +class Region(models.Model): + region_id = models.BigAutoField(primary_key=True, auto_created=True) + ae_meta = models.ForeignKey(AnatomicalEntityMeta, on_delete=models.CASCADE, related_name='region_meta', null=False) + + def __str__(self): + return self.ae_meta.name + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + + class Meta: + verbose_name = "Region" + verbose_name_plural = "Regions" + constraints = [ + models.UniqueConstraint(fields=['ae_meta'], name='unique_region_ae_meta') + ] class AnatomicalEntityIntersection(models.Model): - layer = models.ForeignKey(Layer, on_delete=models.CASCADE) - region = models.ForeignKey(Region, on_delete=models.CASCADE) + layer = models.ForeignKey(AnatomicalEntityMeta, on_delete=models.CASCADE, related_name='layer_intersection') + region = models.ForeignKey(AnatomicalEntityMeta, on_delete=models.CASCADE, related_name='region_intersection') + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) class Meta: verbose_name = "Region/Layer Combination" verbose_name_plural = "Region/Layer Combinations" + constraints = [ + models.UniqueConstraint(fields=['layer', 'region'], name='unique_layer_region_combination') + ] def __str__(self): return f"{self.region.name} ({self.layer.name})" @@ -272,7 +306,7 @@ def name(self): if self.region_layer: return str(self.region_layer) return 'Unknown Anatomical Entity' - + @property def ontology_uri(self): if self.simple_entity: diff --git a/backend/composer/services/cs_ingestion/helpers/anatomical_entities_helper.py b/backend/composer/services/cs_ingestion/helpers/anatomical_entities_helper.py index 81f5f3fa..4cfe9849 100644 --- a/backend/composer/services/cs_ingestion/helpers/anatomical_entities_helper.py +++ b/backend/composer/services/cs_ingestion/helpers/anatomical_entities_helper.py @@ -69,34 +69,34 @@ def add_entity_to_instance(instance, entity_field, entity, update_anatomic_entit def get_or_create_complex_entity(region_uri, layer_uri, update_anatomical_entities=False): try: - layer = Layer.objects.get(ontology_uri=layer_uri) + layer = Layer.objects.get(ae_meta__ontology_uri=layer_uri) except Layer.DoesNotExist: layer = None try: - region = Region.objects.get(ontology_uri=region_uri) + region = Region.objects.get(ae_meta__ontology_uri=region_uri) except Region.DoesNotExist: region = None if update_anatomical_entities: if not layer: try: - layer_meta = AnatomicalEntityMeta.objects.get(ontology_uri=layer_uri) - layer, _ = convert_anatomical_entity_to_specific_type(layer_meta, Layer) + layer_meta = AnatomicalEntityMeta.objects.get(ontology_uri=layer_uri) + layer, _ = create_region_layer_from_anatomical_entity(layer_meta, Layer) except AnatomicalEntityMeta.DoesNotExist: raise EntityNotFoundException(f"Layer meta not found for URI: {layer_uri}") if not region: try: region_meta = AnatomicalEntityMeta.objects.get(ontology_uri=region_uri) - region, _ = convert_anatomical_entity_to_specific_type(region_meta, Region) + region, _ = create_region_layer_from_anatomical_entity(region_meta, Region) except AnatomicalEntityMeta.DoesNotExist: raise EntityNotFoundException(f"Region meta not found for URI: {layer_uri}") else: if not layer or not region: raise EntityNotFoundException("Required Layer or Region not found.") - intersection, _ = AnatomicalEntityIntersection.objects.get_or_create(layer=layer, region=region) + intersection, _ = AnatomicalEntityIntersection.objects.get_or_create(layer=layer.ae_meta, region=region.ae_meta) anatomical_entity, created = AnatomicalEntity.objects.get_or_create(region_layer=intersection) return anatomical_entity, created @@ -111,22 +111,20 @@ def get_or_create_simple_entity(ontology_uri: str): raise EntityNotFoundException(f"Anatomical entity meta not found for URI: {ontology_uri}") -def convert_anatomical_entity_to_specific_type(entity_meta, target_model): +def create_region_layer_from_anatomical_entity(entity_meta, target_model): """ - Convert an AnatomicalEntityMeta instance to a more specific subclass type (Layer or Region). - Attempts to delete the original instance and create the new specific instance atomically. + Create a new specific entity (Layer or Region) from an AnatomicalEntityMeta instance. """ defaults = {'name': entity_meta.name, 'ontology_uri': entity_meta.ontology_uri} try: with transaction.atomic(): - # Delete the anatomical entity meta in the incorrect type - entity_meta.delete() - # Create a new anatomical entity in the new specific type - specific_entity, created = target_model.objects.get_or_create( - ontology_uri=entity_meta.ontology_uri, - defaults=defaults - ) + # Depending on the target_model, we need to create the specific entity - Layer/Region + specific_entity, created = None, False + if target_model == Layer: + specific_entity, created = Layer.objects.get_or_create(ae_meta=entity_meta) + elif target_model == Region: + specific_entity, created = Region.objects.get_or_create(ae_meta=entity_meta) return specific_entity, created except IntegrityError as e: raise IntegrityError( diff --git a/backend/composer/services/cs_ingestion/helpers/validators.py b/backend/composer/services/cs_ingestion/helpers/validators.py index 12312e96..2ab8c3da 100644 --- a/backend/composer/services/cs_ingestion/helpers/validators.py +++ b/backend/composer/services/cs_ingestion/helpers/validators.py @@ -92,5 +92,5 @@ def annotate_invalid_forward_connections(statement: Dict, statement_ids: Set[str def found_entity(uri: str, Model: Type[DjangoModel] = None) -> bool: if Model: - return Model.objects.filter(ontology_uri=uri).exists() + return Model.objects.filter(ae_meta__ontology_uri=uri).exists() return AnatomicalEntityMeta.objects.filter(ontology_uri=uri).exists() diff --git a/backend/composer/services/export_services.py b/backend/composer/services/export_services.py index 474d7d58..ffc7a96c 100644 --- a/backend/composer/services/export_services.py +++ b/backend/composer/services/export_services.py @@ -100,7 +100,8 @@ def get_neuron_population_label(cs: ConnectivityStatement, row: Row): def get_type(cs: ConnectivityStatement, row: Row): - return cs.phenotype.name + return cs.phenotype.name if cs.phenotype else "" + def get_structure(cs: ConnectivityStatement, row: Row): @@ -336,9 +337,11 @@ def get_soma_phenotype_row(cs: ConnectivityStatement): def get_phenotype_row(cs: ConnectivityStatement): + phenotype_name = cs.phenotype.name if cs.phenotype else "" + phenotype_ontology_uri = cs.phenotype.ontology_uri if cs.phenotype else "" return Row( - cs.phenotype.name, - cs.phenotype.ontology_uri, + phenotype_name, + phenotype_ontology_uri, ExportRelationships.hasAnatomicalSystemPhenotype.label, ExportRelationships.hasAnatomicalSystemPhenotype.value, "", @@ -347,9 +350,12 @@ def get_phenotype_row(cs: ConnectivityStatement): def get_projection_phenotype_row(cs: ConnectivityStatement): + projection_phenotype = cs.projection_phenotype if cs.projection_phenotype else "" + projection_phenotype_ontology_uri = cs.projection_phenotype.ontology_uri if cs.projection_phenotype else "" + return Row( - cs.projection_phenotype, - cs.projection_phenotype.ontology_uri, + projection_phenotype, + projection_phenotype_ontology_uri, ExportRelationships.hasProjectionPhenotype.label, ExportRelationships.hasProjectionPhenotype.value, "", diff --git a/backend/composer/signals.py b/backend/composer/signals.py index a8690f8f..f0a40a8e 100644 --- a/backend/composer/signals.py +++ b/backend/composer/signals.py @@ -1,12 +1,12 @@ from django.core.exceptions import ValidationError from django.dispatch import receiver -from django.db.models.signals import post_save, m2m_changed, pre_save +from django.db.models.signals import post_save, m2m_changed, pre_save, post_delete from django_fsm.signals import post_transition from .enums import CSState, NoteType from .models import ConnectivityStatement, ExportBatch, Note, Sentence, Synonym, \ - AnatomicalEntity, Layer, Region + AnatomicalEntity, Layer, Region, AnatomicalEntityMeta from .services.export_services import compute_metrics, ConnectivityStatementStateService @@ -48,12 +48,25 @@ 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) + AnatomicalEntity.objects.get_or_create(simple_entity=instance.ae_meta) + @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) + AnatomicalEntity.objects.get_or_create(simple_entity=instance.ae_meta) + + +@receiver(post_delete, sender=AnatomicalEntity) +def delete_associated_entities(sender, instance, **kwargs): + # Delete the associated simple_entity if it exists + if instance.simple_entity: + instance.simple_entity.delete() + + # Delete the associated region_layer if it exists + if instance.region_layer: + instance.region_layer.delete() \ No newline at end of file diff --git a/k8s/composer.yaml b/k8s/composer.yaml index ee2e10f6..d785b835 100644 --- a/k8s/composer.yaml +++ b/k8s/composer.yaml @@ -35,24 +35,24 @@ spec: value: "{{DEBUG}}" ports: - containerPort: 8000 - livenessProbe: - failureThreshold: 3 - httpGet: - path: /admin/login/ - port: 8000 - scheme: HTTP - initialDelaySeconds: 45 - periodSeconds: 30 - timeoutSeconds: 2 - readinessProbe: - failureThreshold: 3 - httpGet: - path: /admin/login/ - port: 8000 - scheme: HTTP - initialDelaySeconds: 15 - periodSeconds: 30 - timeoutSeconds: 2 + # livenessProbe: + # failureThreshold: 3 + # httpGet: + # path: /admin/login/ + # port: 8000 + # scheme: HTTP + # initialDelaySeconds: 45 + # periodSeconds: 30 + # timeoutSeconds: 2 + # readinessProbe: + # failureThreshold: 3 + # httpGet: + # path: /admin/login/ + # port: 8000 + # scheme: HTTP + # initialDelaySeconds: 15 + # periodSeconds: 30 + # timeoutSeconds: 2 resources: limits: cpu: 1500m