@@ -530,6 +530,10 @@ def create_new_version_for(
530530 cls , instance , data , user , create_parent_version = True , add_prev_version_children = True ,
531531 _hierarchy_processing = False , is_patch = False
532532 ): # pylint: disable=too-many-arguments
533+ mappings_payload = data .pop ('mappings_payload' , None )
534+ prev_latest = Concept .objects .filter (
535+ mnemonic = instance .mnemonic , parent_id = instance .parent_id , is_latest_version = True
536+ ).first ()
533537 instance .id = None # Clear id so it is persisted as a new object
534538 instance .version = data .get ('version' , None )
535539 instance .concept_class = data .get ('concept_class' , instance .concept_class )
@@ -557,14 +561,25 @@ def create_new_version_for(
557561 if not parent_concept_uris and has_parent_concept_uris_attr :
558562 parent_concept_uris = []
559563
560- return instance .save_as_new_version (
564+ errors = instance .save_as_new_version (
561565 user = user ,
562566 create_parent_version = create_parent_version ,
563567 parent_concept_uris = parent_concept_uris ,
564568 add_prev_version_children = add_prev_version_children ,
565569 _hierarchy_processing = _hierarchy_processing
566570 )
567571
572+ if errors or mappings_payload is None :
573+ return errors
574+ mappings_result , has_mapping_errors = instance .upsert_or_delete_mappings (
575+ mappings_payload , user
576+ )
577+ if has_mapping_errors :
578+ instance .rollback_latest_version_to (prev_latest )
579+ errors ['mappings' ] = instance ._get_errors_from_mappings (mappings_result )
580+
581+ return errors
582+
568583 def set_parent_concepts_from_uris (self , create_parent_version = True ):
569584 parent_concepts = get (self , '_parent_concepts' , [])
570585 if create_parent_version :
@@ -655,14 +670,140 @@ def create_mappings(self, mappings):
655670
656671 def _create_mapping_from_self (self , mapping_data , user ):
657672 from core .mappings .models import Mapping
658- data = {key : value for key , value in mapping_data .items () if not key .startswith ('from_' )}
673+ data = {
674+ key : value for key , value in mapping_data .items ()
675+ if not key .startswith ('from_' ) and key not in ['id' , 'uuid' , 'action' ]
676+ }
659677 data ['from_concept_url' ] = drop_version (self .uri )
660678 if data .get ('to_concept' ) == '__parent_concept' :
661679 data ['to_concept_url' ] = self .uri
662680 data .pop ('to_concept' )
663681
664682 return Mapping .persist_new ({** data , 'parent_id' : self .parent_id }, user )
665683
684+ def _validate_mapping_create_from_self (self , mapping_data , user ):
685+ from core .mappings .models import Mapping
686+ data = {
687+ key : value for key , value in mapping_data .items ()
688+ if not key .startswith ('from_' ) and key not in ['id' , 'uuid' , 'action' ]
689+ }
690+ data ['from_concept_url' ] = drop_version (self .uri )
691+ if data .get ('to_concept' ) == '__parent_concept' :
692+ data ['to_concept_url' ] = self .uri
693+ data .pop ('to_concept' )
694+ data ['parent_id' ] = self .parent_id
695+
696+ related_fields = ['from_concept_url' , 'to_concept_url' , 'to_source_url' , 'from_source_url' ]
697+ field_data = {k : v for k , v in data .items () if k not in related_fields }
698+ url_params = {k : v for k , v in data .items () if k in related_fields }
699+ candidate = Mapping (** field_data , created_by = user , updated_by = user )
700+ candidate .populate_fields_from_relations (url_params )
701+ candidate .full_clean ()
702+
703+ @staticmethod
704+ def _rollback_mapping_operations (applied_operations ):
705+ from core .mappings .models import Mapping
706+ for operation in reversed (applied_operations ):
707+ op_type = operation .get ('__action' )
708+ mapping_id = operation ['versioned_object_id' ]
709+ if not mapping_id or not op_type :
710+ continue
711+ if op_type == 'create' and mapping_id :
712+ Mapping .objects .filter (versioned_object_id = mapping_id ).delete ()
713+ continue
714+ latest_version = Mapping .objects .filter (versioned_object_id = mapping_id , is_latest_version = True ).first ()
715+ prev_latest = latest_version .prev_version
716+ prev_latest .mark_latest_version (True , latest_version .parent )
717+ prev_latest .update_versioned_object ()
718+ latest_version .delete ()
719+
720+ def rollback_latest_version_to (self , prev_latest ):
721+ if not prev_latest :
722+ return
723+ latest = Concept .objects .filter (
724+ mnemonic = self .mnemonic , parent_id = self .parent_id , is_latest_version = True
725+ ).first ()
726+ if latest and latest .id != prev_latest .id :
727+ latest .remove_locales ()
728+ latest .delete ()
729+ prev_latest .mark_latest_version (True , self .parent )
730+ prev_latest .update_versioned_object ()
731+
732+ def find_direct_mapping (self , mapping_id ):
733+ return self .get_unidirectional_mappings ().filter (mnemonic = str (mapping_id )).first () if mapping_id else None
734+
735+ def upsert_or_delete_mappings (self , mappings_payload , user ):
736+ from core .mappings .models import Mapping
737+ results = []
738+ any_with_errors = False
739+ applied_operations = []
740+
741+ for mapping_data in mappings_payload or []:
742+ mapping_data = mapping_data .copy ()
743+ mapping_id = mapping_data .get ('id' )
744+ action = mapping_data .get ('action' )
745+ errors = {}
746+ serializer = None
747+ created = None
748+
749+ try :
750+ if action == '__delete' and not mapping_id :
751+ raise ValidationError ({'id' : ['Mapping id is required when action is __delete.' ]})
752+
753+ if mapping_id :
754+ mapping = self .find_direct_mapping (mapping_id )
755+ if not mapping :
756+ raise ValidationError ({'id' : [f"Mapping '{ mapping_id } ' not found for this concept." ]})
757+
758+ if action == '__delete' :
759+ errors = mapping .retire (
760+ user , mapping_data .get ('update_comment' ) or mapping_data .get ('comment' )
761+ )
762+ if not errors :
763+ applied_operations .append ({
764+ 'action' : '__delete' , 'versioned_object_id' : mapping .versioned_object_id })
765+ created = mapping
766+ else :
767+ updates = {
768+ k : v for k , v in mapping_data .items () if k not in ['id' , 'uuid' , 'action' , 'checksums' ]}
769+ if updates .get ('to_concept' ) == '__parent_concept' :
770+ updates ['to_concept_url' ] = self .uri
771+ updates .pop ('to_concept' )
772+ errors = Mapping .create_new_version_for (mapping .clone (user = user ), updates , user )
773+ if not errors :
774+ applied_operations .append ({
775+ '__action' : 'update' , 'versioned_object_id' : mapping .versioned_object_id })
776+ created = mapping
777+ else :
778+ created = self ._create_mapping_from_self (mapping_data , user )
779+ errors = created .errors
780+ if not errors and not created .id :
781+ errors ['__all__' ] = ['Something bad happened while creating the mapping.' ]
782+ if not errors and created .id :
783+ applied_operations .append ({'__action' : 'create' , 'id' : created .versioned_object_id })
784+ except Exception as ex : # pylint: disable=broad-except
785+ error_dict = get (ex , 'message_dict' ) or get (ex , 'error_dict' )
786+ if error_dict :
787+ errors = error_dict
788+ else :
789+ errors ['__all__' ] = [str (ex )]
790+ any_with_errors = True
791+
792+ if errors :
793+ any_with_errors = True
794+
795+ results .append ({
796+ 'mapping' : mapping_data ,
797+ 'instance' : created ,
798+ 'serializer' : serializer ,
799+ 'errors' : errors
800+ })
801+
802+ if any_with_errors and applied_operations :
803+ self ._rollback_mapping_operations (applied_operations )
804+
805+ return results , any_with_errors
806+
666807 @staticmethod
667808 def _remove_mappings_just_created (mappings_results ):
668809 for mapping in mappings_results :
0 commit comments