Skip to content

Commit 83cdf49

Browse files
authored
[ENH] add ability to clone a project (#1150)
* codex: add ability to clone project * style * add copy to project name * set snapshot to None since the snapshot cannot be inferred. * set snapshot to None the snapshot cannot be inferred. * fix syntax
1 parent f31d58a commit 83cdf49

File tree

4 files changed

+486
-19
lines changed

4 files changed

+486
-19
lines changed

compose/backend/neurosynth_compose/resources/analysis.py

Lines changed: 277 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from collections import ChainMap
2+
from copy import deepcopy
23
import json
34
import pathlib
45
from operator import itemgetter
6+
from urllib.parse import urlencode
57

68
import connexion
79
from connexion.lifecycle import ConnexionResponse
@@ -50,6 +52,7 @@
5052
NeurostoreStudySchema,
5153
ProjectSchema,
5254
)
55+
from .neurostore import neurostore_session
5356
from .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

718994
def create_neurovault_collection(nv_collection):
719995
import flask

0 commit comments

Comments
 (0)