diff --git a/app/db/utils.py b/app/db/utils.py index b46a5ee6c..9bfc4d72f 100644 --- a/app/db/utils.py +++ b/app/db/utils.py @@ -76,8 +76,10 @@ def load_db_model_from_pydantic[I: DeclarativeBase]( created_by_id: uuid.UUID | None, updated_by_id: uuid.UUID | None, ignore_attributes: set[str] | None = None, + *, + exclude_defaults: bool = False, ) -> I: - data = json_model.model_dump(by_alias=True) + data = json_model.model_dump(by_alias=True, exclude_defaults=exclude_defaults) if created_by_id or updated_by_id: data["created_by_id"] = created_by_id diff --git a/app/queries/common.py b/app/queries/common.py index c6318c55c..95bfa3513 100644 --- a/app/queries/common.py +++ b/app/queries/common.py @@ -31,6 +31,7 @@ from app.schemas.activity import ActivityCreate, ActivityUpdate from app.schemas.auth import UserContext, UserContextWithProjectId, UserProfile from app.schemas.types import ListResponse, PaginationResponse +from app.schemas.utils import NOT_SET from app.utils.uuid import create_uuid @@ -354,7 +355,7 @@ def router_update_one[T: BaseModel, I: Identifiable]( id_: uuid.UUID, db: Session, db_model_class: type[I], - user_context: UserContext, + user_context: UserContext | None, json_model: BaseModel, response_schema_class: type[T], apply_operations: ApplyOperations | None = None, @@ -362,7 +363,9 @@ def router_update_one[T: BaseModel, I: Identifiable]( query = ( sa.select(db_model_class).where(db_model_class.id == id_).with_for_update(of=db_model_class) ) - if id_model_class := get_declaring_class(db_model_class, "authorized_project_id"): + if user_context and ( + id_model_class := get_declaring_class(db_model_class, "authorized_project_id") + ): query = constrain_to_private_entities(query, user_context, db_model_class=id_model_class) if apply_operations: query = apply_operations(query) @@ -427,13 +430,15 @@ def router_update_activity_one[T: BaseModel, I: Activity]( id_: uuid.UUID, db: Session, db_model_class: type[I], - user_context: UserContext | UserContextWithProjectId, + user_context: UserContext | UserContextWithProjectId | None, json_model: ActivityUpdate, response_schema_class: type[T], apply_operations: ApplyOperations | None = None, ) -> T: query = sa.select(db_model_class).where(db_model_class.id == id_) - if id_model_class := get_declaring_class(db_model_class, "authorized_project_id"): + if user_context and ( + id_model_class := get_declaring_class(db_model_class, "authorized_project_id") + ): query = constrain_to_accessible_entities( query, user_context.project_id, db_model_class=id_model_class ) @@ -446,21 +451,24 @@ def router_update_activity_one[T: BaseModel, I: Activity]( update_data = json_model.model_dump( exclude_unset=True, exclude_none=True, - exclude_defaults=True, exclude={"used_ids", "generated_ids"}, + exclude_defaults=True, # ignore NOT_SET default values ) for key, value in update_data.items(): setattr(obj, key, value) - if generated_ids := json_model.generated_ids: + # ignore NOT_SET values + generated_ids = json_model.generated_ids if json_model.generated_ids != NOT_SET else [] + + if generated_ids: if obj.generated: raise HTTPException( status_code=404, detail="It is forbidden to update generated_ids if they exist.", ) - if ( + if user_context and ( unaccessible_entities := db.execute( select_unauthorized_entities(generated_ids, user_context.project_id) ) diff --git a/app/routers/brain_region.py b/app/routers/brain_region.py index 79225c878..316777cef 100644 --- a/app/routers/brain_region.py +++ b/app/routers/brain_region.py @@ -1,11 +1,21 @@ from fastapi import APIRouter import app.service.brain_region +from app.routers.admin import router as admin_router + +ROUTE = "brain-region" router = APIRouter( - prefix="/brain-region", - tags=["brain-region"], + prefix=f"/{ROUTE}", + tags=[ROUTE], ) read_many = router.get("")(app.service.brain_region.read_many) read_one = router.get("/{id_}")(app.service.brain_region.read_one) +create_one = router.post("")(app.service.brain_region.create_one) +update_one = router.patch("/{id_}")(app.service.brain_region.update_one) + +admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.brain_region.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")( + app.service.brain_region.admin_update_one +) diff --git a/app/routers/calibration.py b/app/routers/calibration.py index c31cefd58..d02918c94 100644 --- a/app/routers/calibration.py +++ b/app/routers/calibration.py @@ -17,3 +17,4 @@ update_one = router.patch("/{id_}")(app.service.calibration.update_one) admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.calibration.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")(app.service.calibration.admin_update_one) diff --git a/app/routers/circuit.py b/app/routers/circuit.py index 576a77922..df51c47dc 100644 --- a/app/routers/circuit.py +++ b/app/routers/circuit.py @@ -18,3 +18,4 @@ update_one = router.patch("/{id_}")(app.service.circuit.update_one) admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.circuit.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")(app.service.circuit.admin_update_one) diff --git a/app/routers/electrical_cell_recording.py b/app/routers/electrical_cell_recording.py index fee8b918f..ac8429b8c 100644 --- a/app/routers/electrical_cell_recording.py +++ b/app/routers/electrical_cell_recording.py @@ -18,3 +18,6 @@ admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")( app.service.electrical_cell_recording.admin_read_one ) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")( + app.service.electrical_cell_recording.admin_update_one +) diff --git a/app/routers/electrical_recording_stimulus.py b/app/routers/electrical_recording_stimulus.py index e0e3c0adc..b62ad793f 100644 --- a/app/routers/electrical_recording_stimulus.py +++ b/app/routers/electrical_recording_stimulus.py @@ -18,3 +18,6 @@ admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")( app.service.electrical_recording_stimulus.admin_read_one ) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")( + app.service.electrical_recording_stimulus.admin_update_one +) diff --git a/app/routers/em_cell_mesh.py b/app/routers/em_cell_mesh.py index d89cc06e4..5398eb568 100644 --- a/app/routers/em_cell_mesh.py +++ b/app/routers/em_cell_mesh.py @@ -1,13 +1,21 @@ from fastapi import APIRouter import app.service.em_cell_mesh +from app.routers.admin import router as admin_router + +ROUTE = "em-cell-mesh" router = APIRouter( - prefix="/em-cell-mesh", - tags=["em-cell-mesh"], + prefix=f"/{ROUTE}", + tags=[ROUTE], ) read_many = router.get("")(app.service.em_cell_mesh.read_many) read_one = router.get("/{id_}")(app.service.em_cell_mesh.read_one) create_one = router.post("")(app.service.em_cell_mesh.create_one) update_one = router.patch("/{id_}")(app.service.em_cell_mesh.update_one) + +admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.em_cell_mesh.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")( + app.service.em_cell_mesh.admin_update_one +) diff --git a/app/routers/emodel.py b/app/routers/emodel.py index 5470af4f5..9049ef510 100644 --- a/app/routers/emodel.py +++ b/app/routers/emodel.py @@ -17,3 +17,4 @@ update_one = router.patch("/{id_}")(app.service.emodel.update_one) admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.emodel.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")(app.service.emodel.admin_update_one) diff --git a/app/routers/etype.py b/app/routers/etype.py index a93204066..bde9b450a 100644 --- a/app/routers/etype.py +++ b/app/routers/etype.py @@ -12,5 +12,8 @@ read_many = router.get("")(app.service.etype.read_many) read_one = router.get("/{id_}")(app.service.etype.read_one) +create_one = router.post("")(app.service.etype.create_one) +update_one = router.patch("/{id_}")(app.service.etype.update_one) -admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.etype.read_one) +admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.etype.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")(app.service.etype.admin_update_one) diff --git a/app/routers/experimental_bouton_density.py b/app/routers/experimental_bouton_density.py index 3c6a31176..6046fdcd7 100644 --- a/app/routers/experimental_bouton_density.py +++ b/app/routers/experimental_bouton_density.py @@ -18,3 +18,6 @@ admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")( app.service.experimental_bouton_density.admin_read_one ) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")( + app.service.experimental_bouton_density.admin_update_one +) diff --git a/app/routers/experimental_neuron_density.py b/app/routers/experimental_neuron_density.py index 6119319f7..2a1707ceb 100644 --- a/app/routers/experimental_neuron_density.py +++ b/app/routers/experimental_neuron_density.py @@ -18,3 +18,6 @@ admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")( app.service.experimental_neuron_density.admin_read_one ) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")( + app.service.experimental_neuron_density.admin_update_one +) diff --git a/app/routers/experimental_synapses_per_connection.py b/app/routers/experimental_synapses_per_connection.py index 1e126325b..3b6f1e205 100644 --- a/app/routers/experimental_synapses_per_connection.py +++ b/app/routers/experimental_synapses_per_connection.py @@ -18,3 +18,6 @@ admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")( app.service.experimental_synapses_per_connection.admin_read_one ) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")( + app.service.experimental_synapses_per_connection.admin_update_one +) diff --git a/app/routers/ion_channel.py b/app/routers/ion_channel.py index 9c5f04562..6367cbc00 100644 --- a/app/routers/ion_channel.py +++ b/app/routers/ion_channel.py @@ -1,12 +1,19 @@ from fastapi import APIRouter import app.service.ion_channel +from app.routers.admin import router as admin_router + +ROUTE = "ion-channel" router = APIRouter( - prefix="/ion-channel", - tags=["ion-channel"], + prefix=f"/{ROUTE}", + tags=[ROUTE], ) read_many = router.get("")(app.service.ion_channel.read_many) read_one = router.get("/{id_}")(app.service.ion_channel.read_one) create_one = router.post("")(app.service.ion_channel.create_one) +update_one = router.patch("/{id_}")(app.service.ion_channel.update_one) + +admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.ion_channel.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")(app.service.ion_channel.admin_update_one) diff --git a/app/routers/ion_channel_model.py b/app/routers/ion_channel_model.py index d52f02b68..220fdddd6 100644 --- a/app/routers/ion_channel_model.py +++ b/app/routers/ion_channel_model.py @@ -16,3 +16,6 @@ update_one = router.patch("/{id_}")(app.service.ion_channel_model.update_one) admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.ion_channel_model.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")( + app.service.ion_channel_model.admin_update_one +) diff --git a/app/routers/ion_channel_recording.py b/app/routers/ion_channel_recording.py index 80fc79d0c..0fdfc13a4 100644 --- a/app/routers/ion_channel_recording.py +++ b/app/routers/ion_channel_recording.py @@ -1,13 +1,23 @@ from fastapi import APIRouter import app.service.ion_channel_recording +from app.routers.admin import router as admin_router + +ROUTE = "ion-channel-recording" router = APIRouter( - prefix="/ion-channel-recording", - tags=["ion-channel-recording"], + prefix=f"/{ROUTE}", + tags=[ROUTE], ) read_many = router.get("")(app.service.ion_channel_recording.read_many) read_one = router.get("/{id_}")(app.service.ion_channel_recording.read_one) create_one = router.post("")(app.service.ion_channel_recording.create_one) update_one = router.patch("/{id_}")(app.service.ion_channel_recording.update_one) + +admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")( + app.service.ion_channel_recording.admin_read_one +) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")( + app.service.ion_channel_recording.admin_update_one +) diff --git a/app/routers/license.py b/app/routers/license.py index 5c212b16b..4f857bf38 100644 --- a/app/routers/license.py +++ b/app/routers/license.py @@ -13,5 +13,7 @@ read_many = router.get("")(app.service.license.read_many) read_one = router.get("/{id_}")(app.service.license.read_one) create_one = router.post("")(app.service.license.create_one) +update_one = router.patch("/{id_}")(app.service.license.update_one) -admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.license.read_one) +admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.license.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")(app.service.license.admin_update_one) diff --git a/app/routers/measurement_annotation.py b/app/routers/measurement_annotation.py index 20fe39908..a407f4698 100644 --- a/app/routers/measurement_annotation.py +++ b/app/routers/measurement_annotation.py @@ -14,7 +14,11 @@ read_one = router.get("/{id_}")(app.service.measurement_annotation.read_one) create_one = router.post("")(app.service.measurement_annotation.create_one) delete_one = router.delete("/{id_}")(app.service.measurement_annotation.delete_one) +update_one = router.patch("/{id_}")(app.service.measurement_annotation.update_one) admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")( app.service.measurement_annotation.admin_read_one ) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")( + app.service.measurement_annotation.admin_update_one +) diff --git a/app/routers/memodel.py b/app/routers/memodel.py index 8a8699061..94e2fb046 100644 --- a/app/routers/memodel.py +++ b/app/routers/memodel.py @@ -16,3 +16,4 @@ update_one = router.patch("/{id_}")(app.service.memodel.update_one) admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.memodel.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")(app.service.memodel.admin_update_one) diff --git a/app/routers/memodel_calibration_result.py b/app/routers/memodel_calibration_result.py index 9816b4355..ea468e1f9 100644 --- a/app/routers/memodel_calibration_result.py +++ b/app/routers/memodel_calibration_result.py @@ -18,3 +18,6 @@ admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")( app.service.memodel_calibration_result.admin_read_one ) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")( + app.service.memodel_calibration_result.admin_update_one +) diff --git a/app/routers/morphology.py b/app/routers/morphology.py index a67e87887..255721d14 100644 --- a/app/routers/morphology.py +++ b/app/routers/morphology.py @@ -16,3 +16,4 @@ update_one = router.patch("/{id_}")(app.service.morphology.update_one) admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.morphology.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")(app.service.morphology.admin_update_one) diff --git a/app/routers/mtype.py b/app/routers/mtype.py index d04d3c7a9..8d4ea9d2d 100644 --- a/app/routers/mtype.py +++ b/app/routers/mtype.py @@ -12,5 +12,8 @@ read_many = router.get("")(app.service.mtype.read_many) read_one = router.get("/{id_}")(app.service.mtype.read_one) +create_one = router.post("")(app.service.mtype.create_one) +update_one = router.patch("/{id_}")(app.service.mtype.update_one) -admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.mtype.read_one) +admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.mtype.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")(app.service.mtype.admin_update_one) diff --git a/app/routers/publication.py b/app/routers/publication.py index 3dffe4f75..0ba3b2543 100644 --- a/app/routers/publication.py +++ b/app/routers/publication.py @@ -13,5 +13,7 @@ read_one = router.get("/{id_}")(app.service.publication.read_one) read_many = router.get("")(app.service.publication.read_many) create_one = router.post("")(app.service.publication.create_one) +update_one = router.patch("/{id_}")(app.service.publication.update_one) admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.publication.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")(app.service.publication.admin_update_one) diff --git a/app/routers/role.py b/app/routers/role.py index b1277b4af..d334ca1a1 100644 --- a/app/routers/role.py +++ b/app/routers/role.py @@ -13,5 +13,7 @@ read_many = router.get("")(app.service.role.read_many) read_one = router.get("/{id_}")(app.service.role.read_one) create_one = router.post("")(app.service.role.create_one) +update_one = router.patch("/{id_}")(app.service.role.update_one) -admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.role.read_one) +admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.role.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")(app.service.role.admin_update_one) diff --git a/app/routers/simulation.py b/app/routers/simulation.py index ecb9d94b9..72b01adab 100644 --- a/app/routers/simulation.py +++ b/app/routers/simulation.py @@ -12,3 +12,4 @@ update_one = router.patch("/{id_}")(app.service.simulation.update_one) admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.simulation.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")(app.service.simulation.admin_update_one) diff --git a/app/routers/simulation_campaign.py b/app/routers/simulation_campaign.py index 34ad6b855..b9f6a1e50 100644 --- a/app/routers/simulation_campaign.py +++ b/app/routers/simulation_campaign.py @@ -14,3 +14,6 @@ admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")( app.service.simulation_campaign.admin_read_one ) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")( + app.service.simulation_campaign.admin_update_one +) diff --git a/app/routers/simulation_execution.py b/app/routers/simulation_execution.py index fea12cb3e..282a0147f 100644 --- a/app/routers/simulation_execution.py +++ b/app/routers/simulation_execution.py @@ -15,3 +15,6 @@ admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")( app.service.simulation_execution.admin_read_one ) +admin_update_one = admin_router.patch("/simulation-execution/{id_}")( + app.service.simulation_execution.admin_update_one +) diff --git a/app/routers/simulation_generation.py b/app/routers/simulation_generation.py index 9416d8c3d..b0da5dbb6 100644 --- a/app/routers/simulation_generation.py +++ b/app/routers/simulation_generation.py @@ -15,3 +15,6 @@ admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")( app.service.simulation_generation.admin_read_one ) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")( + app.service.simulation_generation.admin_update_one +) diff --git a/app/routers/simulation_result.py b/app/routers/simulation_result.py index d2efd4a83..f2e30baa1 100644 --- a/app/routers/simulation_result.py +++ b/app/routers/simulation_result.py @@ -12,3 +12,6 @@ update_one = router.patch("/{id_}")(app.service.simulation_result.update_one) admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.simulation_result.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")( + app.service.simulation_result.admin_update_one +) diff --git a/app/routers/single_neuron_simulation.py b/app/routers/single_neuron_simulation.py index b3c153ab3..f8f63b3b5 100644 --- a/app/routers/single_neuron_simulation.py +++ b/app/routers/single_neuron_simulation.py @@ -14,3 +14,6 @@ admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")( app.service.single_neuron_simulation.admin_read_one ) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")( + app.service.single_neuron_simulation.admin_update_one +) diff --git a/app/routers/single_neuron_synaptome.py b/app/routers/single_neuron_synaptome.py index 3a5258659..e1c65dcad 100644 --- a/app/routers/single_neuron_synaptome.py +++ b/app/routers/single_neuron_synaptome.py @@ -14,3 +14,6 @@ admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")( app.service.single_neuron_synaptome.admin_read_one ) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")( + app.service.single_neuron_synaptome.admin_update_one +) diff --git a/app/routers/single_neuron_synaptome_simulation.py b/app/routers/single_neuron_synaptome_simulation.py index cea04a01f..5a9d05b59 100644 --- a/app/routers/single_neuron_synaptome_simulation.py +++ b/app/routers/single_neuron_synaptome_simulation.py @@ -14,3 +14,6 @@ admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")( app.service.single_neuron_synaptome_simulation.admin_read_one ) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")( + app.service.single_neuron_synaptome_simulation.admin_update_one +) diff --git a/app/routers/species.py b/app/routers/species.py index 462e6d996..741e2507c 100644 --- a/app/routers/species.py +++ b/app/routers/species.py @@ -1,12 +1,19 @@ from fastapi import APIRouter import app.service.species +from app.routers.admin import router as admin_router + +ROUTE = "species" router = APIRouter( - prefix="/species", - tags=["species"], + prefix=f"/{ROUTE}", + tags=[ROUTE], ) read_many = router.get("")(app.service.species.read_many) read_one = router.get("/{id_}")(app.service.species.read_one) create_one = router.post("")(app.service.species.create_one) +update_one = router.patch("/{id_}")(app.service.species.update_one) + +admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.species.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")(app.service.species.admin_update_one) diff --git a/app/routers/strain.py b/app/routers/strain.py index b50e84c8e..78e822d9f 100644 --- a/app/routers/strain.py +++ b/app/routers/strain.py @@ -1,12 +1,19 @@ from fastapi import APIRouter import app.service.strain +from app.routers.admin import router as admin_router + +ROUTE = "strain" router = APIRouter( - prefix="/strain", - tags=["strain"], + prefix=f"/{ROUTE}", + tags=[ROUTE], ) read_many = router.get("")(app.service.strain.read_many) read_one = router.get("/{id_}")(app.service.strain.read_one) create_one = router.post("")(app.service.strain.create_one) +update_one = router.patch("/{id_}")(app.service.strain.update_one) + +admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.strain.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")(app.service.strain.admin_update_one) diff --git a/app/routers/subject.py b/app/routers/subject.py index 70c52b5de..fd73815a9 100644 --- a/app/routers/subject.py +++ b/app/routers/subject.py @@ -12,3 +12,4 @@ update_one = router.patch("/{id_}")(app.service.subject.update_one) admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.subject.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")(app.service.subject.admin_update_one) diff --git a/app/routers/validation.py b/app/routers/validation.py index 0c7df324c..870bf082e 100644 --- a/app/routers/validation.py +++ b/app/routers/validation.py @@ -13,3 +13,4 @@ update_one = router.patch("/{id_}")(app.service.validation.update_one) admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.validation.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")(app.service.validation.admin_update_one) diff --git a/app/routers/validation_result.py b/app/routers/validation_result.py index 34ce4c0d0..9157585d7 100644 --- a/app/routers/validation_result.py +++ b/app/routers/validation_result.py @@ -12,3 +12,6 @@ update_one = router.patch("/{id_}")(app.service.validation_result.update_one) admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")(app.service.validation_result.admin_read_one) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")( + app.service.validation_result.admin_update_one +) diff --git a/app/schemas/activity.py b/app/schemas/activity.py index ecb54c8e5..c5b662c12 100644 --- a/app/schemas/activity.py +++ b/app/schemas/activity.py @@ -12,6 +12,7 @@ IdentifiableMixin, ) from app.schemas.entity import NestedEntityRead +from app.schemas.utils import NOT_SET, NotSet class ActivityBase(BaseModel): @@ -34,6 +35,7 @@ class ActivityCreate(ActivityBase, AuthorizationOptionalPublicMixin): generated_ids: list[uuid.UUID] = [] -class ActivityUpdate(ActivityBase, AuthorizationOptionalPublicMixin): - end_time: datetime | None = None - generated_ids: list[uuid.UUID] | None = None +class ActivityUpdate(BaseModel): + start_time: datetime | NotSet | None = NOT_SET + end_time: datetime | NotSet | None = NOT_SET + generated_ids: list[uuid.UUID] | NotSet | None = NOT_SET diff --git a/app/schemas/annotation.py b/app/schemas/annotation.py index 4e70df4aa..d152c6d7f 100644 --- a/app/schemas/annotation.py +++ b/app/schemas/annotation.py @@ -1,11 +1,35 @@ +from pydantic import BaseModel + from app.schemas.base import CreationMixin, IdentifiableMixin +from app.schemas.utils import make_update_schema -class Annotation(CreationMixin, IdentifiableMixin): +class AnnotationBase(BaseModel): pref_label: str alt_label: str definition: str -MTypeClassRead = Annotation -ETypeClassRead = Annotation +class AnnotationRead(AnnotationBase, CreationMixin, IdentifiableMixin): + pass + + +class AnnotationCreate(AnnotationBase): + pass + + +AnnotationAdminUpdate = make_update_schema( + AnnotationCreate, + "AnnotationAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] + + +MTypeClassRead = AnnotationRead +ETypeClassRead = AnnotationRead + +MTypeClassCreate = AnnotationCreate +ETypeClassCreate = AnnotationCreate + +MTypeClassAdminUpdate = AnnotationAdminUpdate +ETypeClassAdminUpdate = AnnotationAdminUpdate diff --git a/app/schemas/base.py b/app/schemas/base.py index ef1ed9486..67b900ce3 100644 --- a/app/schemas/base.py +++ b/app/schemas/base.py @@ -4,6 +4,7 @@ from pydantic import UUID4, BaseModel, ConfigDict from app.db.types import ActivityType, EntityType +from app.schemas.utils import make_update_schema class ActivityTypeMixin: @@ -63,6 +64,13 @@ class LicenseRead(LicenseCreate, CreationMixin, IdentifiableMixin): pass +LicenseAdminUpdate = make_update_schema( + LicenseCreate, + "LicenseAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] + + class LicenseReadMixin: license: LicenseRead | None = None @@ -71,17 +79,32 @@ class LicenseCreateMixin: license_id: uuid.UUID | None = None -class BrainRegionRead(IdentifiableMixin, CreationMixin): +class BrainRegionBase(BaseModel): model_config = ConfigDict(from_attributes=True) annotation_value: int name: str acronym: str color_hex_triplet: str - parent_structure_id: uuid.UUID | None + parent_structure_id: uuid.UUID | None = None hierarchy_id: uuid.UUID +class BrainRegionRead(BrainRegionBase, IdentifiableMixin, CreationMixin): + pass + + +class BrainRegionCreate(BrainRegionBase): + pass + + +BrainRegionAdminUpdate = make_update_schema( + BrainRegionCreate, + "BrainRegionAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] + + class BrainRegionCreateMixin(BaseModel): model_config = ConfigDict(from_attributes=True) brain_region_id: uuid.UUID diff --git a/app/schemas/calibration.py b/app/schemas/calibration.py index 5c59627c9..bd02d0d66 100644 --- a/app/schemas/calibration.py +++ b/app/schemas/calibration.py @@ -1,4 +1,5 @@ from app.schemas.activity import ActivityCreate, ActivityRead, ActivityUpdate +from app.schemas.utils import make_update_schema class CalibrationCreate(ActivityCreate): @@ -9,5 +10,12 @@ class CalibrationRead(ActivityRead): pass -class CalibrationUpdate(ActivityUpdate): +class CalibrationUserUpdate(ActivityUpdate): pass + + +CalibrationAdminUpdate = make_update_schema( + CalibrationCreate, + "CalibrationAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] diff --git a/app/schemas/circuit.py b/app/schemas/circuit.py index 466924d1e..93e581b46 100644 --- a/app/schemas/circuit.py +++ b/app/schemas/circuit.py @@ -37,4 +37,9 @@ class CircuitCreate(CircuitBase, ScientificArtifactCreate): pass -CircuitUpdate = make_update_schema(CircuitCreate, "CircuitUpdate") # pyright: ignore [reportInvalidTypeForm] +CircuitUserUpdate = make_update_schema(CircuitCreate, "CircuitUserUpdate") # pyright: ignore [reportInvalidTypeForm] +CircuitAdminUpdate = make_update_schema( + CircuitCreate, + "CircuitAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] diff --git a/app/schemas/density.py b/app/schemas/density.py index 96d705536..b75a75b68 100644 --- a/app/schemas/density.py +++ b/app/schemas/density.py @@ -74,18 +74,36 @@ class ExperimentalSynapsesPerConnectionCreate(ExperimentalDensityCreate): post_region_id: uuid.UUID -ExperimentalSynapsesPerConnectionUpdate = make_update_schema( - ExperimentalSynapsesPerConnectionCreate, "ExperimentalSynapsesPerConnectionUpdate" +ExperimentalSynapsesPerConnectionUserUpdate = make_update_schema( + ExperimentalSynapsesPerConnectionCreate, "ExperimentalSynapsesPerConnectionUserUpdate" ) # pyright: ignore [reportInvalidTypeForm] -ExperimentalBoutonDensityUpdate = make_update_schema( - ExperimentalBoutonDensityCreate, "ExperimentalBoutonDensityUpdate" +ExperimentalSynapsesPerConnectionAdminUpdate = make_update_schema( + ExperimentalSynapsesPerConnectionCreate, + "ExperimentalSynapsesPerConnectionAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] + +ExperimentalBoutonDensityUserUpdate = make_update_schema( + ExperimentalBoutonDensityCreate, "ExperimentalBoutonDensityUserUpdate" ) # pyright: ignore [reportInvalidTypeForm] -ExperimentalNeuronDensityUpdate = make_update_schema( - ExperimentalNeuronDensityCreate, "ExperimentalNeuronDensityUpdate" +ExperimentalBoutonDensityAdminUpdate = make_update_schema( + ExperimentalBoutonDensityCreate, + "ExperimentalBoutonDensityAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] + +ExperimentalNeuronDensityUserUpdate = make_update_schema( + ExperimentalNeuronDensityCreate, "ExperimentalNeuronDensityUserUpdate" ) # pyright: ignore [reportInvalidTypeForm] +ExperimentalNeuronDensityAdminUpdate = make_update_schema( + ExperimentalNeuronDensityCreate, + "ExperimentalNeuronDensityAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] + class ExperimentalNeuronDensityRead(ExperimentalDensityRead): mtypes: list[MTypeClassRead] | None diff --git a/app/schemas/electrical_cell_recording.py b/app/schemas/electrical_cell_recording.py index 76477872e..e116e3234 100644 --- a/app/schemas/electrical_cell_recording.py +++ b/app/schemas/electrical_cell_recording.py @@ -72,8 +72,15 @@ class ElectricalCellRecordingCreate(ElectricalCellRecordingBase, ScientificArtif pass -ElectricalCellRecordingUpdate = make_update_schema( - ElectricalCellRecordingCreate, "ElectricalCellRecordingUpdate" +ElectricalCellRecordingUserUpdate = make_update_schema( + ElectricalCellRecordingCreate, + "ElectricalCellRecordingUserUpdate", +) # pyright : ignore [reportInvalidTypeForm] + +ElectricalCellRecordingAdminUpdate = make_update_schema( + ElectricalCellRecordingCreate, + "ElectricalCellRecordingAdminUpdate", + excluded_fields=set(), ) # pyright : ignore [reportInvalidTypeForm] diff --git a/app/schemas/electrical_recording_stimulus.py b/app/schemas/electrical_recording_stimulus.py index 2c529974f..06ffe5998 100644 --- a/app/schemas/electrical_recording_stimulus.py +++ b/app/schemas/electrical_recording_stimulus.py @@ -49,6 +49,11 @@ class ElectricalRecordingStimulusCreate( pass -ElectricalRecordingStimulusUpdate = make_update_schema( - ElectricalRecordingStimulusCreate, "ElectricalRecordingStimulusUpdate" +ElectricalRecordingStimulusUserUpdate = make_update_schema( + ElectricalRecordingStimulusCreate, "ElectricalRecordingStimulusUserUpdate" ) # pyright: ignore [reportInvalidTypeForm] +ElectricalRecordingStimulusAdminUpdate = make_update_schema( + ElectricalRecordingStimulusCreate, + "ElectricalRecordingStimulusAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] diff --git a/app/schemas/em_cell_mesh.py b/app/schemas/em_cell_mesh.py index 1e56f0026..810296425 100644 --- a/app/schemas/em_cell_mesh.py +++ b/app/schemas/em_cell_mesh.py @@ -32,6 +32,6 @@ class EMCellMeshCreate( em_dense_reconstruction_dataset_id: uuid.UUID -EMCellMeshUpdate = make_update_schema( - EMCellMeshCreate, "EMCellMeshUpdate" +EMCellMeshUserUpdate = make_update_schema( + EMCellMeshCreate, "EMCellMeshUserUpdate" ) # pyright : ignore [reportInvalidTypeForm] diff --git a/app/schemas/emodel.py b/app/schemas/emodel.py index f942ec3ab..43c3362f5 100644 --- a/app/schemas/emodel.py +++ b/app/schemas/emodel.py @@ -40,7 +40,12 @@ class EModelCreate(EModelBase, AuthorizationOptionalPublicMixin): exemplar_morphology_id: uuid.UUID -EModelUpdate = make_update_schema(EModelCreate, "EModelUpdate") # pyright: ignore [reportInvalidTypeForm] +EModelUserUpdate = make_update_schema(EModelCreate, "EModelUserUpdate") # pyright: ignore [reportInvalidTypeForm] +EModelAdminUpdate = make_update_schema( + EModelCreate, + "EModelAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] class EModelRead( diff --git a/app/schemas/ion_channel.py b/app/schemas/ion_channel.py index f7fde08bb..6f4307a89 100644 --- a/app/schemas/ion_channel.py +++ b/app/schemas/ion_channel.py @@ -5,6 +5,7 @@ CreationMixin, IdentifiableMixin, ) +from app.schemas.utils import make_update_schema class IonChannelBase(BaseModel): @@ -34,3 +35,10 @@ class IonChannelRead( class IonChannelCreate(IonChannelBase): """Create model for ion channel.""" + + +IonChannelAdminUpdate = make_update_schema( + IonChannelCreate, + "IonChannelAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] diff --git a/app/schemas/ion_channel_model.py b/app/schemas/ion_channel_model.py index dc38cd3ca..621bea74e 100644 --- a/app/schemas/ion_channel_model.py +++ b/app/schemas/ion_channel_model.py @@ -45,7 +45,12 @@ class IonChannelModelCreate( pass -IonChannelModelUpdate = make_update_schema(IonChannelModelCreate, "IonChannelModelUpdate") # pyright: ignore [reportInvalidTypeForm] +IonChannelModelUserUpdate = make_update_schema(IonChannelModelCreate, "IonChannelModelUserUpdate") # pyright: ignore [reportInvalidTypeForm] +IonChannelModelAdminUpdate = make_update_schema( + IonChannelModelCreate, + "IonChannelModelAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] class IonChannelModelRead( diff --git a/app/schemas/ion_channel_recording.py b/app/schemas/ion_channel_recording.py index c9b49bf3e..9649daf4f 100644 --- a/app/schemas/ion_channel_recording.py +++ b/app/schemas/ion_channel_recording.py @@ -34,8 +34,14 @@ class IonChannelRecordingCreate( ] -IonChannelRecordingUpdate = make_update_schema( - IonChannelRecordingCreate, "IonChannelRecordingUpdate" +IonChannelRecordingUserUpdate = make_update_schema( + IonChannelRecordingCreate, "IonChannelRecordingUserUpdate" +) # pyright : ignore [reportInvalidTypeForm] + +IonChannelRecordingAdminUpdate = make_update_schema( + IonChannelRecordingCreate, + "IonChannelRecordingAdminUpdate", + excluded_fields=set(), ) # pyright : ignore [reportInvalidTypeForm] diff --git a/app/schemas/me_model.py b/app/schemas/me_model.py index 5c1c82d15..fda66d270 100644 --- a/app/schemas/me_model.py +++ b/app/schemas/me_model.py @@ -42,7 +42,13 @@ class MEModelCreate(MEModelBase, AuthorizationOptionalPublicMixin): strain_id: uuid.UUID | None = None -MEModelUpdate = make_update_schema(MEModelCreate, "MEModelUpdate") # pyright: ignore [reportInvalidTypeForm] +MEModelUserUpdate = make_update_schema(MEModelCreate, "MEModelUserUpdate") # pyright: ignore [reportInvalidTypeForm] + +MEModelAdminUpdate = make_update_schema( + MEModelCreate, + "MEModelAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] class MEModelRead( diff --git a/app/schemas/measurement_annotation.py b/app/schemas/measurement_annotation.py index 682b1b4e9..4bcb59625 100644 --- a/app/schemas/measurement_annotation.py +++ b/app/schemas/measurement_annotation.py @@ -6,6 +6,7 @@ from app.db.types import MeasurementStatistic, MeasurementUnit, StructuralDomain from app.db.utils import MeasurableEntityType from app.schemas.base import CreationMixin, IdentifiableMixin +from app.schemas.utils import make_update_schema class MeasurementItem(BaseModel): @@ -43,3 +44,15 @@ class MeasurementAnnotationRead(MeasurementAnnotationBase, CreationMixin, Identi class MeasurementAnnotationCreate(MeasurementAnnotationBase): measurement_kinds: Sequence[MeasurementKindCreate] + + +MeasurementAnnotationUserUpdate = make_update_schema( + MeasurementAnnotationCreate, + "MeasurementAnnotationUserUpdate", +) # pyright : ignore [reportInvalidTypeForm] + +MeasurementAnnotationAdminUpdate = make_update_schema( + MeasurementAnnotationCreate, + "MeasurementAnnotationAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] diff --git a/app/schemas/memodel_calibration_result.py b/app/schemas/memodel_calibration_result.py index 89a9023d3..7cf4563d2 100644 --- a/app/schemas/memodel_calibration_result.py +++ b/app/schemas/memodel_calibration_result.py @@ -37,6 +37,11 @@ class MEModelCalibrationResultCreate( """Create model for MEModel calibration results.""" -MEModelCalibrationResultUpdate = make_update_schema( - MEModelCalibrationResultCreate, "MEModelCalibrationResultUpdate" +MEModelCalibrationResultUserUpdate = make_update_schema( + MEModelCalibrationResultCreate, "MEModelCalibrationResultUserUpdate" ) # pyright: ignore [reportInvalidTypeForm] +MEModelCalibrationResultAdminUpdate = make_update_schema( + MEModelCalibrationResultCreate, + "MEModelCalibrationResultAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] diff --git a/app/schemas/morphology.py b/app/schemas/morphology.py index 8428f4f91..cf899d843 100644 --- a/app/schemas/morphology.py +++ b/app/schemas/morphology.py @@ -41,9 +41,14 @@ class ReconstructionMorphologyCreate( legacy_id: list[str] | None = None -ReconstructionMorphologyUpdate = make_update_schema( - ReconstructionMorphologyCreate, "ReconstructionMorphologyUpdate" +ReconstructionMorphologyUserUpdate = make_update_schema( + ReconstructionMorphologyCreate, "ReconstructionMorphologyUserUpdate" ) # pyright: ignore [reportInvalidTypeForm] +ReconstructionMorphologyAdminUpdate = make_update_schema( + ReconstructionMorphologyCreate, + "ReconstructionMorphologyAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] class ReconstructionMorphologyRead( diff --git a/app/schemas/publication.py b/app/schemas/publication.py index 0f9f88097..e370c2a2d 100644 --- a/app/schemas/publication.py +++ b/app/schemas/publication.py @@ -7,6 +7,7 @@ CreationMixin, IdentifiableMixin, ) +from app.schemas.utils import make_update_schema from app.utils.doi import is_doi @@ -39,6 +40,13 @@ def validate_doi(cls, value: str): return value +PublicationAdminUpdate = make_update_schema( + PublicationCreate, + "PublicationAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] + + class NestedPublicationRead( PublicationBase, IdentifiableMixin, diff --git a/app/schemas/role.py b/app/schemas/role.py index b178c1ba0..3cca12273 100644 --- a/app/schemas/role.py +++ b/app/schemas/role.py @@ -4,6 +4,7 @@ CreationMixin, IdentifiableMixin, ) +from app.schemas.utils import make_update_schema class RoleBase(BaseModel): @@ -18,3 +19,10 @@ class RoleCreate(RoleBase): class RoleRead(RoleBase, CreationMixin, IdentifiableMixin): pass + + +RoleAdminUpdate = make_update_schema( + RoleCreate, + "RoleAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] diff --git a/app/schemas/simulation.py b/app/schemas/simulation.py index f7539ea4e..78feaf7a6 100644 --- a/app/schemas/simulation.py +++ b/app/schemas/simulation.py @@ -37,10 +37,16 @@ class SingleNeuronSimulationCreate( me_model_id: uuid.UUID -SingleNeuronSimulationUpdate = make_update_schema( - SingleNeuronSimulationCreate, "SingleNeuronSimulationUpdate" +SingleNeuronSimulationUserUpdate = make_update_schema( + SingleNeuronSimulationCreate, "SingleNeuronSimulationUserUpdate" ) # pyright: ignore [reportInvalidTypeForm] +SingleNeuronSimulationAdminUpdate = make_update_schema( + SingleNeuronSimulationCreate, + "SingleNeuronSimulationAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] + class SingleNeuronSimulationRead( SingleNeuronSimulationBase, @@ -63,10 +69,16 @@ class SingleNeuronSynaptomeSimulationCreate( synaptome_id: uuid.UUID -SingleNeuronSynaptomeSimulationUpdate = make_update_schema( - SingleNeuronSynaptomeSimulationCreate, "SingleNeuronSynaptomeSimulationUpdate" +SingleNeuronSynaptomeSimulationUserUpdate = make_update_schema( + SingleNeuronSynaptomeSimulationCreate, "SingleNeuronSynaptomeSimulationUserUpdate" ) # pyright: ignore [reportInvalidTypeForm] +SingleNeuronSynaptomeSimulationAdminUpdate = make_update_schema( + SingleNeuronSynaptomeSimulationCreate, + "SingleNeuronSynaptomeSimulationAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] + class SingleNeuronSynaptomeSimulationRead( SingleNeuronSimulationBase, @@ -94,7 +106,13 @@ class SimulationCreate(SimulationBase, AuthorizationOptionalPublicMixin): pass -SimulationUpdate = make_update_schema(SimulationCreate, "SimulationUpdate") # pyright: ignore [reportInvalidTypeForm] +SimulationUserUpdate = make_update_schema(SimulationCreate, "SimulationUserUpdate") # pyright: ignore [reportInvalidTypeForm] + +SimulationAdminUpdate = make_update_schema( + SimulationCreate, + "SimulationAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] class NestedSimulationRead(SimulationBase, EntityTypeMixin, IdentifiableMixin): diff --git a/app/schemas/simulation_campaign.py b/app/schemas/simulation_campaign.py index 937825402..3e0c43f6e 100644 --- a/app/schemas/simulation_campaign.py +++ b/app/schemas/simulation_campaign.py @@ -28,7 +28,14 @@ class SimulationCampaignCreate(SimulationCampaignBase, AuthorizationOptionalPubl pass -SimulationCampaignUpdate = make_update_schema(SimulationCampaignCreate, "SimulationCampaignUpdate") # pyright: ignore [reportInvalidTypeForm] +SimulationCampaignUserUpdate = make_update_schema( + SimulationCampaignCreate, "SimulationCampaignUserUpdate" +) # pyright: ignore [reportInvalidTypeForm] +SimulationCampaignAdminUpdate = make_update_schema( + SimulationCampaignCreate, + "SimulationCampaignAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] class NestedSimulationCampaignRead(SimulationCampaignBase, EntityTypeMixin, IdentifiableMixin): diff --git a/app/schemas/simulation_execution.py b/app/schemas/simulation_execution.py index b48a7e0e6..130e0e787 100644 --- a/app/schemas/simulation_execution.py +++ b/app/schemas/simulation_execution.py @@ -1,5 +1,6 @@ from app.db.types import SimulationExecutionStatus from app.schemas.activity import ActivityCreate, ActivityRead, ActivityUpdate +from app.schemas.utils import make_update_schema class SimulationExecutionCreate(ActivityCreate): @@ -10,5 +11,12 @@ class SimulationExecutionRead(ActivityRead): status: SimulationExecutionStatus -class SimulationExecutionUpdate(ActivityUpdate): +class SimulationExecutionUserUpdate(ActivityUpdate): status: SimulationExecutionStatus | None = None + + +SimulationExecutionAdminUpdate = make_update_schema( + SimulationExecutionCreate, + "SimulationExecutionAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] diff --git a/app/schemas/simulation_generation.py b/app/schemas/simulation_generation.py index 5fd819c24..a78004a9d 100644 --- a/app/schemas/simulation_generation.py +++ b/app/schemas/simulation_generation.py @@ -1,4 +1,5 @@ from app.schemas.activity import ActivityCreate, ActivityRead, ActivityUpdate +from app.schemas.utils import make_update_schema class SimulationGenerationCreate(ActivityCreate): @@ -9,5 +10,12 @@ class SimulationGenerationRead(ActivityRead): pass -class SimulationGenerationUpdate(ActivityUpdate): +class SimulationGenerationUserUpdate(ActivityUpdate): pass + + +SimulationGenerationAdminUpdate = make_update_schema( + SimulationGenerationCreate, + "SimulationGenerationAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] diff --git a/app/schemas/simulation_result.py b/app/schemas/simulation_result.py index a838be3a7..7415fbadf 100644 --- a/app/schemas/simulation_result.py +++ b/app/schemas/simulation_result.py @@ -25,7 +25,14 @@ class SimulationResultCreate(SimulationResultBase, AuthorizationOptionalPublicMi pass -SimulationResultUpdate = make_update_schema(SimulationResultCreate, "SimulationResultUpdate") # pyright: ignore [reportInvalidTypeForm] +SimulationResultUserUpdate = make_update_schema( + SimulationResultCreate, "SimulationResultUserUpdate" +) # pyright: ignore [reportInvalidTypeForm] +SimulationResultAdminUpdate = make_update_schema( + SimulationResultCreate, + "SimulationResultAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] class NestedSimulationResultRead(SimulationResultBase, EntityTypeMixin, IdentifiableMixin): diff --git a/app/schemas/species.py b/app/schemas/species.py index 8aa2a1068..82265e427 100644 --- a/app/schemas/species.py +++ b/app/schemas/species.py @@ -4,6 +4,7 @@ from app.schemas.agent import CreatedByUpdatedByMixin from app.schemas.base import CreationMixin, IdentifiableMixin +from app.schemas.utils import make_update_schema class SpeciesCreate(BaseModel): @@ -16,6 +17,13 @@ class SpeciesRead(SpeciesCreate, CreationMixin, CreatedByUpdatedByMixin, Identif pass +SpeciesAdminUpdate = make_update_schema( + SpeciesCreate, + "SpeciesAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] + + class NestedSpeciesRead(SpeciesCreate, IdentifiableMixin): pass @@ -31,5 +39,12 @@ class StrainRead(StrainCreate, CreationMixin, CreatedByUpdatedByMixin, Identifia pass +StrainAdminUpdate = make_update_schema( + StrainCreate, + "StrainAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] + + class NestedStrainRead(StrainCreate, IdentifiableMixin): pass diff --git a/app/schemas/subject.py b/app/schemas/subject.py index 31ee8b838..188604900 100644 --- a/app/schemas/subject.py +++ b/app/schemas/subject.py @@ -80,7 +80,12 @@ class SubjectCreate(AuthorizationOptionalPublicMixin, SubjectBase): strain_id: uuid.UUID | None = None -SubjectUpdate = make_update_schema(SubjectCreate, "SubjectUpdate") # pyright: ignore [reportInvalidTypeForm] +SubjectUserUpdate = make_update_schema(SubjectCreate, "SubjectUserUpdate") # pyright: ignore [reportInvalidTypeForm] +SubjectAdminUpdate = make_update_schema( + SubjectCreate, + "SubjectAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] class NestedSubjectRead(SubjectBase, IdentifiableMixin): diff --git a/app/schemas/synaptome.py b/app/schemas/synaptome.py index f6926ae4d..1d8a394ba 100644 --- a/app/schemas/synaptome.py +++ b/app/schemas/synaptome.py @@ -32,10 +32,16 @@ class SingleNeuronSynaptomeCreate( brain_region_id: uuid.UUID -SingleNeuronSynaptomeUpdate = make_update_schema( - SingleNeuronSynaptomeCreate, "SingleNeuronSynaptomeUpdate" +SingleNeuronSynaptomeUserUpdate = make_update_schema( + SingleNeuronSynaptomeCreate, "SingleNeuronSynaptomeUserUpdate" ) # pyright: ignore [reportInvalidTypeForm] +SingleNeuronSynaptomeAdminUpdate = make_update_schema( + SingleNeuronSynaptomeCreate, + "SingleNeuronSynaptomeAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] + class NestedSynaptome(SingleNeuronSynaptomeBase, CreationMixin, IdentifiableMixin): pass diff --git a/app/schemas/utils.py b/app/schemas/utils.py index 65843fc39..6dc7c2676 100644 --- a/app/schemas/utils.py +++ b/app/schemas/utils.py @@ -1,14 +1,21 @@ -from typing import Annotated +from typing import Annotated, Literal from pydantic import BaseModel, Field, create_model +type NotSet = Literal[""] + NOT_SET = "" + EXCLUDED_FIELDS = { "authorized_public", } -def make_update_schema(schema: type[BaseModel], new_schema_name: str): +def make_update_schema( + schema: type[BaseModel], + new_schema_name: str | None = None, + excluded_fields: set = EXCLUDED_FIELDS, +): """Create a new pydantic schema from current schema where all fields are optional. In order to differentiate between the user providing a None value and an actual not set by the @@ -27,6 +34,6 @@ def make_optional(field): fields = { name: make_optional(field) for name, field in schema.model_fields.items() - if name not in EXCLUDED_FIELDS + if name not in excluded_fields } return create_model(new_schema_name, **fields) # pyright: ignore reportArgumentType diff --git a/app/schemas/validation.py b/app/schemas/validation.py index f20b4df64..f46ea22a0 100644 --- a/app/schemas/validation.py +++ b/app/schemas/validation.py @@ -22,7 +22,7 @@ class ValidationRead(ActivityRead): pass -class ValidationUpdate(ActivityUpdate): +class ValidationUserUpdate(ActivityUpdate): pass @@ -47,4 +47,18 @@ class ValidationResultCreate(ValidationResultBase, AuthorizationOptionalPublicMi pass -ValidationResultUpdate = make_update_schema(ValidationResultCreate, "ValidationResultUpdate") # pyright: ignore [reportInvalidTypeForm] +ValidationResultUserUpdate = make_update_schema( + ValidationResultCreate, "ValidationResultUserUpdate" +) # pyright: ignore [reportInvalidTypeForm] + +ValidationResultAdminUpdate = make_update_schema( + ValidationResultCreate, + "ValidationResultAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] + +ValidationAdminUpdate = make_update_schema( + ValidationCreate, + "ValidationAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] diff --git a/app/service/brain_region.py b/app/service/brain_region.py index c14cee032..babfb468e 100644 --- a/app/service/brain_region.py +++ b/app/service/brain_region.py @@ -4,11 +4,12 @@ import app.queries.common from app.db.model import BrainRegion +from app.dependencies.auth import AdminContextDep from app.dependencies.common import PaginationQuery from app.dependencies.db import SessionDep from app.errors import ensure_result from app.filters.brain_region import BrainRegionFilterDep -from app.schemas.base import BrainRegionRead +from app.schemas.base import BrainRegionAdminUpdate, BrainRegionCreate, BrainRegionRead from app.schemas.types import ListResponse from app.utils.embedding import generate_embedding @@ -48,3 +49,56 @@ def read_one(db: SessionDep, id_: uuid.UUID) -> BrainRegionRead: stmt = sa.select(BrainRegion).filter(BrainRegion.id == id_) row = db.execute(stmt).scalar_one() return BrainRegionRead.model_validate(row) + + +def admin_read_one(db: SessionDep, id_: uuid.UUID) -> BrainRegionRead: + return read_one(db, id_) + + +def create_one( + *, + db: SessionDep, + json_model: BrainRegionCreate, + user_context: AdminContextDep, +) -> BrainRegionRead: + embedding = generate_embedding(json_model.name) + + return app.queries.common.router_create_one( + db=db, + db_model_class=BrainRegion, + user_context=user_context, + json_model=json_model, + response_schema_class=BrainRegionRead, + embedding=embedding, + ) + + +def update_one( + db: SessionDep, + user_context: AdminContextDep, # noqa: ARG001 + id_: uuid.UUID, + json_model: BrainRegionAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> BrainRegionRead: + return app.queries.common.router_update_one( + id_=id_, + db=db, + db_model_class=BrainRegion, + user_context=None, + json_model=json_model, + response_schema_class=BrainRegionRead, + ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: BrainRegionAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> BrainRegionRead: + return app.queries.common.router_update_one( + id_=id_, + db=db, + db_model_class=BrainRegion, + user_context=None, + json_model=json_model, + response_schema_class=BrainRegionRead, + ) diff --git a/app/service/calibration.py b/app/service/calibration.py index a2271867f..d88c3e776 100644 --- a/app/service/calibration.py +++ b/app/service/calibration.py @@ -23,9 +23,10 @@ ) from app.queries.factory import query_params_factory from app.schemas.calibration import ( + CalibrationAdminUpdate, CalibrationCreate, CalibrationRead, - CalibrationUpdate, + CalibrationUserUpdate, ) from app.schemas.types import ListResponse @@ -167,7 +168,7 @@ def delete_one( def update_one( db: SessionDep, id_: uuid.UUID, - json_model: CalibrationUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: CalibrationUserUpdate, # pyright: ignore [reportInvalidTypeForm] user_context: UserContextWithProjectIdDep, ) -> CalibrationRead: return router_update_activity_one( @@ -179,3 +180,19 @@ def update_one( response_schema_class=CalibrationRead, apply_operations=_load, ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: CalibrationAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> CalibrationRead: + return router_update_activity_one( + db=db, + id_=id_, + json_model=json_model, + user_context=None, + db_model_class=Calibration, + response_schema_class=CalibrationRead, + apply_operations=_load, + ) diff --git a/app/service/circuit.py b/app/service/circuit.py index 5467bc3cf..7e0c0aa2f 100644 --- a/app/service/circuit.py +++ b/app/service/circuit.py @@ -27,9 +27,10 @@ ) from app.queries.factory import query_params_factory from app.schemas.circuit import ( + CircuitAdminUpdate, CircuitCreate, CircuitRead, - CircuitUpdate, + CircuitUserUpdate, ) from app.schemas.types import ListResponse @@ -100,7 +101,7 @@ def update_one( user_context: UserContextDep, db: SessionDep, id_: uuid.UUID, - json_model: CircuitUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: CircuitUserUpdate, # pyright: ignore [reportInvalidTypeForm] ) -> CircuitRead: return router_update_one( id_=id_, @@ -113,6 +114,22 @@ def update_one( ) +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: CircuitAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> CircuitRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=Circuit, + user_context=None, + json_model=json_model, + response_schema_class=CircuitRead, + apply_operations=_load, + ) + + def read_many( user_context: UserContextDep, db: SessionDep, diff --git a/app/service/electrical_cell_recording.py b/app/service/electrical_cell_recording.py index 9a87a9143..9017675f2 100644 --- a/app/service/electrical_cell_recording.py +++ b/app/service/electrical_cell_recording.py @@ -27,9 +27,10 @@ ) from app.queries.factory import query_params_factory from app.schemas.electrical_cell_recording import ( + ElectricalCellRecordingAdminUpdate, ElectricalCellRecordingCreate, ElectricalCellRecordingRead, - ElectricalCellRecordingUpdate, + ElectricalCellRecordingUserUpdate, ) from app.schemas.types import ListResponse @@ -171,7 +172,7 @@ def update_one( user_context: UserContextDep, db: SessionDep, id_: uuid.UUID, - json_model: ElectricalCellRecordingUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: ElectricalCellRecordingUserUpdate, # pyright: ignore [reportInvalidTypeForm] ) -> ElectricalCellRecordingRead: return router_update_one( id_=id_, @@ -182,3 +183,19 @@ def update_one( response_schema_class=ElectricalCellRecordingRead, apply_operations=_load, ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: ElectricalCellRecordingAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> ElectricalCellRecordingRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=ElectricalCellRecording, + user_context=None, + json_model=json_model, + response_schema_class=ElectricalCellRecordingRead, + apply_operations=_load, + ) diff --git a/app/service/electrical_recording_stimulus.py b/app/service/electrical_recording_stimulus.py index ddc13537a..c526d6257 100644 --- a/app/service/electrical_recording_stimulus.py +++ b/app/service/electrical_recording_stimulus.py @@ -25,9 +25,10 @@ ) from app.queries.factory import query_params_factory from app.schemas.electrical_recording_stimulus import ( + ElectricalRecordingStimulusAdminUpdate, ElectricalRecordingStimulusCreate, ElectricalRecordingStimulusRead, - ElectricalRecordingStimulusUpdate, + ElectricalRecordingStimulusUserUpdate, ) from app.schemas.types import ListResponse @@ -91,7 +92,7 @@ def update_one( user_context: UserContextDep, db: SessionDep, id_: uuid.UUID, - json_model: ElectricalRecordingStimulusUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: ElectricalRecordingStimulusUserUpdate, # pyright: ignore [reportInvalidTypeForm] ) -> ElectricalRecordingStimulusRead: return router_update_one( id_=id_, @@ -152,3 +153,19 @@ def read_many( authorized_project_id=user_context.project_id, filter_joins=filter_joins, ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: ElectricalRecordingStimulusAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> ElectricalRecordingStimulusRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=ElectricalRecordingStimulus, + user_context=None, + json_model=json_model, + response_schema_class=ElectricalRecordingStimulusRead, + apply_operations=_load, + ) diff --git a/app/service/em_cell_mesh.py b/app/service/em_cell_mesh.py index a8b6c0ca8..a96d9d73a 100644 --- a/app/service/em_cell_mesh.py +++ b/app/service/em_cell_mesh.py @@ -20,7 +20,7 @@ router_update_one, ) from app.queries.factory import query_params_factory -from app.schemas.em_cell_mesh import EMCellMeshCreate, EMCellMeshRead, EMCellMeshUpdate +from app.schemas.em_cell_mesh import EMCellMeshCreate, EMCellMeshRead, EMCellMeshUserUpdate from app.schemas.types import ListResponse, Select if TYPE_CHECKING: @@ -113,6 +113,20 @@ def read_one( ) +def admin_read_one( + db: SessionDep, + id_: uuid.UUID, +) -> EMCellMeshRead: + return router_read_one( + id_=id_, + db=db, + db_model_class=EMCellMesh, + authorized_project_id=None, + response_schema_class=EMCellMeshRead, + apply_operations=_load, + ) + + def create_one( user_context: UserContextWithProjectIdDep, db: SessionDep, @@ -132,7 +146,7 @@ def update_one( user_context: UserContextDep, db: SessionDep, id_: uuid.UUID, - json_model: EMCellMeshUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: EMCellMeshUserUpdate, # pyright: ignore [reportInvalidTypeForm] ) -> EMCellMeshRead: return router_update_one( id_=id_, @@ -143,3 +157,19 @@ def update_one( response_schema_class=EMCellMeshRead, apply_operations=_load, ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: EMCellMeshUserUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> EMCellMeshRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=EMCellMesh, + user_context=None, + json_model=json_model, + response_schema_class=EMCellMeshRead, + apply_operations=_load, + ) diff --git a/app/service/emodel.py b/app/service/emodel.py index 7f2c9af35..7c17f4e9a 100644 --- a/app/service/emodel.py +++ b/app/service/emodel.py @@ -28,7 +28,13 @@ router_update_one, ) from app.queries.factory import query_params_factory -from app.schemas.emodel import EModelCreate, EModelRead, EModelReadExpanded, EModelUpdate +from app.schemas.emodel import ( + EModelAdminUpdate, + EModelCreate, + EModelRead, + EModelReadExpanded, + EModelUserUpdate, +) from app.schemas.types import ListResponse if TYPE_CHECKING: @@ -108,7 +114,7 @@ def update_one( user_context: UserContextDep, db: SessionDep, id_: uuid.UUID, - json_model: EModelUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: EModelUserUpdate, # pyright: ignore [reportInvalidTypeForm] ) -> EModelRead: return router_update_one( id_=id_, @@ -121,6 +127,22 @@ def update_one( ) +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: EModelAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> EModelRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=EModel, + user_context=None, + json_model=json_model, + response_schema_class=EModelRead, + apply_operations=_load, + ) + + def read_many( *, user_context: UserContextDep, diff --git a/app/service/etype.py b/app/service/etype.py index 14e4b1334..01ea11b3f 100644 --- a/app/service/etype.py +++ b/app/service/etype.py @@ -1,11 +1,17 @@ import uuid from app.db.model import ETypeClass +from app.dependencies.auth import AdminContextDep from app.dependencies.common import PaginationQuery from app.dependencies.db import SessionDep from app.filters.common import ETypeClassFilterDep -from app.queries.common import router_read_many, router_read_one -from app.schemas.annotation import ETypeClassRead +from app.queries.common import ( + router_create_one, + router_read_many, + router_read_one, + router_update_one, +) +from app.schemas.annotation import ETypeClassAdminUpdate, ETypeClassCreate, ETypeClassRead from app.schemas.types import ListResponse @@ -40,3 +46,53 @@ def read_one(id_: uuid.UUID, db: SessionDep) -> ETypeClassRead: response_schema_class=ETypeClassRead, apply_operations=None, ) + + +def admin_read_one(db: SessionDep, id_: uuid.UUID) -> ETypeClassRead: + return read_one(db=db, id_=id_) + + +def create_one( + *, + db: SessionDep, + json_model: ETypeClassCreate, + user_context: AdminContextDep, +) -> ETypeClassRead: + return router_create_one( + db=db, + db_model_class=ETypeClass, + user_context=user_context, + json_model=json_model, + response_schema_class=ETypeClassRead, + ) + + +def update_one( + db: SessionDep, + user_context: AdminContextDep, # noqa: ARG001 + id_: uuid.UUID, + json_model: ETypeClassAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> ETypeClassRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=ETypeClass, + user_context=None, + json_model=json_model, + response_schema_class=ETypeClassRead, + ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: ETypeClassAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> ETypeClassRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=ETypeClass, + user_context=None, + json_model=json_model, + response_schema_class=ETypeClassRead, + ) diff --git a/app/service/experimental_bouton_density.py b/app/service/experimental_bouton_density.py index 118d02ea4..40bc0a55a 100644 --- a/app/service/experimental_bouton_density.py +++ b/app/service/experimental_bouton_density.py @@ -31,9 +31,10 @@ ) from app.queries.factory import query_params_factory from app.schemas.density import ( + ExperimentalBoutonDensityAdminUpdate, ExperimentalBoutonDensityCreate, ExperimentalBoutonDensityRead, - ExperimentalBoutonDensityUpdate, + ExperimentalBoutonDensityUserUpdate, ) from app.schemas.types import ListResponse @@ -167,7 +168,7 @@ def update_one( user_context: UserContextDep, db: SessionDep, id_: uuid.UUID, - json_model: ExperimentalBoutonDensityUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: ExperimentalBoutonDensityUserUpdate, # pyright: ignore [reportInvalidTypeForm] ) -> ExperimentalBoutonDensityRead: return router_update_one( id_=id_, @@ -178,3 +179,19 @@ def update_one( response_schema_class=ExperimentalBoutonDensityRead, apply_operations=_load, ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: ExperimentalBoutonDensityAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> ExperimentalBoutonDensityRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=ExperimentalBoutonDensity, + user_context=None, + json_model=json_model, + response_schema_class=ExperimentalBoutonDensityRead, + apply_operations=_load, + ) diff --git a/app/service/experimental_neuron_density.py b/app/service/experimental_neuron_density.py index 5474cf7b4..a8165e86d 100644 --- a/app/service/experimental_neuron_density.py +++ b/app/service/experimental_neuron_density.py @@ -31,9 +31,10 @@ ) from app.queries.factory import query_params_factory from app.schemas.density import ( + ExperimentalNeuronDensityAdminUpdate, ExperimentalNeuronDensityCreate, ExperimentalNeuronDensityRead, - ExperimentalNeuronDensityUpdate, + ExperimentalNeuronDensityUserUpdate, ) from app.schemas.types import ListResponse @@ -170,7 +171,7 @@ def update_one( user_context: UserContextDep, db: SessionDep, id_: uuid.UUID, - json_model: ExperimentalNeuronDensityUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: ExperimentalNeuronDensityUserUpdate, # pyright: ignore [reportInvalidTypeForm] ) -> ExperimentalNeuronDensityRead: return router_update_one( id_=id_, @@ -181,3 +182,19 @@ def update_one( response_schema_class=ExperimentalNeuronDensityRead, apply_operations=_load, ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: ExperimentalNeuronDensityAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> ExperimentalNeuronDensityRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=ExperimentalNeuronDensity, + user_context=None, + json_model=json_model, + response_schema_class=ExperimentalNeuronDensityRead, + apply_operations=_load, + ) diff --git a/app/service/experimental_synapses_per_connection.py b/app/service/experimental_synapses_per_connection.py index dc5e56f6b..7d0bb0640 100644 --- a/app/service/experimental_synapses_per_connection.py +++ b/app/service/experimental_synapses_per_connection.py @@ -33,9 +33,10 @@ ) from app.queries.factory import query_params_factory from app.schemas.density import ( + ExperimentalSynapsesPerConnectionAdminUpdate, ExperimentalSynapsesPerConnectionCreate, ExperimentalSynapsesPerConnectionRead, - ExperimentalSynapsesPerConnectionUpdate, + ExperimentalSynapsesPerConnectionUserUpdate, ) from app.schemas.types import ListResponse @@ -193,7 +194,7 @@ def update_one( user_context: UserContextDep, db: SessionDep, id_: uuid.UUID, - json_model: ExperimentalSynapsesPerConnectionUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: ExperimentalSynapsesPerConnectionUserUpdate, # pyright: ignore [reportInvalidTypeForm] ) -> ExperimentalSynapsesPerConnectionRead: return router_update_one( id_=id_, @@ -204,3 +205,19 @@ def update_one( response_schema_class=ExperimentalSynapsesPerConnectionRead, apply_operations=_load, ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: ExperimentalSynapsesPerConnectionAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> ExperimentalSynapsesPerConnectionRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=ExperimentalSynapsesPerConnection, + user_context=None, + json_model=json_model, + response_schema_class=ExperimentalSynapsesPerConnectionRead, + apply_operations=_load, + ) diff --git a/app/service/external_url.py b/app/service/external_url.py index 8636daca4..e661f845c 100644 --- a/app/service/external_url.py +++ b/app/service/external_url.py @@ -48,15 +48,7 @@ def read_one( ) -def admin_read_one(db: SessionDep, id_: uuid.UUID) -> ExternalUrlRead: - return router_read_one( - db=db, - id_=id_, - db_model_class=ExternalUrl, - authorized_project_id=None, - response_schema_class=ExternalUrlRead, - apply_operations=_load, - ) +admin_read_one = read_one def create_one( diff --git a/app/service/ion_channel.py b/app/service/ion_channel.py index bf64ba1a1..24046c59f 100644 --- a/app/service/ion_channel.py +++ b/app/service/ion_channel.py @@ -13,9 +13,15 @@ ) from app.dependencies.db import SessionDep from app.filters.ion_channel import IonChannelFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one +from app.queries.common import ( + router_create_one, + router_read_many, + router_read_one, + router_update_one, +) from app.queries.factory import query_params_factory from app.schemas.ion_channel import ( + IonChannelAdminUpdate, IonChannelCreate, IonChannelRead, ) @@ -48,6 +54,14 @@ def read_one( ) +def admin_read_one( + *, + db: SessionDep, + id_: uuid.UUID, +) -> IonChannelRead: + return read_one(id_=id_, db=db) + + def create_one( *, db: SessionDep, @@ -111,3 +125,34 @@ def read_many( authorized_project_id=None, filter_joins=filter_joins, ) + + +def update_one( + db: SessionDep, + user_context: AdminContextDep, # noqa: ARG001 + id_: uuid.UUID, + json_model: IonChannelAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> IonChannelRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=IonChannel, + user_context=None, + json_model=json_model, + response_schema_class=IonChannelRead, + ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: IonChannelAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> IonChannelRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=IonChannel, + user_context=None, + json_model=json_model, + response_schema_class=IonChannelRead, + ) diff --git a/app/service/ion_channel_model.py b/app/service/ion_channel_model.py index 035b77a73..74fe798fc 100644 --- a/app/service/ion_channel_model.py +++ b/app/service/ion_channel_model.py @@ -24,10 +24,11 @@ ) from app.queries.factory import query_params_factory from app.schemas.ion_channel_model import ( + IonChannelModelAdminUpdate, IonChannelModelCreate, IonChannelModelExpanded, IonChannelModelRead, - IonChannelModelUpdate, + IonChannelModelUserUpdate, ) from app.schemas.types import ListResponse, Select @@ -169,7 +170,7 @@ def update_one( user_context: UserContextDep, db: SessionDep, id_: uuid.UUID, - json_model: IonChannelModelUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: IonChannelModelUserUpdate, # pyright: ignore [reportInvalidTypeForm] ) -> IonChannelModelRead: return router_update_one( id_=id_, @@ -180,3 +181,19 @@ def update_one( response_schema_class=IonChannelModelRead, apply_operations=_load_minimal, ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: IonChannelModelAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> IonChannelModelRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=IonChannelModel, + user_context=None, + json_model=json_model, + response_schema_class=IonChannelModelRead, + apply_operations=_load_minimal, + ) diff --git a/app/service/ion_channel_recording.py b/app/service/ion_channel_recording.py index c0638dca2..62b36b580 100644 --- a/app/service/ion_channel_recording.py +++ b/app/service/ion_channel_recording.py @@ -27,9 +27,10 @@ ) from app.queries.factory import query_params_factory from app.schemas.ion_channel_recording import ( + IonChannelRecordingAdminUpdate, IonChannelRecordingCreate, IonChannelRecordingRead, - IonChannelRecordingUpdate, + IonChannelRecordingUserUpdate, ) from app.schemas.types import ListResponse @@ -73,6 +74,20 @@ def read_one( ) +def admin_read_one( + db: SessionDep, + id_: uuid.UUID, +) -> IonChannelRecordingRead: + return router_read_one( + db=db, + id_=id_, + db_model_class=IonChannelRecording, + authorized_project_id=None, + response_schema_class=IonChannelRecordingRead, + apply_operations=_load, + ) + + def create_one( db: SessionDep, json_model: IonChannelRecordingCreate, @@ -157,7 +172,7 @@ def update_one( user_context: UserContextDep, db: SessionDep, id_: uuid.UUID, - json_model: IonChannelRecordingUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: IonChannelRecordingUserUpdate, # pyright: ignore [reportInvalidTypeForm] ) -> IonChannelRecordingRead: return router_update_one( id_=id_, @@ -168,3 +183,19 @@ def update_one( response_schema_class=IonChannelRecordingRead, apply_operations=_load, ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: IonChannelRecordingAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> IonChannelRecordingRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=IonChannelRecording, + user_context=None, + json_model=json_model, + response_schema_class=IonChannelRecordingRead, + apply_operations=_load, + ) diff --git a/app/service/license.py b/app/service/license.py index e1177097e..a350a48ef 100644 --- a/app/service/license.py +++ b/app/service/license.py @@ -5,8 +5,13 @@ from app.dependencies.common import PaginationQuery from app.dependencies.db import SessionDep from app.filters.license import LicenseFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one -from app.schemas.base import LicenseCreate, LicenseRead +from app.queries.common import ( + router_create_one, + router_read_many, + router_read_one, + router_update_one, +) +from app.schemas.base import LicenseAdminUpdate, LicenseCreate, LicenseRead from app.schemas.types import ListResponse @@ -41,6 +46,14 @@ def read_one(id_: uuid.UUID, db: SessionDep) -> LicenseRead: ) +def admin_read_one( + *, + db: SessionDep, + id_: uuid.UUID, +) -> LicenseRead: + return read_one(id_=id_, db=db) + + def create_one( license: LicenseCreate, db: SessionDep, user_context: AdminContextDep ) -> LicenseRead: @@ -51,3 +64,34 @@ def create_one( response_schema_class=LicenseRead, user_context=user_context, ) + + +def update_one( + db: SessionDep, + user_context: AdminContextDep, # noqa: ARG001 + id_: uuid.UUID, + json_model: LicenseAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> LicenseRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=License, + user_context=None, + json_model=json_model, + response_schema_class=LicenseRead, + ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: LicenseAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> LicenseRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=License, + user_context=None, + json_model=json_model, + response_schema_class=LicenseRead, + ) diff --git a/app/service/measurement_annotation.py b/app/service/measurement_annotation.py index ebbac1c54..010d8bd2d 100644 --- a/app/service/measurement_annotation.py +++ b/app/service/measurement_annotation.py @@ -2,6 +2,7 @@ import sqlalchemy as sa from sqlalchemy.orm import ( + aliased, contains_eager, raiseload, selectinload, @@ -25,12 +26,15 @@ router_delete_one, router_read_many, router_read_one, + router_update_one, ) from app.queries.entity import get_writable_entity from app.queries.factory import query_params_factory from app.schemas.measurement_annotation import ( + MeasurementAnnotationAdminUpdate, MeasurementAnnotationCreate, MeasurementAnnotationRead, + MeasurementAnnotationUserUpdate, ) from app.schemas.types import ListResponse @@ -45,6 +49,16 @@ def _load_from_db(q: sa.Select) -> sa.Select: ) +def _load_from_db_with_entity(q: sa.Select) -> sa.Select: + return q.options( + selectinload(MeasurementAnnotation.measurement_kinds).selectinload( + MeasurementKind.measurement_items + ), + selectinload(MeasurementAnnotation.entity), + raiseload("*"), + ) + + def read_many( user_context: UserContextDep, db: SessionDep, @@ -109,13 +123,17 @@ def admin_read_one( db: SessionDep, id_: uuid.UUID, ) -> MeasurementAnnotationRead: + def apply_operations(q): + q = q.join(Entity, Entity.id == MeasurementAnnotation.entity_id) + return _load_from_db(q=q) + return router_read_one( id_=id_, db=db, db_model_class=MeasurementAnnotation, authorized_project_id=None, response_schema_class=MeasurementAnnotationRead, - apply_operations=_load_from_db, + apply_operations=apply_operations, ) @@ -171,3 +189,47 @@ def apply_operations(q): authorized_project_id=None, # already validated ) return one + + +def update_one( + user_context: UserContextWithProjectIdDep, + db: SessionDep, + id_: uuid.UUID, + json_model: MeasurementAnnotationUserUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> MeasurementAnnotationRead: + def apply_operations(q): + entity_alias = aliased(Entity) + q = q.join(entity_alias, entity_alias.id == MeasurementAnnotation.entity_id) + q = q.where(entity_alias.authorized_project_id == user_context.project_id) + return _load_from_db_with_entity(q=q) + + return router_update_one( + id_=id_, + db=db, + db_model_class=MeasurementAnnotation, + user_context=None, + json_model=json_model, + response_schema_class=MeasurementAnnotationRead, + apply_operations=apply_operations, + ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: MeasurementAnnotationAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> MeasurementAnnotationRead: + def apply_operations(q): + entity_alias = aliased(Entity) + q = q.join(entity_alias, entity_alias.id == MeasurementAnnotation.entity_id) + return _load_from_db_with_entity(q=q) + + return router_update_one( + id_=id_, + db=db, + db_model_class=MeasurementAnnotation, + user_context=None, + json_model=json_model, + response_schema_class=MeasurementAnnotationRead, + apply_operations=apply_operations, + ) diff --git a/app/service/memodel.py b/app/service/memodel.py index fac7c2a8a..ba874cdfd 100644 --- a/app/service/memodel.py +++ b/app/service/memodel.py @@ -33,7 +33,7 @@ router_update_one, ) from app.queries.factory import query_params_factory -from app.schemas.me_model import MEModelCreate, MEModelRead, MEModelUpdate +from app.schemas.me_model import MEModelAdminUpdate, MEModelCreate, MEModelRead, MEModelUserUpdate from app.schemas.types import ListResponse if TYPE_CHECKING: @@ -123,7 +123,7 @@ def update_one( user_context: UserContextDep, db: SessionDep, id_: uuid.UUID, - json_model: MEModelUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: MEModelUserUpdate, # pyright: ignore [reportInvalidTypeForm] ) -> MEModelRead: return router_update_one( id_=id_, @@ -136,6 +136,22 @@ def update_one( ) +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: MEModelAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> MEModelRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=MEModel, + user_context=None, + json_model=json_model, + response_schema_class=MEModelRead, + apply_operations=_load, + ) + + def read_many( *, db: SessionDep, diff --git a/app/service/memodel_calibration_result.py b/app/service/memodel_calibration_result.py index 21db781a8..7d9bba027 100644 --- a/app/service/memodel_calibration_result.py +++ b/app/service/memodel_calibration_result.py @@ -20,9 +20,10 @@ router_update_one, ) from app.schemas.memodel_calibration_result import ( + MEModelCalibrationResultAdminUpdate, MEModelCalibrationResultCreate, MEModelCalibrationResultRead, - MEModelCalibrationResultUpdate, + MEModelCalibrationResultUserUpdate, ) from app.schemas.types import ListResponse @@ -80,7 +81,7 @@ def update_one( user_context: UserContextDep, db: SessionDep, id_: uuid.UUID, - json_model: MEModelCalibrationResultUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: MEModelCalibrationResultUserUpdate, # pyright: ignore [reportInvalidTypeForm] ) -> MEModelCalibrationResultRead: return router_update_one( id_=id_, @@ -93,6 +94,22 @@ def update_one( ) +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: MEModelCalibrationResultAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> MEModelCalibrationResultRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=MEModelCalibrationResult, + user_context=None, + json_model=json_model, + response_schema_class=MEModelCalibrationResultRead, + apply_operations=_load, + ) + + def read_many( user_context: UserContextDep, db: SessionDep, diff --git a/app/service/morphology.py b/app/service/morphology.py index c173a51f4..559770b50 100644 --- a/app/service/morphology.py +++ b/app/service/morphology.py @@ -35,10 +35,11 @@ ) from app.queries.factory import query_params_factory from app.schemas.morphology import ( + ReconstructionMorphologyAdminUpdate, ReconstructionMorphologyAnnotationExpandedRead, ReconstructionMorphologyCreate, ReconstructionMorphologyRead, - ReconstructionMorphologyUpdate, + ReconstructionMorphologyUserUpdate, ) from app.schemas.types import ListResponse @@ -128,7 +129,7 @@ def update_one( user_context: UserContextDep, db: SessionDep, id_: uuid.UUID, - json_model: ReconstructionMorphologyUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: ReconstructionMorphologyUserUpdate, # pyright: ignore [reportInvalidTypeForm] ) -> ReconstructionMorphologyRead: return router_update_one( id_=id_, @@ -141,6 +142,22 @@ def update_one( ) +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: ReconstructionMorphologyAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> ReconstructionMorphologyRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=ReconstructionMorphology, + user_context=None, + json_model=json_model, + response_schema_class=ReconstructionMorphologyRead, + apply_operations=_load_from_db, + ) + + def read_many( *, user_context: UserContextDep, diff --git a/app/service/mtype.py b/app/service/mtype.py index 6d4c1a98f..1908e55e8 100644 --- a/app/service/mtype.py +++ b/app/service/mtype.py @@ -1,11 +1,17 @@ import uuid from app.db.model import MTypeClass +from app.dependencies.auth import AdminContextDep from app.dependencies.common import PaginationQuery from app.dependencies.db import SessionDep from app.filters.common import MTypeClassFilterDep -from app.queries.common import router_read_many, router_read_one -from app.schemas.annotation import MTypeClassRead +from app.queries.common import ( + router_create_one, + router_read_many, + router_read_one, + router_update_one, +) +from app.schemas.annotation import MTypeClassAdminUpdate, MTypeClassCreate, MTypeClassRead from app.schemas.types import ListResponse @@ -40,3 +46,53 @@ def read_one(id_: uuid.UUID, db: SessionDep) -> MTypeClassRead: response_schema_class=MTypeClassRead, apply_operations=None, ) + + +def admin_read_one(db: SessionDep, id_: uuid.UUID) -> MTypeClassRead: + return read_one(db=db, id_=id_) + + +def create_one( + *, + db: SessionDep, + json_model: MTypeClassCreate, + user_context: AdminContextDep, +) -> MTypeClassRead: + return router_create_one( + db=db, + db_model_class=MTypeClass, + user_context=user_context, + json_model=json_model, + response_schema_class=MTypeClassRead, + ) + + +def update_one( + db: SessionDep, + user_context: AdminContextDep, # noqa: ARG001 + id_: uuid.UUID, + json_model: MTypeClassAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> MTypeClassRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=MTypeClass, + user_context=None, + json_model=json_model, + response_schema_class=MTypeClassRead, + ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: MTypeClassAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> MTypeClassRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=MTypeClass, + user_context=None, + json_model=json_model, + response_schema_class=MTypeClassRead, + ) diff --git a/app/service/publication.py b/app/service/publication.py index 2ef761ee0..1e652f556 100644 --- a/app/service/publication.py +++ b/app/service/publication.py @@ -14,9 +14,15 @@ ) from app.dependencies.db import SessionDep from app.filters.publication import PublicationFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one +from app.queries.common import ( + router_create_one, + router_read_many, + router_read_one, + router_update_one, +) from app.queries.factory import query_params_factory from app.schemas.publication import ( + PublicationAdminUpdate, PublicationCreate, PublicationRead, ) @@ -49,14 +55,7 @@ def read_one( def admin_read_one(db: SessionDep, id_: uuid.UUID) -> PublicationRead: - return router_read_one( - db=db, - id_=id_, - db_model_class=Publication, - authorized_project_id=None, - response_schema_class=PublicationRead, - apply_operations=_load, - ) + return read_one(db=db, id_=id_) def create_one( @@ -121,3 +120,34 @@ def read_many( authorized_project_id=None, filter_joins=filter_joins, ) + + +def update_one( + db: SessionDep, + user_context: AdminContextDep, # noqa: ARG001 + id_: uuid.UUID, + json_model: PublicationAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> PublicationRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=Publication, + user_context=None, + json_model=json_model, + response_schema_class=PublicationRead, + ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: PublicationAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> PublicationRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=Publication, + user_context=None, + json_model=json_model, + response_schema_class=PublicationRead, + ) diff --git a/app/service/role.py b/app/service/role.py index 0cdbdc616..703cd660f 100644 --- a/app/service/role.py +++ b/app/service/role.py @@ -5,8 +5,13 @@ from app.dependencies.common import PaginationQuery from app.dependencies.db import SessionDep from app.filters.role import RoleFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one -from app.schemas.role import RoleCreate, RoleRead +from app.queries.common import ( + router_create_one, + router_read_many, + router_read_one, + router_update_one, +) +from app.schemas.role import RoleAdminUpdate, RoleCreate, RoleRead from app.schemas.types import ListResponse @@ -41,6 +46,10 @@ def read_one(id_: uuid.UUID, db: SessionDep) -> RoleRead: ) +def admin_read_one(db: SessionDep, id_: uuid.UUID) -> RoleRead: + return read_one(db=db, id_=id_) + + def create_one(json_model: RoleCreate, db: SessionDep, user_context: AdminContextDep) -> RoleRead: return router_create_one( db=db, @@ -49,3 +58,34 @@ def create_one(json_model: RoleCreate, db: SessionDep, user_context: AdminContex response_schema_class=RoleRead, user_context=user_context, ) + + +def update_one( + db: SessionDep, + user_context: AdminContextDep, # noqa: ARG001 + id_: uuid.UUID, + json_model: RoleAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> RoleRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=Role, + user_context=None, + json_model=json_model, + response_schema_class=RoleRead, + ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: RoleAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> RoleRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=Role, + user_context=None, + json_model=json_model, + response_schema_class=RoleRead, + ) diff --git a/app/service/simulation.py b/app/service/simulation.py index 6db63ec31..5cc17509e 100644 --- a/app/service/simulation.py +++ b/app/service/simulation.py @@ -25,9 +25,10 @@ ) from app.queries.factory import query_params_factory from app.schemas.simulation import ( + SimulationAdminUpdate, SimulationCreate, SimulationRead, - SimulationUpdate, + SimulationUserUpdate, ) from app.schemas.types import ListResponse @@ -93,7 +94,7 @@ def update_one( user_context: UserContextDep, db: SessionDep, id_: uuid.UUID, - json_model: SimulationUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: SimulationUserUpdate, # pyright: ignore [reportInvalidTypeForm] ) -> SimulationRead: return router_update_one( id_=id_, @@ -106,6 +107,22 @@ def update_one( ) +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: SimulationAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> SimulationRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=Simulation, + user_context=None, + json_model=json_model, + response_schema_class=SimulationRead, + apply_operations=_load, + ) + + def read_many( user_context: UserContextDep, db: SessionDep, diff --git a/app/service/simulation_campaign.py b/app/service/simulation_campaign.py index cf3e08466..5a47ca781 100644 --- a/app/service/simulation_campaign.py +++ b/app/service/simulation_campaign.py @@ -27,9 +27,10 @@ ) from app.queries.factory import query_params_factory from app.schemas.simulation_campaign import ( + SimulationCampaignAdminUpdate, SimulationCampaignCreate, SimulationCampaignRead, - SimulationCampaignUpdate, + SimulationCampaignUserUpdate, ) from app.schemas.types import ListResponse @@ -96,7 +97,7 @@ def update_one( user_context: UserContextDep, db: SessionDep, id_: uuid.UUID, - json_model: SimulationCampaignUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: SimulationCampaignUserUpdate, # pyright: ignore [reportInvalidTypeForm] ) -> SimulationCampaignRead: return router_update_one( id_=id_, @@ -109,6 +110,22 @@ def update_one( ) +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: SimulationCampaignAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> SimulationCampaignRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=SimulationCampaign, + user_context=None, + json_model=json_model, + response_schema_class=SimulationCampaignRead, + apply_operations=_load, + ) + + def read_many( user_context: UserContextDep, db: SessionDep, diff --git a/app/service/simulation_execution.py b/app/service/simulation_execution.py index 3ec985606..7d9ad8efc 100644 --- a/app/service/simulation_execution.py +++ b/app/service/simulation_execution.py @@ -23,9 +23,10 @@ ) from app.queries.factory import query_params_factory from app.schemas.simulation_execution import ( + SimulationExecutionAdminUpdate, SimulationExecutionCreate, SimulationExecutionRead, - SimulationExecutionUpdate, + SimulationExecutionUserUpdate, ) from app.schemas.types import ListResponse @@ -167,7 +168,7 @@ def delete_one( def update_one( db: SessionDep, id_: uuid.UUID, - json_model: SimulationExecutionUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: SimulationExecutionUserUpdate, # pyright: ignore [reportInvalidTypeForm] user_context: UserContextWithProjectIdDep, ) -> SimulationExecutionRead: return router_update_activity_one( @@ -179,3 +180,19 @@ def update_one( response_schema_class=SimulationExecutionRead, apply_operations=_load, ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: SimulationExecutionAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> SimulationExecutionRead: + return router_update_activity_one( + db=db, + id_=id_, + json_model=json_model, + user_context=None, + db_model_class=SimulationExecution, + response_schema_class=SimulationExecutionRead, + apply_operations=_load, + ) diff --git a/app/service/simulation_generation.py b/app/service/simulation_generation.py index 85b5f12a1..6e14f527c 100644 --- a/app/service/simulation_generation.py +++ b/app/service/simulation_generation.py @@ -23,9 +23,10 @@ ) from app.queries.factory import query_params_factory from app.schemas.simulation_generation import ( + SimulationGenerationAdminUpdate, SimulationGenerationCreate, SimulationGenerationRead, - SimulationGenerationUpdate, + SimulationGenerationUserUpdate, ) from app.schemas.types import ListResponse @@ -162,7 +163,7 @@ def delete_one( def update_one( db: SessionDep, id_: uuid.UUID, - json_model: SimulationGenerationUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: SimulationGenerationUserUpdate, # pyright: ignore [reportInvalidTypeForm] user_context: UserContextWithProjectIdDep, ) -> SimulationGenerationRead: return router_update_activity_one( @@ -174,3 +175,19 @@ def update_one( response_schema_class=SimulationGenerationRead, apply_operations=_load, ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: SimulationGenerationAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> SimulationGenerationRead: + return router_update_activity_one( + db=db, + id_=id_, + json_model=json_model, + user_context=None, + db_model_class=SimulationGeneration, + response_schema_class=SimulationGenerationRead, + apply_operations=_load, + ) diff --git a/app/service/simulation_result.py b/app/service/simulation_result.py index 5f12e5b75..f78b8abf6 100644 --- a/app/service/simulation_result.py +++ b/app/service/simulation_result.py @@ -25,9 +25,10 @@ ) from app.queries.factory import query_params_factory from app.schemas.simulation_result import ( + SimulationResultAdminUpdate, SimulationResultCreate, SimulationResultRead, - SimulationResultUpdate, + SimulationResultUserUpdate, ) from app.schemas.types import ListResponse @@ -92,7 +93,7 @@ def update_one( user_context: UserContextDep, db: SessionDep, id_: uuid.UUID, - json_model: SimulationResultUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: SimulationResultUserUpdate, # pyright: ignore [reportInvalidTypeForm] ) -> SimulationResultRead: return router_update_one( id_=id_, @@ -105,6 +106,22 @@ def update_one( ) +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: SimulationResultAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> SimulationResultRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=SimulationResult, + user_context=None, + json_model=json_model, + response_schema_class=SimulationResultRead, + apply_operations=_load, + ) + + def read_many( user_context: UserContextDep, db: SessionDep, diff --git a/app/service/single_neuron_simulation.py b/app/service/single_neuron_simulation.py index 007b2a4a8..e1025d0f0 100644 --- a/app/service/single_neuron_simulation.py +++ b/app/service/single_neuron_simulation.py @@ -21,9 +21,10 @@ ) from app.queries.factory import query_params_factory from app.schemas.simulation import ( + SingleNeuronSimulationAdminUpdate, SingleNeuronSimulationCreate, SingleNeuronSimulationRead, - SingleNeuronSimulationUpdate, + SingleNeuronSimulationUserUpdate, ) from app.schemas.types import ListResponse @@ -88,7 +89,7 @@ def update_one( user_context: UserContextDep, db: SessionDep, id_: uuid.UUID, - json_model: SingleNeuronSimulationUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: SingleNeuronSimulationUserUpdate, # pyright: ignore [reportInvalidTypeForm] ) -> SingleNeuronSimulationRead: return router_update_one( id_=id_, @@ -101,6 +102,22 @@ def update_one( ) +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: SingleNeuronSimulationAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> SingleNeuronSimulationRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=SingleNeuronSimulation, + user_context=None, + json_model=json_model, + response_schema_class=SingleNeuronSimulationRead, + apply_operations=_load, + ) + + def read_many( user_context: UserContextDep, db: SessionDep, diff --git a/app/service/single_neuron_synaptome.py b/app/service/single_neuron_synaptome.py index 61e2ea654..d9bd61396 100644 --- a/app/service/single_neuron_synaptome.py +++ b/app/service/single_neuron_synaptome.py @@ -21,9 +21,10 @@ ) from app.queries.factory import query_params_factory from app.schemas.synaptome import ( + SingleNeuronSynaptomeAdminUpdate, SingleNeuronSynaptomeCreate, SingleNeuronSynaptomeRead, - SingleNeuronSynaptomeUpdate, + SingleNeuronSynaptomeUserUpdate, ) from app.schemas.types import ListResponse @@ -90,7 +91,7 @@ def update_one( user_context: UserContextDep, db: SessionDep, id_: uuid.UUID, - json_model: SingleNeuronSynaptomeUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: SingleNeuronSynaptomeUserUpdate, # pyright: ignore [reportInvalidTypeForm] ) -> SingleNeuronSynaptomeRead: return router_update_one( id_=id_, @@ -103,6 +104,22 @@ def update_one( ) +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: SingleNeuronSynaptomeAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> SingleNeuronSynaptomeRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=SingleNeuronSynaptome, + user_context=None, + json_model=json_model, + response_schema_class=SingleNeuronSynaptomeRead, + apply_operations=_load, + ) + + def read_many( user_context: UserContextDep, db: SessionDep, diff --git a/app/service/single_neuron_synaptome_simulation.py b/app/service/single_neuron_synaptome_simulation.py index 2ff346663..ae5764198 100644 --- a/app/service/single_neuron_synaptome_simulation.py +++ b/app/service/single_neuron_synaptome_simulation.py @@ -27,9 +27,10 @@ ) from app.queries.factory import query_params_factory from app.schemas.simulation import ( + SingleNeuronSynaptomeSimulationAdminUpdate, SingleNeuronSynaptomeSimulationCreate, SingleNeuronSynaptomeSimulationRead, - SingleNeuronSynaptomeSimulationUpdate, + SingleNeuronSynaptomeSimulationUserUpdate, ) from app.schemas.types import ListResponse @@ -98,7 +99,7 @@ def update_one( user_context: UserContextDep, db: SessionDep, id_: uuid.UUID, - json_model: SingleNeuronSynaptomeSimulationUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: SingleNeuronSynaptomeSimulationUserUpdate, # pyright: ignore [reportInvalidTypeForm] ) -> SingleNeuronSynaptomeSimulationRead: return router_update_one( id_=id_, @@ -111,6 +112,22 @@ def update_one( ) +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: SingleNeuronSynaptomeSimulationAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> SingleNeuronSynaptomeSimulationRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=SingleNeuronSynaptomeSimulation, + user_context=None, + json_model=json_model, + response_schema_class=SingleNeuronSynaptomeSimulationRead, + apply_operations=_load, + ) + + def read_many( user_context: UserContextDep, db: SessionDep, diff --git a/app/service/species.py b/app/service/species.py index ad8806904..866fd88c4 100644 --- a/app/service/species.py +++ b/app/service/species.py @@ -10,7 +10,7 @@ from app.dependencies.db import SessionDep from app.filters.common import SpeciesFilterDep from app.queries.factory import query_params_factory -from app.schemas.species import SpeciesCreate, SpeciesRead +from app.schemas.species import SpeciesAdminUpdate, SpeciesCreate, SpeciesRead from app.schemas.types import ListResponse from app.utils.embedding import generate_embedding @@ -38,6 +38,14 @@ def read_one( ) +def admin_read_one( + *, + db: SessionDep, + id_: uuid.UUID, +) -> SpeciesRead: + return read_one(id_=id_, db=db) + + def create_one( *, db: SessionDep, @@ -96,3 +104,34 @@ def read_many( filter_joins=filter_joins, embedding=embedding, ) + + +def update_one( + db: SessionDep, + user_context: AdminContextDep, # noqa: ARG001 + id_: uuid.UUID, + json_model: SpeciesAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> SpeciesRead: + return app.queries.common.router_update_one( + id_=id_, + db=db, + db_model_class=Species, + user_context=None, + json_model=json_model, + response_schema_class=SpeciesRead, + ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: SpeciesAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> SpeciesRead: + return app.queries.common.router_update_one( + id_=id_, + db=db, + db_model_class=Species, + user_context=None, + json_model=json_model, + response_schema_class=SpeciesRead, + ) diff --git a/app/service/strain.py b/app/service/strain.py index 9ec5efc75..73ea22343 100644 --- a/app/service/strain.py +++ b/app/service/strain.py @@ -10,7 +10,7 @@ from app.dependencies.db import SessionDep from app.filters.common import StrainFilterDep from app.queries.factory import query_params_factory -from app.schemas.species import StrainCreate, StrainRead +from app.schemas.species import StrainAdminUpdate, StrainCreate, StrainRead from app.schemas.types import ListResponse from app.utils.embedding import generate_embedding @@ -75,6 +75,14 @@ def read_one(id_: uuid.UUID, db: SessionDep) -> StrainRead: ) +def admin_read_one( + *, + db: SessionDep, + id_: uuid.UUID, +) -> StrainRead: + return read_one(id_=id_, db=db) + + def create_one( json_model: StrainCreate, db: SessionDep, user_context: AdminContextDep ) -> StrainRead: @@ -89,3 +97,34 @@ def create_one( apply_operations=_load, embedding=embedding, ) + + +def update_one( + db: SessionDep, + user_context: AdminContextDep, # noqa: ARG001 + id_: uuid.UUID, + json_model: StrainAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> StrainRead: + return app.queries.common.router_update_one( + id_=id_, + db=db, + db_model_class=Strain, + user_context=None, + json_model=json_model, + response_schema_class=StrainRead, + ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: StrainAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> StrainRead: + return app.queries.common.router_update_one( + id_=id_, + db=db, + db_model_class=Strain, + user_context=None, + json_model=json_model, + response_schema_class=StrainRead, + ) diff --git a/app/service/subject.py b/app/service/subject.py index 29f6b1b23..3e4468578 100644 --- a/app/service/subject.py +++ b/app/service/subject.py @@ -15,7 +15,7 @@ router_update_one, ) from app.queries.factory import query_params_factory -from app.schemas.subject import SubjectCreate, SubjectRead, SubjectUpdate +from app.schemas.subject import SubjectAdminUpdate, SubjectCreate, SubjectRead, SubjectUserUpdate from app.schemas.types import ListResponse @@ -77,7 +77,7 @@ def update_one( user_context: UserContextDep, db: SessionDep, id_: uuid.UUID, - json_model: SubjectUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: SubjectUserUpdate, # pyright: ignore [reportInvalidTypeForm] ) -> SubjectRead: return router_update_one( id_=id_, @@ -90,6 +90,22 @@ def update_one( ) +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: SubjectAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> SubjectRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=Subject, + user_context=None, + json_model=json_model, + response_schema_class=SubjectRead, + apply_operations=_load, + ) + + def read_many( user_context: UserContextDep, db: SessionDep, diff --git a/app/service/validation.py b/app/service/validation.py index a33ace216..4741ec9ec 100644 --- a/app/service/validation.py +++ b/app/service/validation.py @@ -24,9 +24,10 @@ from app.queries.factory import query_params_factory from app.schemas.types import ListResponse from app.schemas.validation import ( + ValidationAdminUpdate, ValidationCreate, ValidationRead, - ValidationUpdate, + ValidationUserUpdate, ) if TYPE_CHECKING: @@ -167,7 +168,7 @@ def delete_one( def update_one( db: SessionDep, id_: uuid.UUID, - json_model: ValidationUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: ValidationUserUpdate, # pyright: ignore [reportInvalidTypeForm] user_context: UserContextWithProjectIdDep, ) -> ValidationRead: return router_update_activity_one( @@ -179,3 +180,19 @@ def update_one( response_schema_class=ValidationRead, apply_operations=_load, ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: ValidationAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> ValidationRead: + return router_update_activity_one( + db=db, + id_=id_, + json_model=json_model, + user_context=None, + db_model_class=Validation, + response_schema_class=ValidationRead, + apply_operations=_load, + ) diff --git a/app/service/validation_result.py b/app/service/validation_result.py index ea67a15cf..334c7ab9e 100644 --- a/app/service/validation_result.py +++ b/app/service/validation_result.py @@ -21,9 +21,10 @@ ) from app.schemas.types import ListResponse from app.schemas.validation import ( + ValidationResultAdminUpdate, ValidationResultCreate, ValidationResultRead, - ValidationResultUpdate, + ValidationResultUserUpdate, ) @@ -85,7 +86,7 @@ def update_one( user_context: UserContextDep, db: SessionDep, id_: uuid.UUID, - json_model: ValidationResultUpdate, # pyright: ignore [reportInvalidTypeForm] + json_model: ValidationResultUserUpdate, # pyright: ignore [reportInvalidTypeForm] ) -> ValidationResultRead: return router_update_one( id_=id_, @@ -98,6 +99,22 @@ def update_one( ) +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: ValidationResultAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> ValidationResultRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=ValidationResult, + user_context=None, + json_model=json_model, + response_schema_class=ValidationResultRead, + apply_operations=_load, + ) + + def read_many( user_context: UserContextDep, db: SessionDep, diff --git a/app/utils/routers.py b/app/utils/routers.py index 3d2ffd3de..fffca7250 100644 --- a/app/utils/routers.py +++ b/app/utils/routers.py @@ -1,7 +1,7 @@ from enum import StrEnum from typing import TYPE_CHECKING, Any -from app.db.types import EntityType, ResourceType +from app.db.types import EntityType, GlobalType, ResourceType def _convert_resource_type_to_route(name): @@ -17,12 +17,17 @@ def _convert_resource_type_to_route(name): EntityRoute = StrEnum( "EntityRoute", {item.name: item.name.replace("_", "-") for item in EntityType} ) + GlobalRoute = StrEnum( + "GlobalRoute", + {item.name: _convert_resource_type_to_route(item.name) for item in GlobalType}, + ) ResourceRoute = StrEnum( "ResourceRoute", {item.name: _convert_resource_type_to_route(item.name) for item in ResourceType}, ) else: EntityRoute = StrEnum + GlobalRoute = StrEnum ResourceRoute = StrEnum diff --git a/tests/conftest.py b/tests/conftest.py index c2d14a426..6f0022db1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -66,6 +66,7 @@ USER_SUB_ID_1, USER_SUB_ID_2, VIRTUAL_LAB_ID, + ClientProxies, ClientProxy, add_contribution, add_db, @@ -269,6 +270,16 @@ def client(client_user_1): return client_user_1 +@pytest.fixture +def clients(client_user_1, client_user_2, client_no_project, client_admin): + return ClientProxies( + user_1=client_user_1, + user_2=client_user_2, + no_project=client_no_project, + admin=client_admin, + ) + + @pytest.fixture(scope="session") def database_session_manager() -> Iterator[DatabaseSessionManager]: manager = configure_database_session_manager() @@ -472,6 +483,23 @@ def morphology_id(db, client, species_id, strain_id, brain_region_id, person_id) return model_id +@pytest.fixture +def public_morphology_id( + client, + species_id, + strain_id, + brain_region_id, +): + model_id = utils.create_reconstruction_morphology_id( + client, + species_id=species_id, + strain_id=strain_id, + brain_region_id=brain_region_id, + authorized_public=True, + ) + return model_id + + @pytest.fixture def mtype_class_id(db, person_id): return str( @@ -633,6 +661,26 @@ def emodel_id(create_emodel_ids: CreateIds) -> str: return create_emodel_ids(1)[0] +@pytest.fixture +def public_emodel_id(client, brain_region_id, species_id, strain_id, public_morphology_id): + return assert_request( + client.post, + url="/emodel", + json={ + "name": "name", + "brain_region_id": str(brain_region_id), + "description": "description", + "species_id": str(species_id), + "strain_id": str(strain_id), + "iteration": "test iteration", + "score": 10, + "seed": -1, + "exemplar_morphology_id": str(public_morphology_id), + "authorized_public": True, + }, + ).json()["id"] + + @pytest.fixture def create_memodel_ids( db, morphology_id, brain_region_id, species_id, strain_id, emodel_id, agents, person_id diff --git a/tests/test_brain_region.py b/tests/test_brain_region.py index 8d10d6624..cb5b2d279 100644 --- a/tests/test_brain_region.py +++ b/tests/test_brain_region.py @@ -1,9 +1,12 @@ import itertools as it from unittest.mock import ANY +import pytest + from . import utils ROUTE = "/brain-region" +ADMIN_ROUTE = "/admin/brain-region" HIERARCHY = { @@ -42,7 +45,50 @@ } -def test_brain_region_id(db, client, person_id): +@pytest.fixture +def json_data(brain_region_hierarchy_id): + return { + "name": "my-region", + "acronym": "grey", + "color_hex_triplet": "BFDAE3", + "annotation_value": 9999, + "hierarchy_id": str(brain_region_hierarchy_id), + } + + +def _assert_read_response(data, json_data): + assert data["name"] == json_data["name"] + assert data["acronym"] == json_data["acronym"] + assert data["color_hex_triplet"] == json_data["color_hex_triplet"] + assert data["annotation_value"] == json_data["annotation_value"] + + +def test_create_one(client_admin, json_data): + data = utils.assert_request(client_admin.post, url=ROUTE, json=json_data).json() + _assert_read_response(data, json_data) + + +def test_read_one(clients, json_data): + utils.check_global_read_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + validator=_assert_read_response, + ) + + +def test_update_one(clients, json_data): + utils.check_global_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + patch_payload={"annotation_value": 800, "acronym": "blue"}, + ) + + +def test_brain_region_id(db, client, client_admin, person_id): hierarchy_name = utils.create_hiearchy_name(db, "test_hierarchy", created_by_id=person_id) utils.add_brain_region_hierarchy(db, HIERARCHY, hierarchy_name.id) @@ -108,6 +154,13 @@ def test_brain_region_id(db, client, person_id): assert data["name"] == "root" assert data["acronym"] == "root" + response = client_admin.get(f"{ADMIN_ROUTE}/{root_id}") + assert response.status_code == 200 + data = response.json() + assert data["annotation_value"] == 997 + assert data["name"] == "root" + assert data["acronym"] == "root" + # test semantic_search response = client.get(ROUTE, params={"semantic_search": "Blue region"}) assert response.status_code == 200 diff --git a/tests/test_calibration.py b/tests/test_calibration.py index 93aa65c98..c95fdbce6 100644 --- a/tests/test_calibration.py +++ b/tests/test_calibration.py @@ -19,6 +19,7 @@ DateTimeAdapter = TypeAdapter(datetime) ROUTE = "calibration" +ADMIN_ROUTE = "/admin/calibration" MODEL = Validation TYPE = str(ActivityType.calibration) @@ -319,7 +320,7 @@ def _is_deleted(db, model_id): return db.get(MODEL, model_id) is None -def test_update_one(client, root_circuit, simulation_result, create_id): +def test_update_one(client, client_admin, root_circuit, simulation_result, create_id): gen1 = create_id( used_ids=[str(root_circuit.id)], generated_ids=[], @@ -337,6 +338,39 @@ def test_update_one(client, root_circuit, simulation_result, create_id): assert len(data["generated"]) == 1 assert data["generated"][0]["id"] == str(simulation_result.id) + # only admin client can hit admin endpoint + data = assert_request( + client.patch, + url=f"{ADMIN_ROUTE}/{gen1}", + json={ + "end_time": str(end_time), + }, + expected_status_code=403, + ).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request( + client_admin.patch, + url=f"{ADMIN_ROUTE}/{gen1}", + json={ + "end_time": str(end_time), + }, + ).json() + + assert DateTimeAdapter.validate_python(data["end_time"]) == end_time + + # admin is treated as regular user for regular route (no project context) + data = assert_request( + client_admin.patch, + url=f"{ROUTE}/{gen1}", + json={ + "end_time": str(end_time), + }, + expected_status_code=403, + ).json() + assert data["error_code"] == "NOT_AUTHORIZED" + def test_update_one__fail_if_generated_ids_unauthorized( client_user_1, client_user_2, json_data, species_id, brain_region_id diff --git a/tests/test_circuit.py b/tests/test_circuit.py index ab5c39f4d..d89c377a3 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -9,6 +9,7 @@ assert_request, check_authorization, check_creation_fields, + check_entity_update_one, check_missing, check_pagination, delete_entity_contributions, @@ -56,61 +57,20 @@ def test_create_one(client, circuit_json_data): _assert_read_response(data, circuit_json_data) -def test_update_one(client, circuit): - new_name = "my_new_name" - new_description = "my_new_description" - - data = assert_request( - client.patch, - url=f"{ROUTE}/{circuit.id}", - json={ - "name": new_name, - "description": new_description, +def test_update_one(clients, circuit_json_data): + check_entity_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=circuit_json_data, + patch_payload={ + "name": "name", + "description": "description", }, - ).json() - - assert data["name"] == new_name - assert data["description"] == new_description - - # set number_connections - data = assert_request( - client.patch, - url=f"{ROUTE}/{circuit.id}", - json={ - "number_connections": 500, + optional_payload={ + "number_connections": 750, }, - ).json() - assert data["number_connections"] == 500 - - # unset number_connections - data = assert_request( - client.patch, - url=f"{ROUTE}/{circuit.id}", - json={ - "number_connections": None, - }, - ).json() - assert data["number_connections"] is None - - -def test_update_one__public(client, root_circuit_json_data): - data = assert_request( - client.post, - url=ROUTE, - json=root_circuit_json_data - | { - "authorized_public": True, - }, - ).json() - - # should not be allowed to update it once public - data = assert_request( - client.patch, - url=f"{ROUTE}/{data['id']}", - json={"name": "foo"}, - expected_status_code=404, - ).json() - assert data["error_code"] == "ENTITY_NOT_FOUND" + ) def test_read_one(client, client_admin, circuit, circuit_json_data): diff --git a/tests/test_electrical_cell_recording.py b/tests/test_electrical_cell_recording.py index 960a34aa1..4eb556133 100644 --- a/tests/test_electrical_cell_recording.py +++ b/tests/test_electrical_cell_recording.py @@ -23,6 +23,7 @@ assert_request, check_authorization, check_brain_region_filter, + check_entity_update_one, check_missing, count_db_class, create_brain_region, @@ -56,62 +57,20 @@ def test_create_one( assert data["etypes"] == [] -def test_update_one(client, trace_id_with_assets): - new_name = "my_new_name" - new_description = "my_new_description" - - data = assert_request( - client.patch, - url=f"{ROUTE}/{trace_id_with_assets}", - json={ - "name": new_name, - "description": new_description, +def test_update_one(clients, electrical_cell_recording_json_data): + check_entity_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=electrical_cell_recording_json_data, + patch_payload={ + "name": "name", + "description": "description", }, - ).json() - - assert data["name"] == new_name - assert data["description"] == new_description - - # set temperature - data = assert_request( - client.patch, - url=f"{ROUTE}/{trace_id_with_assets}", - json={ + optional_payload={ "temperature": 10.0, }, - ).json() - assert data["temperature"] == 10.0 - - # unset temperature - data = assert_request( - client.patch, - url=f"{ROUTE}/{trace_id_with_assets}", - json={ - "temperature": None, - }, - ).json() - assert data["temperature"] is None - - -def test_update_one__public(client, electrical_cell_recording_json_data): - # make private entity public - data = assert_request( - client.post, - url=ROUTE, - json=electrical_cell_recording_json_data - | { - "authorized_public": True, - }, - ).json() - - # should not be allowed to update it once public - data = assert_request( - client.patch, - url=f"{ROUTE}/{data['id']}", - json={"name": "foo"}, - expected_status_code=404, - ).json() - assert data["error_code"] == "ENTITY_NOT_FOUND" + ) def test_user_read_one(client, subject_id, license_id, brain_region_id, trace_id_with_assets): diff --git a/tests/test_electrical_recording_stimulus.py b/tests/test_electrical_recording_stimulus.py index af8a55e49..9aa5f066e 100644 --- a/tests/test_electrical_recording_stimulus.py +++ b/tests/test_electrical_recording_stimulus.py @@ -8,6 +8,7 @@ from .utils import ( assert_request, check_authorization, + check_entity_update_one, check_missing, check_pagination, ) @@ -57,67 +58,21 @@ def test_create_one(client, json_data): _assert_read_response(data, json_data) -def test_update_one(client, model_id): - new_name = "my_new_stimulus_name" - new_description = "my_new_stimulus_description" - - data = assert_request( - client.patch, - url=f"{ROUTE}/{model_id}", - json={ - "name": new_name, - "description": new_description, +def test_update_one(clients, json_data): + check_entity_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + patch_payload={ + "name": "name", + "description": "description", }, - ).json() - - assert data["name"] == new_name - assert data["description"] == new_description - - # Test setting and unsetting optional fields - data = assert_request( - client.patch, - url=f"{ROUTE}/{model_id}", - json={ - "dt": 0.2, + optional_payload={ "start_time": 1.0, "end_time": 5.0, }, - ).json() - assert data["dt"] == 0.2 - assert data["start_time"] == 1.0 - assert data["end_time"] == 5.0 - - data = assert_request( - client.patch, - url=f"{ROUTE}/{model_id}", - json={ - "start_time": None, - "end_time": None, - }, - ).json() - assert data["start_time"] is None - assert data["end_time"] is None - - -def test_update_one__public(client, json_data): - # make private entity public - data = assert_request( - client.post, - url=ROUTE, - json=json_data - | { - "authorized_public": True, - }, - ).json() - - # should not be allowed to update it once public - data = assert_request( - client.patch, - url=f"{ROUTE}/{data['id']}", - json={"name": "foo"}, - expected_status_code=404, - ).json() - assert data["error_code"] == "ENTITY_NOT_FOUND" + ) def test_user_read_one(client, model_id, json_data): diff --git a/tests/test_em_cell_mesh.py b/tests/test_em_cell_mesh.py index 55e01b9a1..08535643c 100644 --- a/tests/test_em_cell_mesh.py +++ b/tests/test_em_cell_mesh.py @@ -14,6 +14,7 @@ ) ROUTE = "/em-cell-mesh" +ADMIN_ROUTE = "/admin/em-cell-mesh" MODEL = EMCellMesh @@ -53,39 +54,6 @@ def test_create_one(client, json_data): _assert_read_response(data, json_data) -def test_update_one(client, model): - new_level_of_detail = 999 - - data = assert_request( - client.patch, - url=f"{ROUTE}/{model.id}", - json={"level_of_detail": new_level_of_detail}, - ).json() - - assert data["level_of_detail"] == new_level_of_detail - - -def test_update_one__public(client, json_data): - # make private entity public - data = assert_request( - client.post, - url=ROUTE, - json=json_data - | { - "authorized_public": True, - }, - ).json() - - # should not be allowed to update it once public - data = assert_request( - client.patch, - url=f"{ROUTE}/{data['id']}", - json={"name": "foo"}, - expected_status_code=404, - ).json() - assert data["error_code"] == "ENTITY_NOT_FOUND" - - def test_read_one(client, model, json_data): data = assert_request(client.get, url=f"{ROUTE}/{model.id}").json() _assert_read_response(data, json_data) diff --git a/tests/test_emodel.py b/tests/test_emodel.py index 171ee8997..6724d37a1 100644 --- a/tests/test_emodel.py +++ b/tests/test_emodel.py @@ -11,6 +11,7 @@ from .utils import ( TEST_DATA_DIR, assert_request, + check_entity_update_one, count_db_class, create_reconstruction_morphology_id, delete_entity_classifications, @@ -59,21 +60,23 @@ def test_create_emodel(client: TestClient, species_id, strain_id, brain_region_i assert data[0]["created_by"]["id"] == data[0]["updated_by"]["id"] -def test_update_one(client, emodel_id): - new_name = "my_new_name" - new_description = "my_new_description" - - data = assert_request( - client.patch, - url=f"{ROUTE}/{emodel_id}", - json={ - "name": new_name, - "description": new_description, +@pytest.fixture +def public_json_data(json_data, public_morphology_id): + return json_data | {"exemplar_morphology_id": str(public_morphology_id)} + + +def test_update_one(clients, public_json_data): + check_entity_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + json_data=public_json_data, + clients=clients, + patch_payload={ + "name": "name", + "description": "description", }, - ).json() - - assert data["name"] == new_name - assert data["description"] == new_description + optional_payload=None, + ) def test_get_emodel(client: TestClient, emodel_id: str): diff --git a/tests/test_etype.py b/tests/test_etype.py index aa0f704f6..d85d5c391 100644 --- a/tests/test_etype.py +++ b/tests/test_etype.py @@ -1,3 +1,5 @@ +import pytest + from app.db.model import EModel, ETypeClass, ETypeClassification from .utils import ( @@ -5,6 +7,8 @@ add_all_db, add_db, assert_request, + check_global_read_one, + check_global_update_one, check_missing, count_db_class, with_creation_fields, @@ -15,6 +19,48 @@ ROUTE_EMODEL = "/emodel" +@pytest.fixture +def json_data(): + return { + "pref_label": "pref_label_etype", + "alt_label": "alt_label_etype", + "definition": "definition_etype", + } + + +def _assert_read_response(data, json_data): + assert "id" in data + assert data["pref_label"] == json_data["pref_label"] + assert data["alt_label"] == json_data["alt_label"] + assert data["definition"] == json_data["definition"] + assert "creation_date" in data + assert "update_date" in data + + +def test_read_one(clients, json_data): + check_global_read_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + validator=_assert_read_response, + ) + + +def test_update_one(clients, json_data): + check_global_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + patch_payload={ + "pref_label": "new_pref_label_etype", + "alt_label": "new_alt_label_etype", + "definition": "new_definition_etype", + }, + ) + + def test_retrieve(db, client, person_id): count = 10 items = [ diff --git a/tests/test_experimental_neuron_density.py b/tests/test_experimental_neuron_density.py index 727c46460..af41d1d58 100644 --- a/tests/test_experimental_neuron_density.py +++ b/tests/test_experimental_neuron_density.py @@ -24,6 +24,7 @@ assert_request, check_authorization, check_brain_region_filter, + check_entity_update_one, check_missing, check_pagination, count_db_class, @@ -71,41 +72,18 @@ def model_id(create_id): return create_id() -def test_update_one(client, model_id): - new_name = "my_new_density_name" - new_description = "my_new_density_description" - - data = assert_request( - client.patch, - url=f"{ROUTE}/{model_id}", - json={ - "name": new_name, - "description": new_description, - }, - ).json() - - assert data["name"] == new_name - assert data["description"] == new_description - - -def test_update_one__public(client, json_data): - data = assert_request( - client.post, - url=ROUTE, - json=json_data - | { - "authorized_public": True, +def test_update_one(clients, json_data): + check_entity_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + patch_payload={ + "name": "name", + "description": "description", }, - ).json() - - # should not be allowed to update it once public - data = assert_request( - client.patch, - url=f"{ROUTE}/{data['id']}", - json={"name": "foo"}, - expected_status_code=404, - ).json() - assert data["error_code"] == "ENTITY_NOT_FOUND" + optional_payload=None, + ) def test_create_one(client, json_data): diff --git a/tests/test_experimental_synapses_per_connection.py b/tests/test_experimental_synapses_per_connection.py index f3d701433..6a85962cd 100644 --- a/tests/test_experimental_synapses_per_connection.py +++ b/tests/test_experimental_synapses_per_connection.py @@ -16,6 +16,7 @@ assert_request, check_authorization, check_brain_region_filter, + check_entity_update_one, check_missing, check_pagination, count_db_class, @@ -94,42 +95,18 @@ def test_create_one(client, json_data): _assert_read_response(data, json_data) -def test_update_one(client, model_id): - new_name = "my_new_name" - new_description = "my_new_description" - - data = assert_request( - client.patch, - url=f"{ROUTE}/{model_id}", - json={ - "name": new_name, - "description": new_description, - }, - ).json() - - assert data["name"] == new_name - assert data["description"] == new_description - - -def test_update_one__public(client, json_data): - # make private entity public - data = assert_request( - client.post, - url=ROUTE, - json=json_data - | { - "authorized_public": True, +def test_update_one(clients, json_data): + check_entity_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + patch_payload={ + "name": "name", + "description": "description", }, - ).json() - - # should not be allowed to update it once public - data = assert_request( - client.patch, - url=f"{ROUTE}/{data['id']}", - json={"name": "foo"}, - expected_status_code=404, - ).json() - assert data["error_code"] == "ENTITY_NOT_FOUND" + optional_payload=None, + ) def test_read_one(client, client_admin, model_id, json_data): diff --git a/tests/test_ion_channel.py b/tests/test_ion_channel.py index 1bbfe7602..8fb5d5832 100644 --- a/tests/test_ion_channel.py +++ b/tests/test_ion_channel.py @@ -7,10 +7,13 @@ from .utils import ( add_db, assert_request, + check_global_read_one, + check_global_update_one, check_missing, ) ROUTE = "/ion-channel" +ADMIN_ROUTE = "/admin/ion-channel" @pytest.fixture @@ -46,13 +49,30 @@ def test_create_one(client_admin, json_data): _assert_read_response(data, json_data) -def test_read_one(client, model_id, json_data): - data = assert_request(client.get, url=f"{ROUTE}/{model_id}").json() - _assert_read_response(data, json_data) - - data = assert_request(client.get, url=ROUTE).json() - assert len(data["data"]) == 1 - _assert_read_response(data["data"][0], json_data) +def test_read_one(clients, json_data): + check_global_read_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + validator=_assert_read_response, + ) + + +def test_update_one(clients, json_data): + check_global_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + patch_payload={ + "name": "my-ion-channel", + "description": "my-description", + "label": "my-label", + "gene": "my-gene", + "synonyms": ["a", "b", "c"], + }, + ) def test_missing(client): diff --git a/tests/test_ion_channel_model.py b/tests/test_ion_channel_model.py index 33394e8de..bccea9a86 100644 --- a/tests/test_ion_channel_model.py +++ b/tests/test_ion_channel_model.py @@ -14,6 +14,7 @@ assert_request, check_authorization, check_brain_region_filter, + check_entity_update_one, count_db_class, upload_entity_asset, ) @@ -23,6 +24,23 @@ ADMIN_ROUTE = "/admin/ion-channel-model" +@pytest.fixture +def json_data( + subject_id, + brain_region_id, +): + return { + "description": "Test ICM Description", + "name": "test_icm", + "nmodl_suffix": "test_icm", + "temperature_celsius": 0, + "neuron_block": {}, + "brain_region_id": str(brain_region_id), + "subject_id": subject_id, + "authorized_public": False, + } + + def create( client: TestClient, subject_id: str, @@ -66,62 +84,22 @@ def test_create(client: TestClient, subject_id: str, brain_region_id: uuid.UUID) assert response.status_code == 200, f"Failed to get icms: {response.text}" -def test_update_one(client, subject_id, brain_region_id): - # Create an ion channel model first - response = create(client, subject_id, brain_region_id, "test_icm") - assert response.status_code == 200 - icm_id = response.json()["id"] - - new_name = "my_new_name" - new_description = "my_new_description" - - data = assert_request( - client.patch, - url=f"{ROUTE}/{icm_id}", - json={ - "name": new_name, - "description": new_description, - }, - ).json() - - assert data["name"] == new_name - assert data["description"] == new_description - - # set temperature_celsius - data = assert_request( - client.patch, - url=f"{ROUTE}/{icm_id}", - json={ +def test_update_one(clients, json_data): + check_entity_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + patch_payload={ + "name": "name", + "description": "description", "temperature_celsius": 25, - }, - ).json() - assert data["temperature_celsius"] == 25 - - # set is_ljp_corrected - data = assert_request( - client.patch, - url=f"{ROUTE}/{icm_id}", - json={ "is_ljp_corrected": True, }, - ).json() - assert data["is_ljp_corrected"] is True - - -def test_update_one__public(client, subject_id, brain_region_id): - # Create an ion channel model first - response = create(client, subject_id, brain_region_id, "test_icm", authorized_public=True) - assert response.status_code == 200 - icm_id = response.json()["id"] - - # should not be allowed to update it once public - data = assert_request( - client.patch, - url=f"{ROUTE}/{icm_id}", - json={"name": "foo"}, - expected_status_code=404, - ).json() - assert data["error_code"] == "ENTITY_NOT_FOUND" + optional_payload={ + "contact_email": "foo.bar@zee.com", + }, + ) def test_user_read_one(client, client_admin, subject_id: str, brain_region_id: uuid.UUID): diff --git a/tests/test_ion_channel_recording.py b/tests/test_ion_channel_recording.py index aa504cb7d..9c9d9652c 100644 --- a/tests/test_ion_channel_recording.py +++ b/tests/test_ion_channel_recording.py @@ -21,6 +21,7 @@ assert_request, check_authorization, check_brain_region_filter, + check_entity_update_one, check_missing, count_db_class, create_brain_region, @@ -89,64 +90,20 @@ def test_create_one( _assert_read_response(data, data) -def test_update_one(client, ion_channel_recording_id_with_assets): - recording = ion_channel_recording_id_with_assets - new_name = "my_new_name" - new_description = "my_new_description" - - data = assert_request( - client.patch, - url=f"{ROUTE}/{recording}", - json={ - "name": new_name, - "description": new_description, +def test_update_one(clients, ion_channel_recording_json_data): + check_entity_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=ion_channel_recording_json_data, + patch_payload={ + "name": "name", + "description": "description", }, - ).json() - - assert data["name"] == new_name - assert data["description"] == new_description - - # set temperature - data = assert_request( - client.patch, - url=f"{ROUTE}/{recording}", - json={ + optional_payload={ "temperature": 10.0, }, - ).json() - assert data["temperature"] == 10.0 - - # unset temperature - data = assert_request( - client.patch, - url=f"{ROUTE}/{recording}", - json={ - "temperature": None, - }, - ).json() - assert data["temperature"] is None - - -def test_update_one__public(client, ion_channel_recording_json_data): - recording_json_data = ion_channel_recording_json_data - # make private entity public - data = assert_request( - client.post, - url=ROUTE, - json=recording_json_data - | { - "authorized_public": True, - }, - ).json() - - # should not be allowed to update it once public - data = assert_request( - client.patch, - url=f"{ROUTE}/{data['id']}", - json={"name": "foo"}, - expected_status_code=404, - ).json() - assert data["error_code"] == "ENTITY_NOT_FOUND" + ) def test_read_one( diff --git a/tests/test_license.py b/tests/test_license.py index 0b68020a7..c3fb34095 100644 --- a/tests/test_license.py +++ b/tests/test_license.py @@ -1,11 +1,37 @@ +import pytest + from app.db.model import License -from tests.utils import MISSING_ID, MISSING_ID_COMPACT, assert_request, count_db_class +from tests.utils import ( + MISSING_ID, + MISSING_ID_COMPACT, + assert_request, + check_global_read_one, + count_db_class, +) ROUTE = "/license" ADMIN_ROUTE = "/admin/license" +@pytest.fixture +def json_data(): + return { + "name": "Test License", + "description": "a license description", + "label": "a label", + } + + +def _assert_read_response(data, json_data): + assert "id" in data + assert data["name"] == json_data["name"] + assert data["label"] == json_data["label"] + assert data["description"] == json_data["description"] + assert "creation_date" in data + assert "update_date" in data + + def test_create_license(client, client_admin): response = client_admin.post( ROUTE, @@ -36,6 +62,16 @@ def test_create_license(client, client_admin): assert data[0]["description"] == "a license description" +def test_read_one(clients, json_data): + check_global_read_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + validator=_assert_read_response, + ) + + def test_missing(client): response = client.get(f"{ROUTE}/{MISSING_ID}") assert response.status_code == 404 diff --git a/tests/test_measurement_annotation.py b/tests/test_measurement_annotation.py index 8e95bf6d5..3c838b669 100644 --- a/tests/test_measurement_annotation.py +++ b/tests/test_measurement_annotation.py @@ -4,12 +4,14 @@ import pytest from .utils import ( + assert_request, assert_response, check_missing, create_reconstruction_morphology_id, ) ROUTE = "/measurement-annotation" +ADMIN_ROUTE = "/admin/measurement-annotation" MORPHOLOGY_ROUTE = "/reconstruction-morphology" ENTITY_TYPE = "reconstruction_morphology" @@ -19,6 +21,73 @@ def measurement_labels(): return [f"pref_label_{i}" for i in range(5)] +@pytest.fixture +def json_data(morphology_id): + return { + "entity_type": ENTITY_TYPE, + "entity_id": str(morphology_id), + "measurement_kinds": [ + { + "pref_label": "pref_label_0", + "structural_domain": "axon", + "measurement_items": [ + { + "name": "mean", + "unit": "μm", + "value": 54.2, + }, + { + "name": "median", + "unit": "μm", + "value": 44.6, + }, + ], + }, + ], + } + + +def test_update_one(clients, json_data, species_id, strain_id, brain_region_id): + old_morph_id = json_data["entity_id"] + + new_morph_id = create_reconstruction_morphology_id( + clients.user_1, + species_id, + strain_id, + brain_region_id, + authorized_public=True, + ) + + model_id = assert_request(clients.user_1.post, url=ROUTE, json=json_data).json()["id"] + + patch_data = { + "entity_id": str(new_morph_id), + } + data = assert_request(clients.user_1.patch, url=f"{ROUTE}/{model_id}", json=patch_data).json() + assert data["entity_id"] == patch_data["entity_id"] + + # user 2 shouldn't have access to user's 1 entity to update. + data = assert_request( + clients.user_2.patch, url=f"{ROUTE}/{model_id}", json=patch_data, expected_status_code=404 + ).json() + assert data["error_code"] == "ENTITY_NOT_FOUND" + + patch_data = { + "entity_id": str(old_morph_id), + } + + # users shouldn't have access to service admin endpoints + data = assert_request( + clients.user_1.patch, url=f"{ADMIN_ROUTE}/{model_id}", json={}, expected_status_code=403 + ).json() + assert data["message"] == "Service admin role required" + + data = assert_request( + clients.admin.patch, url=f"{ADMIN_ROUTE}/{model_id}", json=patch_data + ).json() + assert data["entity_id"] == patch_data["entity_id"] + + def _get_request_payload_1(entity_id, labels): return { "entity_type": ENTITY_TYPE, @@ -94,7 +163,9 @@ def _get_return_payload(request_payload): return payload -def test_create_and_retrieve(client, species_id, strain_id, brain_region_id, measurement_labels): +def test_create_and_retrieve(clients, species_id, strain_id, brain_region_id, measurement_labels): + client = clients.user_1 + reconstruction_morphology_id = create_reconstruction_morphology_id( client, species_id, @@ -120,6 +191,11 @@ def test_create_and_retrieve(client, species_id, strain_id, brain_region_id, mea data = response.json() assert data == expected_payload_1 + response = clients.admin.get(f"{ADMIN_ROUTE}/{measurement_annotation_id_1}") + assert_response(response, expected_status_code=200) + data = response.json() + assert data == expected_payload_1 + # read the morphology without measurements response = client.get(f"{MORPHOLOGY_ROUTE}/{reconstruction_morphology_id}") assert_response(response, expected_status_code=200) diff --git a/tests/test_memodel.py b/tests/test_memodel.py index 2936b018c..8081ebc8d 100644 --- a/tests/test_memodel.py +++ b/tests/test_memodel.py @@ -23,6 +23,7 @@ PROJECT_ID, assert_request, check_brain_region_filter, + check_entity_update_one, count_db_class, create_reconstruction_morphology_id, delete_entity_classifications, @@ -47,21 +48,26 @@ def json_data(brain_region_id, species_id, strain_id, morphology_id, emodel_id): } -def test_update_one(client, memodel_id): - new_name = "my_new_memodel_name" - new_description = "my_new_memodel_description" +@pytest.fixture +def public_json_data(json_data, public_morphology_id, public_emodel_id): + return json_data | { + "morphology_id": str(public_morphology_id), + "emodel_id": str(public_emodel_id), + } - data = assert_request( - client.patch, - url=f"{ROUTE}/{memodel_id}", - json={ - "name": new_name, - "description": new_description, - }, - ).json() - assert data["name"] == new_name - assert data["description"] == new_description +def test_update_one(clients, public_json_data): + check_entity_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=public_json_data, + patch_payload={ + "name": "name", + "description": "description", + }, + optional_payload=None, + ) def test_get_memodel(client: TestClient, memodel_id): diff --git a/tests/test_memodel_calibration_result.py b/tests/test_memodel_calibration_result.py index d3a4a7db1..e32abdb87 100644 --- a/tests/test_memodel_calibration_result.py +++ b/tests/test_memodel_calibration_result.py @@ -5,7 +5,7 @@ from app.db.model import MEModelCalibrationResult -from .utils import assert_request, count_db_class +from .utils import assert_request, check_entity_update_one, count_db_class MODEL = MEModelCalibrationResult ROUTE = "/memodel-calibration-result" @@ -41,61 +41,18 @@ def _assert_read_response(data, json_data): assert data["authorized_public"] is json_data["authorized_public"] -def test_update_one(client, memodel_calibration_result_id): - new_threshold_current = 1.2 - new_holding_current = 0.5 - - data = assert_request( - client.patch, - url=f"{ROUTE}/{memodel_calibration_result_id}", - json={ - "threshold_current": new_threshold_current, - "holding_current": new_holding_current, - }, - ).json() - - assert data["threshold_current"] == new_threshold_current - assert data["holding_current"] == new_holding_current - - # Test setting and unsetting optional fields - data = assert_request( - client.patch, - url=f"{ROUTE}/{memodel_calibration_result_id}", - json={ - "rin": 150.0, - }, - ).json() - assert data["rin"] == 150.0 - - data = assert_request( - client.patch, - url=f"{ROUTE}/{memodel_calibration_result_id}", - json={ - "rin": None, +def test_update_one(clients, json_data): + check_entity_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + patch_payload={ + "threshold_current": 1.2, + "holding_current": 0.5, }, - ).json() - assert data["rin"] is None - - -def test_update_one__public(client, json_data): - # make private entity public - data = assert_request( - client.post, - url=ROUTE, - json=json_data - | { - "authorized_public": True, - }, - ).json() - - # should not be allowed to update it once public - data = assert_request( - client.patch, - url=f"{ROUTE}/{data['id']}", - json={"threshold_current": 2.0}, - expected_status_code=404, - ).json() - assert data["error_code"] == "ENTITY_NOT_FOUND" + optional_payload={"rin": 150.0}, + ) def test_read_one(client, client_admin, memodel_calibration_result_id, json_data): diff --git a/tests/test_morphology.py b/tests/test_morphology.py index 243800c7d..24f9e3c4a 100644 --- a/tests/test_morphology.py +++ b/tests/test_morphology.py @@ -21,6 +21,7 @@ assert_request, check_authorization, check_brain_region_filter, + check_entity_update_one, count_db_class, create_reconstruction_morphology_id, delete_entity_classifications, @@ -120,64 +121,20 @@ def test_delete_one(db, client, client_admin, morphology_id, person_id, role_id) assert count_db_class(db, MTypeClassification) == 0 -def test_update_one(client, morphology_id): - new_name = "my_new_name" - new_description = "my_new_description" - - data = assert_request( - client.patch, - url=f"{ROUTE}/{morphology_id}", - json={ - "name": new_name, - "description": new_description, +def test_update_one(clients, json_data): + check_entity_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + patch_payload={ + "name": "name", + "description": "description", }, - ).json() - - assert data["name"] == new_name - assert data["description"] == new_description - - # set location - data = assert_request( - client.patch, - url=f"{ROUTE}/{morphology_id}", - json={ + optional_payload={ "location": {"x": 100, "y": 200, "z": 300}, }, - ).json() - assert data["location"]["x"] == 100 - assert data["location"]["y"] == 200 - assert data["location"]["z"] == 300 - - # unset location - data = assert_request( - client.patch, - url=f"{ROUTE}/{morphology_id}", - json={ - "location": None, - }, - ).json() - assert data["location"] is None - - -def test_update_one__public(client, json_data): - # make private entity public - data = assert_request( - client.post, - url=ROUTE, - json=json_data - | { - "authorized_public": True, - }, - ).json() - - # should not be allowed to update it once public - data = assert_request( - client.patch, - url=f"{ROUTE}/{data['id']}", - json={"name": "foo"}, - expected_status_code=404, - ).json() - assert data["error_code"] == "ENTITY_NOT_FOUND" + ) def test_missing(client): diff --git a/tests/test_mtype.py b/tests/test_mtype.py index 416c42f48..9220f8f0e 100644 --- a/tests/test_mtype.py +++ b/tests/test_mtype.py @@ -1,3 +1,5 @@ +import pytest + from app.db.model import MTypeClass, MTypeClassification from .utils import ( @@ -5,6 +7,8 @@ add_all_db, add_db, assert_request, + check_global_read_one, + check_global_update_one, check_missing, count_db_class, create_reconstruction_morphology_id, @@ -16,6 +20,48 @@ ROUTE_MORPH = "/reconstruction-morphology" +@pytest.fixture +def json_data(): + return { + "pref_label": "pref_label_mtype", + "alt_label": "alt_label_mtype", + "definition": "definition_mtype", + } + + +def _assert_read_response(data, json_data): + assert "id" in data + assert data["pref_label"] == json_data["pref_label"] + assert data["alt_label"] == json_data["alt_label"] + assert data["definition"] == json_data["definition"] + assert "creation_date" in data + assert "update_date" in data + + +def test_read_one(clients, json_data): + check_global_read_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + validator=_assert_read_response, + ) + + +def test_update_one(clients, json_data): + check_global_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + patch_payload={ + "pref_label": "new_pref_label_mtype", + "alt_label": "new_alt_label_mtype", + "definition": "new_definition_mtype", + }, + ) + + def test_retrieve(db, client, person_id): count = 10 items = [ diff --git a/tests/test_publication.py b/tests/test_publication.py index 272f41472..390107429 100644 --- a/tests/test_publication.py +++ b/tests/test_publication.py @@ -5,11 +5,13 @@ from .utils import ( add_db, assert_request, + check_global_read_one, + check_global_update_one, check_missing, ) ROUTE = "/publication" -ADMIN_ROUTE = "/publication" +ADMIN_ROUTE = "/admin/publication" @pytest.fixture @@ -89,16 +91,27 @@ def test_create_one__doi_validation(client_admin, json_data): assert data["error_code"] == "ENTITY_DUPLICATED" -def test_read_one(client, client_admin, model_id, json_data): - data = assert_request(client.get, url=f"{ROUTE}/{model_id}").json() - _assert_read_response(data, json_data) - - data = assert_request(client.get, url=ROUTE).json() - assert len(data["data"]) == 1 - _assert_read_response(data["data"][0], json_data) - - data = assert_request(client_admin.get, url=f"{ADMIN_ROUTE}/{model_id}").json() - _assert_read_response(data, json_data) +def test_read_one(clients, json_data): + check_global_read_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + validator=_assert_read_response, + ) + + +def test_update_one(clients, json_data): + check_global_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + patch_payload={ + "title": "publication", + "DOI": "10.1080/10509585.2015.1092083", + }, + ) def test_missing(client): diff --git a/tests/test_role.py b/tests/test_role.py index 4bea2faa1..c01d7325d 100644 --- a/tests/test_role.py +++ b/tests/test_role.py @@ -1,11 +1,36 @@ +import pytest + from app.db.model import Role -from tests.utils import MISSING_ID, MISSING_ID_COMPACT, assert_request, count_db_class +from tests.utils import ( + MISSING_ID, + MISSING_ID_COMPACT, + assert_request, + check_global_read_one, + check_global_update_one, + count_db_class, +) ROUTE = "/role" ADMIN_ROUTE = "/admin/role" +@pytest.fixture +def json_data(): + return { + "name": "role", + "role_id": "role_id", + } + + +def _assert_read_response(data, json_data): + assert "id" in data + assert data["name"] == json_data["name"] + assert data["role_id"] == json_data["role_id"] + assert "creation_date" in data + assert "update_date" in data + + def test_create_role(client, client_admin): name = "important role" role_id = "important role id" @@ -32,6 +57,29 @@ def test_create_role(client, client_admin): assert data[0]["id"] == id_ +def test_read_one(clients, json_data): + check_global_read_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + validator=_assert_read_response, + ) + + +def test_update_one(clients, json_data): + check_global_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + patch_payload={ + "name": "new-name", + "role_id": "new_role_id", + }, + ) + + def test_delete_one(db, client, client_admin): response = client_admin.post(ROUTE, json={"name": "foo", "role_id": "bar"}) assert response.status_code == 200 diff --git a/tests/test_routes.py b/tests/test_routes.py new file mode 100644 index 000000000..2a834571b --- /dev/null +++ b/tests/test_routes.py @@ -0,0 +1,99 @@ +import json +import re +from collections import defaultdict + +import pytest + +from app.application import app +from app.utils.routers import EntityRoute, GlobalRoute + +REGEX_PATTERN = r"^\/(?:admin\/)?{route}(?:\/{{.+}})?$" + + +@pytest.fixture(scope="module") +def entity_routes(): + return [str(s) for s in EntityRoute] + + +@pytest.fixture(scope="module") +def global_routes(): + return [str(s) for s in GlobalRoute] + + +def _group_routes(app, routes): + per_route_methods = defaultdict(list) + + for route_name in routes: + pattern = re.compile(REGEX_PATTERN.format(route=route_name)) + for route in app.routes: + if pattern.match(route.path): + per_route_methods[route_name].append(route) + + return dict(per_route_methods) + + +@pytest.fixture(scope="module") +def entity_route_paths(entity_routes): + return _group_routes(app, entity_routes) + + +@pytest.fixture(scope="module") +def global_route_paths(global_routes): + return _group_routes(app, global_routes) + + +def _assert_routes(route_paths, expected_method_names, skip): + missing = {} + + for route, paths in route_paths.items(): + if route in skip: + continue + + method_names = {p.name for p in paths} + + if missing_method_names := [ + name for name in expected_method_names if name not in method_names + ]: + missing[route] = missing_method_names + + assert not missing, f"Missing route methods: {json.dumps(missing, indent=2)}" + + +def test_entity_route_methods(entity_route_paths): + expected_method_names = { + "read_one", + "read_many", + "create_one", + "update_one", + "admin_read_one", + "admin_update_one", + } + + # the following must be clarified: + # why is ion-channel is an entity? + # why is em-dense-recostruction-dataset an entity? + # why is external-url an entity? + skip = { + "brain-atlas", + "cell-composition", + "ion-channel", + "em-dense-reconstruction-dataset", + "external-url", + } + + _assert_routes(entity_route_paths, expected_method_names, skip) + + +def test_global_route_methods(global_route_paths): + expected_method_names = { + "read_one", + "read_many", + "create_one", + "update_one", + "admin_read_one", + "admin_update_one", + } + + skip = {"brain-region-hierarchy"} + + _assert_routes(global_route_paths, expected_method_names, skip) diff --git a/tests/test_simulation.py b/tests/test_simulation.py index 30798ece3..0a1884d3e 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -9,6 +9,7 @@ assert_request, check_authorization, check_creation_fields, + check_entity_update_one, check_missing, check_pagination, count_db_class, @@ -54,53 +55,19 @@ def test_create_one(client, json_data): _assert_read_response(data, json_data) -def test_update_one(client, model): - new_name = "my_new_name" - new_description = "my_new_description" - - data = assert_request( - client.patch, - url=f"{ROUTE}/{model.id}", - json={ - "name": new_name, - "description": new_description, - }, - ).json() - - assert data["name"] == new_name - assert data["description"] == new_description - - # set scan_parameters - data = assert_request( - client.patch, - url=f"{ROUTE}/{model.id}", - json={ +def test_update_one(clients, json_data): + check_entity_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + patch_payload={ + "name": "name", + "description": "description", "scan_parameters": {"param1": "value1", "param2": "value2"}, }, - ).json() - assert data["scan_parameters"]["param1"] == "value1" - assert data["scan_parameters"]["param2"] == "value2" - - -def test_update_one__public(client, json_data): - # make private entity public - data = assert_request( - client.post, - url=ROUTE, - json=json_data - | { - "authorized_public": True, - }, - ).json() - - # should not be allowed to update it once public - data = assert_request( - client.patch, - url=f"{ROUTE}/{data['id']}", - json={"name": "foo"}, - expected_status_code=404, - ).json() - assert data["error_code"] == "ENTITY_NOT_FOUND" + optional_payload=None, + ) def test_read_one(client, client_admin, model, json_data): diff --git a/tests/test_simulation_execution.py b/tests/test_simulation_execution.py index 60e51501d..10efb69f7 100644 --- a/tests/test_simulation_execution.py +++ b/tests/test_simulation_execution.py @@ -19,6 +19,7 @@ DateTimeAdapter = TypeAdapter(datetime) ROUTE = "simulation-execution" +ADMIN_ROUTE = "/admin/simulation-execution" MODEL = SimulationExecution @@ -318,7 +319,7 @@ def _is_deleted(db, model_id): return db.get(MODEL, model_id) is None -def test_update_one(client, root_circuit, simulation_result, create_id): +def test_update_one(client, client_admin, root_circuit, simulation_result, create_id): gen1 = create_id( used_ids=[str(root_circuit.id)], generated_ids=[], @@ -336,6 +337,42 @@ def test_update_one(client, root_circuit, simulation_result, create_id): assert len(data["generated"]) == 1 assert data["generated"][0]["id"] == str(simulation_result.id) + gen2 = create_id( + used_ids=[str(root_circuit.id)], + generated_ids=[], + ) + + # only admin client can hit admin endpoint + data = assert_request( + client.patch, + url=f"{ADMIN_ROUTE}/{gen2}", + json=update_json, + expected_status_code=403, + ).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request( + client_admin.patch, + url=f"{ADMIN_ROUTE}/{gen2}", + json=update_json, + ).json() + + assert DateTimeAdapter.validate_python(data["end_time"]) == end_time + assert len(data["generated"]) == 1 + assert data["generated"][0]["id"] == str(simulation_result.id) + + # admin is treated as regular user for regular route (no authorized project ids) + data = assert_request( + client_admin.patch, + url=f"{ROUTE}/{gen2}", + json={ + "end_time": str(end_time), + }, + expected_status_code=403, + ).json() + assert data["error_code"] == "NOT_AUTHORIZED" + def test_update_one__fail_if_generated_ids_unauthorized( client_user_1, client_user_2, json_data, species_id, brain_region_id diff --git a/tests/test_simulation_generation.py b/tests/test_simulation_generation.py index 862560ae4..776bc1a42 100644 --- a/tests/test_simulation_generation.py +++ b/tests/test_simulation_generation.py @@ -19,6 +19,7 @@ DateTimeAdapter = TypeAdapter(datetime) ROUTE = "simulation-generation" +ADMIN_ROUTE = "/admin/simulation-generation" MODEL = SimulationGeneration @@ -297,7 +298,7 @@ def _is_deleted(db, model_id): return db.get(MODEL, model_id) is None -def test_update_one(client, root_circuit, simulation_result, create_id): +def test_update_one(client, client_admin, root_circuit, simulation_result, create_id): gen1 = create_id( used_ids=[str(root_circuit.id)], generated_ids=[], @@ -315,6 +316,40 @@ def test_update_one(client, root_circuit, simulation_result, create_id): assert len(data["generated"]) == 1 assert data["generated"][0]["id"] == str(simulation_result.id) + gen2 = create_id( + used_ids=[str(root_circuit.id)], + generated_ids=[], + ) + + # only admin client can hit admin endpoint + data = assert_request( + client.patch, + url=f"{ADMIN_ROUTE}/{gen2}", + json=update_json, + expected_status_code=403, + ).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request( + client_admin.patch, + url=f"{ADMIN_ROUTE}/{gen2}", + json=update_json, + ).json() + + assert DateTimeAdapter.validate_python(data["end_time"]) == end_time + assert len(data["generated"]) == 1 + assert data["generated"][0]["id"] == str(simulation_result.id) + + # admin is treated as regular user for regular route (no project context) + data = assert_request( + client_admin.patch, + url=f"{ROUTE}/{gen2}", + json=update_json, + expected_status_code=403, + ).json() + assert data["error_code"] == "NOT_AUTHORIZED" + def test_update_one__fail_if_generated_ids_unauthorized( client_user_1, client_user_2, json_data, species_id, brain_region_id diff --git a/tests/test_simulation_result.py b/tests/test_simulation_result.py index c4494c4ff..77b19ed3c 100644 --- a/tests/test_simulation_result.py +++ b/tests/test_simulation_result.py @@ -7,6 +7,7 @@ assert_request, check_authorization, check_creation_fields, + check_entity_update_one, check_missing, check_pagination, count_db_class, @@ -47,41 +48,18 @@ def _assert_read_response(data, json_data): check_creation_fields(data) -def test_update_one(client, model): - new_name = "my_new_simulation_result_name" - new_description = "my_new_simulation_result_description" - - data = assert_request( - client.patch, - url=f"{ROUTE}/{model.id}", - json={ - "name": new_name, - "description": new_description, - }, - ).json() - - assert data["name"] == new_name - assert data["description"] == new_description - - -def test_update_one__public(client, json_data): - data = assert_request( - client.post, - url=ROUTE, - json=json_data - | { - "authorized_public": True, +def test_update_one(clients, json_data): + check_entity_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + patch_payload={ + "name": "name", + "description": "description", }, - ).json() - - # should not be allowed to update it once public - data = assert_request( - client.patch, - url=f"{ROUTE}/{data['id']}", - json={"name": "foo"}, - expected_status_code=404, - ).json() - assert data["error_code"] == "ENTITY_NOT_FOUND" + optional_payload=None, + ) def test_create_one(client, json_data): diff --git a/tests/test_single_neuron_simulation.py b/tests/test_single_neuron_simulation.py index d5deded50..63496b096 100644 --- a/tests/test_single_neuron_simulation.py +++ b/tests/test_single_neuron_simulation.py @@ -15,6 +15,7 @@ assert_request, check_authorization, check_brain_region_filter, + check_entity_update_one, count_db_class, create_brain_region, delete_entity_assets, @@ -66,93 +67,37 @@ def single_neuron_simulation_id(client, memodel_id, brain_region_id): return data["id"] -def test_update_one(client, brain_region_id, memodel_id): - # Create a simulation to update - data = assert_request( - client.post, - url=ROUTE, - json={ - "name": "original-sim", - "description": "original-description", - "injection_location": ["soma[0]"], - "recording_location": ["soma[0]_0.5"], - "me_model_id": memodel_id, - "status": "success", - "seed": 1, - "authorized_public": False, - "brain_region_id": str(brain_region_id), - }, - ).json() - simulation_id = data["id"] - - # Test updating name and description - new_name = "updated-sim" - new_description = "updated-description" - - data = assert_request( - client.patch, - url=f"{ROUTE}/{simulation_id}", - json={ - "name": new_name, - "description": new_description, - }, - ).json() +@pytest.fixture +def json_data(brain_region_id, memodel_id): + return { + "name": "original-sim", + "description": "original-description", + "injection_location": ["soma[0]"], + "recording_location": ["soma[0]_0.5"], + "me_model_id": memodel_id, + "status": "success", + "seed": 1, + "authorized_public": False, + "brain_region_id": str(brain_region_id), + } - assert data["name"] == new_name - assert data["description"] == new_description - # Test updating status and seed - data = assert_request( - client.patch, - url=f"{ROUTE}/{simulation_id}", - json={ +def test_update_one(clients, json_data): + check_entity_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + patch_payload={ + "name": "name", + "description": "description", "status": "failure", "seed": 42, - }, - ).json() - assert data["status"] == "failure" - assert data["seed"] == 42 - - # Test updating injection and recording locations - data = assert_request( - client.patch, - url=f"{ROUTE}/{simulation_id}", - json={ "injection_location": ["dendrite[0]"], "recording_location": ["dendrite[0]_0.5"], }, - ).json() - assert data["injection_location"] == ["dendrite[0]"] - assert data["recording_location"] == ["dendrite[0]_0.5"] - - -def test_update_one__public(client, brain_region_id, memodel_id): - # Create a simulation to update - data = assert_request( - client.post, - url=ROUTE, - json={ - "name": "original-sim", - "description": "original-description", - "injection_location": ["soma[0]"], - "recording_location": ["soma[0]_0.5"], - "me_model_id": memodel_id, - "status": "success", - "seed": 1, - "authorized_public": True, - "brain_region_id": str(brain_region_id), - }, - ).json() - simulation_id = data["id"] - - # should not be allowed to update it once public - data = assert_request( - client.patch, - url=f"{ROUTE}/{simulation_id}", - json={"name": "foo"}, - expected_status_code=404, - ).json() - assert data["error_code"] == "ENTITY_NOT_FOUND" + optional_payload=None, + ) def test_single_neuron_simulation(client, brain_region_id, memodel_id, single_neuron_simulation_id): diff --git a/tests/test_single_neuron_synaptome.py b/tests/test_single_neuron_synaptome.py index 9276fc305..6758f1836 100644 --- a/tests/test_single_neuron_synaptome.py +++ b/tests/test_single_neuron_synaptome.py @@ -19,6 +19,7 @@ assert_request, check_authorization, check_brain_region_filter, + check_entity_update_one, count_db_class, create_brain_region, delete_entity_contributions, @@ -106,46 +107,19 @@ def test_create_one(client, json_data): _assert_create_response(data, json_data) -def test_update_one(client, model_id): - new_name = "my_new_synaptome_name" - new_description = "my_new_synaptome_description" - - data = assert_request( - client.patch, - url=f"{ROUTE}/{model_id}", - json={ - "name": new_name, - "description": new_description, - }, - ).json() - - assert data["name"] == new_name - assert data["description"] == new_description - - # Test updating seed - data = assert_request( - client.patch, - url=f"{ROUTE}/{model_id}", - json={ +def test_update_one(clients, json_data): + check_entity_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + patch_payload={ + "name": "name", + "description": "description", "seed": 42, }, - ).json() - assert data["seed"] == 42 - - -def test_update_one__public(client, json_data): - data = assert_request( - client.post, url=ROUTE, json=json_data | {"authorized_public": True} - ).json() - - # should not be allowed to update it once public - data = assert_request( - client.patch, - url=f"{ROUTE}/{data['id']}", - json={"name": "foo"}, - expected_status_code=404, - ).json() - assert data["error_code"] == "ENTITY_NOT_FOUND" + optional_payload=None, + ) def test_read_one(client, client_admin, model_id, json_data): diff --git a/tests/test_single_neuron_synaptome_simulation.py b/tests/test_single_neuron_synaptome_simulation.py index a71631f1c..b7f346140 100644 --- a/tests/test_single_neuron_synaptome_simulation.py +++ b/tests/test_single_neuron_synaptome_simulation.py @@ -15,6 +15,7 @@ assert_request, check_authorization, check_brain_region_filter, + check_entity_update_one, count_db_class, create_brain_region, ) @@ -138,66 +139,22 @@ def test_create_one(client, json_data, brain_region_id, synaptome_id): assert data["authorized_public"] is False -def test_update_one(client, simulation_id): - new_name = "my_new_simulation_name" - new_description = "my_new_simulation_description" - - data = assert_request( - client.patch, - url=f"{ROUTE}/{simulation_id}", - json={ - "name": new_name, - "description": new_description, - }, - ).json() - - assert data["name"] == new_name - assert data["description"] == new_description - - # Test updating status and seed - data = assert_request( - client.patch, - url=f"{ROUTE}/{simulation_id}", - json={ +def test_update_one(clients, json_data): + check_entity_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + patch_payload={ + "name": "name", + "description": "description", "status": "failure", "seed": 42, - }, - ).json() - assert data["status"] == "failure" - assert data["seed"] == 42 - - # Test updating injection and recording locations - data = assert_request( - client.patch, - url=f"{ROUTE}/{simulation_id}", - json={ "injection_location": ["dendrite[0]"], "recording_location": ["dendrite[0]_0.5"], }, - ).json() - assert data["injection_location"] == ["dendrite[0]"] - assert data["recording_location"] == ["dendrite[0]_0.5"] - - -def test_update_one__public(client, json_data): - # make private entity public - data = assert_request( - client.post, - url=ROUTE, - json=json_data - | { - "authorized_public": True, - }, - ).json() - - # should not be allowed to update it once public - data = assert_request( - client.patch, - url=f"{ROUTE}/{data['id']}", - json={"name": "foo"}, - expected_status_code=404, - ).json() - assert data["error_code"] == "ENTITY_NOT_FOUND" + optional_payload=None, + ) def test_create_one__public(client, json_data): diff --git a/tests/test_species.py b/tests/test_species.py index 99ba9fa71..2fd1b6a6f 100644 --- a/tests/test_species.py +++ b/tests/test_species.py @@ -1,12 +1,37 @@ +import pytest + from app.db.model import Species from .utils import check_creation_fields -from tests.utils import MISSING_ID, MISSING_ID_COMPACT, assert_request, count_db_class +from tests.utils import ( + MISSING_ID, + MISSING_ID_COMPACT, + assert_request, + check_global_read_one, + check_global_update_one, + count_db_class, +) ROUTE = "/species" ADMIN_ROUTE = "/admin/species" +@pytest.fixture +def json_data(): + return { + "name": "my-species", + "taxonomy_id": "NCBITaxon:1000", + } + + +def _assert_read_response(data, json_data): + assert "id" in data + assert data["name"] == json_data["name"] + assert data["taxonomy_id"] == json_data["taxonomy_id"] + assert "created_by" in data + assert "updated_by" in data + + def test_create_species(client, client_admin): count = 3 items = [] @@ -50,6 +75,29 @@ def test_create_species(client, client_admin): assert len(data) == 3 # semantic search just reorders - it does not filter out +def test_read_one(clients, json_data): + check_global_read_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + validator=_assert_read_response, + ) + + +def test_update_one(clients, json_data): + check_global_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + patch_payload={ + "name": "my-new-species", + "taxonomy_id": "NCBITaxon:9000", + }, + ) + + def test_delete_one(db, client, client_admin, species_id): model_id = species_id diff --git a/tests/test_strain.py b/tests/test_strain.py index 6f1fe3814..4d6a28648 100644 --- a/tests/test_strain.py +++ b/tests/test_strain.py @@ -1,3 +1,5 @@ +import pytest + from app.db.model import Strain from tests.utils import ( @@ -5,6 +7,8 @@ MISSING_ID_COMPACT, assert_request, check_creation_fields, + check_global_read_one, + check_global_update_one, count_db_class, ) @@ -12,6 +16,23 @@ ADMIN_ROUTE = "/admin/strain" +@pytest.fixture +def json_data(species_id): + return { + "name": "strain", + "taxonomy_id": "NCBITaxon:2000", + "species_id": str(species_id), + } + + +def _assert_read_response(data, json_data): + assert "id" in data + assert data["name"] == json_data["name"] + assert data["taxonomy_id"] == json_data["taxonomy_id"] + assert "created_by" in data + assert "updated_by" in data + + def test_create_strain(client, client_admin, species_id, person_id): count = 3 items = [] @@ -92,6 +113,29 @@ def test_create_strain(client, client_admin, species_id, person_id): assert len(data) == 3 # semantic search just reorders - it does not filter out +def test_read_one(clients, json_data): + check_global_read_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + validator=_assert_read_response, + ) + + +def test_update_one(clients, json_data): + check_global_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + patch_payload={ + "name": "new-strain", + "taxonomy_id": "NCBITaxon:9000", + }, + ) + + def test_delete_one(db, client, client_admin, strain_id): model_id = strain_id diff --git a/tests/test_subject.py b/tests/test_subject.py index 3f2f22dd0..3a38d8ed8 100644 --- a/tests/test_subject.py +++ b/tests/test_subject.py @@ -10,6 +10,7 @@ add_all_db, assert_request, check_authorization, + check_entity_update_one, check_missing, check_pagination, count_db_class, @@ -80,56 +81,20 @@ def test_create_one(client, json_data, json_data_partial): _assert_read_response(data, json_data_partial) -def test_update_one(client, model_id): - new_name = "my_new_name" - new_description = "my_new_description" - - data = assert_request( - client.patch, - url=f"{ROUTE}/{model_id}", - json={ - "name": new_name, - "description": new_description, +def test_update_one(clients, json_data): + check_entity_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + patch_payload={ + "name": "name", + "description": "description", }, - ).json() - - assert data["name"] == new_name - assert data["description"] == new_description - - # set weight - data = assert_request( - client.patch, - url=f"{ROUTE}/{model_id}", - json={ + optional_payload={ "weight": 25.5, }, - ).json() - assert data["weight"] == 25.5 - - # unset weight - data = assert_request( - client.patch, - url=f"{ROUTE}/{model_id}", - json={ - "weight": None, - }, - ).json() - assert data["weight"] is None - - -def test_update_one__public(client, json_data): - data = assert_request( - client.post, url=ROUTE, json=json_data | {"authorized_public": True} - ).json() - - # should not be allowed to update it once public - data = assert_request( - client.patch, - url=f"{ROUTE}/{data['id']}", - json={"name": "foo"}, - expected_status_code=404, - ).json() - assert data["error_code"] == "ENTITY_NOT_FOUND" + ) def test_read_one(client, client_admin, model_id, json_data, json_data_partial): diff --git a/tests/test_validation.py b/tests/test_validation.py index 1346bb301..6ec2e86d4 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -19,6 +19,7 @@ DateTimeAdapter = TypeAdapter(datetime) ROUTE = "validation" +ADMIN_ROUTE = "/admin/validation" MODEL = Validation TYPE = str(ActivityType.validation) @@ -319,8 +320,8 @@ def _is_deleted(db, model_id): return db.get(MODEL, model_id) is None -def test_update_one(client, root_circuit, simulation_result, create_id): - gen1 = create_id( +def test_update_one(client, client_admin, root_circuit, simulation_result, create_id): + entity_id = create_id( used_ids=[str(root_circuit.id)], generated_ids=[], ) @@ -332,11 +333,45 @@ def test_update_one(client, root_circuit, simulation_result, create_id): "generated_ids": [str(simulation_result.id)], } - data = assert_request(client.patch, url=f"{ROUTE}/{gen1}", json=update_json).json() + data = assert_request(client.patch, url=f"{ROUTE}/{entity_id}", json=update_json).json() + assert DateTimeAdapter.validate_python(data["end_time"]) == end_time + assert len(data["generated"]) == 1 + assert data["generated"][0]["id"] == str(simulation_result.id) + + # only admin client can hit admin endpoint + data = assert_request( + client.patch, + url=f"{ADMIN_ROUTE}/{entity_id}", + json=update_json, + expected_status_code=403, + ).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + entity_id = create_id( + used_ids=[str(root_circuit.id)], + generated_ids=[], + ) + + data = assert_request( + client_admin.patch, + url=f"{ADMIN_ROUTE}/{entity_id}", + json=update_json, + ).json() + assert DateTimeAdapter.validate_python(data["end_time"]) == end_time assert len(data["generated"]) == 1 assert data["generated"][0]["id"] == str(simulation_result.id) + # admin is treated as regular user for regular route (no project context) + data = assert_request( + client_admin.patch, + url=f"{ROUTE}/{entity_id}", + json=update_json, + expected_status_code=403, + ).json() + assert data["error_code"] == "NOT_AUTHORIZED" + def test_update_one__fail_if_generated_ids_unauthorized( client_user_1, client_user_2, json_data, species_id, brain_region_id diff --git a/tests/test_validation_result.py b/tests/test_validation_result.py index ff8dbf184..6322995fa 100644 --- a/tests/test_validation_result.py +++ b/tests/test_validation_result.py @@ -5,7 +5,7 @@ from app.db.model import ValidationResult -from .utils import assert_request, count_db_class +from .utils import assert_request, check_entity_update_one, count_db_class MODEL = ValidationResult ROUTE = "/validation-result" @@ -44,43 +44,18 @@ def _assert_read_response(data, json_data): assert "assets" in data -def test_update_one(client, validation_result_id): - new_name = "my_new_validation_result_name" - - data = assert_request( - client.patch, - url=f"{ROUTE}/{validation_result_id}", - json={ - "name": new_name, - }, - ).json() - - assert data["name"] == new_name - - # Test updating passed status - data = assert_request( - client.patch, - url=f"{ROUTE}/{validation_result_id}", - json={ +def test_update_one(clients, json_data): + check_entity_update_one( + route=ROUTE, + admin_route=ADMIN_ROUTE, + clients=clients, + json_data=json_data, + patch_payload={ + "name": "name", "passed": False, }, - ).json() - assert data["passed"] is False - - -def test_update_one__public(client, json_data): - data = assert_request( - client.post, url=ROUTE, json=json_data | {"authorized_public": True} - ).json() - - # should not be allowed to update it once public - data = assert_request( - client.patch, - url=f"{ROUTE}/{data['id']}", - json={"name": "foo"}, - expected_status_code=404, - ).json() - assert data["error_code"] == "ENTITY_NOT_FOUND" + optional_payload=None, + ) def test_read_one(client, client_admin, validation_result_id, json_data): diff --git a/tests/utils.py b/tests/utils.py index e43466784..facc5fe50 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,6 +1,8 @@ import functools import uuid +from collections.abc import Callable from pathlib import Path +from typing import NamedTuple from unittest.mock import ANY from uuid import UUID @@ -89,6 +91,13 @@ def wrapper(*args, headers=None, **kwargs): return decorator(method) if name in self._methods else method +class ClientProxies(NamedTuple): + user_1: ClientProxy + user_2: ClientProxy + no_project: ClientProxy + admin: ClientProxy + + def create_reconstruction_morphology_id( client, species_id, @@ -759,3 +768,168 @@ def check_sort_by_field(items, field_name): assert all(items[i][field_name] < items[i + 1][field_name] for i in range(len(items) - 1)), ( f"Items unsorted by {field_name}" ) + + +def check_global_read_one( + *, + route: str, + admin_route: str, + clients: ClientProxies, + json_data: dict, + validator: Callable[[dict, dict], None], +): + model_id = assert_request(clients.admin.post, url=route, json=json_data).json()["id"] + + def _req(client, client_route): + data = assert_request(client.get, url=f"{client_route}/{model_id}").json() + validator(data, json_data) + + # user that created the resource can read it + _req(clients.user_1, route) + + # but cannot use the admin endpoint + data = assert_request( + clients.user_1.get, + url=f"{admin_route}/{model_id}", + expected_status_code=403, + ).json() + assert data["message"] == "Service admin role required" + + # any other user can read it too because it is global + _req(clients.user_2, route) + + # but cannot use the admin endpoint + data = assert_request( + clients.user_2.get, + url=f"{admin_route}/{model_id}", + expected_status_code=403, + ).json() + assert data["message"] == "Service admin role required" + + # service admins can read from both regular and admin routes + _req(clients.admin, route) + _req(clients.admin, admin_route) + + +def check_global_update_one( + *, + route: str, + admin_route: str, + clients: ClientProxies, + json_data: dict, + patch_payload: dict, +): + def _patch_compare(method, url, patch_data): + data = assert_request(method, url=url, json=patch_data).json() + for key, value in patch_data.items(): + assert data[key] == value, f"Key: {key} Expected: {value} Actual: {data[key]}" + + data = assert_request(clients.admin.post, url=route, json=json_data).json() + model_id = data["id"] + + old_values = {k: data[k] for k in patch_payload} + + # global resource update endpoint requires admin client + data = assert_request( + clients.user_1.patch, + url=f"{route}/{model_id}", + json=patch_payload, + expected_status_code=403, + ).json() + assert data["message"] == "Service admin role required" + + # update using admin client and regular route + _patch_compare(clients.admin.patch, f"{route}/{model_id}", patch_payload) + + # revert + _patch_compare(clients.admin.patch, f"{route}/{model_id}", old_values) + + # global resource admin endpoint requires admin client + data = assert_request( + clients.user_1.patch, + url=f"{admin_route}/{model_id}", + json=patch_payload, + expected_status_code=403, + ).json() + assert data["message"] == "Service admin role required" + + # update using admin client and admin route + _patch_compare(clients.admin.patch, f"{admin_route}/{model_id}", patch_payload) + + # revert + _patch_compare(clients.admin.patch, f"{admin_route}/{model_id}", old_values) + + +def check_entity_update_one( + *, + route: str, + admin_route: str, + clients: ClientProxies, + json_data: dict, + patch_payload: dict, + optional_payload: dict | None, +): + def _create(client, json_data): + return assert_request(client, url=route, json=json_data).json() + + def _patch_compare(method, url, patch_data): + data = assert_request(method, url=url, json=patch_data).json() + for key, value in patch_data.items(): + assert data[key] == value, f"Key: {key} Expected: {value} Actual: {data[key]}" + + public_1_data = _create(clients.user_1.post, json_data | {"authorized_public": True}) + private_1_data = _create(clients.user_1.post, json_data | {"authorized_public": False}) + + assert public_1_data["authorized_public"] is True + assert private_1_data["authorized_public"] is False + + public_1_id = public_1_data["id"] + private_1_id = private_1_data["id"] + + # user updates resource + _patch_compare(clients.user_1.patch, f"{route}/{private_1_id}", patch_payload) + + # user restores resource + _patch_compare(clients.user_1.patch, f"{route}/{private_1_id}", private_1_data) + + # Test setting and unsetting optional fields + if optional_payload: + _patch_compare(clients.user_1.patch, f"{route}/{private_1_id}", optional_payload) + _patch_compare( + clients.user_1.patch, f"{route}/{private_1_id}", dict.fromkeys(optional_payload) + ) + + # only admin client can hit admin endpoint + data = assert_request( + clients.user_1.patch, + url=f"{admin_route}/{private_1_id}", + json={}, + expected_status_code=403, + ).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + _patch_compare(clients.admin.patch, f"{admin_route}/{private_1_id}", patch_payload) + + # admin is treated as regular user for regular route (no authorized project ids) + data = assert_request( + clients.admin.patch, + url=f"{route}/{private_1_id}", + json={}, + expected_status_code=404, + ).json() + assert data["error_code"] == "ENTITY_NOT_FOUND" + + # user should not be allowed to update a public resource + data = assert_request( + clients.user_1.patch, + url=f"{route}/{public_1_id}", + json={}, + expected_status_code=404, + ).json() + assert data["error_code"] == "ENTITY_NOT_FOUND" + + # admin has no such restrictions + _patch_compare( + clients.admin.patch, f"{admin_route}/{public_1_id}", {"authorized_public": False} + )