11from collections import ChainMap
2+ from copy import deepcopy
23import json
34import pathlib
45from operator import itemgetter
6+ from urllib .parse import urlencode
57
68import connexion
79from connexion .lifecycle import ConnexionResponse
5052 NeurostoreStudySchema ,
5153 ProjectSchema ,
5254)
55+ from .neurostore import neurostore_session
5356from .singular import singularize
5457
5558
@@ -697,14 +700,28 @@ def db_validation(self, data):
697700 )
698701
699702 def post (self ):
703+ clone_args = parser .parse (
704+ {
705+ "source_id" : fields .String (missing = None ),
706+ "copy_annotations" : fields .Boolean (missing = True ),
707+ },
708+ request ,
709+ location = "query" ,
710+ )
711+
712+ source_id = clone_args .get ("source_id" )
713+ if source_id :
714+ return self ._clone_project (
715+ source_id , clone_args .get ("copy_annotations" , True )
716+ )
717+
700718 try :
701719 data = parser .parse (self .__class__ ._schema , request )
702720 except ValidationError as e :
703721 abort (422 , description = f"input does not conform to specification: { str (e )} " )
704722
705723 with db .session .no_autoflush :
706724 record = self .__class__ .update_or_create (data )
707- # create neurostore study
708725 ns_study = NeurostoreStudy (project = record )
709726 db .session .add (ns_study )
710727 commit_session ()
@@ -714,6 +731,265 @@ def post(self):
714731 payload = self .__class__ ._schema ().dump (record )
715732 return _make_json_response (payload )
716733
734+ def _clone_project (self , source_id , copy_annotations ):
735+ current_user = self ._ensure_current_user ()
736+
737+ source_project = db .session .execute (
738+ select (Project )
739+ .options (
740+ selectinload (Project .studyset ).options (
741+ selectinload (Studyset .studyset_reference )
742+ ),
743+ selectinload (Project .annotation ).options (
744+ selectinload (Annotation .annotation_reference )
745+ ),
746+ selectinload (Project .meta_analyses ).options (
747+ selectinload (MetaAnalysis .specification ).options (
748+ selectinload (Specification .specification_conditions )
749+ ),
750+ selectinload (MetaAnalysis .results ),
751+ ),
752+ )
753+ .where (Project .id == source_id )
754+ ).scalar_one_or_none ()
755+
756+ if source_project is None :
757+ abort (404 )
758+
759+ if (
760+ not source_project .public
761+ and source_project .user_id != current_user .external_id
762+ ):
763+ abort (403 , description = "project is not public" )
764+
765+ access_token = request .headers .get ("Authorization" )
766+ if not access_token :
767+ from auth0 .v3 .authentication .get_token import GetToken
768+
769+ domain = current_app .config ["AUTH0_BASE_URL" ].lstrip ("https://" )
770+ g_token = GetToken (domain )
771+ token_resp = g_token .client_credentials (
772+ client_id = current_app .config ["AUTH0_CLIENT_ID" ],
773+ client_secret = current_app .config ["AUTH0_CLIENT_SECRET" ],
774+ audience = current_app .config ["AUTH0_API_AUDIENCE" ],
775+ )
776+ access_token = " " .join (
777+ [token_resp ["token_type" ], token_resp ["access_token" ]]
778+ )
779+
780+ ns_session = neurostore_session (access_token )
781+
782+ with db .session .no_autoflush :
783+ new_studyset , new_annotation = self ._clone_studyset_and_annotation (
784+ ns_session , source_project , current_user , copy_annotations
785+ )
786+
787+ cloned_project = Project (
788+ name = source_project .name + "Copy" ,
789+ description = source_project .description ,
790+ provenance = self ._clone_provenance (
791+ source_project .provenance ,
792+ new_studyset .studyset_reference .id if new_studyset else None ,
793+ new_annotation .annotation_reference .id if new_annotation else None ,
794+ ),
795+ user = current_user ,
796+ public = False ,
797+ draft = True ,
798+ studyset = new_studyset ,
799+ annotation = new_annotation ,
800+ )
801+
802+ cloned_metas = []
803+ for meta in source_project .meta_analyses :
804+ cloned_meta = self ._clone_meta_analysis (
805+ meta ,
806+ current_user ,
807+ cloned_project ,
808+ new_studyset ,
809+ new_annotation ,
810+ )
811+ cloned_metas .append (cloned_meta )
812+
813+ cloned_project .meta_analyses = cloned_metas
814+
815+ db .session .add (cloned_project )
816+ for meta in cloned_metas :
817+ db .session .add (meta )
818+
819+ commit_session ()
820+
821+ ns_study = NeurostoreStudy (project = cloned_project )
822+ db .session .add (ns_study )
823+ commit_session ()
824+ create_or_update_neurostore_study (ns_study )
825+ db .session .add (ns_study )
826+ commit_session ()
827+
828+ payload = self .__class__ ._schema ().dump (cloned_project )
829+ return _make_json_response (payload )
830+
831+ def _ensure_current_user (self ):
832+ current_user = get_current_user ()
833+ if current_user :
834+ return current_user
835+ current_user = create_user ()
836+ if current_user :
837+ db .session .add (current_user )
838+ commit_session ()
839+ return current_user
840+ abort (401 , description = "user authentication required" )
841+
842+ def _clone_studyset_and_annotation (
843+ self , ns_session , source_project , current_user , copy_annotations
844+ ):
845+ source_studyset = getattr (source_project , "studyset" , None )
846+ new_studyset = None
847+ new_annotation = None
848+
849+ if source_studyset and source_studyset .studyset_reference :
850+ query_params = {
851+ "source_id" : source_studyset .studyset_reference .id ,
852+ }
853+ if copy_annotations is not None :
854+ query_params ["copy_annotations" ] = str (bool (copy_annotations )).lower ()
855+
856+ path = "/api/studysets/"
857+ if query_params :
858+ path = f"{ path } ?{ urlencode (query_params )} "
859+
860+ ns_response = ns_session .post (path , json = {})
861+ ns_payload = ns_response .json ()
862+
863+ ss_ref = self ._get_or_create_reference (
864+ StudysetReference , ns_payload .get ("id" )
865+ )
866+ new_studyset = Studyset (
867+ user = current_user ,
868+ snapshot = None ,
869+ version = source_studyset .version ,
870+ studyset_reference = ss_ref ,
871+ )
872+ db .session .add (new_studyset )
873+
874+ source_annotation = getattr (source_project , "annotation" , None )
875+ annotations_payload = ns_payload .get ("annotations" ) or []
876+ annotation_id = None
877+ if annotations_payload :
878+ annotation_id = annotations_payload [0 ].get ("id" )
879+ if source_annotation and copy_annotations and annotation_id :
880+ annot_ref = self ._get_or_create_reference (
881+ AnnotationReference , annotation_id
882+ )
883+ new_annotation = Annotation (
884+ user = current_user ,
885+ snapshot = None ,
886+ annotation_reference = annot_ref ,
887+ studyset = new_studyset ,
888+ )
889+ db .session .add (new_annotation )
890+
891+ return new_studyset , new_annotation
892+
893+ def _clone_meta_analysis (self , meta , user , project , new_studyset , new_annotation ):
894+ cloned_spec = self ._clone_specification (meta .specification , user )
895+ cloned_meta = MetaAnalysis (
896+ name = meta .name ,
897+ description = meta .description ,
898+ specification = cloned_spec ,
899+ studyset = new_studyset ,
900+ annotation = new_annotation ,
901+ user = user ,
902+ project = project ,
903+ provenance = self ._clone_meta_provenance (
904+ meta .provenance ,
905+ new_studyset .studyset_reference .id if new_studyset else None ,
906+ new_annotation .annotation_reference .id if new_annotation else None ,
907+ ),
908+ )
909+
910+ if new_studyset and new_studyset .studyset_reference :
911+ cloned_meta .neurostore_studyset_id = new_studyset .studyset_reference .id
912+ cloned_meta .cached_studyset = new_studyset
913+
914+ if new_annotation and new_annotation .annotation_reference :
915+ cloned_meta .neurostore_annotation_id = (
916+ new_annotation .annotation_reference .id
917+ )
918+ cloned_meta .cached_annotation = new_annotation
919+
920+ return cloned_meta
921+
922+ @staticmethod
923+ def _clone_specification (specification , user ):
924+ if specification is None :
925+ return None
926+ cloned_spec = Specification (
927+ type = specification .type ,
928+ estimator = deepcopy (specification .estimator ),
929+ database_studyset = specification .database_studyset ,
930+ filter = specification .filter ,
931+ corrector = deepcopy (specification .corrector ),
932+ user = user ,
933+ )
934+ for spec_cond in specification .specification_conditions :
935+ cloned_cond = SpecificationCondition (
936+ weight = spec_cond .weight ,
937+ condition = spec_cond .condition ,
938+ user = user ,
939+ )
940+ cloned_spec .specification_conditions .append (cloned_cond )
941+ return cloned_spec
942+
943+ @staticmethod
944+ def _clone_provenance (provenance , studyset_id , annotation_id ):
945+ if provenance is None :
946+ return None
947+ cloned = deepcopy (provenance )
948+ extraction = cloned .get ("extractionMetadata" , {})
949+ if studyset_id :
950+ extraction ["studysetId" ] = studyset_id
951+ if annotation_id :
952+ extraction ["annotationId" ] = annotation_id
953+ cloned ["extractionMetadata" ] = extraction
954+
955+ meta_meta = cloned .get ("metaAnalysisMetadata" )
956+ if isinstance (meta_meta , dict ):
957+ meta_meta ["canEditMetaAnalyses" ] = True
958+ cloned ["metaAnalysisMetadata" ] = meta_meta
959+
960+ return cloned
961+
962+ @staticmethod
963+ def _clone_meta_provenance (provenance , studyset_id , annotation_id ):
964+ if provenance is None :
965+ return None
966+ cloned = deepcopy (provenance )
967+ if studyset_id :
968+ for key in ("studysetId" , "studyset_id" ):
969+ if key in cloned :
970+ cloned [key ] = studyset_id
971+ if annotation_id :
972+ for key in ("annotationId" , "annotation_id" ):
973+ if key in cloned :
974+ cloned [key ] = annotation_id
975+ for key in ("hasResults" , "has_results" ):
976+ if key in cloned :
977+ cloned [key ] = False
978+ return cloned
979+
980+ @staticmethod
981+ def _get_or_create_reference (model_cls , identifier ):
982+ if identifier is None :
983+ return None
984+ existing = db .session .execute (
985+ select (model_cls ).where (model_cls .id == identifier )
986+ ).scalar_one_or_none ()
987+ if existing :
988+ return existing
989+ reference = model_cls (id = identifier )
990+ db .session .add (reference )
991+ return reference
992+
717993
718994def create_neurovault_collection (nv_collection ):
719995 import flask
0 commit comments