diff --git a/alembic/versions/20250715_132810_b26912f54fb6_ondelete_cascades.py b/alembic/versions/20250715_132810_b26912f54fb6_ondelete_cascades.py new file mode 100644 index 00000000..f9513c55 --- /dev/null +++ b/alembic/versions/20250715_132810_b26912f54fb6_ondelete_cascades.py @@ -0,0 +1,201 @@ +"""ondelete_cascades + +Revision ID: b26912f54fb6 +Revises: c41a40d022fb +Create Date: 2025-07-15 13:28:10.964046 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +from sqlalchemy import Text +import app.db.types + +# revision identifiers, used by Alembic. +revision: str = "b26912f54fb6" +down_revision: Union[str, None] = "c41a40d022fb" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(op.f("fk_asset_entity_id_entity"), "asset", type_="foreignkey") + op.create_foreign_key( + op.f("fk_asset_entity_id_entity"), + "asset", + "entity", + ["entity_id"], + ["id"], + ondelete="CASCADE", + ) + op.drop_constraint(op.f("fk_contribution_agent_id_agent"), "contribution", type_="foreignkey") + op.drop_constraint(op.f("fk_contribution_entity_id_entity"), "contribution", type_="foreignkey") + op.drop_constraint(op.f("fk_contribution_role_id_role"), "contribution", type_="foreignkey") + op.create_foreign_key( + op.f("fk_contribution_entity_id_entity"), + "contribution", + "entity", + ["entity_id"], + ["id"], + ondelete="CASCADE", + ) + op.create_foreign_key( + op.f("fk_contribution_role_id_role"), + "contribution", + "role", + ["role_id"], + ["id"], + ondelete="CASCADE", + ) + op.create_foreign_key( + op.f("fk_contribution_agent_id_agent"), + "contribution", + "agent", + ["agent_id"], + ["id"], + ondelete="CASCADE", + ) + op.drop_constraint( + op.f("fk_electrical_recording_stimulus_recording_id_electrica_985a"), + "electrical_recording_stimulus", + type_="foreignkey", + ) + op.create_foreign_key( + op.f("fk_electrical_recording_stimulus_recording_id_electrical_cell_recording"), + "electrical_recording_stimulus", + "electrical_cell_recording", + ["recording_id"], + ["id"], + ondelete="CASCADE", + ) + op.drop_constraint( + op.f("fk_etype_classification_etype_class_id_etype_class"), + "etype_classification", + type_="foreignkey", + ) + op.drop_constraint( + op.f("fk_etype_classification_entity_id_entity"), "etype_classification", type_="foreignkey" + ) + op.create_foreign_key( + op.f("fk_etype_classification_entity_id_entity"), + "etype_classification", + "entity", + ["entity_id"], + ["id"], + ondelete="CASCADE", + ) + op.create_foreign_key( + op.f("fk_etype_classification_etype_class_id_etype_class"), + "etype_classification", + "etype_class", + ["etype_class_id"], + ["id"], + ondelete="CASCADE", + ) + op.drop_constraint( + op.f("fk_mtype_classification_entity_id_entity"), "mtype_classification", type_="foreignkey" + ) + op.drop_constraint( + op.f("fk_mtype_classification_mtype_class_id_mtype_class"), + "mtype_classification", + type_="foreignkey", + ) + op.create_foreign_key( + op.f("fk_mtype_classification_entity_id_entity"), + "mtype_classification", + "entity", + ["entity_id"], + ["id"], + ondelete="CASCADE", + ) + op.create_foreign_key( + op.f("fk_mtype_classification_mtype_class_id_mtype_class"), + "mtype_classification", + "mtype_class", + ["mtype_class_id"], + ["id"], + ondelete="CASCADE", + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint( + op.f("fk_mtype_classification_mtype_class_id_mtype_class"), + "mtype_classification", + type_="foreignkey", + ) + op.drop_constraint( + op.f("fk_mtype_classification_entity_id_entity"), "mtype_classification", type_="foreignkey" + ) + op.create_foreign_key( + op.f("fk_mtype_classification_mtype_class_id_mtype_class"), + "mtype_classification", + "mtype_class", + ["mtype_class_id"], + ["id"], + ) + op.create_foreign_key( + op.f("fk_mtype_classification_entity_id_entity"), + "mtype_classification", + "entity", + ["entity_id"], + ["id"], + ) + op.drop_constraint( + op.f("fk_etype_classification_etype_class_id_etype_class"), + "etype_classification", + type_="foreignkey", + ) + op.drop_constraint( + op.f("fk_etype_classification_entity_id_entity"), "etype_classification", type_="foreignkey" + ) + op.create_foreign_key( + op.f("fk_etype_classification_entity_id_entity"), + "etype_classification", + "entity", + ["entity_id"], + ["id"], + ) + op.create_foreign_key( + op.f("fk_etype_classification_etype_class_id_etype_class"), + "etype_classification", + "etype_class", + ["etype_class_id"], + ["id"], + ) + op.drop_constraint( + op.f("fk_electrical_recording_stimulus_recording_id_electrical_cell_recording"), + "electrical_recording_stimulus", + type_="foreignkey", + ) + op.create_foreign_key( + op.f("fk_electrical_recording_stimulus_recording_id_electrica_985a"), + "electrical_recording_stimulus", + "electrical_cell_recording", + ["recording_id"], + ["id"], + ) + op.drop_constraint(op.f("fk_contribution_agent_id_agent"), "contribution", type_="foreignkey") + op.drop_constraint(op.f("fk_contribution_role_id_role"), "contribution", type_="foreignkey") + op.drop_constraint(op.f("fk_contribution_entity_id_entity"), "contribution", type_="foreignkey") + op.create_foreign_key( + op.f("fk_contribution_role_id_role"), "contribution", "role", ["role_id"], ["id"] + ) + op.create_foreign_key( + op.f("fk_contribution_entity_id_entity"), "contribution", "entity", ["entity_id"], ["id"] + ) + op.create_foreign_key( + op.f("fk_contribution_agent_id_agent"), "contribution", "agent", ["agent_id"], ["id"] + ) + op.drop_constraint(op.f("fk_asset_entity_id_entity"), "asset", type_="foreignkey") + op.create_foreign_key( + op.f("fk_asset_entity_id_entity"), "asset", "entity", ["entity_id"], ["id"] + ) + # ### end Alembic commands ### diff --git a/app/db/model.py b/app/db/model.py index b0df0dfe..19a5f191 100644 --- a/app/db/model.py +++ b/app/db/model.py @@ -382,8 +382,12 @@ class MTypeClassification(Identifiable): authorized_project_id: Mapped[uuid.UUID] authorized_public: Mapped[bool] = mapped_column(default=False) - mtype_class_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("mtype_class.id"), index=True) - entity_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), index=True) + mtype_class_id: Mapped[uuid.UUID] = mapped_column( + ForeignKey("mtype_class.id", ondelete="CASCADE"), index=True + ) + entity_id: Mapped[uuid.UUID] = mapped_column( + ForeignKey("entity.id", ondelete="CASCADE"), index=True + ) class ETypeClassification(Identifiable): @@ -392,8 +396,12 @@ class ETypeClassification(Identifiable): authorized_project_id: Mapped[uuid.UUID] authorized_public: Mapped[bool] = mapped_column(default=False) - etype_class_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("etype_class.id"), index=True) - entity_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), index=True) + etype_class_id: Mapped[uuid.UUID] = mapped_column( + ForeignKey("etype_class.id", ondelete="CASCADE"), index=True + ) + entity_id: Mapped[uuid.UUID] = mapped_column( + ForeignKey("entity.id", ondelete="CASCADE"), index=True + ) class MTypesMixin: @@ -408,8 +416,8 @@ def mtypes(cls) -> Mapped[list["MTypeClass"]]: primaryjoin=f"{cls.__name__}.id == MTypeClassification.entity_id", secondary="mtype_classification", uselist=True, - viewonly=True, order_by="MTypeClass.pref_label", + passive_deletes=True, ) @@ -425,8 +433,8 @@ def etypes(cls) -> Mapped[list["ETypeClass"]]: primaryjoin=f"{cls.__name__}.id == ETypeClassification.entity_id", secondary="etype_classification", uselist=True, - viewonly=True, order_by="ETypeClass.pref_label", + passive_deletes=True, ) @@ -459,14 +467,16 @@ class Entity(LegacyMixin, Identifiable): authorized_project_id: Mapped[uuid.UUID] authorized_public: Mapped[bool] = mapped_column(default=False) - contributions: Mapped[list["Contribution"]] = relationship(uselist=True, viewonly=True) + contributions: Mapped[list["Contribution"]] = relationship( + "Contribution", uselist=True, passive_deletes=True, back_populates="entity" + ) assets: Mapped[list["Asset"]] = relationship( "Asset", uselist=True, - viewonly=True, primaryjoin=lambda: sa.and_( Entity.id == Asset.entity_id, Asset.status != AssetStatus.DELETED ), + passive_deletes=True, ) __mapper_args__ = { # noqa: RUF012 @@ -573,12 +583,18 @@ class AnalysisSoftwareSourceCode(NameDescriptionVectorMixin, Entity): class Contribution(Identifiable): __tablename__ = "contribution" - agent_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("agent.id"), index=True) + agent_id: Mapped[uuid.UUID] = mapped_column( + ForeignKey("agent.id", ondelete="CASCADE"), index=True + ) agent = relationship("Agent", uselist=False, foreign_keys=agent_id) - role_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("role.id"), index=True) + role_id: Mapped[uuid.UUID] = mapped_column( + ForeignKey("role.id", ondelete="CASCADE"), index=True + ) role = relationship("Role", uselist=False) - entity_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), index=True) - entity = relationship("Entity", uselist=False) + entity_id: Mapped[uuid.UUID] = mapped_column( + ForeignKey("entity.id", ondelete="CASCADE"), index=True + ) + entity = relationship("Entity", uselist=False, back_populates="contributions") __table_args__ = ( UniqueConstraint("entity_id", "role_id", "agent_id", name="unique_contribution_1"), @@ -784,7 +800,7 @@ class ElectricalRecordingStimulus(Entity, NameDescriptionVectorMixin): end_time: Mapped[float | None] recording_id: Mapped[uuid.UUID] = mapped_column( - ForeignKey("electrical_cell_recording.id"), + ForeignKey("electrical_cell_recording.id", ondelete="CASCADE"), index=True, ) @@ -810,6 +826,7 @@ class ElectricalCellRecording( stimuli: Mapped[list[ElectricalRecordingStimulus]] = relationship( uselist=True, foreign_keys="ElectricalRecordingStimulus.recording_id", + passive_deletes=True, ) __mapper_args__ = {"polymorphic_identity": __tablename__} # noqa: RUF012 @@ -1025,7 +1042,9 @@ class Asset(Identifiable): sha256_digest: Mapped[bytes | None] = mapped_column(LargeBinary(32)) meta: Mapped[JSON_DICT] # not used yet. can be useful? label: Mapped[AssetLabel] - entity_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), index=True) + entity_id: Mapped[uuid.UUID] = mapped_column( + ForeignKey("entity.id", ondelete="CASCADE"), index=True + ) # partial unique index __table_args__ = ( diff --git a/app/queries/common.py b/app/queries/common.py index cd726134..35334d5c 100644 --- a/app/queries/common.py +++ b/app/queries/common.py @@ -56,7 +56,9 @@ def router_read_one[T: BaseModel, I: Identifiable]( the model data as a Pydantic model. """ 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 authorized_project_id and ( + id_model_class := get_declaring_class(db_model_class, "authorized_project_id") + ): query = constrain_to_accessible_entities( query, authorized_project_id, db_model_class=id_model_class ) @@ -343,20 +345,20 @@ def router_delete_one[T: BaseModel, I: Identifiable]( query = constrain_to_accessible_entities( query, authorized_project_id, db_model_class=id_model_class ) - with ( - ensure_result(error_message=f"{db_model_class.__name__} not found"), - ensure_foreign_keys_integrity( - error_message=( - f"{db_model_class.__name__} cannot be deleted " - f"because of foreign keys integrity violation" - ) - ), - ): + + with ensure_result(error_message=f"{db_model_class.__name__} not found"): obj = db.execute(query).scalars().one() + with ensure_foreign_keys_integrity( + error_message=( + f"{db_model_class.__name__} cannot be deleted " + f"because of foreign keys integrity violation" + ) + ): # Use ORM delete in order to ensure that ondelete cascades are triggered in parents when # subclasses are deleted as it is the case with Activity/SimulationGeneration. db.delete(obj) + db.flush() def router_update_activity_one[T: BaseModel, I: Activity]( diff --git a/app/routers/circuit.py b/app/routers/circuit.py index 3c65e418..10c5c3f5 100644 --- a/app/routers/circuit.py +++ b/app/routers/circuit.py @@ -10,3 +10,4 @@ read_many = router.get("")(app.service.circuit.read_many) read_one = router.get("/{id_}")(app.service.circuit.read_one) create_one = router.post("")(app.service.circuit.create_one) +delete_one = router.delete("/{id_}")(app.service.circuit.delete_one) diff --git a/app/routers/contribution.py b/app/routers/contribution.py index 9bac2153..2ebc3e6a 100644 --- a/app/routers/contribution.py +++ b/app/routers/contribution.py @@ -10,3 +10,4 @@ read_many = router.get("")(app.service.contribution.read_many) read_one = router.get("/{id_}")(app.service.contribution.read_one) create_one = router.post("")(app.service.contribution.create_one) +delete_one = router.delete("/{id_}")(app.service.contribution.delete_one) diff --git a/app/routers/electrical_cell_recording.py b/app/routers/electrical_cell_recording.py index a8aed295..0d9f36a4 100644 --- a/app/routers/electrical_cell_recording.py +++ b/app/routers/electrical_cell_recording.py @@ -10,3 +10,4 @@ read_many = router.get("")(app.service.electrical_cell_recording.read_many) read_one = router.get("/{id_}")(app.service.electrical_cell_recording.read_one) create_one = router.post("")(app.service.electrical_cell_recording.create_one) +delete_one = router.delete("/{id_}")(app.service.electrical_cell_recording.delete_one) diff --git a/app/routers/emodel.py b/app/routers/emodel.py index ffb9edbd..512bdd0b 100644 --- a/app/routers/emodel.py +++ b/app/routers/emodel.py @@ -11,3 +11,4 @@ read_many = router.get("")(app.service.emodel.read_many) read_one = router.get("/{id_}")(app.service.emodel.read_one) create_one = router.post("")(app.service.emodel.create_one) +delete_one = router.delete("/{id_}")(app.service.emodel.delete_one) diff --git a/app/routers/etype.py b/app/routers/etype.py index 1d44216a..560e7a86 100644 --- a/app/routers/etype.py +++ b/app/routers/etype.py @@ -9,3 +9,4 @@ read_many = router.get("")(app.service.etype.read_many) read_one = router.get("/{id_}")(app.service.etype.read_one) +delete_one = router.delete("/{id_}")(app.service.etype.delete_one) diff --git a/app/routers/etype_classification.py b/app/routers/etype_classification.py index 1e22720f..080231bf 100644 --- a/app/routers/etype_classification.py +++ b/app/routers/etype_classification.py @@ -8,3 +8,4 @@ ) create_one = router.post("")(app.service.etype_classification.create_one) +delete_one = router.delete("/{id_}")(app.service.etype_classification.delete_one) diff --git a/app/routers/experimental_bouton_density.py b/app/routers/experimental_bouton_density.py index c7f48b56..3612ef66 100644 --- a/app/routers/experimental_bouton_density.py +++ b/app/routers/experimental_bouton_density.py @@ -10,3 +10,4 @@ read_many = router.get("")(app.service.experimental_bouton_density.read_many) read_onw = router.get("/{id_}")(app.service.experimental_bouton_density.read_one) create_one = router.post("")(app.service.experimental_bouton_density.create_one) +delete_one = router.delete("/{id_}")(app.service.experimental_bouton_density.delete_one) diff --git a/app/routers/experimental_neuron_density.py b/app/routers/experimental_neuron_density.py index 63e0ba2c..bce33651 100644 --- a/app/routers/experimental_neuron_density.py +++ b/app/routers/experimental_neuron_density.py @@ -10,3 +10,4 @@ read_many = router.get("")(app.service.experimental_neuron_density.read_many) read_one = router.get("/{id_}")(app.service.experimental_neuron_density.read_one) create_one = router.post("")(app.service.experimental_neuron_density.create_one) +delete_one = router.delete("/{id_}")(app.service.experimental_neuron_density.delete_one) diff --git a/app/routers/experimental_synapses_per_connection.py b/app/routers/experimental_synapses_per_connection.py index 0b9d6487..1ecf95a8 100644 --- a/app/routers/experimental_synapses_per_connection.py +++ b/app/routers/experimental_synapses_per_connection.py @@ -10,3 +10,4 @@ read_many = router.get("")(app.service.experimental_synapses_per_connection.read_many) read_one = router.get("/{id_}")(app.service.experimental_synapses_per_connection.read_one) create_one = router.post("")(app.service.experimental_synapses_per_connection.create_one) +delete_one = router.delete("/{id_}")(app.service.experimental_synapses_per_connection.delete_one) diff --git a/app/routers/ion_channel_model.py b/app/routers/ion_channel_model.py index d4fea16d..d781af87 100644 --- a/app/routers/ion_channel_model.py +++ b/app/routers/ion_channel_model.py @@ -10,3 +10,4 @@ read_many = router.get("")(app.service.ion_channel_model.read_many) read_one = router.get("/{id_}")(app.service.ion_channel_model.read_one) create_one = router.post("")(app.service.ion_channel_model.create_one) +delete_one = router.delete("/{id_}")(app.service.ion_channel_model.delete_one) diff --git a/app/routers/license.py b/app/routers/license.py index a21392bd..1b6b6218 100644 --- a/app/routers/license.py +++ b/app/routers/license.py @@ -10,3 +10,4 @@ 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) +delete_one = router.delete("/{id_}")(app.service.license.delete_one) diff --git a/app/routers/memodel.py b/app/routers/memodel.py index 2c707c64..682c09da 100644 --- a/app/routers/memodel.py +++ b/app/routers/memodel.py @@ -10,3 +10,4 @@ read_many = router.get("")(app.service.memodel.read_many) read_one = router.get("/{id_}")(app.service.memodel.read_one) create_one = router.post("")(app.service.memodel.create_one) +delete_one = router.delete("/{id_}")(app.service.memodel.delete_one) diff --git a/app/routers/memodel_calibration_result.py b/app/routers/memodel_calibration_result.py index 9c67f3a1..f974ddeb 100644 --- a/app/routers/memodel_calibration_result.py +++ b/app/routers/memodel_calibration_result.py @@ -9,3 +9,4 @@ read_many = router.get("")(app.service.memodel_calibration_result.read_many) read_one = router.get("/{id_}")(app.service.memodel_calibration_result.read_one) create_one = router.post("")(app.service.memodel_calibration_result.create_one) +delete_one = router.delete("/{id_}")(app.service.memodel_calibration_result.delete_one) diff --git a/app/routers/morphology.py b/app/routers/morphology.py index e4f5dca9..872ccf05 100644 --- a/app/routers/morphology.py +++ b/app/routers/morphology.py @@ -10,3 +10,4 @@ read_many = router.get("")(app.service.morphology.read_many) read_one = router.get("/{id_}")(app.service.morphology.read_one) create_one = router.post("")(app.service.morphology.create_one) +delete_one = router.delete("/{id_}")(app.service.morphology.delete_one) diff --git a/app/routers/mtype.py b/app/routers/mtype.py index b5f56369..de54884d 100644 --- a/app/routers/mtype.py +++ b/app/routers/mtype.py @@ -9,3 +9,4 @@ read_many = router.get("")(app.service.mtype.read_many) read_one = router.get("/{id_}")(app.service.mtype.read_one) +delete_one = router.delete("/{id_}")(app.service.mtype.delete_one) diff --git a/app/routers/mtype_classification.py b/app/routers/mtype_classification.py index 566b812c..add7d508 100644 --- a/app/routers/mtype_classification.py +++ b/app/routers/mtype_classification.py @@ -8,3 +8,4 @@ ) create_one = router.post("")(app.service.mtype_classification.create_one) +delete_one = router.delete("/{id_}")(app.service.mtype_classification.delete_one) diff --git a/app/routers/organization.py b/app/routers/organization.py index d79560af..77c2ff4b 100644 --- a/app/routers/organization.py +++ b/app/routers/organization.py @@ -10,3 +10,4 @@ read_many = router.get("")(app.service.organization.read_many) read_one = router.get("/{id_}")(app.service.organization.read_one) create_one = router.post("")(app.service.organization.create_one) +delete_one = router.delete("/{id_}")(app.service.organization.delete_one) diff --git a/app/routers/person.py b/app/routers/person.py index a1c3ab9f..a2157449 100644 --- a/app/routers/person.py +++ b/app/routers/person.py @@ -11,3 +11,4 @@ read_many = router.get("")(app.service.person.read_many) read_one = router.get("/{id_}")(app.service.person.read_one) create_one = router.post("")(app.service.person.create_one) +delete_one = router.delete("/{id_}")(app.service.person.delete_one) diff --git a/app/routers/role.py b/app/routers/role.py index a48c97eb..e3433df0 100644 --- a/app/routers/role.py +++ b/app/routers/role.py @@ -10,3 +10,4 @@ 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) +delete_one = router.delete("/{id_}")(app.service.role.delete_one) diff --git a/app/routers/simulation.py b/app/routers/simulation.py index 30fedab7..42dc7d99 100644 --- a/app/routers/simulation.py +++ b/app/routers/simulation.py @@ -10,3 +10,4 @@ read_many = router.get("")(app.service.simulation.read_many) read_one = router.get("/{id_}")(app.service.simulation.read_one) create_one = router.post("")(app.service.simulation.create_one) +delete_one = router.delete("/{id_}")(app.service.simulation.delete_one) diff --git a/app/routers/simulation_campaign.py b/app/routers/simulation_campaign.py index aa862f5c..c1d5299c 100644 --- a/app/routers/simulation_campaign.py +++ b/app/routers/simulation_campaign.py @@ -10,3 +10,4 @@ read_many = router.get("")(app.service.simulation_campaign.read_many) read_one = router.get("/{id_}")(app.service.simulation_campaign.read_one) create_one = router.post("")(app.service.simulation_campaign.create_one) +delete_one = router.delete("/{id_}")(app.service.simulation_campaign.delete_one) diff --git a/app/routers/simulation_result.py b/app/routers/simulation_result.py index aef54f1a..8183d23a 100644 --- a/app/routers/simulation_result.py +++ b/app/routers/simulation_result.py @@ -10,3 +10,4 @@ read_many = router.get("")(app.service.simulation_result.read_many) read_one = router.get("/{id_}")(app.service.simulation_result.read_one) create_one = router.post("")(app.service.simulation_result.create_one) +delete_one = router.delete("/{id_}")(app.service.simulation_result.delete_one) diff --git a/app/routers/single_neuron_simulation.py b/app/routers/single_neuron_simulation.py index 93682611..ec6be30e 100644 --- a/app/routers/single_neuron_simulation.py +++ b/app/routers/single_neuron_simulation.py @@ -10,3 +10,4 @@ read_many = router.get("")(app.service.single_neuron_simulation.read_many) read_one = router.get("/{id_}")(app.service.single_neuron_simulation.read_one) create_one = router.post("")(app.service.single_neuron_simulation.create_one) +delete_one = router.delete("/{id_}")(app.service.single_neuron_simulation.delete_one) diff --git a/app/routers/single_neuron_synaptome.py b/app/routers/single_neuron_synaptome.py index 7797bdb8..27de83dc 100644 --- a/app/routers/single_neuron_synaptome.py +++ b/app/routers/single_neuron_synaptome.py @@ -10,3 +10,4 @@ read_many = router.get("")(app.service.single_neuron_synaptome.read_many) read_one = router.get("/{id_}")(app.service.single_neuron_synaptome.read_one) create_one = router.post("")(app.service.single_neuron_synaptome.create_one) +delete_one = router.delete("/{id_}")(app.service.single_neuron_synaptome.delete_one) diff --git a/app/routers/single_neuron_synaptome_simulation.py b/app/routers/single_neuron_synaptome_simulation.py index 752b53bd..55e5558d 100644 --- a/app/routers/single_neuron_synaptome_simulation.py +++ b/app/routers/single_neuron_synaptome_simulation.py @@ -10,3 +10,4 @@ read_many = router.get("")(app.service.single_neuron_synaptome_simulation.read_many) read_one = router.get("/{id_}")(app.service.single_neuron_synaptome_simulation.read_one) create_one = router.post("")(app.service.single_neuron_synaptome_simulation.create_one) +delete_one = router.delete("/{id_}")(app.service.single_neuron_synaptome_simulation.delete_one) diff --git a/app/routers/species.py b/app/routers/species.py index 462e6d99..578aacbb 100644 --- a/app/routers/species.py +++ b/app/routers/species.py @@ -10,3 +10,4 @@ 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) +delete_one = router.delete("/{id_}")(app.service.species.delete_one) diff --git a/app/routers/strain.py b/app/routers/strain.py index b50e84c8..4393d9b4 100644 --- a/app/routers/strain.py +++ b/app/routers/strain.py @@ -10,3 +10,4 @@ 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) +delete_one = router.delete("/{id_}")(app.service.strain.delete_one) diff --git a/app/routers/subject.py b/app/routers/subject.py index 880de5f7..3fe918ae 100644 --- a/app/routers/subject.py +++ b/app/routers/subject.py @@ -10,3 +10,4 @@ read_many = router.get("")(app.service.subject.read_many) read_one = router.get("/{id_}")(app.service.subject.read_one) create_one = router.post("")(app.service.subject.create_one) +delete_one = router.delete("/{id_}")(app.service.subject.delete_one) diff --git a/app/routers/validation_result.py b/app/routers/validation_result.py index 504f31fa..5f197701 100644 --- a/app/routers/validation_result.py +++ b/app/routers/validation_result.py @@ -10,3 +10,4 @@ read_many = router.get("")(app.service.validation_result.read_many) read_one = router.get("/{id_}")(app.service.validation_result.read_one) create_one = router.post("")(app.service.validation_result.create_one) +delete_one = router.delete("/{id_}")(app.service.validation_result.delete_one) diff --git a/app/schemas/classification.py b/app/schemas/classification.py index 81fd1a1f..0f34c060 100644 --- a/app/schemas/classification.py +++ b/app/schemas/classification.py @@ -7,6 +7,7 @@ AuthorizationMixin, AuthorizationOptionalPublicMixin, CreationMixin, + IdentifiableMixin, ) @@ -20,7 +21,11 @@ class MTypeClassificationCreate(ClassificationBase, AuthorizationOptionalPublicM class MTypeClassificationRead( - ClassificationBase, CreatedByUpdatedByMixin, CreationMixin, AuthorizationMixin + ClassificationBase, + CreatedByUpdatedByMixin, + CreationMixin, + IdentifiableMixin, + AuthorizationMixin, ): mtype_class_id: uuid.UUID @@ -30,6 +35,10 @@ class ETypeClassificationCreate(ClassificationBase, AuthorizationOptionalPublicM class ETypeClassificationRead( - ClassificationBase, CreatedByUpdatedByMixin, CreationMixin, AuthorizationMixin + ClassificationBase, + CreatedByUpdatedByMixin, + CreationMixin, + IdentifiableMixin, + AuthorizationMixin, ): etype_class_id: uuid.UUID diff --git a/app/service/circuit.py b/app/service/circuit.py index 02f43490..f27ad482 100644 --- a/app/service/circuit.py +++ b/app/service/circuit.py @@ -9,7 +9,7 @@ Circuit, Subject, ) -from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.auth import AdminContextDep, UserContextDep, UserContextWithProjectIdDep from app.dependencies.common import ( FacetsDep, InBrainRegionDep, @@ -18,7 +18,12 @@ ) from app.dependencies.db import SessionDep from app.filters.circuit import CircuitFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one +from app.queries.common import ( + router_create_one, + router_delete_one, + router_read_many, + router_read_one, +) from app.queries.factory import query_params_factory from app.schemas.circuit import ( CircuitCreate, @@ -120,3 +125,25 @@ def read_many( authorized_project_id=user_context.project_id, filter_joins=filter_joins, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> CircuitRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=Circuit, + authorized_project_id=None, + response_schema_class=CircuitRead, + apply_operations=_load, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=Circuit, + authorized_project_id=None, # already validated + ) + return one diff --git a/app/service/contribution.py b/app/service/contribution.py index 41f90f1d..19d2baee 100644 --- a/app/service/contribution.py +++ b/app/service/contribution.py @@ -7,7 +7,7 @@ import app.queries.common from app.db.auth import constrain_entity_query_to_project, constrain_to_accessible_entities from app.db.model import Agent, Contribution, Entity -from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.auth import AdminContextDep, UserContextDep, UserContextWithProjectIdDep from app.dependencies.common import PaginationQuery from app.dependencies.db import SessionDep from app.filters.common import ContributionFilterDep @@ -121,3 +121,25 @@ def create_one( user_context=user_context, apply_operations=_load, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> ContributionRead: + one = app.queries.common.router_read_one( + id_=id_, + db=db, + db_model_class=Contribution, + authorized_project_id=None, + response_schema_class=ContributionRead, + apply_operations=_load, + ) + app.queries.common.router_delete_one( + id_=id_, + db=db, + db_model_class=Contribution, + authorized_project_id=None, + ) + return one diff --git a/app/service/electrical_cell_recording.py b/app/service/electrical_cell_recording.py index e7c744a2..d238b3ec 100644 --- a/app/service/electrical_cell_recording.py +++ b/app/service/electrical_cell_recording.py @@ -10,7 +10,7 @@ ElectricalCellRecording, Subject, ) -from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.auth import AdminContextDep, UserContextDep, UserContextWithProjectIdDep from app.dependencies.common import ( FacetsDep, InBrainRegionDep, @@ -19,7 +19,12 @@ ) from app.dependencies.db import SessionDep from app.filters.electrical_cell_recording import ElectricalCellRecordingFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one +from app.queries.common import ( + router_create_one, + router_delete_one, + router_read_many, + router_read_one, +) from app.queries.factory import query_params_factory from app.schemas.electrical_cell_recording import ( ElectricalCellRecordingCreate, @@ -141,3 +146,25 @@ def read_many( authorized_project_id=user_context.project_id, filter_joins=filter_joins, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> ElectricalCellRecordingRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=ElectricalCellRecording, + authorized_project_id=None, + response_schema_class=ElectricalCellRecordingRead, + apply_operations=_load, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=ElectricalCellRecording, + authorized_project_id=None, + ) + return one diff --git a/app/service/emodel.py b/app/service/emodel.py index 1767f2c0..ad24d19c 100644 --- a/app/service/emodel.py +++ b/app/service/emodel.py @@ -11,7 +11,7 @@ IonChannelModel, ReconstructionMorphology, ) -from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.auth import AdminContextDep, UserContextDep, UserContextWithProjectIdDep from app.dependencies.common import ( FacetsDep, InBrainRegionDep, @@ -20,7 +20,12 @@ ) from app.dependencies.db import SessionDep from app.filters.emodel import EModelFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one +from app.queries.common import ( + router_create_one, + router_delete_one, + router_read_many, + router_read_one, +) from app.queries.factory import query_params_factory from app.schemas.emodel import EModelCreate, EModelRead, EModelReadExpanded from app.schemas.types import ListResponse @@ -134,3 +139,25 @@ def read_many( filter_model=emodel_filter, filter_joins=filter_joins, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> EModelRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=EModel, + authorized_project_id=None, + response_schema_class=EModelRead, + apply_operations=_load, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=EModel, + authorized_project_id=None, + ) + return one diff --git a/app/service/etype.py b/app/service/etype.py index 14e4b133..805cc69f 100644 --- a/app/service/etype.py +++ b/app/service/etype.py @@ -1,10 +1,11 @@ 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.queries.common import router_delete_one, router_read_many, router_read_one from app.schemas.annotation import ETypeClassRead from app.schemas.types import ListResponse @@ -40,3 +41,25 @@ def read_one(id_: uuid.UUID, db: SessionDep) -> ETypeClassRead: response_schema_class=ETypeClassRead, apply_operations=None, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> ETypeClassRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=ETypeClass, + authorized_project_id=None, + response_schema_class=ETypeClassRead, + apply_operations=None, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=ETypeClass, + authorized_project_id=None, + ) + return one diff --git a/app/service/etype_classification.py b/app/service/etype_classification.py index 5fbe2f02..9d59256b 100644 --- a/app/service/etype_classification.py +++ b/app/service/etype_classification.py @@ -1,3 +1,5 @@ +import uuid + import sqlalchemy as sa from fastapi import HTTPException from sqlalchemy.orm import joinedload, raiseload @@ -7,10 +9,10 @@ Entity, ETypeClassification, ) -from app.dependencies.auth import UserContextWithProjectIdDep +from app.dependencies.auth import AdminContextDep, UserContextWithProjectIdDep from app.dependencies.db import SessionDep from app.logger import L -from app.queries.common import router_create_one +from app.queries.common import router_create_one, router_delete_one, router_read_one from app.schemas.classification import ( ETypeClassificationCreate, ETypeClassificationRead, @@ -52,3 +54,25 @@ def create_one( response_schema_class=ETypeClassificationRead, apply_operations=_load, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> ETypeClassificationRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=ETypeClassification, + authorized_project_id=None, + response_schema_class=ETypeClassificationRead, + apply_operations=_load, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=ETypeClassification, + authorized_project_id=None, + ) + return one diff --git a/app/service/experimental_bouton_density.py b/app/service/experimental_bouton_density.py index eae2dea0..8313d758 100644 --- a/app/service/experimental_bouton_density.py +++ b/app/service/experimental_bouton_density.py @@ -14,7 +14,7 @@ ExperimentalBoutonDensity, Subject, ) -from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.auth import AdminContextDep, UserContextDep, UserContextWithProjectIdDep from app.dependencies.common import ( FacetsDep, InBrainRegionDep, @@ -23,7 +23,12 @@ ) from app.dependencies.db import SessionDep from app.filters.density import ExperimentalBoutonDensityFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one +from app.queries.common import ( + router_create_one, + router_delete_one, + router_read_many, + router_read_one, +) from app.queries.factory import query_params_factory from app.schemas.density import ExperimentalBoutonDensityCreate, ExperimentalBoutonDensityRead from app.schemas.types import ListResponse @@ -138,3 +143,25 @@ def create_one( response_schema_class=ExperimentalBoutonDensityRead, apply_operations=_load, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> ExperimentalBoutonDensityRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=ExperimentalBoutonDensity, + authorized_project_id=None, + response_schema_class=ExperimentalBoutonDensityRead, + apply_operations=_load, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=ExperimentalBoutonDensity, + authorized_project_id=None, + ) + return one diff --git a/app/service/experimental_neuron_density.py b/app/service/experimental_neuron_density.py index 14fb6ddb..4c8aabc6 100644 --- a/app/service/experimental_neuron_density.py +++ b/app/service/experimental_neuron_density.py @@ -14,7 +14,7 @@ ExperimentalNeuronDensity, Subject, ) -from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.auth import AdminContextDep, UserContextDep, UserContextWithProjectIdDep from app.dependencies.common import ( FacetsDep, InBrainRegionDep, @@ -23,7 +23,12 @@ ) from app.dependencies.db import SessionDep from app.filters.density import ExperimentalNeuronDensityFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one +from app.queries.common import ( + router_create_one, + router_delete_one, + router_read_many, + router_read_one, +) from app.queries.factory import query_params_factory from app.schemas.density import ExperimentalNeuronDensityCreate, ExperimentalNeuronDensityRead from app.schemas.types import ListResponse @@ -141,3 +146,25 @@ def create_one( response_schema_class=ExperimentalNeuronDensityRead, apply_operations=_load, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> ExperimentalNeuronDensityRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=ExperimentalNeuronDensity, + authorized_project_id=None, + response_schema_class=ExperimentalNeuronDensityRead, + apply_operations=_load, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=ExperimentalNeuronDensity, + authorized_project_id=None, + ) + return one diff --git a/app/service/experimental_synapses_per_connection.py b/app/service/experimental_synapses_per_connection.py index d2040183..e87f7aa8 100644 --- a/app/service/experimental_synapses_per_connection.py +++ b/app/service/experimental_synapses_per_connection.py @@ -16,7 +16,7 @@ MTypeClass, Subject, ) -from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.auth import AdminContextDep, UserContextDep, UserContextWithProjectIdDep from app.dependencies.common import ( FacetsDep, InBrainRegionDep, @@ -25,7 +25,12 @@ ) from app.dependencies.db import SessionDep from app.filters.density import ExperimentalSynapsesPerConnectionFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one +from app.queries.common import ( + router_create_one, + router_delete_one, + router_read_many, + router_read_one, +) from app.queries.factory import query_params_factory from app.schemas.density import ( ExperimentalSynapsesPerConnectionCreate, @@ -167,3 +172,25 @@ def create_one( response_schema_class=ExperimentalSynapsesPerConnectionRead, apply_operations=_load, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> ExperimentalSynapsesPerConnectionRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=ExperimentalSynapsesPerConnection, + authorized_project_id=None, + response_schema_class=ExperimentalSynapsesPerConnectionRead, + apply_operations=_load, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=ExperimentalSynapsesPerConnection, + authorized_project_id=None, + ) + return one diff --git a/app/service/ion_channel_model.py b/app/service/ion_channel_model.py index 1dc0e216..5d7c9829 100644 --- a/app/service/ion_channel_model.py +++ b/app/service/ion_channel_model.py @@ -6,7 +6,7 @@ from sqlalchemy.orm import joinedload, raiseload, selectinload from app.db.model import Contribution, Ion, IonChannelModel -from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.auth import AdminContextDep, UserContextDep, UserContextWithProjectIdDep from app.dependencies.common import ( FacetsDep, InBrainRegionDep, @@ -16,7 +16,12 @@ from app.dependencies.db import SessionDep from app.errors import ApiError, ApiErrorCode from app.filters.ion_channel_model import IonChannelModelFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one +from app.queries.common import ( + router_create_one, + router_delete_one, + router_read_many, + router_read_one, +) from app.queries.factory import query_params_factory from app.schemas.ion_channel_model import ( IonChannelModelCreate, @@ -130,3 +135,25 @@ def create_one( db_model_class=IonChannelModel, response_schema_class=IonChannelModelRead, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> IonChannelModelRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=IonChannelModel, + authorized_project_id=None, + response_schema_class=IonChannelModelRead, + apply_operations=_load, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=IonChannelModel, + authorized_project_id=None, + ) + return one diff --git a/app/service/license.py b/app/service/license.py index cea1b478..9f032fc1 100644 --- a/app/service/license.py +++ b/app/service/license.py @@ -7,7 +7,7 @@ from app.dependencies.common import PaginationQuery from app.dependencies.db import SessionDep from app.errors import ensure_result -from app.queries.common import router_create_one +from app.queries.common import router_create_one, router_delete_one, router_read_one from app.schemas.base import LicenseCreate, LicenseRead from app.schemas.types import ListResponse, PaginationResponse @@ -54,3 +54,25 @@ def create_one( response_schema_class=LicenseRead, user_context=user_context, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> LicenseRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=License, + authorized_project_id=None, + response_schema_class=LicenseRead, + apply_operations=None, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=License, + authorized_project_id=None, + ) + return one diff --git a/app/service/memodel.py b/app/service/memodel.py index 2e45e62f..a1105736 100644 --- a/app/service/memodel.py +++ b/app/service/memodel.py @@ -17,7 +17,7 @@ MEModelCalibrationResult, ReconstructionMorphology, ) -from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.auth import AdminContextDep, UserContextDep, UserContextWithProjectIdDep from app.dependencies.common import ( FacetsDep, InBrainRegionDep, @@ -26,7 +26,12 @@ ) from app.dependencies.db import SessionDep from app.filters.memodel import MEModelFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one +from app.queries.common import ( + router_create_one, + router_delete_one, + router_read_many, + router_read_one, +) from app.queries.factory import query_params_factory from app.schemas.me_model import MEModelCreate, MEModelRead from app.schemas.types import ListResponse @@ -162,3 +167,25 @@ def read_many( filter_model=memodel_filter, filter_joins=filter_joins, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> MEModelRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=MEModel, + authorized_project_id=None, + response_schema_class=MEModelRead, + apply_operations=_load, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=MEModel, + authorized_project_id=None, + ) + return one diff --git a/app/service/memodel_calibration_result.py b/app/service/memodel_calibration_result.py index 01603b1e..05b0776d 100644 --- a/app/service/memodel_calibration_result.py +++ b/app/service/memodel_calibration_result.py @@ -4,7 +4,7 @@ from sqlalchemy.orm import joinedload from app.db.model import MEModelCalibrationResult, Subject -from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.auth import AdminContextDep, UserContextDep, UserContextWithProjectIdDep from app.dependencies.common import ( FacetsDep, InBrainRegionDep, @@ -13,7 +13,12 @@ ) from app.dependencies.db import SessionDep from app.filters.memodel_calibration_result import MEModelCalibrationResultFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one +from app.queries.common import ( + router_create_one, + router_delete_one, + router_read_many, + router_read_one, +) from app.schemas.memodel_calibration_result import ( MEModelCalibrationResultCreate, MEModelCalibrationResultRead, @@ -83,3 +88,25 @@ def read_many( authorized_project_id=user_context.project_id, filter_joins=None, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> MEModelCalibrationResultRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=MEModelCalibrationResult, + authorized_project_id=None, + response_schema_class=MEModelCalibrationResultRead, + apply_operations=_load, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=MEModelCalibrationResult, + authorized_project_id=None, + ) + return one diff --git a/app/service/morphology.py b/app/service/morphology.py index a337dc20..3809dc22 100644 --- a/app/service/morphology.py +++ b/app/service/morphology.py @@ -18,7 +18,7 @@ MeasurementKind, ReconstructionMorphology, ) -from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.auth import AdminContextDep, UserContextDep, UserContextWithProjectIdDep from app.dependencies.common import ( FacetsDep, InBrainRegionDep, @@ -27,7 +27,12 @@ ) from app.dependencies.db import SessionDep from app.filters.morphology import MorphologyFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one +from app.queries.common import ( + router_create_one, + router_delete_one, + router_read_many, + router_read_one, +) from app.queries.factory import query_params_factory from app.schemas.morphology import ( ReconstructionMorphologyAnnotationExpandedRead, @@ -161,3 +166,22 @@ def read_many( filter_model=morphology_filter, filter_joins=filter_joins, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> ReconstructionMorphologyRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=ReconstructionMorphology, + authorized_project_id=None, + response_schema_class=ReconstructionMorphologyRead, + apply_operations=partial(_load_from_db, expand_measurement_annotation=False), + ) + router_delete_one( + id_=id_, db=db, db_model_class=ReconstructionMorphology, authorized_project_id=None + ) + return one diff --git a/app/service/mtype.py b/app/service/mtype.py index 6d4c1a98..8a673b86 100644 --- a/app/service/mtype.py +++ b/app/service/mtype.py @@ -1,10 +1,11 @@ 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.queries.common import router_delete_one, router_read_many, router_read_one from app.schemas.annotation import MTypeClassRead from app.schemas.types import ListResponse @@ -40,3 +41,25 @@ def read_one(id_: uuid.UUID, db: SessionDep) -> MTypeClassRead: response_schema_class=MTypeClassRead, apply_operations=None, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> MTypeClassRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=MTypeClass, + authorized_project_id=None, + response_schema_class=MTypeClassRead, + apply_operations=None, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=MTypeClass, + authorized_project_id=None, + ) + return one diff --git a/app/service/mtype_classification.py b/app/service/mtype_classification.py index dd8953a4..7f1b1c41 100644 --- a/app/service/mtype_classification.py +++ b/app/service/mtype_classification.py @@ -1,3 +1,5 @@ +import uuid + import sqlalchemy as sa from fastapi import HTTPException from sqlalchemy.orm import joinedload, raiseload @@ -7,10 +9,10 @@ Entity, MTypeClassification, ) -from app.dependencies.auth import UserContextWithProjectIdDep +from app.dependencies.auth import AdminContextDep, UserContextWithProjectIdDep from app.dependencies.db import SessionDep from app.logger import L -from app.queries.common import router_create_one +from app.queries.common import router_create_one, router_delete_one, router_read_one from app.schemas.classification import ( MTypeClassificationCreate, MTypeClassificationRead, @@ -53,3 +55,22 @@ def create_one( response_schema_class=MTypeClassificationRead, apply_operations=_load, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> MTypeClassificationRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=MTypeClassification, + authorized_project_id=None, + response_schema_class=MTypeClassificationRead, + apply_operations=_load, + ) + router_delete_one( + id_=id_, db=db, db_model_class=MTypeClassification, authorized_project_id=None + ) + return one diff --git a/app/service/organization.py b/app/service/organization.py index bc475423..1252134c 100644 --- a/app/service/organization.py +++ b/app/service/organization.py @@ -67,3 +67,25 @@ def create_one( user_context=user_context, apply_operations=_load, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> OrganizationRead: + one = app.queries.common.router_read_one( + id_=id_, + db=db, + db_model_class=Organization, + authorized_project_id=None, + response_schema_class=OrganizationRead, + apply_operations=_load, + ) + app.queries.common.router_delete_one( + id_=id_, + db=db, + db_model_class=Organization, + authorized_project_id=None, + ) + return one diff --git a/app/service/person.py b/app/service/person.py index 2f9af75c..8b179bb6 100644 --- a/app/service/person.py +++ b/app/service/person.py @@ -9,7 +9,12 @@ from app.dependencies.common import PaginationQuery from app.dependencies.db import SessionDep from app.filters.person import PersonFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one +from app.queries.common import ( + router_create_one, + router_delete_one, + router_read_many, + router_read_one, +) from app.queries.factory import query_params_factory from app.schemas.agent import PersonCreate, PersonRead from app.schemas.types import ListResponse @@ -92,3 +97,25 @@ def create_one(person: PersonCreate, db: SessionDep, user_context: AdminContextD user_context=user_context, apply_operations=_load, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> PersonRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=Person, + authorized_project_id=None, + response_schema_class=PersonRead, + apply_operations=_load, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=Person, + authorized_project_id=None, + ) + return one diff --git a/app/service/role.py b/app/service/role.py index 4c8b25eb..40f72021 100644 --- a/app/service/role.py +++ b/app/service/role.py @@ -51,3 +51,25 @@ def create_one(json_model: RoleCreate, db: SessionDep, user_context: AdminContex response_schema_class=RoleRead, user_context=user_context, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> RoleRead: + one = app.queries.common.router_read_one( + id_=id_, + db=db, + db_model_class=Role, + authorized_project_id=None, + response_schema_class=RoleRead, + apply_operations=None, + ) + app.queries.common.router_delete_one( + id_=id_, + db=db, + db_model_class=Role, + authorized_project_id=None, + ) + return one diff --git a/app/service/simulation.py b/app/service/simulation.py index f592b640..2f77a375 100644 --- a/app/service/simulation.py +++ b/app/service/simulation.py @@ -8,7 +8,7 @@ Agent, Simulation, ) -from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.auth import AdminContextDep, UserContextDep, UserContextWithProjectIdDep from app.dependencies.common import ( FacetsDep, InBrainRegionDep, @@ -17,7 +17,12 @@ ) from app.dependencies.db import SessionDep from app.filters.simulation import SimulationFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one +from app.queries.common import ( + router_create_one, + router_delete_one, + router_read_many, + router_read_one, +) from app.queries.factory import query_params_factory from app.schemas.simulation import ( SimulationCreate, @@ -116,3 +121,25 @@ def read_many( authorized_project_id=user_context.project_id, filter_joins=filter_joins, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> SimulationRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=Simulation, + authorized_project_id=None, + response_schema_class=SimulationRead, + apply_operations=_load, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=Simulation, + authorized_project_id=None, + ) + return one diff --git a/app/service/simulation_campaign.py b/app/service/simulation_campaign.py index fbedfe6b..d2d5b6ec 100644 --- a/app/service/simulation_campaign.py +++ b/app/service/simulation_campaign.py @@ -9,7 +9,7 @@ Simulation, SimulationCampaign, ) -from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.auth import AdminContextDep, UserContextDep, UserContextWithProjectIdDep from app.dependencies.common import ( FacetsDep, InBrainRegionDep, @@ -18,7 +18,12 @@ ) from app.dependencies.db import SessionDep from app.filters.simulation_campaign import SimulationCampaignFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one +from app.queries.common import ( + router_create_one, + router_delete_one, + router_read_many, + router_read_one, +) from app.queries.factory import query_params_factory from app.schemas.simulation_campaign import ( SimulationCampaignCreate, @@ -121,3 +126,25 @@ def read_many( authorized_project_id=user_context.project_id, filter_joins=filter_joins, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> SimulationCampaignRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=SimulationCampaign, + authorized_project_id=None, + response_schema_class=SimulationCampaignRead, + apply_operations=_load, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=SimulationCampaign, + authorized_project_id=None, + ) + return one diff --git a/app/service/simulation_result.py b/app/service/simulation_result.py index 5721fa30..09f6c804 100644 --- a/app/service/simulation_result.py +++ b/app/service/simulation_result.py @@ -8,7 +8,7 @@ Agent, SimulationResult, ) -from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.auth import AdminContextDep, UserContextDep, UserContextWithProjectIdDep from app.dependencies.common import ( FacetsDep, InBrainRegionDep, @@ -17,7 +17,12 @@ ) from app.dependencies.db import SessionDep from app.filters.simulation_result import SimulationResultFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one +from app.queries.common import ( + router_create_one, + router_delete_one, + router_read_many, + router_read_one, +) from app.queries.factory import query_params_factory from app.schemas.simulation_result import ( SimulationResultCreate, @@ -115,3 +120,25 @@ def read_many( authorized_project_id=user_context.project_id, filter_joins=filter_joins, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> SimulationResultRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=SimulationResult, + authorized_project_id=None, + response_schema_class=SimulationResultRead, + apply_operations=_load, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=SimulationResult, + authorized_project_id=None, + ) + return one diff --git a/app/service/single_neuron_simulation.py b/app/service/single_neuron_simulation.py index 250ef2f4..4e2af229 100644 --- a/app/service/single_neuron_simulation.py +++ b/app/service/single_neuron_simulation.py @@ -4,7 +4,7 @@ from sqlalchemy.orm import aliased, joinedload, raiseload, selectinload from app.db.model import Agent, MEModel, SingleNeuronSimulation -from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.auth import AdminContextDep, UserContextDep, UserContextWithProjectIdDep from app.dependencies.common import ( FacetsDep, InBrainRegionDep, @@ -13,7 +13,12 @@ ) from app.dependencies.db import SessionDep from app.filters.single_neuron_simulation import SingleNeuronSimulationFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one +from app.queries.common import ( + router_create_one, + router_delete_one, + router_read_many, + router_read_one, +) from app.queries.factory import query_params_factory from app.schemas.simulation import SingleNeuronSimulationCreate, SingleNeuronSimulationRead from app.schemas.types import ListResponse @@ -111,3 +116,25 @@ def read_many( authorized_project_id=user_context.project_id, filter_joins=filter_joins, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> SingleNeuronSimulationRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=SingleNeuronSimulation, + authorized_project_id=None, + response_schema_class=SingleNeuronSimulationRead, + apply_operations=_load, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=SingleNeuronSimulation, + authorized_project_id=None, + ) + return one diff --git a/app/service/single_neuron_synaptome.py b/app/service/single_neuron_synaptome.py index f47a73ff..0ca9fb5c 100644 --- a/app/service/single_neuron_synaptome.py +++ b/app/service/single_neuron_synaptome.py @@ -4,7 +4,7 @@ from sqlalchemy.orm import aliased, joinedload, raiseload, selectinload from app.db.model import Agent, Contribution, MEModel, SingleNeuronSynaptome -from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.auth import AdminContextDep, UserContextDep, UserContextWithProjectIdDep from app.dependencies.common import ( FacetsDep, InBrainRegionDep, @@ -13,7 +13,12 @@ ) from app.dependencies.db import SessionDep from app.filters.single_neuron_synaptome import SingleNeuronSynaptomeFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one +from app.queries.common import ( + router_create_one, + router_delete_one, + router_read_many, + router_read_one, +) from app.queries.factory import query_params_factory from app.schemas.synaptome import SingleNeuronSynaptomeCreate, SingleNeuronSynaptomeRead from app.schemas.types import ListResponse @@ -113,3 +118,25 @@ def read_many( authorized_project_id=user_context.project_id, filter_joins=filter_joins, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> SingleNeuronSynaptomeRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=SingleNeuronSynaptome, + authorized_project_id=None, + response_schema_class=SingleNeuronSynaptomeRead, + apply_operations=_load, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=SingleNeuronSynaptome, + authorized_project_id=None, + ) + return one diff --git a/app/service/single_neuron_synaptome_simulation.py b/app/service/single_neuron_synaptome_simulation.py index 1b79e846..86f157ff 100644 --- a/app/service/single_neuron_synaptome_simulation.py +++ b/app/service/single_neuron_synaptome_simulation.py @@ -8,7 +8,7 @@ SingleNeuronSynaptome, SingleNeuronSynaptomeSimulation, ) -from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.auth import AdminContextDep, UserContextDep, UserContextWithProjectIdDep from app.dependencies.common import ( FacetsDep, InBrainRegionDep, @@ -19,7 +19,12 @@ from app.filters.single_neuron_synaptome_simulation import ( SingleNeuronSynaptomeSimulationFilterDep, ) -from app.queries.common import router_create_one, router_read_many, router_read_one +from app.queries.common import ( + router_create_one, + router_delete_one, + router_read_many, + router_read_one, +) from app.queries.factory import query_params_factory from app.schemas.simulation import ( SingleNeuronSynaptomeSimulationCreate, @@ -124,3 +129,25 @@ def read_many( authorized_project_id=user_context.project_id, filter_joins=filter_joins, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> SingleNeuronSynaptomeSimulationRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=SingleNeuronSynaptomeSimulation, + authorized_project_id=None, + response_schema_class=SingleNeuronSynaptomeSimulationRead, + apply_operations=_load, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=SingleNeuronSynaptomeSimulation, + authorized_project_id=None, + ) + return one diff --git a/app/service/species.py b/app/service/species.py index bfc0453b..ef2c268e 100644 --- a/app/service/species.py +++ b/app/service/species.py @@ -85,3 +85,25 @@ def read_many( filter_model=species_filter, filter_joins=filter_joins, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> SpeciesRead: + one = app.queries.common.router_read_one( + id_=id_, + db=db, + db_model_class=Species, + authorized_project_id=None, + response_schema_class=SpeciesRead, + apply_operations=None, + ) + app.queries.common.router_delete_one( + id_=id_, + db=db, + db_model_class=Species, + authorized_project_id=None, + ) + return one diff --git a/app/service/strain.py b/app/service/strain.py index a4660e7c..71ee976e 100644 --- a/app/service/strain.py +++ b/app/service/strain.py @@ -77,3 +77,25 @@ def create_one( response_schema_class=StrainRead, apply_operations=_load, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> StrainRead: + one = app.queries.common.router_read_one( + id_=id_, + db=db, + db_model_class=Strain, + authorized_project_id=None, + response_schema_class=StrainRead, + apply_operations=_load, + ) + app.queries.common.router_delete_one( + id_=id_, + db=db, + db_model_class=Strain, + authorized_project_id=None, + ) + return one diff --git a/app/service/subject.py b/app/service/subject.py index 180f06b2..f6b3820d 100644 --- a/app/service/subject.py +++ b/app/service/subject.py @@ -4,11 +4,16 @@ from sqlalchemy.orm import joinedload, raiseload from app.db.model import Subject -from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.auth import AdminContextDep, UserContextDep, UserContextWithProjectIdDep from app.dependencies.common import FacetsDep, PaginationQuery, SearchDep from app.dependencies.db import SessionDep from app.filters.subject import SubjectFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one +from app.queries.common import ( + router_create_one, + router_delete_one, + router_read_many, + router_read_one, +) from app.queries.factory import query_params_factory from app.schemas.subject import SubjectCreate, SubjectRead from app.schemas.types import ListResponse @@ -86,3 +91,25 @@ def read_many( authorized_project_id=user_context.project_id, filter_joins=filter_joins, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> SubjectRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=Subject, + authorized_project_id=None, + response_schema_class=SubjectRead, + apply_operations=_load, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=Subject, + authorized_project_id=None, + ) + return one diff --git a/app/service/validation_result.py b/app/service/validation_result.py index fa421093..2396b879 100644 --- a/app/service/validation_result.py +++ b/app/service/validation_result.py @@ -4,7 +4,7 @@ from sqlalchemy.orm import joinedload, raiseload from app.db.model import Subject, ValidationResult -from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.auth import AdminContextDep, UserContextDep, UserContextWithProjectIdDep from app.dependencies.common import ( FacetsDep, InBrainRegionDep, @@ -13,7 +13,12 @@ ) from app.dependencies.db import SessionDep from app.filters.validation_result import ValidationResultFilterDep -from app.queries.common import router_create_one, router_read_many, router_read_one +from app.queries.common import ( + router_create_one, + router_delete_one, + router_read_many, + router_read_one, +) from app.schemas.types import ListResponse from app.schemas.validation import ValidationResultCreate, ValidationResultRead @@ -84,3 +89,25 @@ def read_many( authorized_project_id=user_context.project_id, filter_joins=None, ) + + +def delete_one( + _: AdminContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> ValidationResultRead: + one = router_read_one( + id_=id_, + db=db, + db_model_class=ValidationResult, + authorized_project_id=None, + response_schema_class=ValidationResultRead, + apply_operations=None, + ) + router_delete_one( + id_=id_, + db=db, + db_model_class=ValidationResult, + authorized_project_id=None, + ) + return one diff --git a/tests/test_circuit.py b/tests/test_circuit.py index 5ee987ad..e547f075 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -59,6 +59,26 @@ def test_read_one(client, circuit, circuit_json_data): _assert_read_response(data, circuit_json_data) +def test_delete_one(client, client_admin, circuit): + data = assert_request( + client.delete, url=f"{ROUTE}/{circuit.id}", expected_status_code=403 + ).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + # root circuit cannot be deleted because circuit points to it + data = assert_request( + client_admin.delete, url=f"{ROUTE}/{circuit.root_circuit_id}", expected_status_code=409 + ).json() + assert data["error_code"] == "INVALID_REQUEST" + assert ( + data["message"] == "Circuit cannot be deleted because of foreign keys integrity violation" + ) + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{circuit.id}").json() + assert data["id"] == str(circuit.id) + + def test_read_many(client, circuit, circuit_json_data): data = assert_request(client.get, url=f"{ROUTE}").json()["data"] diff --git a/tests/test_contribution.py b/tests/test_contribution.py index 86ac4a15..a514cafb 100644 --- a/tests/test_contribution.py +++ b/tests/test_contribution.py @@ -4,6 +4,7 @@ MISSING_ID, MISSING_ID_COMPACT, add_db, + assert_request, check_creation_fields, create_person, create_reconstruction_morphology_id, @@ -107,6 +108,49 @@ def test_create_contribution( ] +def test_delete_one( + client, client_admin, role_id, person_id, brain_region_id, strain_id, species_id +): + morphology_id = create_reconstruction_morphology_id( + client, + species_id=species_id, + strain_id=strain_id, + brain_region_id=brain_region_id, + authorized_public=False, + ) + contribution = assert_request( + client.post, + url=ROUTE, + json={ + "agent_id": str(person_id), + "role_id": str(role_id), + "entity_id": str(morphology_id), + }, + ).json() + + model_id = contribution["id"] + + data = assert_request( + client.get, + url=f"{ROUTE_MORPH}/{morphology_id}", + ).json() + assert len(data["contributions"]) == 1 + assert data["contributions"][0]["id"] == str(contribution["id"]) + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + data = assert_request( + client.get, + url=f"{ROUTE_MORPH}/{morphology_id}", + ).json() + assert len(data["contributions"]) == 0 + + def test_missing(client): response = client.get(f"{ROUTE}/{MISSING_ID}") assert response.status_code == 404 diff --git a/tests/test_electrical_cell_recording.py b/tests/test_electrical_cell_recording.py index e5d31bdf..e5b1eecd 100644 --- a/tests/test_electrical_cell_recording.py +++ b/tests/test_electrical_cell_recording.py @@ -4,7 +4,15 @@ import pytest -from app.db.model import BrainRegion, ElectricalCellRecording, Species, Subject +from app.db.model import ( + Asset, + BrainRegion, + ElectricalCellRecording, + ElectricalRecordingStimulus, + ETypeClassification, + Species, + Subject, +) from app.db.types import EntityType from .utils import ( @@ -15,6 +23,7 @@ check_authorization, check_brain_region_filter, check_missing, + count_db_class, create_brain_region, create_electrical_cell_recording_db, create_electrical_cell_recording_id, @@ -72,6 +81,27 @@ def test_read_one(client, subject_id, license_id, brain_region_id, trace_id_with ] +def test_delete_one(db, client, client_admin, trace_id_with_assets): + assert count_db_class(db, ElectricalCellRecording) == 1 + assert count_db_class(db, ElectricalRecordingStimulus) == 2 + assert count_db_class(db, Asset) == 1 + assert count_db_class(db, ETypeClassification) == 1 + + data = assert_request( + client.delete, url=f"{ROUTE}/{trace_id_with_assets}", expected_status_code=403 + ).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{trace_id_with_assets}").json() + assert data["id"] == str(trace_id_with_assets) + + assert count_db_class(db, ElectricalCellRecording) == 0 + assert count_db_class(db, ElectricalRecordingStimulus) == 0 + assert count_db_class(db, Asset) == 0 + assert count_db_class(db, ETypeClassification) == 0 + + def test_missing(client): check_missing(ROUTE, client) diff --git a/tests/test_emodel.py b/tests/test_emodel.py index 38a0b354..19dfd357 100644 --- a/tests/test_emodel.py +++ b/tests/test_emodel.py @@ -3,12 +3,14 @@ from fastapi.testclient import TestClient +from app.db.model import Contribution, EModel, ETypeClass, ETypeClassification from app.db.types import EntityType from .conftest import CreateIds, EModelIds from .utils import ( TEST_DATA_DIR, assert_request, + count_db_class, create_reconstruction_morphology_id, upload_entity_asset, ) @@ -69,6 +71,27 @@ def test_get_emodel(client: TestClient, emodel_id: str): assert data["created_by"]["id"] == data["updated_by"]["id"] +def test_delete_one(db, client, client_admin, emodel_id): + assert count_db_class(db, EModel) == 1 + assert count_db_class(db, Contribution) == 2 + assert count_db_class(db, ETypeClassification) == 1 + assert count_db_class(db, ETypeClass) == 1 + + data = assert_request( + client.delete, url=f"{ROUTE}/{emodel_id}", expected_status_code=403 + ).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{emodel_id}").json() + assert data["id"] == str(emodel_id) + + assert count_db_class(db, EModel) == 0 + assert count_db_class(db, Contribution) == 0 + assert count_db_class(db, ETypeClassification) == 0 + assert count_db_class(db, ETypeClass) == 1 + + def test_missing(client): response = client.get(f"{ROUTE}/{uuid.uuid4()}") assert response.status_code == 404 diff --git a/tests/test_etype.py b/tests/test_etype.py index 7801c57a..ae80b299 100644 --- a/tests/test_etype.py +++ b/tests/test_etype.py @@ -4,7 +4,9 @@ PROJECT_ID, add_all_db, add_db, + assert_request, check_missing, + count_db_class, with_creation_fields, ) @@ -155,3 +157,59 @@ def test_emodel_etypes( assert facets["etype"] == [ {"id": str(etype1.id), "label": "e1", "count": 1, "type": "etype"}, ] + + +def test_delete_one( + db, client, client_admin, person_id, brain_region_id, species_id, morphology_id +): + emodel_id = add_db( + db, + EModel( + name="Test name", + description="Test description", + brain_region_id=brain_region_id, + species_id=species_id, + strain_id=None, + exemplar_morphology_id=morphology_id, + authorized_public=False, + authorized_project_id=PROJECT_ID, + created_by_id=person_id, + updated_by_id=person_id, + ), + ).id + + etype_json = { + "pref_label": "e1", + "alt_label": "e1", + "definition": "e1d", + } + etype = add_db( + db, ETypeClass(**etype_json | {"created_by_id": person_id, "updated_by_id": person_id}) + ) + + add_db( + db, + ETypeClassification( + entity_id=emodel_id, + etype_class_id=etype.id, + created_by_id=person_id, + updated_by_id=person_id, + authorized_public=False, + authorized_project_id=PROJECT_ID, + ), + ) + + assert count_db_class(db, EModel) == 1 + assert count_db_class(db, ETypeClass) == 1 + assert count_db_class(db, ETypeClassification) == 1 + + data = assert_request(client.delete, url=f"{ROUTE}/{etype.id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{etype.id}").json() + assert data["id"] == str(etype.id) + + assert count_db_class(db, EModel) == 1 + assert count_db_class(db, ETypeClass) == 0 + assert count_db_class(db, ETypeClassification) == 0 diff --git a/tests/test_etype_classification.py b/tests/test_etype_classification.py index 0224162e..abd8e34f 100644 --- a/tests/test_etype_classification.py +++ b/tests/test_etype_classification.py @@ -1,8 +1,11 @@ import pytest +from app.db.model import EModel, ETypeClass, ETypeClassification + from .utils import ( assert_request, check_creation_fields, + count_db_class, create_etype, create_reconstruction_morphology_id, ) @@ -60,6 +63,31 @@ def test_create_one(client, json_data, emodel_id): assert json_data["etype_class_id"] in {m["id"] for m in data} +def test_delete_one(db, client, client_admin, json_data): + classification = assert_request( + client.post, + url=ROUTE, + json=json_data, + ).json() + + assert count_db_class(db, EModel) == 1 + assert count_db_class(db, ETypeClassification) == 2 + assert count_db_class(db, ETypeClass) == 2 + + data = assert_request( + client.delete, url=f"{ROUTE}/{classification['id']}", expected_status_code=403 + ).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{classification['id']}").json() + assert data["id"] == str(classification["id"]) + + assert count_db_class(db, EModel) == 1 + assert count_db_class(db, ETypeClassification) == 1 + assert count_db_class(db, ETypeClass) == 2 + + def test_create_one__unauthorized_entity(client_user_1, unauthorized_morph_id, json_data): # Doesn't matter it isn't an emodel because the check is done first json_data |= {"entity_id": str(unauthorized_morph_id)} diff --git a/tests/test_experimental_bouton_density.py b/tests/test_experimental_bouton_density.py index 311ae301..fbcf2593 100644 --- a/tests/test_experimental_bouton_density.py +++ b/tests/test_experimental_bouton_density.py @@ -3,7 +3,15 @@ import pytest -from app.db.model import BrainRegion, Contribution, ExperimentalBoutonDensity, Species, Subject +from app.db.model import ( + BrainRegion, + Contribution, + ExperimentalBoutonDensity, + MTypeClass, + MTypeClassification, + Species, + Subject, +) from app.db.types import EntityType from app.filters.density import ExperimentalBoutonDensityFilter from app.schemas.density import ExperimentalBoutonDensityCreate @@ -17,6 +25,7 @@ check_brain_region_filter, check_missing, check_pagination, + count_db_class, ) ROUTE = "/experimental-bouton-density" @@ -71,6 +80,64 @@ def test_read_one(client, model_id, json_data): _assert_read_response(data["data"][0], json_data) +def test_delete_one( + db, + client, + client_admin, + model_id, + person_id, + role_id, +): + add_db( + db, + Contribution( + agent_id=person_id, + role_id=role_id, + entity_id=model_id, + created_by_id=person_id, + updated_by_id=person_id, + ), + ) + mtype = add_db( + db, + MTypeClass( + pref_label="m1", + alt_label="m1", + definition="e1d", + created_by_id=person_id, + updated_by_id=person_id, + ), + ) + add_db( + db, + MTypeClassification( + entity_id=model_id, + mtype_class_id=mtype.id, + created_by_id=person_id, + updated_by_id=person_id, + authorized_public=False, + authorized_project_id=PROJECT_ID, + ), + ) + + assert count_db_class(db, ExperimentalBoutonDensity) == 1 + assert count_db_class(db, Contribution) == 1 + assert count_db_class(db, MTypeClass) == 1 + assert count_db_class(db, MTypeClassification) == 1 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, ExperimentalBoutonDensity) == 0 + assert count_db_class(db, Contribution) == 0 + assert count_db_class(db, MTypeClass) == 1 + assert count_db_class(db, MTypeClassification) == 0 + + def test_missing(client): check_missing(ROUTE, client) diff --git a/tests/test_experimental_neuron_density.py b/tests/test_experimental_neuron_density.py index c5150004..77ac9aba 100644 --- a/tests/test_experimental_neuron_density.py +++ b/tests/test_experimental_neuron_density.py @@ -3,7 +3,15 @@ import pytest -from app.db.model import BrainRegion, Contribution, ExperimentalNeuronDensity, Species, Subject +from app.db.model import ( + BrainRegion, + Contribution, + ExperimentalNeuronDensity, + MTypeClass, + MTypeClassification, + Species, + Subject, +) from app.db.types import EntityType from app.filters.density import ExperimentalNeuronDensityFilter @@ -16,6 +24,7 @@ check_brain_region_filter, check_missing, check_pagination, + count_db_class, ) ROUTE = "/experimental-neuron-density" @@ -71,6 +80,64 @@ def test_read_one(client, model_id, json_data): _assert_read_response(data["data"][0], json_data) +def test_delete_one( + db, + client, + client_admin, + model_id, + person_id, + role_id, +): + add_db( + db, + Contribution( + agent_id=person_id, + role_id=role_id, + entity_id=model_id, + created_by_id=person_id, + updated_by_id=person_id, + ), + ) + mtype = add_db( + db, + MTypeClass( + pref_label="m1", + alt_label="m1", + definition="e1d", + created_by_id=person_id, + updated_by_id=person_id, + ), + ) + add_db( + db, + MTypeClassification( + entity_id=model_id, + mtype_class_id=mtype.id, + created_by_id=person_id, + updated_by_id=person_id, + authorized_public=False, + authorized_project_id=PROJECT_ID, + ), + ) + + assert count_db_class(db, ExperimentalNeuronDensity) == 1 + assert count_db_class(db, Contribution) == 1 + assert count_db_class(db, MTypeClass) == 1 + assert count_db_class(db, MTypeClassification) == 1 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, ExperimentalNeuronDensity) == 0 + assert count_db_class(db, Contribution) == 0 + assert count_db_class(db, MTypeClass) == 1 + assert count_db_class(db, MTypeClassification) == 0 + + def test_missing(client): check_missing(ROUTE, client) diff --git a/tests/test_experimental_synapses_per_connection.py b/tests/test_experimental_synapses_per_connection.py index 7c4b1cc1..44a14392 100644 --- a/tests/test_experimental_synapses_per_connection.py +++ b/tests/test_experimental_synapses_per_connection.py @@ -2,16 +2,23 @@ import pytest -from app.db.model import ExperimentalSynapsesPerConnection +from app.db.model import ( + BrainRegion, + Contribution, + ExperimentalSynapsesPerConnection, + MTypeClass, +) from app.db.types import EntityType from app.filters.density import ExperimentalSynapsesPerConnectionFilter from .utils import ( + add_db, assert_request, check_authorization, check_brain_region_filter, check_missing, check_pagination, + count_db_class, create_brain_region, create_mtype, ) @@ -93,6 +100,43 @@ def test_read_one(client, model_id, json_data): _assert_read_response(data["data"][0], json_data) +def test_delete_one( + db, + client, + client_admin, + model_id, + person_id, + role_id, +): + add_db( + db, + Contribution( + agent_id=person_id, + role_id=role_id, + entity_id=model_id, + created_by_id=person_id, + updated_by_id=person_id, + ), + ) + + assert count_db_class(db, ExperimentalSynapsesPerConnection) == 1 + assert count_db_class(db, Contribution) == 1 + assert count_db_class(db, MTypeClass) == 1 + assert count_db_class(db, BrainRegion) == 1 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, ExperimentalSynapsesPerConnection) == 0 + assert count_db_class(db, Contribution) == 0 + assert count_db_class(db, MTypeClass) == 1 + assert count_db_class(db, BrainRegion) == 1 + + def test_missing(client): check_missing(ROUTE, client) diff --git a/tests/test_ion_channel_model.py b/tests/test_ion_channel_model.py index e1807cf6..de1f48d4 100644 --- a/tests/test_ion_channel_model.py +++ b/tests/test_ion_channel_model.py @@ -1,13 +1,21 @@ import itertools as it import uuid +import pytest from fastapi.testclient import TestClient from app.db.model import IonChannelModel from app.db.types import EntityType from app.schemas.ion_channel_model import IonChannelModelRead -from .utils import PROJECT_ID, TEST_DATA_DIR, check_brain_region_filter, upload_entity_asset +from .utils import ( + PROJECT_ID, + TEST_DATA_DIR, + assert_request, + check_brain_region_filter, + count_db_class, + upload_entity_asset, +) FILE_EXAMPLE_PATH = TEST_DATA_DIR / "example.json" ROUTE = "/ion-channel-model" @@ -40,6 +48,13 @@ def create( return response +@pytest.fixture +def model_id(client, species_id, strain_id, brain_region_id): + response = create(client, species_id, strain_id, brain_region_id) + assert response.status_code == 200, f"Failed to create icm: {response.text}" + return response.json()["id"] + + def test_create(client: TestClient, species_id: str, strain_id: str, brain_region_id: uuid.UUID): response = create(client, species_id, strain_id, brain_region_id) assert response.status_code == 200, f"Failed to create icm: {response.text}" @@ -102,6 +117,19 @@ def test_read_many(client: TestClient, species_id: str, strain_id: str, brain_re IonChannelModelRead.model_validate(icm_res[0].json()) +def test_delete_one(db, client, client_admin, model_id): + assert count_db_class(db, IonChannelModel) == 1 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, IonChannelModel) == 0 + + def test_sorted(client: TestClient, species_id: str, strain_id: str, brain_region_id: uuid.UUID): count = 11 icm_res = [create(client, species_id, strain_id, brain_region_id) for _ in range(count)] diff --git a/tests/test_license.py b/tests/test_license.py index 62321cce..d19e4dd7 100644 --- a/tests/test_license.py +++ b/tests/test_license.py @@ -1,4 +1,6 @@ -from tests.utils import MISSING_ID, MISSING_ID_COMPACT +from app.db.model import License + +from tests.utils import MISSING_ID, MISSING_ID_COMPACT, assert_request, count_db_class ROUTE = "/license" @@ -45,3 +47,27 @@ def test_missing(client): response = client.get(f"{ROUTE}/notanumber") assert response.status_code == 422 + + +def test_delete_one(db, client, client_admin): + response = client_admin.post( + ROUTE, + json={ + "name": "Test License", + "description": "a license description", + "label": "a label", + }, + ) + assert response.status_code == 200 + model_id = response.json()["id"] + + assert count_db_class(db, License) == 1 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, License) == 0 diff --git a/tests/test_memodel.py b/tests/test_memodel.py index c22c4288..04108216 100644 --- a/tests/test_memodel.py +++ b/tests/test_memodel.py @@ -5,7 +5,16 @@ import pytest from fastapi.testclient import TestClient -from app.db.model import MEModel +from app.db.model import ( + Contribution, + EModel, + ETypeClass, + ETypeClassification, + MEModel, + MTypeClass, + MTypeClassification, + ReconstructionMorphology, +) from app.filters.memodel import MEModelFilter from app.schemas.me_model import MEModelRead @@ -14,6 +23,7 @@ PROJECT_ID, assert_request, check_brain_region_filter, + count_db_class, create_reconstruction_morphology_id, ) @@ -87,6 +97,40 @@ def test_create_memodel( _assert_read_response(data, json_data) +def test_delete_one(db, client, client_admin, memodel_id): + model_id = memodel_id + + assert count_db_class(db, MEModel) == 1 + # 1 morph, 1 EModel, 2 MEModel + assert count_db_class(db, Contribution) == 4 + assert count_db_class(db, MTypeClass) == 1 + # 1 for morphology 1 for MEModel for the same MTypeClass + assert count_db_class(db, MTypeClassification) == 2 + assert count_db_class(db, ETypeClass) == 1 + # 1 for EModel 1 for MEModel for the same ETypeClass + assert count_db_class(db, ETypeClassification) == 2 + assert count_db_class(db, ReconstructionMorphology) == 1 + assert count_db_class(db, EModel) == 1 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, MEModel) == 0 + assert count_db_class(db, Contribution) == 2 + assert count_db_class(db, MTypeClass) == 1 + # 1 for morphology 1 for MEModel for the same MTypeClass + assert count_db_class(db, MTypeClassification) == 1 + assert count_db_class(db, ETypeClass) == 1 + # 1 for EModel 1 for MEModel for the same ETypeClass + assert count_db_class(db, ETypeClassification) == 1 + assert count_db_class(db, ReconstructionMorphology) == 1 + assert count_db_class(db, EModel) == 1 + + def test_facets(client: TestClient, faceted_memodels: MEModels): ids = faceted_memodels diff --git a/tests/test_memodel_calibration_result.py b/tests/test_memodel_calibration_result.py index 910f1257..88d5e22f 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 +from .utils import assert_request, count_db_class MODEL = MEModelCalibrationResult ROUTE = "/memodel-calibration-result" @@ -57,6 +57,21 @@ def test_create_one(client: TestClient, json_data): _assert_read_response(data, json_data) +def test_delete_one(db, client, client_admin, memodel_calibration_result_id): + model_id = memodel_calibration_result_id + + assert count_db_class(db, MEModelCalibrationResult) == 1 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, MEModelCalibrationResult) == 0 + + def test_missing(client): response = client.get(f"{ROUTE}/{uuid.uuid4()}") assert response.status_code == 404 diff --git a/tests/test_morphology.py b/tests/test_morphology.py index 48f84752..e9801c49 100644 --- a/tests/test_morphology.py +++ b/tests/test_morphology.py @@ -1,6 +1,14 @@ import itertools as it -from app.db.model import Agent, ReconstructionMorphology, Species, Strain +from app.db.model import ( + Agent, + Contribution, + MTypeClass, + MTypeClassification, + ReconstructionMorphology, + Species, + Strain, +) from app.db.types import EntityType from .utils import ( @@ -10,6 +18,7 @@ add_db, assert_request, check_brain_region_filter, + count_db_class, create_reconstruction_morphology_id, ) @@ -67,6 +76,38 @@ def test_create_reconstruction_morphology( assert data[0]["created_by"]["id"] == data[0]["updated_by"]["id"] +def test_delete_one(db, client, client_admin, morphology_id, person_id, role_id): + model_id = morphology_id + + add_db( + db, + Contribution( + agent_id=person_id, + role_id=role_id, + entity_id=model_id, + created_by_id=person_id, + updated_by_id=person_id, + ), + ) + + assert count_db_class(db, ReconstructionMorphology) == 1 + assert count_db_class(db, Contribution) == 1 + assert count_db_class(db, MTypeClass) == 1 + assert count_db_class(db, MTypeClassification) == 1 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, ReconstructionMorphology) == 0 + assert count_db_class(db, Contribution) == 0 + assert count_db_class(db, MTypeClass) == 1 + assert count_db_class(db, MTypeClassification) == 0 + + def test_missing(client): response = client.get(f"{ROUTE}/{MISSING_ID}") assert response.status_code == 404 diff --git a/tests/test_mtype.py b/tests/test_mtype.py index 18026208..d5d7f03e 100644 --- a/tests/test_mtype.py +++ b/tests/test_mtype.py @@ -4,7 +4,9 @@ PROJECT_ID, add_all_db, add_db, + assert_request, check_missing, + count_db_class, create_reconstruction_morphology_id, with_creation_fields, ) @@ -66,6 +68,32 @@ def test_retrieve(db, client, person_id): assert data == with_creation_fields(items[0]) | {"id": str(mtypes[0].id)} +def test_delete_one(db, client, client_admin, person_id): + mtype = add_db( + db, + MTypeClass( + pref_label="m1", + alt_label="m1", + definition="m1d", + created_by_id=person_id, + updated_by_id=person_id, + ), + ) + + model_id = mtype.id + + assert count_db_class(db, MTypeClass) == 1 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, MTypeClass) == 0 + + def test_missing(client): check_missing(ROUTE, client) diff --git a/tests/test_mtype_classification.py b/tests/test_mtype_classification.py index 2f0cc8b3..f5019cad 100644 --- a/tests/test_mtype_classification.py +++ b/tests/test_mtype_classification.py @@ -1,8 +1,11 @@ import pytest +from app.db.model import MTypeClass, MTypeClassification, ReconstructionMorphology + from .utils import ( assert_request, check_creation_fields, + count_db_class, create_mtype, create_reconstruction_morphology_id, ) @@ -60,6 +63,31 @@ def test_create_one(client, json_data, morphology_id): assert json_data["mtype_class_id"] in {m["id"] for m in data} +def test_delete_one(db, client, client_admin, json_data): + data = assert_request( + client.post, + url=ROUTE, + json=json_data, + ).json() + + model_id = data["id"] + + assert count_db_class(db, ReconstructionMorphology) == 1 + assert count_db_class(db, MTypeClass) == 2 + assert count_db_class(db, MTypeClassification) == 2 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, ReconstructionMorphology) == 1 + assert count_db_class(db, MTypeClass) == 2 + assert count_db_class(db, MTypeClassification) == 1 + + def test_create_one__unauthorized_entity(client_user_1, unauthorized_morph_id, json_data): json_data |= {"entity_id": str(unauthorized_morph_id)} diff --git a/tests/test_organization.py b/tests/test_organization.py index 756071b3..63260fc6 100644 --- a/tests/test_organization.py +++ b/tests/test_organization.py @@ -1,4 +1,6 @@ -from tests.utils import MISSING_ID, MISSING_ID_COMPACT +from app.db.model import Agent, Organization + +from tests.utils import MISSING_ID, MISSING_ID_COMPACT, assert_request, count_db_class ROUTE = "/organization" @@ -32,6 +34,32 @@ def test_create_organization(client, client_admin): assert len(data) == 1 +def test_delete_one(db, client, client_admin): + response = client_admin.post( + ROUTE, + json={"pref_label": "foo", "alternative_name": "bar"}, + ) + assert response.status_code == 200 + data = response.json() + + model_id = data["id"] + + assert count_db_class(db, Organization) == 1 + + # 1 created_by/updated_by 1 Organization + assert count_db_class(db, Agent) == 2 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, Organization) == 0 + assert count_db_class(db, Agent) == 1 + + def test_missing(client): response = client.get(f"{ROUTE}/{MISSING_ID}") assert response.status_code == 404 diff --git a/tests/test_person.py b/tests/test_person.py index 3ab9958c..ece5db54 100644 --- a/tests/test_person.py +++ b/tests/test_person.py @@ -1,6 +1,8 @@ from unittest.mock import ANY -from tests.utils import ADMIN_SUB_ID, MISSING_ID, MISSING_ID_COMPACT +from app.db.model import Agent, Person + +from tests.utils import ADMIN_SUB_ID, MISSING_ID, MISSING_ID_COMPACT, assert_request, count_db_class ROUTE = "/person" @@ -57,6 +59,31 @@ def test_create_person(client, client_admin): assert sum(1 for d in data if d["sub_id"] is not None) == 1 +def test_delete_one(db, client, client_admin): + response = client_admin.post( + ROUTE, + json={"given_name": "jd", "family_name": "courcol", "pref_label": "jd courcol"}, + ) + assert response.status_code == 200 + data = response.json() + + model_id = data["id"] + + # 1 created_by/updated_by 1 Person + assert count_db_class(db, Person) == 2 + assert count_db_class(db, Agent) == 2 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, Person) == 1 + assert count_db_class(db, Agent) == 1 + + def _classify_agents(data): if data[0]["sub_id"] is None: return data diff --git a/tests/test_role.py b/tests/test_role.py index c7a1438e..a6fdab4c 100644 --- a/tests/test_role.py +++ b/tests/test_role.py @@ -1,4 +1,6 @@ -from tests.utils import MISSING_ID, MISSING_ID_COMPACT +from app.db.model import Role + +from tests.utils import MISSING_ID, MISSING_ID_COMPACT, assert_request, count_db_class ROUTE = "/role" @@ -29,6 +31,25 @@ def test_create_role(client, client_admin): assert data[0]["id"] == 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 + data = response.json() + + model_id = data["id"] + + assert count_db_class(db, Role) == 1 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, Role) == 0 + + def test_missing(client): response = client.get(f"{ROUTE}/{MISSING_ID}") assert response.status_code == 404 diff --git a/tests/test_simulation.py b/tests/test_simulation.py index 97e36838..e0f5d302 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -1,6 +1,6 @@ import pytest -from app.db.model import Simulation +from app.db.model import Simulation, SimulationCampaign from app.db.types import EntityType from .utils import ( @@ -11,6 +11,7 @@ check_creation_fields, check_missing, check_pagination, + count_db_class, ) ROUTE = "simulation" @@ -61,6 +62,23 @@ def test_read_one(client, model, json_data): _assert_read_response(data[0], json_data) +def test_delete_one(db, client, client_admin, model): + model_id = model.id + + assert count_db_class(db, Simulation) == 1 + assert count_db_class(db, SimulationCampaign) == 1 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, Simulation) == 0 + assert count_db_class(db, SimulationCampaign) == 1 + + def test_missing(client): check_missing(ROUTE, client) diff --git a/tests/test_simulation_campaign.py b/tests/test_simulation_campaign.py index cb935c35..a780c07a 100644 --- a/tests/test_simulation_campaign.py +++ b/tests/test_simulation_campaign.py @@ -12,6 +12,7 @@ check_creation_fields, check_missing, check_pagination, + count_db_class, ) ROUTE = "simulation-campaign" @@ -64,6 +65,21 @@ def test_read_one(client, model, json_data): _assert_read_response(data[0], json_data) +def test_delete_one(db, client, client_admin, model): + model_id = model.id + + assert count_db_class(db, SimulationCampaign) == 1 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, SimulationCampaign) == 0 + + def test_missing(client): check_missing(ROUTE, client) diff --git a/tests/test_simulation_result.py b/tests/test_simulation_result.py index 1aa71a20..2f3c99ee 100644 --- a/tests/test_simulation_result.py +++ b/tests/test_simulation_result.py @@ -1,5 +1,6 @@ import pytest +from app.db.model import SimulationResult from app.db.types import EntityType from .utils import ( @@ -8,6 +9,7 @@ check_creation_fields, check_missing, check_pagination, + count_db_class, ) ROUTE = "simulation-result" @@ -64,6 +66,21 @@ def test_read_many(client, model, json_data): _assert_read_response(data[0], json_data) +def test_delete_one(db, client, client_admin, model): + model_id = model.id + + assert count_db_class(db, SimulationResult) == 1 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, SimulationResult) == 0 + + def test_missing(client): check_missing(ROUTE, client) diff --git a/tests/test_single_neuron_simulation.py b/tests/test_single_neuron_simulation.py index 25c7848d..e9865dac 100644 --- a/tests/test_single_neuron_simulation.py +++ b/tests/test_single_neuron_simulation.py @@ -14,6 +14,7 @@ add_db, assert_request, check_brain_region_filter, + count_db_class, create_brain_region, upload_entity_asset, ) @@ -30,7 +31,8 @@ def _create_single_neuron_simulation_id(db, data): return add_db(db, SingleNeuronSimulation(**data)).id -def test_single_neuron_simulation(client, brain_region_id, memodel_id): +@pytest.fixture +def single_neuron_simulation_id(client, memodel_id, brain_region_id): response = assert_request( client.post, url=ROUTE, @@ -57,6 +59,38 @@ def test_single_neuron_simulation(client, brain_region_id, memodel_id): label=AssetLabel.single_cell_simulation_data, files={"file": ("c.json", f, "application/json")}, ) + + return data["id"] + + +def test_single_neuron_simulation(client, brain_region_id, memodel_id, single_neuron_simulation_id): + response = assert_request( + client.post, + url=ROUTE, + json={ + "name": "foo", + "description": "my-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), + }, + ) + + data = response.json() + + with FILE_EXAMPLE_PATH.open("rb") as f: + upload_entity_asset( + client, + EntityType.single_neuron_simulation, + data["id"], + label=AssetLabel.single_cell_simulation_data, + files={"file": ("c.json", f, "application/json")}, + ) + assert data["brain_region"]["id"] == str(brain_region_id), ( f"Failed to get id for reconstruction morphology: {data}" ) @@ -71,7 +105,7 @@ def test_single_neuron_simulation(client, brain_region_id, memodel_id): assert data["created_by"]["id"] == data["updated_by"]["id"] assert data["authorized_public"] is False - response = assert_request(client.get, url=f"{ROUTE}/{data['id']}") + response = assert_request(client.get, url=f"{ROUTE}/{single_neuron_simulation_id}") data = response.json() assert data["brain_region"]["id"] == str(brain_region_id), ( f"Failed to get id for reconstruction morphology: {data}" @@ -109,6 +143,23 @@ def test_single_neuron_simulation__public(client, brain_region_id, memodel_id): assert data["authorized_public"] is True +def test_delete_one(db, client, client_admin, single_neuron_simulation_id): + model_id = single_neuron_simulation_id + + assert count_db_class(db, SingleNeuronSimulation) == 1 + assert count_db_class(db, MEModel) == 1 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, SingleNeuronSimulation) == 0 + assert count_db_class(db, MEModel) == 1 + + @pytest.mark.parametrize( ("route_id", "expected_status_code"), [ diff --git a/tests/test_single_neuron_synaptome.py b/tests/test_single_neuron_synaptome.py index d4d6d191..afa66d30 100644 --- a/tests/test_single_neuron_synaptome.py +++ b/tests/test_single_neuron_synaptome.py @@ -18,6 +18,7 @@ add_db, assert_request, check_brain_region_filter, + count_db_class, create_brain_region, ) from tests.conftest import CreateIds @@ -120,6 +121,23 @@ def test_read_many(client, model_id, json_data): _assert_read_response(items[0], json_data) +def test_delete_one(db, client, client_admin, model_id): + assert count_db_class(db, SingleNeuronSynaptome) == 1 + assert count_db_class(db, MEModel) == 1 + assert count_db_class(db, Contribution) == 6 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, SingleNeuronSynaptome) == 0 + assert count_db_class(db, MEModel) == 1 + assert count_db_class(db, Contribution) == 4 + + @pytest.mark.parametrize( ("route_id", "expected_status_code"), [ diff --git a/tests/test_single_neuron_synaptome_simulation.py b/tests/test_single_neuron_synaptome_simulation.py index 12deec84..55502897 100644 --- a/tests/test_single_neuron_synaptome_simulation.py +++ b/tests/test_single_neuron_synaptome_simulation.py @@ -14,6 +14,7 @@ add_db, assert_request, check_brain_region_filter, + count_db_class, create_brain_region, ) @@ -162,6 +163,23 @@ def test_read_one(client, brain_region_id, synaptome_id, simulation_id): assert data["authorized_public"] is False +def test_delete_one(db, client, client_admin, simulation_id): + model_id = simulation_id + + assert count_db_class(db, SingleNeuronSynaptomeSimulation) == 1 + assert count_db_class(db, SingleNeuronSynaptome) == 1 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, SingleNeuronSynaptomeSimulation) == 0 + assert count_db_class(db, SingleNeuronSynaptome) == 1 + + @pytest.mark.parametrize( ("route_id", "expected_status_code"), [ diff --git a/tests/test_species.py b/tests/test_species.py index 3e0f29b8..2b29b1cf 100644 --- a/tests/test_species.py +++ b/tests/test_species.py @@ -1,5 +1,7 @@ +from app.db.model import Species + from .utils import check_creation_fields -from tests.utils import MISSING_ID, MISSING_ID_COMPACT +from tests.utils import MISSING_ID, MISSING_ID_COMPACT, assert_request, count_db_class ROUTE = "/species" @@ -41,6 +43,21 @@ def test_create_species(client, client_admin): check_creation_fields(data[0]) +def test_delete_one(db, client, client_admin, species_id): + model_id = species_id + + assert count_db_class(db, Species) == 1 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, Species) == 0 + + def test_missing(client): response = client.get(f"{ROUTE}/{MISSING_ID}") assert response.status_code == 404 diff --git a/tests/test_strain.py b/tests/test_strain.py index fcac1dbd..690ee9fa 100644 --- a/tests/test_strain.py +++ b/tests/test_strain.py @@ -1,4 +1,12 @@ -from tests.utils import MISSING_ID, MISSING_ID_COMPACT, check_creation_fields +from app.db.model import Strain + +from tests.utils import ( + MISSING_ID, + MISSING_ID_COMPACT, + assert_request, + check_creation_fields, + count_db_class, +) ROUTE = "/strain" @@ -77,6 +85,21 @@ def test_create_strain(client, client_admin, species_id, person_id): } +def test_delete_one(db, client, client_admin, strain_id): + model_id = strain_id + + assert count_db_class(db, Strain) == 1 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, Strain) == 0 + + def test_missing(client): response = client.get(f"{ROUTE}/{MISSING_ID}") assert response.status_code == 404 diff --git a/tests/test_subject.py b/tests/test_subject.py index 17e57b68..af2fc35f 100644 --- a/tests/test_subject.py +++ b/tests/test_subject.py @@ -11,6 +11,7 @@ check_authorization, check_missing, check_pagination, + count_db_class, ) ROUTE = "/subject" @@ -63,6 +64,19 @@ def test_read_one(client, model_id, json_data): _assert_read_response(data, json_data) +def test_delete_one(db, client, client_admin, model_id): + assert count_db_class(db, Subject) == 1 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, Subject) == 0 + + def test_missing(client): check_missing(ROUTE, client) diff --git a/tests/test_validation_result.py b/tests/test_validation_result.py index ea201e08..1a7b33f9 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 +from .utils import assert_request, count_db_class MODEL = ValidationResult ROUTE = "/validation-result" @@ -29,6 +29,11 @@ def _create(**kwargs): return _create +@pytest.fixture +def model(create): + return create() + + def _assert_read_response(data, json_data): assert data["name"] == json_data["name"] assert data["passed"] == json_data["passed"] @@ -73,6 +78,21 @@ def test_create_one__public(client, json_data): assert data["authorized_public"] is True +def test_delete_one(db, client, client_admin, model): + model_id = model["id"] + + assert count_db_class(db, ValidationResult) == 1 + + data = assert_request(client.delete, url=f"{ROUTE}/{model_id}", expected_status_code=403).json() + assert data["error_code"] == "NOT_AUTHORIZED" + assert data["message"] == "Service admin role required" + + data = assert_request(client_admin.delete, url=f"{ROUTE}/{model_id}").json() + assert data["id"] == str(model_id) + + assert count_db_class(db, ValidationResult) == 0 + + def test_missing(client): response = client.get(f"{ROUTE}/{uuid.uuid4()}") assert response.status_code == 404 diff --git a/tests/utils.py b/tests/utils.py index 08de2248..2925cde0 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -558,3 +558,7 @@ def check_creation_fields(data: dict): assert data["update_date"] == ANY assert data["created_by"]["id"] == ANY assert data["updated_by"]["id"] == ANY + + +def count_db_class(db, db_class): + return db.execute(sa.select(sa.func.count(db_class.id))).scalar()