From 416f255e52e14efe86f77001571b84ce249f341f Mon Sep 17 00:00:00 2001 From: afonso Date: Sat, 16 Mar 2024 00:04:57 +0000 Subject: [PATCH] SCKAN-275 feat: Update neurondm ingestion --- backend/composer/admin.py | 16 +- ...r_anatomicalentity_ontology_uri_region.py} | 41 ++--- backend/composer/models.py | 10 +- .../cs_ingestion/cs_ingestion_services.py | 137 ++++++++------- .../services/cs_ingestion/exceptions.py | 4 + .../composer/services/cs_ingestion/helpers.py | 5 +- .../services/cs_ingestion/neurondm_script.py | 156 ++++++++++++------ backend/composer/signals.py | 6 +- 8 files changed, 218 insertions(+), 157 deletions(-) rename backend/composer/migrations/{0045_layer_region_regionlayerpair_region_layers.py => 0045_layer_alter_anatomicalentity_ontology_uri_region.py} (62%) diff --git a/backend/composer/admin.py b/backend/composer/admin.py index c3b76394..0f3adc89 100644 --- a/backend/composer/admin.py +++ b/backend/composer/admin.py @@ -19,7 +19,7 @@ Tag, Via, FunctionalCircuitRole, - ProjectionPhenotype, Destination, Synonym + ProjectionPhenotype, Destination, Synonym, Layer, Region ) @@ -101,6 +101,18 @@ class AnatomicalEntityAdmin(admin.ModelAdmin): inlines = [SynonymInline] +class LayerAdmin(AnatomicalEntityAdmin): + pass + + +class RegionAdmin(AnatomicalEntityAdmin): + def associated_layers_display(self, obj): + return ", ".join([layer.name for layer in obj.layer.all()]) + + associated_layers_display.short_description = 'Associated Layers' + list_display = AnatomicalEntityAdmin.list_display + ('associated_layers_display',) + + class ViaInline(SortableStackedInline): model = Via extra = 0 @@ -206,6 +218,8 @@ def get_form(self, request, obj=None, change=False, **kwargs): # admin.site.register(AnatomicalEntity, AnatomicalEntityAdmin) +admin.site.register(Layer, LayerAdmin) +admin.site.register(Region, RegionAdmin) admin.site.register(Phenotype) admin.site.register(Sex) admin.site.register(ConnectivityStatement, ConnectivityStatementAdmin) diff --git a/backend/composer/migrations/0045_layer_region_regionlayerpair_region_layers.py b/backend/composer/migrations/0045_layer_alter_anatomicalentity_ontology_uri_region.py similarity index 62% rename from backend/composer/migrations/0045_layer_region_regionlayerpair_region_layers.py rename to backend/composer/migrations/0045_layer_alter_anatomicalentity_ontology_uri_region.py index 810bef72..d8cfc3f3 100644 --- a/backend/composer/migrations/0045_layer_region_regionlayerpair_region_layers.py +++ b/backend/composer/migrations/0045_layer_alter_anatomicalentity_ontology_uri_region.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.4 on 2024-03-14 23:17 +# Generated by Django 4.1.4 on 2024-03-16 00:13 from django.db import migrations, models import django.db.models.deletion @@ -27,6 +27,11 @@ class Migration(migrations.Migration): ], bases=("composer.anatomicalentity",), ), + migrations.AlterField( + model_name="anatomicalentity", + name="ontology_uri", + field=models.URLField(), + ), migrations.CreateModel( name="Region", fields=[ @@ -41,41 +46,15 @@ class Migration(migrations.Migration): to="composer.anatomicalentity", ), ), - ], - bases=("composer.anatomicalentity",), - ), - migrations.CreateModel( - name="RegionLayerPair", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), ( - "layer", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="composer.layer" - ), - ), - ( - "region", + "associated_layer", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, - to="composer.region", + related_name="regions", + to="composer.layer", ), ), ], - ), - migrations.AddField( - model_name="region", - name="layers", - field=models.ManyToManyField( - through="composer.RegionLayerPair", to="composer.layer" - ), + bases=("composer.anatomicalentity",), ), ] diff --git a/backend/composer/models.py b/backend/composer/models.py index 61066aff..859bb418 100644 --- a/backend/composer/models.py +++ b/backend/composer/models.py @@ -2,7 +2,6 @@ from django.db import models, transaction from django.db.models import Q from django.db.models.expressions import F -from django.db.models.functions import Upper from django.forms.widgets import Input as InputWidget from django_fsm import FSMField, transition @@ -229,7 +228,7 @@ class AnatomicalEntity(models.Model): """Anatomical Entity""" name = models.CharField(max_length=200, db_index=True) - ontology_uri = models.URLField(unique=True) + ontology_uri = models.URLField() def __str__(self): return self.name @@ -245,12 +244,7 @@ class Layer(AnatomicalEntity): class Region(AnatomicalEntity): ... - layers = models.ManyToManyField(Layer, through='RegionLayerPair') - - -class RegionLayerPair(models.Model): - region = models.ForeignKey(Region, on_delete=models.CASCADE) - layer = models.ForeignKey(Layer, on_delete=models.CASCADE) + associated_layer = models.ForeignKey(Layer, on_delete=models.CASCADE, related_name='regions') class Synonym(models.Model): diff --git a/backend/composer/services/cs_ingestion/cs_ingestion_services.py b/backend/composer/services/cs_ingestion/cs_ingestion_services.py index a540cfeb..41d798a8 100644 --- a/backend/composer/services/cs_ingestion/cs_ingestion_services.py +++ b/backend/composer/services/cs_ingestion/cs_ingestion_services.py @@ -3,9 +3,11 @@ from typing import List, Dict, Optional, Tuple, Set, Any from django.db import transaction +from neurondm import orders from composer.models import AnatomicalEntity, Sentence, ConnectivityStatement, Sex, FunctionalCircuitRole, \ - ProjectionPhenotype, Phenotype, Specie, Provenance, Via, Note, User, Destination + ProjectionPhenotype, Phenotype, Specie, Provenance, Via, Note, User, Destination, Region, Layer +from .exceptions import EntityNotFoundException from .helpers import get_value_or_none, found_entity, \ ORIGINS, DESTINATIONS, VIAS, LABEL, SEX, SPECIES, ID, FORWARD_CONNECTION, SENTENCE_NUMBER, \ FUNCTIONAL_CIRCUIT_ROLE, CIRCUIT_TYPE, CIRCUIT_TYPE_MAPPING, PHENOTYPE, OTHER_PHENOTYPE, NOTE_ALERT, PROVENANCE, \ @@ -14,7 +16,6 @@ from .models import LoggableAnomaly, ValidationErrors, Severity from .neurondm_script import main as get_statements_from_neurondm from ...enums import ( - CircuitType, NoteType, CSState, SentenceState ) @@ -114,16 +115,25 @@ def validate_statements(statements: List[Dict[str, Any]]) -> List[Dict[str, Any] def annotate_invalid_entities(statement: Dict) -> bool: has_invalid_entities = False - # Consolidate all URIs to check - uris_to_check = list(statement[ORIGINS].anatomical_entities) - uris_to_check.extend(uri for dest in statement[DESTINATIONS] for uri in dest.anatomical_entities) - uris_to_check.extend(uri for via in statement[VIAS] for uri in via.anatomical_entities) - - # Check all URIs and log if not found - for uri in uris_to_check: - if not found_entity(uri): - statement[VALIDATION_ERRORS].entities.add(uri) - has_invalid_entities = True + entities_to_check = list(statement[ORIGINS].anatomical_entities) + entities_to_check.extend(entity for dest in statement[DESTINATIONS] for entity in dest.anatomical_entities) + entities_to_check.extend(entity for via in statement[VIAS] for entity in via.anatomical_entities) + + for entity in entities_to_check: + if isinstance(entity, orders.rl): + region_found = found_entity(entity.region) + layer_found = found_entity(entity.layer) + if not region_found: + statement[VALIDATION_ERRORS].entities.add(entity.region) + has_invalid_entities = True + if not layer_found: + statement[VALIDATION_ERRORS].entities.add(entity.layer) + has_invalid_entities = True + else: + uri = str(entity) + if not found_entity(uri): + statement[VALIDATION_ERRORS].entities.add(uri) + has_invalid_entities = True return has_invalid_entities @@ -318,7 +328,6 @@ def has_changes(connectivity_statement, statement, defaults): return True # Not checking the Notes because they are kept - return False @@ -415,63 +424,67 @@ def update_many_to_many_fields(connectivity_statement: ConnectivityStatement, st def add_origins(connectivity_statement: ConnectivityStatement, statement: Dict): - origin_uris = statement[ORIGINS].anatomical_entities - origins = [] - for uri in origin_uris: - anatomical_entity = AnatomicalEntity.objects.filter(ontology_uri=uri).first() - if anatomical_entity: - origins.append(anatomical_entity) - else: + for entity in statement[ORIGINS].anatomical_entities: + try: + add_entity_to_instance(connectivity_statement, 'origins', entity) + except (EntityNotFoundException, AnatomicalEntity.DoesNotExist): assert connectivity_statement.state == CSState.INVALID - if origins: - connectivity_statement.origins.add(*origins) - def add_vias(connectivity_statement: ConnectivityStatement, statement: Dict): - vias_data = [ - Via(connectivity_statement=connectivity_statement, type=via.type, order=via.order) - for via in statement[VIAS] - ] - created_vias = Via.objects.bulk_create(vias_data) - - for via_instance, via_data in zip(created_vias, statement[VIAS]): - for uri in via_data.anatomical_entities: - anatomical_entity = AnatomicalEntity.objects.filter(ontology_uri=uri).first() - if anatomical_entity: - via_instance.anatomical_entities.add(anatomical_entity) - else: - assert connectivity_statement.state == CSState.INVALID - - for uri in via_data.from_entities: - from_entity = AnatomicalEntity.objects.filter(ontology_uri=uri).first() - if from_entity: - via_instance.from_entities.add(from_entity) - else: - assert connectivity_statement.state == CSState.INVALID + for neurondm_via in statement[VIAS]: + via_instance = Via.objects.create(connectivity_statement=connectivity_statement, + type=neurondm_via.type, + order=neurondm_via.order) + add_entities_to_connection(via_instance, + neurondm_via.anatomical_entities, + neurondm_via.from_entities, + connectivity_statement) def add_destinations(connectivity_statement: ConnectivityStatement, statement: Dict): - destinations_data = [ - Destination(connectivity_statement=connectivity_statement, type=dest.type) - for dest in statement[DESTINATIONS] - ] - created_destinations = Destination.objects.bulk_create(destinations_data) - - for destination_instance, dest_data in zip(created_destinations, statement[DESTINATIONS]): - for uri in dest_data.anatomical_entities: - anatomical_entity = AnatomicalEntity.objects.filter(ontology_uri=uri).first() - if anatomical_entity: - destination_instance.anatomical_entities.add(anatomical_entity) - else: - assert connectivity_statement.state == CSState.INVALID + for neurondm_destination in statement[DESTINATIONS]: + destination_instance = Destination.objects.create(connectivity_statement=connectivity_statement, + type=neurondm_destination.type) + add_entities_to_connection(destination_instance, + neurondm_destination.anatomical_entities, + neurondm_destination.from_entities, + connectivity_statement) - for uri in dest_data.from_entities: - from_entity = AnatomicalEntity.objects.filter(ontology_uri=uri).first() - if from_entity: - destination_instance.from_entities.add(from_entity) - else: - assert connectivity_statement.state == CSState.INVALID + +def add_entities_to_connection(instance, anatomical_entities, from_entities, connectivity_statement): + try: + for entity in anatomical_entities: + add_entity_to_instance(instance, 'anatomical_entities', entity) + + for entity in from_entities: + add_entity_to_instance(instance, 'from_entities', entity) + + except (EntityNotFoundException, AnatomicalEntity.DoesNotExist): + assert connectivity_statement.state == CSState.INVALID + + +def add_entity_to_instance(instance, entity_field, entity): + if isinstance(entity, orders.rl): + region, _ = get_or_create_complex_entity(entity.region, entity.layer) + getattr(instance, entity_field).add(region) + else: + anatomical_entity = AnatomicalEntity.objects.filter(ontology_uri=str(entity)).first() + getattr(instance, entity_field).add(anatomical_entity) + + +def get_or_create_complex_entity(region_uri, layer_uri): + region_entity = AnatomicalEntity.objects.filter(ontology_uri=region_uri).first() + layer_entity = AnatomicalEntity.objects.filter(ontology_uri=layer_uri).first() + + if not region_entity or not layer_entity: + raise EntityNotFoundException(f"Region or layer not found for URIs: {region_uri}, {layer_uri}") + + + layer, _ = Layer.objects.get_or_create(ontology_uri=layer_uri, defaults={'name': layer_entity.name}) + region, _ = Region.objects.get_or_create(ontology_uri=region_uri, + defaults={'name': region_entity.name, 'associated_layer': layer}) + return region, layer def add_notes(connectivity_statement: ConnectivityStatement, statement: Dict): diff --git a/backend/composer/services/cs_ingestion/exceptions.py b/backend/composer/services/cs_ingestion/exceptions.py index 40d875b8..1ae89f89 100644 --- a/backend/composer/services/cs_ingestion/exceptions.py +++ b/backend/composer/services/cs_ingestion/exceptions.py @@ -4,3 +4,7 @@ def __init__(self, statement_id, entity_id, message): self.entity_id = entity_id self.message = message super().__init__(f"StatementID: {statement_id}, EntityID: {entity_id}, Error: {message}") + + +class EntityNotFoundException(Exception): + pass diff --git a/backend/composer/services/cs_ingestion/helpers.py b/backend/composer/services/cs_ingestion/helpers.py index 7016e6ad..ba57cc42 100644 --- a/backend/composer/services/cs_ingestion/helpers.py +++ b/backend/composer/services/cs_ingestion/helpers.py @@ -1,5 +1,8 @@ import logging +import rdflib +from neurondm import orders + from composer.enums import CircuitType from composer.models import AnatomicalEntity @@ -44,4 +47,4 @@ def get_value_or_none(model, prop: str): def found_entity(uri: str) -> bool: - return AnatomicalEntity.objects.filter(ontology_uri=uri).exists() + return AnatomicalEntity.objects.filter(ontology_uri=uri).exists() \ No newline at end of file diff --git a/backend/composer/services/cs_ingestion/neurondm_script.py b/backend/composer/services/cs_ingestion/neurondm_script.py index 2dbc5ed0..b00cc219 100644 --- a/backend/composer/services/cs_ingestion/neurondm_script.py +++ b/backend/composer/services/cs_ingestion/neurondm_script.py @@ -85,8 +85,7 @@ def get_connections(n, lpes): tmp_origins, tmp_vias, tmp_destinations, validation_errors = process_connections(partial_order, set(origins_from_axioms), vias_from_axioms, - destinations_from_axioms - ) + destinations_from_axioms) validation_errors = validate_partial_order_and_axioms(origins_from_axioms, vias_from_axioms, destinations_from_axioms, tmp_origins, @@ -109,9 +108,13 @@ def create_uri_type_dict(lpes_func, predicate_type_map): return uri_type_dict -def process_connections(path, origins_from_axioms: Set[str], vias_from_axioms: Dict[str, str], - destinations_from_axioms: Dict[str, str], from_entities: Optional[Set[str]] = None, - depth: int = 0, result: Optional[Dict] = None) -> Tuple[ +def process_connections(path, + origins_from_axioms: Set[str], + vias_from_axioms: Dict[str, str], + destinations_from_axioms: Dict[str, str], + from_entities: Optional[Set[str]] = None, + depth: int = 0, + result: Optional[Dict] = None) -> Tuple[ List[NeuronDMOrigin], List[NeuronDMVia], List[NeuronDMDestination], ValidationErrors]: if result is None: result = {ORIGINS: [], DESTINATIONS: [], VIAS: [], VALIDATION_ERRORS: ValidationErrors()} @@ -124,26 +127,27 @@ def process_connections(path, origins_from_axioms: Set[str], vias_from_axioms: D else: current_entity = path[0] - current_entity_uri, current_entity_axiom_types = get_current_entity_metadata(current_entity, - origins_from_axioms, - vias_from_axioms, - destinations_from_axioms) + current_entity_axiom_types = get_matched_axiom_types(current_entity, + origins_from_axioms, + vias_from_axioms, + destinations_from_axioms) - if not current_entity_uri or len(current_entity_axiom_types) == 0: - result[VALIDATION_ERRORS].axiom_not_found.add(entity_to_string(current_entity)) + if not current_entity or len(current_entity_axiom_types) == 0: + current_entity_representation = entity_to_string(current_entity) + result[VALIDATION_ERRORS].axiom_not_found.add(current_entity_representation) if logger_service: - logger_service.add_anomaly(LoggableAnomaly(None, current_entity, AXIOM_NOT_FOUND)) + logger_service.add_anomaly(LoggableAnomaly(None, current_entity_representation, AXIOM_NOT_FOUND)) else: from_entities = from_entities or set() axiom_type = get_axiom_type(current_entity_axiom_types, path, depth) - update_result(current_entity_uri, axiom_type, from_entities, depth, result, vias_from_axioms, + update_result(current_entity, axiom_type, from_entities, depth, result, vias_from_axioms, destinations_from_axioms) depth += 1 - next_from_entities = {current_entity_uri} if current_entity_uri else from_entities + next_from_entities = {current_entity} if current_entity else from_entities # Process the next level structures, carrying over from_entities as a set for remaining_path in path[1:]: process_connections(remaining_path, origins_from_axioms, vias_from_axioms, destinations_from_axioms, @@ -154,36 +158,36 @@ def process_connections(path, origins_from_axioms: Set[str], vias_from_axioms: D def entity_to_string(entity): if isinstance(entity, orders.rl): - return f"{entity.region} (region) nor {entity.layer} (layer)" + return f"{entity.region} (region), {entity.layer} (layer)" else: return str(entity) -def get_current_entity_metadata(current_entity, origins_from_axioms: Set[str], vias_from_axioms: Dict[str, str], - destinations_from_axioms: Dict[str, str]) -> Tuple[Optional[str], List[AxiomType]]: - primary_uri = current_entity.toPython() if not isinstance(current_entity, - orders.rl) else current_entity.region.toPython() - secondary_uri = current_entity.layer.toPython() if isinstance(current_entity, orders.rl) else None +def get_matched_axiom_types(current_entity: rdflib.term, + origins_from_axioms: Set[str], + vias_from_axioms: Dict[str, str], + destinations_from_axioms: Dict[str, str]) -> List[AxiomType]: + # Check if current_entity is a complex region-layer pair (orders.rl) and extract URIs accordingly + if isinstance(current_entity, orders.rl): + primary_uri = current_entity.region.toPython() + secondary_uri = current_entity.layer.toPython() + else: + primary_uri = current_entity.toPython() + secondary_uri = None + + matched_types = [] uris_in_axioms = [ (origins_from_axioms, AxiomType.ORIGIN), - (vias_from_axioms, AxiomType.VIA), - (destinations_from_axioms, AxiomType.DESTINATION), + (vias_from_axioms.keys(), AxiomType.VIA), + (destinations_from_axioms.keys(), AxiomType.DESTINATION), ] - uris_found = {} for uri_set, node_type in uris_in_axioms: - # Check if the URIs are in the current set of axioms if primary_uri in uri_set or secondary_uri in uri_set: - # Prefer layer if both region and layer URIs are found - matched_uri = secondary_uri if secondary_uri in uri_set else primary_uri - uris_found.setdefault(matched_uri, []).append(node_type) + matched_types.append(node_type) - if not uris_found: - return None, [] - - matched_uri, matched_types = next(iter(uris_found.items()), (None, [])) - return matched_uri, matched_types + return matched_types def get_axiom_type(current_entity_axiom_types: List[AxiomType], path, depth: int) -> Optional[AxiomType]: @@ -210,33 +214,48 @@ def get_axiom_type(current_entity_axiom_types: List[AxiomType], path, depth: int return None -def update_result(current_entity_uri: str, axiom_type: AxiomType, from_entities: Set[str], depth: int, result: Dict, +def update_result(current_entity: rdflib.term, axiom_type: AxiomType, from_entities: Set[str], depth: int, result: Dict, vias_from_axioms: Dict[str, str], destinations_from_axioms: Dict[str, str]) -> Dict: if axiom_type == AxiomType.ORIGIN: - result[ORIGINS].append(NeuronDMOrigin({current_entity_uri})) + result[ORIGINS].append(NeuronDMOrigin({current_entity})) elif axiom_type == AxiomType.VIA: result[VIAS].append( - NeuronDMVia({current_entity_uri}, from_entities, depth, vias_from_axioms.get(current_entity_uri))) + NeuronDMVia({current_entity}, from_entities, depth, get_entity_type(current_entity, vias_from_axioms))) elif axiom_type == AxiomType.DESTINATION: result[DESTINATIONS].append( - NeuronDMDestination({current_entity_uri}, from_entities, destinations_from_axioms.get(current_entity_uri))) + NeuronDMDestination({current_entity}, from_entities, + get_entity_type(current_entity, destinations_from_axioms))) return result -def validate_partial_order_and_axioms(origins_from_axioms, vias_from_axioms, destinations_from_axioms, tmp_origins, - tmp_vias, tmp_destinations, +def get_entity_type(current_entity, axiom_dict): + if isinstance(current_entity, orders.rl): + # Try to find the type based on the layer, then region + return axiom_dict.get(str(current_entity.layer)) or axiom_dict.get(str(current_entity.region)) + else: + return axiom_dict.get(str(current_entity)) + + +def validate_partial_order_and_axioms(origins_from_axioms, vias_from_axioms, destinations_from_axioms, + tmp_origins, tmp_vias, tmp_destinations, validation_errors: ValidationErrors) -> ValidationErrors: anatomical_uris_origins = extract_anatomical_uris(tmp_origins) anatomical_uris_vias = extract_anatomical_uris(tmp_vias) anatomical_uris_destinations = extract_anatomical_uris(tmp_destinations) - validate_partial_order_and_axioms_aux(set(origins_from_axioms), anatomical_uris_origins, "origins", + validate_partial_order_and_axioms_aux(set(origins_from_axioms), + anatomical_uris_origins, + "origins", validation_errors) - validate_partial_order_and_axioms_aux(set(vias_from_axioms.keys()), anatomical_uris_vias, "vias", validation_errors) + validate_partial_order_and_axioms_aux(set(vias_from_axioms.keys()), + anatomical_uris_vias, + "vias", + validation_errors) - validate_partial_order_and_axioms_aux(set(destinations_from_axioms.keys()), anatomical_uris_destinations, + validate_partial_order_and_axioms_aux(set(destinations_from_axioms.keys()), + anatomical_uris_destinations, "destinations", validation_errors) @@ -245,19 +264,54 @@ def validate_partial_order_and_axioms(origins_from_axioms, vias_from_axioms, des def validate_partial_order_and_axioms_aux(axiom_uris: Set[str], actual_uris: Set[str], category: str, validation_errors: ValidationErrors): - missing_uris = axiom_uris - actual_uris - extra_uris = actual_uris - axiom_uris - if missing_uris: - validation_errors.non_specified.append( - f"Missing anatomical URIs in {category}: {', '.join(missing_uris)}") - if extra_uris: - validation_errors.non_specified.append( - f"Unexpected anatomical URIs in {category}: {', '.join(extra_uris)}" - ) + unexpected_uris = get_unexpected_uris(actual_uris, axiom_uris) + missing_uris = get_missing_uris(actual_uris, axiom_uris) + + for uri in unexpected_uris: + uri_str = f"{uri[0]}, {uri[1]}" if isinstance(uri, tuple) else uri + validation_errors.non_specified.append(f"Unexpected {category} URI not in axioms: {uri_str}") + + for uri in missing_uris: + validation_errors.non_specified.append(f"Missing {category} URI not found in actual URIs: {uri}") + + return validation_errors + + +def get_missing_uris(actual_uris, axiom_uris): + flattened_actual_uris = set() + for uri in actual_uris: + if isinstance(uri, tuple): + flattened_actual_uris.update(uri) + else: + flattened_actual_uris.add(uri) + missing_uris = axiom_uris.difference(flattened_actual_uris) + return missing_uris + + +def get_unexpected_uris(actual_uris, axiom_uris): + unexpected_uris = set() + # Identify actual URIs that are unexpectedly present + for actual_uri in actual_uris: + if isinstance(actual_uri, tuple): # Complex entity case (region, layer pairs) + region, layer = actual_uri + # Count as unexpected if neither region nor layer are in axiom_uris + if not (region in axiom_uris or layer in axiom_uris): + unexpected_uris.add(actual_uri) + else: # Simple URI case + if actual_uri not in axiom_uris: + unexpected_uris.add(actual_uri) + return unexpected_uris def extract_anatomical_uris(entities_list): - return set(uri for entity in entities_list for uri in entity.anatomical_entities) + uris = set() + for entity in entities_list: + for anatomical_entity in entity.anatomical_entities: + if isinstance(anatomical_entity, orders.rl): + uris.add((anatomical_entity.region.toPython(), anatomical_entity.layer.toPython())) + else: # Simple URIRef + uris.add(anatomical_entity.toPython()) + return uris def merge_origins(origins: List[NeuronDMOrigin]) -> NeuronDMOrigin: diff --git a/backend/composer/signals.py b/backend/composer/signals.py index 7aa6e54b..86268654 100644 --- a/backend/composer/signals.py +++ b/backend/composer/signals.py @@ -1,11 +1,11 @@ from django.core.exceptions import ValidationError from django.dispatch import receiver -from django.db.models.signals import post_save, m2m_changed +from django.db.models.signals import post_save, m2m_changed, pre_save from django_fsm.signals import post_transition from .enums import CSState, NoteType -from .models import ConnectivityStatement, ExportBatch, Note, Sentence +from .models import ConnectivityStatement, ExportBatch, Note, Sentence, AnatomicalEntity from .services.export_services import compute_metrics, ConnectivityStatementStateService @@ -45,4 +45,4 @@ def post_transition_cs(sender, instance, name, source, target, **kwargs): CSState.EXPORTED, ): # add important tag to CS when transition to COMPOSE_NOW from NPO Approved or Exported - instance = ConnectivityStatementStateService.add_important_tag(instance) + instance = ConnectivityStatementStateService.add_important_tag(instance) \ No newline at end of file