diff --git a/amt/api/routes/project.py b/amt/api/routes/project.py index 8acc230c..580677b2 100644 --- a/amt/api/routes/project.py +++ b/amt/api/routes/project.py @@ -431,6 +431,16 @@ def find_requirement_tasks_by_measure_urn(system_card: SystemCard, measure_urn: return requirement_tasks +@router.delete("/{project_id}") +async def delete_project( + request: Request, + project_id: int, + projects_service: Annotated[ProjectsService, Depends(ProjectsService)], +) -> HTMLResponse: + await projects_service.delete(project_id) + return templates.Redirect(request, "/algorithm-systems/") + + @router.get("/{project_id}/measure/{measure_urn}") async def get_measure( request: Request, diff --git a/amt/locale/base.pot b/amt/locale/base.pot index 47fbbbd5..2c21cdb6 100644 --- a/amt/locale/base.pot +++ b/amt/locale/base.pot @@ -286,7 +286,7 @@ msgstr "" msgid "There is one error:" msgstr "" -#: amt/site/templates/layouts/base.html.j2:1 +#: amt/site/templates/layouts/base.html.j2:11 msgid "Algorithmic Management Toolkit (AMT)" msgstr "" @@ -323,10 +323,10 @@ msgid "Reviewing" msgstr "" #: amt/site/templates/macros/tasks.html.j2:32 -#: amt/site/templates/projects/details_base.html.j2:29 -#: amt/site/templates/projects/details_base.html.j2:55 -#: amt/site/templates/projects/details_base.html.j2:84 -#: amt/site/templates/projects/details_base.html.j2:107 +#: amt/site/templates/projects/details_base.html.j2:74 +#: amt/site/templates/projects/details_base.html.j2:100 +#: amt/site/templates/projects/details_base.html.j2:129 +#: amt/site/templates/projects/details_base.html.j2:152 msgid "Done" msgstr "" @@ -502,28 +502,56 @@ msgstr "" msgid "Select group by" msgstr "" +#: amt/site/templates/projects/details_base.html.j2:19 +msgid "Delete algoritmic system" +msgstr "" + #: amt/site/templates/projects/details_base.html.j2:26 +msgid "Are you sure you want to delete your algoritmic system " +msgstr "" + +#: amt/site/templates/projects/details_base.html.j2:30 +msgid "" +"Data will be stored for at least 45 days before permanent\n" +" deletion." +msgstr "" + +#: amt/site/templates/projects/details_base.html.j2:39 +#: amt/site/templates/projects/new.html.j2:132 +msgid "Yes" +msgstr "" + +#: amt/site/templates/projects/details_base.html.j2:44 +#: amt/site/templates/projects/new.html.j2:142 +msgid "No" +msgstr "" + +#: amt/site/templates/projects/details_base.html.j2:57 +msgid "Delete algorithm system" +msgstr "" + +#: amt/site/templates/projects/details_base.html.j2:71 msgid "Does the algorithm meet the requirements?" msgstr "" -#: amt/site/templates/projects/details_base.html.j2:51 -#: amt/site/templates/projects/details_base.html.j2:105 +#: amt/site/templates/projects/details_base.html.j2:96 +#: amt/site/templates/projects/details_base.html.j2:150 msgid "To do" msgstr "" -#: amt/site/templates/projects/details_base.html.j2:53 +#: amt/site/templates/projects/details_base.html.j2:98 msgid "In progress" msgstr "" -#: amt/site/templates/projects/details_base.html.j2:70 +#: amt/site/templates/projects/details_base.html.j2:115 msgid "Go to all requirements" msgstr "" -#: amt/site/templates/projects/details_base.html.j2:81 +#: amt/site/templates/projects/details_base.html.j2:126 msgid "Which instruments are executed?" msgstr "" -#: amt/site/templates/projects/details_base.html.j2:122 +#: amt/site/templates/projects/details_base.html.j2:167 msgid "Go to all instruments" msgstr "" @@ -687,14 +715,6 @@ msgstr "" msgid "Find your AI Act profile" msgstr "" -#: amt/site/templates/projects/new.html.j2:132 -msgid "Yes" -msgstr "" - -#: amt/site/templates/projects/new.html.j2:142 -msgid "No" -msgstr "" - #: amt/site/templates/projects/new.html.j2:151 msgid "" "Overview of instruments for the responsible development, deployment, " diff --git a/amt/locale/en_US/LC_MESSAGES/messages.mo b/amt/locale/en_US/LC_MESSAGES/messages.mo index bc095a96..280b0d9c 100644 Binary files a/amt/locale/en_US/LC_MESSAGES/messages.mo and b/amt/locale/en_US/LC_MESSAGES/messages.mo differ diff --git a/amt/locale/en_US/LC_MESSAGES/messages.po b/amt/locale/en_US/LC_MESSAGES/messages.po index bc11273b..c063376e 100644 --- a/amt/locale/en_US/LC_MESSAGES/messages.po +++ b/amt/locale/en_US/LC_MESSAGES/messages.po @@ -287,7 +287,7 @@ msgstr "" msgid "There is one error:" msgstr "" -#: amt/site/templates/layouts/base.html.j2:1 +#: amt/site/templates/layouts/base.html.j2:11 msgid "Algorithmic Management Toolkit (AMT)" msgstr "" @@ -324,10 +324,10 @@ msgid "Reviewing" msgstr "" #: amt/site/templates/macros/tasks.html.j2:32 -#: amt/site/templates/projects/details_base.html.j2:29 -#: amt/site/templates/projects/details_base.html.j2:55 -#: amt/site/templates/projects/details_base.html.j2:84 -#: amt/site/templates/projects/details_base.html.j2:107 +#: amt/site/templates/projects/details_base.html.j2:74 +#: amt/site/templates/projects/details_base.html.j2:100 +#: amt/site/templates/projects/details_base.html.j2:129 +#: amt/site/templates/projects/details_base.html.j2:152 msgid "Done" msgstr "" @@ -503,28 +503,56 @@ msgstr "" msgid "Select group by" msgstr "" +#: amt/site/templates/projects/details_base.html.j2:19 +msgid "Delete algoritmic system" +msgstr "" + #: amt/site/templates/projects/details_base.html.j2:26 +msgid "Are you sure you want to delete your algoritmic system " +msgstr "" + +#: amt/site/templates/projects/details_base.html.j2:30 +msgid "" +"Data will be stored for at least 45 days before permanent\n" +" deletion." +msgstr "" + +#: amt/site/templates/projects/details_base.html.j2:39 +#: amt/site/templates/projects/new.html.j2:132 +msgid "Yes" +msgstr "" + +#: amt/site/templates/projects/details_base.html.j2:44 +#: amt/site/templates/projects/new.html.j2:142 +msgid "No" +msgstr "" + +#: amt/site/templates/projects/details_base.html.j2:57 +msgid "Delete algorithm system" +msgstr "" + +#: amt/site/templates/projects/details_base.html.j2:71 msgid "Does the algorithm meet the requirements?" msgstr "" -#: amt/site/templates/projects/details_base.html.j2:51 -#: amt/site/templates/projects/details_base.html.j2:105 +#: amt/site/templates/projects/details_base.html.j2:96 +#: amt/site/templates/projects/details_base.html.j2:150 msgid "To do" msgstr "" -#: amt/site/templates/projects/details_base.html.j2:53 +#: amt/site/templates/projects/details_base.html.j2:98 msgid "In progress" msgstr "" -#: amt/site/templates/projects/details_base.html.j2:70 +#: amt/site/templates/projects/details_base.html.j2:115 msgid "Go to all requirements" msgstr "" -#: amt/site/templates/projects/details_base.html.j2:81 +#: amt/site/templates/projects/details_base.html.j2:126 msgid "Which instruments are executed?" msgstr "" -#: amt/site/templates/projects/details_base.html.j2:122 +#: amt/site/templates/projects/details_base.html.j2:167 msgid "Go to all instruments" msgstr "" @@ -688,14 +716,6 @@ msgstr "" msgid "Find your AI Act profile" msgstr "" -#: amt/site/templates/projects/new.html.j2:132 -msgid "Yes" -msgstr "" - -#: amt/site/templates/projects/new.html.j2:142 -msgid "No" -msgstr "" - #: amt/site/templates/projects/new.html.j2:151 msgid "" "Overview of instruments for the responsible development, deployment, " diff --git a/amt/locale/nl_NL/LC_MESSAGES/messages.mo b/amt/locale/nl_NL/LC_MESSAGES/messages.mo index 03c49a4d..e7456b7c 100644 Binary files a/amt/locale/nl_NL/LC_MESSAGES/messages.mo and b/amt/locale/nl_NL/LC_MESSAGES/messages.mo differ diff --git a/amt/locale/nl_NL/LC_MESSAGES/messages.po b/amt/locale/nl_NL/LC_MESSAGES/messages.po index 4ee93cf0..27d33b70 100644 --- a/amt/locale/nl_NL/LC_MESSAGES/messages.po +++ b/amt/locale/nl_NL/LC_MESSAGES/messages.po @@ -295,7 +295,7 @@ msgstr "Er zijn enkele fouten" msgid "There is one error:" msgstr "Er is één fout:" -#: amt/site/templates/layouts/base.html.j2:1 +#: amt/site/templates/layouts/base.html.j2:11 msgid "Algorithmic Management Toolkit (AMT)" msgstr "Algoritme Management Toolkit (AMT)" @@ -332,10 +332,10 @@ msgid "Reviewing" msgstr "Beoordelen" #: amt/site/templates/macros/tasks.html.j2:32 -#: amt/site/templates/projects/details_base.html.j2:29 -#: amt/site/templates/projects/details_base.html.j2:55 -#: amt/site/templates/projects/details_base.html.j2:84 -#: amt/site/templates/projects/details_base.html.j2:107 +#: amt/site/templates/projects/details_base.html.j2:74 +#: amt/site/templates/projects/details_base.html.j2:100 +#: amt/site/templates/projects/details_base.html.j2:129 +#: amt/site/templates/projects/details_base.html.j2:152 msgid "Done" msgstr "Afgerond" @@ -527,28 +527,58 @@ msgstr "Groeperen op" msgid "Select group by" msgstr "Selecteer groepering" +#: amt/site/templates/projects/details_base.html.j2:19 +msgid "Delete algoritmic system" +msgstr "Verwijder Algoritmesysteem" + #: amt/site/templates/projects/details_base.html.j2:26 +msgid "Are you sure you want to delete your algoritmic system " +msgstr "Weet u zeker dat u uw algoritmische systeem wilt verwijderen?" + +#: amt/site/templates/projects/details_base.html.j2:30 +msgid "" +"Data will be stored for at least 45 days before permanent\n" +" deletion." +msgstr "" +"gegevens worden minimaal 45 dagen bewaard voordat ze definitief worden\n" +" verwijderd." + +#: amt/site/templates/projects/details_base.html.j2:39 +#: amt/site/templates/projects/new.html.j2:132 +msgid "Yes" +msgstr "Ja" + +#: amt/site/templates/projects/details_base.html.j2:44 +#: amt/site/templates/projects/new.html.j2:142 +msgid "No" +msgstr "Nee" + +#: amt/site/templates/projects/details_base.html.j2:57 +msgid "Delete algorithm system" +msgstr "Verwijder Algoritmesysteem" + +#: amt/site/templates/projects/details_base.html.j2:71 msgid "Does the algorithm meet the requirements?" msgstr "Voldoet het algoritme aan de vereisten?" -#: amt/site/templates/projects/details_base.html.j2:51 -#: amt/site/templates/projects/details_base.html.j2:105 +#: amt/site/templates/projects/details_base.html.j2:96 +#: amt/site/templates/projects/details_base.html.j2:150 msgid "To do" msgstr "Te doen" -#: amt/site/templates/projects/details_base.html.j2:53 +#: amt/site/templates/projects/details_base.html.j2:98 msgid "In progress" msgstr "Onderhanden" -#: amt/site/templates/projects/details_base.html.j2:70 +#: amt/site/templates/projects/details_base.html.j2:115 msgid "Go to all requirements" msgstr "Ga naar alle Vereisten" -#: amt/site/templates/projects/details_base.html.j2:81 +#: amt/site/templates/projects/details_base.html.j2:126 msgid "Which instruments are executed?" msgstr "Welke instrumenten zijn uitgevoerd?" -#: amt/site/templates/projects/details_base.html.j2:122 +#: amt/site/templates/projects/details_base.html.j2:167 msgid "Go to all instruments" msgstr "Ga naar all instrumenten" @@ -719,14 +749,6 @@ msgstr "" msgid "Find your AI Act profile" msgstr "Vind uw AI Act profiel" -#: amt/site/templates/projects/new.html.j2:132 -msgid "Yes" -msgstr "Ja" - -#: amt/site/templates/projects/new.html.j2:142 -msgid "No" -msgstr "Nee" - #: amt/site/templates/projects/new.html.j2:151 msgid "" "Overview of instruments for the responsible development, deployment, " diff --git a/amt/migrations/versions/6581a03aabec_add_deleted_at_to_project.py b/amt/migrations/versions/6581a03aabec_add_deleted_at_to_project.py new file mode 100644 index 00000000..03088d7c --- /dev/null +++ b/amt/migrations/versions/6581a03aabec_add_deleted_at_to_project.py @@ -0,0 +1,27 @@ +"""add deleted_at to project + +Revision ID: 6581a03aabec +Revises: 7f20f8562007 +Create Date: 2024-11-01 10:29:58.930558 + +""" + +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import sqlite + +# revision identifiers, used by Alembic. +revision: str = "6581a03aabec" +down_revision: str | None = "7f20f8562007" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.add_column("project", sa.Column("deleted_at", sa.DateTime(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("project", "deleted_at") diff --git a/amt/models/project.py b/amt/models/project.py index aa351552..fe5cf22b 100644 --- a/amt/models/project.py +++ b/amt/models/project.py @@ -59,6 +59,7 @@ class Project(Base): lifecycle: Mapped[Lifecycles | None] = mapped_column(ENUM(Lifecycles, name="lifecycle"), nullable=True) system_card_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict) last_edited: Mapped[datetime] = mapped_column(server_default=func.now(), onupdate=func.now(), nullable=False) + deleted_at: Mapped[datetime | None] = mapped_column(server_default=None, nullable=True) def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 system_card: SystemCard | None = kwargs.pop("system_card", None) diff --git a/amt/repositories/projects.py b/amt/repositories/projects.py index 161b8db5..2ba07223 100644 --- a/amt/repositories/projects.py +++ b/amt/repositories/projects.py @@ -35,7 +35,7 @@ def __init__(self, session: Annotated[AsyncSession, Depends(get_session)]) -> No self.session = session async def find_all(self) -> Sequence[Project]: - result = await self.session.execute(select(Project)) + result = await self.session.execute(select(Project).where(Project.deleted_at.is_(None))) return result.scalars().all() async def delete(self, project: Project) -> None: @@ -66,7 +66,7 @@ async def save(self, project: Project) -> Project: async def find_by_id(self, project_id: int) -> Project: try: - statement = select(Project).where(Project.id == project_id) + statement = select(Project).where(Project.id == project_id).where(Project.deleted_at.is_(None)) result = await self.session.execute(statement) return result.scalars().one() except NoResultFound as e: @@ -103,6 +103,7 @@ async def paginate( # noqa statement = statement.order_by(Project.last_edited.desc()) else: statement = statement.order_by(func.lower(Project.name)) + statement = statement.filter(Project.deleted_at.is_(None)) statement = statement.offset(skip).limit(limit) db_result = await self.session.execute(statement) result = list(db_result.scalars()) diff --git a/amt/services/projects.py b/amt/services/projects.py index 1af5ea1e..c2db33bf 100644 --- a/amt/services/projects.py +++ b/amt/services/projects.py @@ -1,5 +1,6 @@ import json import logging +from datetime import UTC, datetime from functools import lru_cache from os import listdir from os.path import isfile, join @@ -36,6 +37,14 @@ def __init__( async def get(self, project_id: int) -> Project: project = await self.repository.find_by_id(project_id) + if project.deleted_at: + raise AMTNotFound() + return project + + async def delete(self, project_id: int) -> Project: + project = await self.repository.find_by_id(project_id) + project.deleted_at = datetime.now(tz=UTC) + project = await self.repository.save(project) return project async def create(self, project_new: ProjectNew) -> Project: diff --git a/amt/site/static/scss/layout.scss b/amt/site/static/scss/layout.scss index e05c1fa4..53427fbe 100644 --- a/amt/site/static/scss/layout.scss +++ b/amt/site/static/scss/layout.scss @@ -303,3 +303,8 @@ main { color: var(--rvo-form-feedback-error-color); font-weight: var(--rvo-form-feedback-error-font-weight); } + +.amt-flex-container { + display: flex; + justify-content: space-between; +} diff --git a/amt/site/templates/projects/details_base.html.j2 b/amt/site/templates/projects/details_base.html.j2 index c3eb6d80..c58c4ed5 100644 --- a/amt/site/templates/projects/details_base.html.j2 +++ b/amt/site/templates/projects/details_base.html.j2 @@ -9,9 +9,54 @@
+
-

{{ editable(project, "name") }}

+
diff --git a/tests/api/routes/test_project.py b/tests/api/routes/test_project.py index c992b085..a276d702 100644 --- a/tests/api/routes/test_project.py +++ b/tests/api/routes/test_project.py @@ -32,7 +32,7 @@ @pytest.mark.asyncio async def test_get_unknown_project(client: AsyncClient) -> None: # when - response = await client.get("/algorithm-system/1") + response = await client.get("/algorithm-system/1/details") # then assert response.status_code == 404 @@ -381,6 +381,45 @@ async def test_get_project_edit(client: AsyncClient, db: DatabaseTestUtils) -> N assert b"lifecycle" in response.content +@pytest.mark.asyncio +async def test_delete_project(client: AsyncClient, db: DatabaseTestUtils, mocker: MockFixture) -> None: + # given + await db.given([default_project("testproject1")]) + mocker.patch("fastapi_csrf_protect.CsrfProtect.validate_csrf", new_callable=mocker.AsyncMock) + client.cookies["fastapi-csrf-token"] = "1" + + # when + response = await client.delete("/algorithm-system/1", headers={"X-CSRF-Token": "1"}) + + # then + assert response.status_code == 200 + assert response.headers["content-type"] == "text/html; charset=utf-8" + assert "hx-redirect" in response.headers + assert response.headers["hx-redirect"] == "/algorithm-systems/" + + +@pytest.mark.asyncio +async def test_delete_project_and_check_list(client: AsyncClient, db: DatabaseTestUtils, mocker: MockFixture) -> None: + # given + await db.given([default_project("testproject1"), default_project("testproject2")]) + mocker.patch("fastapi_csrf_protect.CsrfProtect.validate_csrf", new_callable=mocker.AsyncMock) + client.cookies["fastapi-csrf-token"] = "1" + + # when + response = await client.delete("/algorithm-system/1", headers={"X-CSRF-Token": "1"}) + + # then + assert response.status_code == 200 + assert response.headers["content-type"] == "text/html; charset=utf-8" + assert "hx-redirect" in response.headers + + response2 = await client.get("/algorithm-systems/") + assert response2.status_code == 200 + assert response2.headers["content-type"] == "text/html; charset=utf-8" + assert b"testproject2" in response2.content + assert b"testproject1" not in response2.content + + @pytest.mark.asyncio async def test_get_project_cancel(client: AsyncClient, db: DatabaseTestUtils) -> None: # given @@ -392,8 +431,6 @@ async def test_get_project_cancel(client: AsyncClient, db: DatabaseTestUtils) -> # then assert response.status_code == 200 assert response.headers["content-type"] == "text/html; charset=utf-8" - assert b"Edit" in response.content - assert b"lifecycle" in response.content @pytest.mark.asyncio