diff --git a/.gitignore b/.gitignore index d6e6b144..b2407927 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,6 @@ amt/site/static/js/* # ignore webpack build files amt/site/templates/layouts/base.html.j2 + +# downloaded system_cards +*00:00.yaml diff --git a/amt/api/routes/algorithm.py b/amt/api/routes/algorithm.py index 42e6c7b8..126f1d3e 100644 --- a/amt/api/routes/algorithm.py +++ b/amt/api/routes/algorithm.py @@ -1,10 +1,12 @@ import asyncio +import datetime import logging from collections.abc import Sequence from typing import Annotated, Any, cast +import yaml from fastapi import APIRouter, Depends, Request -from fastapi.responses import HTMLResponse +from fastapi.responses import FileResponse, HTMLResponse from pydantic import BaseModel, Field from amt.api.deps import templates @@ -736,3 +738,17 @@ async def get_model_card( } return templates.TemplateResponse(request, "pages/model_card.html.j2", context) + + +@router.get("/{algorithm_id}/details/system_card/download") +async def download_algorithm_system_card_as_yaml( + algorithm_id: int, algorithms_service: Annotated[AlgorithmsService, Depends(AlgorithmsService)], request: Request +) -> FileResponse: + algorithm = await get_algorithm_or_error(algorithm_id, algorithms_service, request) + filename = algorithm.name + "_" + datetime.datetime.now(datetime.UTC).isoformat() + ".yaml" + with open(filename, "w") as outfile: + yaml.dump(algorithm.system_card.model_dump(), outfile) + try: + return FileResponse(filename, filename=filename) + except AMTRepositoryError as e: + raise AMTNotFound from e diff --git a/amt/locale/base.pot b/amt/locale/base.pot index 63ed74ae..5945846d 100644 --- a/amt/locale/base.pot +++ b/amt/locale/base.pot @@ -323,40 +323,40 @@ msgstr "" msgid "No" msgstr "" -#: amt/site/templates/algorithms/details_base.html.j2:57 -msgid "Delete algorithm" +#: amt/site/templates/algorithms/details_base.html.j2:63 +msgid "Download as YAML" msgstr "" -#: amt/site/templates/algorithms/details_base.html.j2:74 +#: amt/site/templates/algorithms/details_base.html.j2:87 msgid "Does the algorithm meet the requirements?" msgstr "" -#: amt/site/templates/algorithms/details_base.html.j2:77 -#: amt/site/templates/algorithms/details_base.html.j2:103 -#: amt/site/templates/algorithms/details_base.html.j2:134 -#: amt/site/templates/algorithms/details_base.html.j2:157 +#: amt/site/templates/algorithms/details_base.html.j2:90 +#: amt/site/templates/algorithms/details_base.html.j2:116 +#: amt/site/templates/algorithms/details_base.html.j2:147 +#: amt/site/templates/algorithms/details_base.html.j2:170 #: amt/site/templates/macros/tasks.html.j2:32 msgid "Done" msgstr "" -#: amt/site/templates/algorithms/details_base.html.j2:99 -#: amt/site/templates/algorithms/details_base.html.j2:155 +#: amt/site/templates/algorithms/details_base.html.j2:112 +#: amt/site/templates/algorithms/details_base.html.j2:168 msgid "To do" msgstr "" -#: amt/site/templates/algorithms/details_base.html.j2:101 +#: amt/site/templates/algorithms/details_base.html.j2:114 msgid "In progress" msgstr "" -#: amt/site/templates/algorithms/details_base.html.j2:117 +#: amt/site/templates/algorithms/details_base.html.j2:130 msgid "Go to all requirements" msgstr "" -#: amt/site/templates/algorithms/details_base.html.j2:131 +#: amt/site/templates/algorithms/details_base.html.j2:144 msgid "Which instruments are executed?" msgstr "" -#: amt/site/templates/algorithms/details_base.html.j2:171 +#: amt/site/templates/algorithms/details_base.html.j2:184 msgid "Go to all instruments" msgstr "" diff --git a/amt/locale/en_US/LC_MESSAGES/messages.mo b/amt/locale/en_US/LC_MESSAGES/messages.mo index 738f59d7..87254830 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 288912d0..c95527f3 100644 --- a/amt/locale/en_US/LC_MESSAGES/messages.po +++ b/amt/locale/en_US/LC_MESSAGES/messages.po @@ -324,40 +324,40 @@ msgstr "" msgid "No" msgstr "" -#: amt/site/templates/algorithms/details_base.html.j2:57 -msgid "Delete algorithm" +#: amt/site/templates/algorithms/details_base.html.j2:63 +msgid "Download as YAML" msgstr "" -#: amt/site/templates/algorithms/details_base.html.j2:74 +#: amt/site/templates/algorithms/details_base.html.j2:87 msgid "Does the algorithm meet the requirements?" msgstr "" -#: amt/site/templates/algorithms/details_base.html.j2:77 -#: amt/site/templates/algorithms/details_base.html.j2:103 -#: amt/site/templates/algorithms/details_base.html.j2:134 -#: amt/site/templates/algorithms/details_base.html.j2:157 +#: amt/site/templates/algorithms/details_base.html.j2:90 +#: amt/site/templates/algorithms/details_base.html.j2:116 +#: amt/site/templates/algorithms/details_base.html.j2:147 +#: amt/site/templates/algorithms/details_base.html.j2:170 #: amt/site/templates/macros/tasks.html.j2:32 msgid "Done" msgstr "" -#: amt/site/templates/algorithms/details_base.html.j2:99 -#: amt/site/templates/algorithms/details_base.html.j2:155 +#: amt/site/templates/algorithms/details_base.html.j2:112 +#: amt/site/templates/algorithms/details_base.html.j2:168 msgid "To do" msgstr "" -#: amt/site/templates/algorithms/details_base.html.j2:101 +#: amt/site/templates/algorithms/details_base.html.j2:114 msgid "In progress" msgstr "" -#: amt/site/templates/algorithms/details_base.html.j2:117 +#: amt/site/templates/algorithms/details_base.html.j2:130 msgid "Go to all requirements" msgstr "" -#: amt/site/templates/algorithms/details_base.html.j2:131 +#: amt/site/templates/algorithms/details_base.html.j2:144 msgid "Which instruments are executed?" msgstr "" -#: amt/site/templates/algorithms/details_base.html.j2:171 +#: amt/site/templates/algorithms/details_base.html.j2:184 msgid "Go to all instruments" msgstr "" diff --git a/amt/locale/nl_NL/LC_MESSAGES/messages.mo b/amt/locale/nl_NL/LC_MESSAGES/messages.mo index b82af76b..359c1d3a 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 e1c86fab..26147cbd 100644 --- a/amt/locale/nl_NL/LC_MESSAGES/messages.po +++ b/amt/locale/nl_NL/LC_MESSAGES/messages.po @@ -338,40 +338,40 @@ msgstr "Ja" msgid "No" msgstr "Nee" -#: amt/site/templates/algorithms/details_base.html.j2:57 -msgid "Delete algorithm" -msgstr "Verwijder algoritme" +#: amt/site/templates/algorithms/details_base.html.j2:63 +msgid "Download as YAML" +msgstr "Download naar YAML" -#: amt/site/templates/algorithms/details_base.html.j2:74 +#: amt/site/templates/algorithms/details_base.html.j2:87 msgid "Does the algorithm meet the requirements?" msgstr "Voldoet het algoritme aan de vereisten?" -#: amt/site/templates/algorithms/details_base.html.j2:77 -#: amt/site/templates/algorithms/details_base.html.j2:103 -#: amt/site/templates/algorithms/details_base.html.j2:134 -#: amt/site/templates/algorithms/details_base.html.j2:157 +#: amt/site/templates/algorithms/details_base.html.j2:90 +#: amt/site/templates/algorithms/details_base.html.j2:116 +#: amt/site/templates/algorithms/details_base.html.j2:147 +#: amt/site/templates/algorithms/details_base.html.j2:170 #: amt/site/templates/macros/tasks.html.j2:32 msgid "Done" msgstr "Afgerond" -#: amt/site/templates/algorithms/details_base.html.j2:99 -#: amt/site/templates/algorithms/details_base.html.j2:155 +#: amt/site/templates/algorithms/details_base.html.j2:112 +#: amt/site/templates/algorithms/details_base.html.j2:168 msgid "To do" msgstr "Te doen" -#: amt/site/templates/algorithms/details_base.html.j2:101 +#: amt/site/templates/algorithms/details_base.html.j2:114 msgid "In progress" msgstr "Onderhanden" -#: amt/site/templates/algorithms/details_base.html.j2:117 +#: amt/site/templates/algorithms/details_base.html.j2:130 msgid "Go to all requirements" msgstr "Ga naar alle Vereisten" -#: amt/site/templates/algorithms/details_base.html.j2:131 +#: amt/site/templates/algorithms/details_base.html.j2:144 msgid "Which instruments are executed?" msgstr "Welke instrumenten zijn uitgevoerd?" -#: amt/site/templates/algorithms/details_base.html.j2:171 +#: amt/site/templates/algorithms/details_base.html.j2:184 msgid "Go to all instruments" msgstr "Ga naar all instrumenten" diff --git a/amt/site/static/scss/layout.scss b/amt/site/static/scss/layout.scss index 66003a24..e6dcff3a 100644 --- a/amt/site/static/scss/layout.scss +++ b/amt/site/static/scss/layout.scss @@ -162,6 +162,7 @@ main { &.amt-layout-grid-columns--two { grid-template-columns: repeat(2, 1fr); } + /* stylelint-enable */ } @@ -389,6 +390,40 @@ main { visibility: visible; } +.dropdown-content { + display: none; + position: absolute; + background-color: #f9f9f9; + min-width: 160px; + box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); + z-index: 1; +} + +.dropdown-content a { + color: black; + padding: 12px 16px; + text-decoration: none; + display: block; +} + +.dropdown-content a:hover { + background-color: #f1f1f1; +} + +.dropdown:hover .dropdown-content { + display: block; +} + +.dropdown-underlay { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; +} + /* stylelint-enable */ .amt-cursor-pointer { diff --git a/amt/site/static/ts/amt.ts b/amt/site/static/ts/amt.ts index 47d93a26..88d03875 100644 --- a/amt/site/static/ts/amt.ts +++ b/amt/site/static/ts/amt.ts @@ -308,6 +308,61 @@ export function hide_form_search_options(id: string) { }, 250); } +export function hide_download_dropdown() { + const dropdownContent = document.querySelector( + ".dropdown-content", + ) as HTMLElement; + const dropdownUnderlay = document.querySelector( + ".dropdown-underlay", + ) as HTMLElement; + + if (dropdownContent && dropdownUnderlay) { + dropdownContent.style.display = "none"; + dropdownUnderlay.style.display = "none"; + } else { + console.error("Could not find dropdown elements."); + } +} + +export function show_download_dropdown() { + const dropdownContent = document.querySelector( + ".dropdown-content", + ) as HTMLElement; + const dropdownUnderlay = document.querySelector( + ".dropdown-underlay", + ) as HTMLElement; + + if (dropdownContent && dropdownUnderlay) { + dropdownContent.style.display = "block"; + dropdownUnderlay.style.display = "block"; + } else { + console.error("Could not find dropdown elements."); + } +} + +export async function download_as_yaml( + algorithm_id: string, + algorithm_name: string, +): Promise { + try { + const response = await fetch( + `/algorithm/${algorithm_id}/details/system_card/download`, + ); + const blob = await response.blob(); // Get the response as a Blob + + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + const filename = algorithm_name + "_" + new Date().toISOString() + ".yaml"; + link.setAttribute("download", filename); + document.body.appendChild(link); + link.click(); + } catch (error) { + console.error("Error downloading system card:", error); + } + hide_download_dropdown(); +} + export function add_field_on_enter(id: string) { if (!event) { return; diff --git a/amt/site/templates/algorithms/details_base.html.j2 b/amt/site/templates/algorithms/details_base.html.j2 index c561ac0f..34029a32 100644 --- a/amt/site/templates/algorithms/details_base.html.j2 +++ b/amt/site/templates/algorithms/details_base.html.j2 @@ -49,13 +49,26 @@
diff --git a/tests/api/routes/test_algorithm.py b/tests/api/routes/test_algorithm.py index 6589fa2a..d537fb4c 100644 --- a/tests/api/routes/test_algorithm.py +++ b/tests/api/routes/test_algorithm.py @@ -575,3 +575,20 @@ async def test_update_measure_value(client: AsyncClient, mocker: MockFixture, db ) assert response.status_code == 200 assert response.headers["content-type"] == "text/html; charset=utf-8" + + +@pytest.mark.asyncio +async def test_download_algorithm_system_card_as_yaml( + client: AsyncClient, mocker: MockFixture, db: DatabaseTestUtils +) -> None: + # given + await db.given([default_user(), default_algorithm_with_system_card("testalgorithm1")]) + client.cookies["fastapi-csrf-token"] = "1" + mocker.patch("fastapi_csrf_protect.CsrfProtect.validate_csrf", new_callable=mocker.AsyncMock) + + # happy flow + response = await client.get("/algorithm/1/details/system_card/download") + + assert response.status_code == 200 + assert response.headers["content-type"] == "text/plain; charset=utf-8" + assert b"ai_act_profile:" in response.content