From 8b0adcb34890d8444b729d0ed6e6b678f7bf3633 Mon Sep 17 00:00:00 2001 From: Laurens Weijs Date: Tue, 10 Dec 2024 12:13:09 +0100 Subject: [PATCH 1/2] Add functions to measure which are changable in the modal --- amt/api/forms/measure.py | 33 ++++- amt/api/routes/algorithm.py | 116 ++++++++++++++++-- amt/locale/base.pot | 40 +++--- amt/locale/en_US/LC_MESSAGES/messages.po | 40 +++--- amt/locale/nl_NL/LC_MESSAGES/messages.mo | Bin 14529 -> 14655 bytes amt/locale/nl_NL/LC_MESSAGES/messages.po | 40 +++--- amt/schema/measure.py | 15 +++ amt/site/static/scss/layout.scss | 37 +++++- .../algorithms/details_measure_modal.html.j2 | 111 ++++++++++------- .../algorithms/details_requirements.html.j2 | 2 + amt/site/templates/algorithms/new.html.j2 | 2 +- .../system_card_templates/AMT_Template_1.json | 5 +- tests/api/routes/test_algorithm.py | 38 ++++++ 13 files changed, 381 insertions(+), 98 deletions(-) diff --git a/amt/api/forms/measure.py b/amt/api/forms/measure.py index a4103954..8d9418f1 100644 --- a/amt/api/forms/measure.py +++ b/amt/api/forms/measure.py @@ -1,16 +1,47 @@ +from collections.abc import Sequence from gettext import NullTranslations +from amt.models import User from amt.schema.webform import WebForm, WebFormField, WebFormFieldType, WebFormOption, WebFormTextCloneableField async def get_measure_form( - id: str, current_values: dict[str, str | list[str] | list[tuple[str, str]]], translations: NullTranslations + id: str, + current_values: dict[str, str | list[str] | list[tuple[str, str]]], + members: Sequence[User], + translations: NullTranslations, ) -> WebForm: _ = translations.gettext measure_form: WebForm = WebForm(id="", post_url="") + member_option_list = [WebFormOption(value=member.name, display_value=member.name) for member in members] + member_option_list.append(WebFormOption(value="", display_value="")) measure_form.fields = [ + WebFormField( + type=WebFormFieldType.SELECT, + name="measure_responsible", + label=_("Responsible"), + options=member_option_list, + default_value=current_values.get("measure_responsible"), + group="1", + ), + WebFormField( + type=WebFormFieldType.SELECT, + name="measure_reviewer", + label=_("Reviewer"), + options=member_option_list, + default_value=current_values.get("measure_reviewer"), + group="1", + ), + WebFormField( + type=WebFormFieldType.SELECT, + name="measure_accountable", + label=_("Accountable"), + options=member_option_list, + default_value=current_values.get("measure_accountable"), + group="1", + ), WebFormField( type=WebFormFieldType.SELECT, name="measure_state", diff --git a/amt/api/routes/algorithm.py b/amt/api/routes/algorithm.py index 0a32be7c..e7e5c5c3 100644 --- a/amt/api/routes/algorithm.py +++ b/amt/api/routes/algorithm.py @@ -1,11 +1,12 @@ import asyncio import datetime import logging +from collections import defaultdict from collections.abc import Sequence from typing import Annotated, Any, cast import yaml -from fastapi import APIRouter, Depends, File, Form, Request, Response, UploadFile +from fastapi import APIRouter, Depends, File, Form, Query, Request, Response, UploadFile from fastapi.responses import FileResponse, HTMLResponse from pydantic import BaseModel from ulid import ULID @@ -19,6 +20,7 @@ resolve_base_navigation_items, resolve_navigation_items, ) +from amt.api.routes.shared import get_filters_and_sort_by from amt.core.authorization import get_user from amt.core.exceptions import AMTError, AMTNotFound, AMTRepositoryError from amt.core.internationalization import get_current_translation @@ -26,7 +28,8 @@ from amt.models import Algorithm from amt.models.task import Task from amt.repositories.organizations import OrganizationsRepository -from amt.schema.measure import ExtendedMeasureTask, MeasureTask +from amt.repositories.users import UsersRepository +from amt.schema.measure import ExtendedMeasureTask, MeasureTask, Person from amt.schema.requirement import RequirementTask from amt.schema.system_card import Owner, SystemCard from amt.schema.task import MovedTask @@ -414,6 +417,8 @@ async def get_algorithm_inference( async def get_system_card_requirements( request: Request, algorithm_id: int, + organizations_repository: Annotated[OrganizationsRepository, Depends(OrganizationsRepository)], + users_repository: Annotated[UsersRepository, Depends(UsersRepository)], algorithms_service: Annotated[AlgorithmsService, Depends(AlgorithmsService)], requirements_service: Annotated[RequirementsService, Depends(create_requirements_service)], measures_service: Annotated[MeasuresService, Depends(create_measures_service)], @@ -422,6 +427,9 @@ async def get_system_card_requirements( instrument_state = await get_instrument_state(algorithm.system_card) requirements_state = await get_requirements_state(algorithm.system_card) tab_items = get_algorithm_details_tabs(request) + filters, _, _, sort_by = get_filters_and_sort_by(request) + organization = await organizations_repository.find_by_id(algorithm.organization_id) + filters["organization-id"] = str(organization.id) breadcrumbs = resolve_base_navigation_items( [ @@ -436,14 +444,17 @@ async def get_system_card_requirements( [requirement.urn for requirement in algorithm.system_card.requirements] ) - # Get measures that correspond to the requirements and merge them with the measuretasks + # Get measures that correspond to the requirements and merge them with the measure tasks requirements_and_measures = [] + measure_tasks: list[MeasureTask | None] = [] for requirement in requirements: completed_measures_count = 0 linked_measures = await measures_service.fetch_measures(requirement.links) extended_linked_measures: list[ExtendedMeasureTask] = [] for measure in linked_measures: measure_task = find_measure_task(algorithm.system_card, measure.urn) + if measure_task not in measure_tasks: + measure_tasks.append(measure_task) if measure_task: ext_measure_task = ExtendedMeasureTask( name=measure.name, @@ -458,6 +469,8 @@ async def get_system_card_requirements( extended_linked_measures.append(ext_measure_task) requirements_and_measures.append((requirement, completed_measures_count, extended_linked_measures)) # pyright: ignore [reportUnknownMemberType] + measure_task_functions = await get_measure_task_functions(measure_tasks, users_repository, sort_by, filters) + context = { "instrument_state": instrument_state, "requirements_state": requirements_state, @@ -466,11 +479,49 @@ async def get_system_card_requirements( "tab_items": tab_items, "breadcrumbs": breadcrumbs, "requirements_and_measures": requirements_and_measures, + "measure_task_functions": measure_task_functions, } return templates.TemplateResponse(request, "algorithms/details_requirements.html.j2", context) +async def get_measure_task_functions( + measure_tasks: list[MeasureTask | None], + users_repository: Annotated[UsersRepository, Depends(UsersRepository)], + sort_by: dict[str, str], + filters: dict[str, str], +) -> dict[str, list[Any]]: + measure_task_functions: dict[str, list[Any]] = defaultdict(list) + for measure_task in measure_tasks: + if measure_task.accountable_persons: # pyright: ignore [reportOptionalMemberAccess] + members_accountable = await users_repository.find_all( + search=measure_task.accountable_persons[0].name, # pyright: ignore [reportOptionalMemberAccess] + sort=sort_by, + filters=filters, + ) + if members_accountable: + measure_task_functions[measure_task.urn].append(members_accountable[0]) # pyright: ignore [reportOptionalMemberAccess] + + if measure_task.reviewer_persons: # pyright: ignore [reportOptionalMemberAccess] + members_reviewer = await users_repository.find_all( + search=measure_task.reviewer_persons[0].name, # pyright: ignore [reportOptionalMemberAccess] + sort=sort_by, + filters=filters, + ) + if members_reviewer: + measure_task_functions[measure_task.urn].append(members_reviewer[0]) # pyright: ignore [reportOptionalMemberAccess] + + if measure_task.responsible_persons: # pyright: ignore [reportOptionalMemberAccess] + members_responsible = await users_repository.find_all( + search=measure_task.responsible_persons[0].name, # pyright: ignore [reportOptionalMemberAccess] + sort=sort_by, + filters=filters, + ) + if members_responsible: + measure_task_functions[measure_task.urn].append(members_responsible[0]) # pyright: ignore [reportOptionalMemberAccess] + return measure_task_functions + + def find_measure_task(system_card: SystemCard, urn: str) -> MeasureTask | None: for measure in system_card.measures: if measure.urn == urn: @@ -518,12 +569,16 @@ async def delete_algorithm( @router.get("/{algorithm_id}/measure/{measure_urn}") async def get_measure( request: Request, + organizations_repository: Annotated[OrganizationsRepository, Depends(OrganizationsRepository)], + users_repository: Annotated[UsersRepository, Depends(UsersRepository)], algorithm_id: int, measure_urn: str, algorithms_service: Annotated[AlgorithmsService, Depends(AlgorithmsService)], measures_service: Annotated[MeasuresService, Depends(create_measures_service)], object_storage_service: Annotated[ObjectStorageService, Depends(create_object_storage_service)], + search: str = Query(""), ) -> HTMLResponse: + filters, _, _, sort_by = get_filters_and_sort_by(request) algorithm = await get_algorithm_or_error(algorithm_id, algorithms_service, request) measure = await measures_service.fetch_measures([measure_urn]) measure_task = get_measure_task_or_error(algorithm.system_card, measure_urn) @@ -533,6 +588,14 @@ async def get_measure( metadata = object_storage_service.get_file_metadata_from_object_name(file) filenames.append((file.split("/")[-1], f"{metadata.filename}.{metadata.ext}")) + organization = await organizations_repository.find_by_id(algorithm.organization_id) + filters["organization-id"] = str(organization.id) + members = await users_repository.find_all(search=search, sort=sort_by, filters=filters) + + measure_accountable = measure_task.accountable_persons[0].name if measure_task.accountable_persons else "" # pyright: ignore [reportOptionalMemberAccess] + measure_reviewer = measure_task.reviewer_persons[0].name if measure_task.reviewer_persons else "" # pyright: ignore [reportOptionalMemberAccess] + measure_responsible = measure_task.responsible_persons[0].name if measure_task.responsible_persons else "" # pyright: ignore [reportOptionalMemberAccess] + measure_form = await get_measure_form( id="measure_state", current_values={ @@ -540,32 +603,59 @@ async def get_measure( "measure_value": measure_task.value, "measure_links": measure_task.links, "measure_files": filenames, + "measure_accountable": measure_accountable, + "measure_reviewer": measure_reviewer, + "measure_responsible": measure_responsible, }, + members=members, translations=get_current_translation(request), ) - context = { - "measure": measure[0], - "algorithm_id": algorithm_id, - "form": measure_form, - } + context = {"measure": measure[0], "algorithm_id": algorithm_id, "form": measure_form} return templates.TemplateResponse(request, "algorithms/details_measure_modal.html.j2", context) +async def get_users_from_function_name( + measure_accountable: Annotated[str | None, Form()], + measure_reviewer: Annotated[str | None, Form()], + measure_responsible: Annotated[str | None, Form()], + users_repository: Annotated[UsersRepository, Depends(UsersRepository)], + sort_by: dict[str, str], + filters: dict[str, str], +) -> tuple[list[Person], list[Person], list[Person]]: + accountable_persons, reviewer_persons, responsible_persons = [], [], [] + if measure_accountable: + accountable_member = await users_repository.find_all(search=measure_accountable, sort=sort_by, filters=filters) + accountable_persons = [Person(name=accountable_member[0].name, uuid=str(accountable_member[0].id))] # pyright: ignore [reportOptionalMemberAccess] + if measure_reviewer: + reviewer_member = await users_repository.find_all(search=measure_reviewer, sort=sort_by, filters=filters) + reviewer_persons = [Person(name=reviewer_member[0].name, uuid=str(reviewer_member[0].id))] # pyright: ignore [reportOptionalMemberAccess] + if measure_responsible: + responsible_member = await users_repository.find_all(search=measure_responsible, sort=sort_by, filters=filters) + responsible_persons = [Person(name=responsible_member[0].name, uuid=str(responsible_member[0].id))] # pyright: ignore [reportOptionalMemberAccess] + return accountable_persons, reviewer_persons, responsible_persons + + @router.post("/{algorithm_id}/measure/{measure_urn}") async def update_measure_value( request: Request, algorithm_id: int, measure_urn: str, + organizations_repository: Annotated[OrganizationsRepository, Depends(OrganizationsRepository)], + users_repository: Annotated[UsersRepository, Depends(UsersRepository)], algorithms_service: Annotated[AlgorithmsService, Depends(AlgorithmsService)], requirements_service: Annotated[RequirementsService, Depends(create_requirements_service)], object_storage_service: Annotated[ObjectStorageService, Depends(create_object_storage_service)], measure_state: Annotated[str, Form()], + measure_responsible: Annotated[str | None, Form()] = None, + measure_reviewer: Annotated[str | None, Form()] = None, + measure_accountable: Annotated[str | None, Form()] = None, measure_value: Annotated[str | None, Form()] = None, measure_links: Annotated[list[str] | None, Form()] = None, measure_files: Annotated[list[UploadFile] | None, File()] = None, ) -> HTMLResponse: + filters, _, _, sort_by = get_filters_and_sort_by(request) algorithm = await get_algorithm_or_error(algorithm_id, algorithms_service, request) user_id = get_user_id_or_error(request) measure_task = get_measure_task_or_error(algorithm.system_card, measure_urn) @@ -577,7 +667,15 @@ async def update_measure_value( if measure_files else None ) - measure_task.update(measure_state, measure_value, measure_links, paths) + accountable_persons, reviewer_persons, responsible_persons = await get_users_from_function_name( + measure_accountable, measure_reviewer, measure_responsible, users_repository, sort_by, filters + ) + + measure_task.update( + measure_state, measure_value, measure_links, paths, responsible_persons, accountable_persons, reviewer_persons + ) + organization = await organizations_repository.find_by_id(algorithm.organization_id) + filters["organization-id"] = str(organization.id) # update for the linked requirements the state based on all it's measures requirement_tasks = await find_requirement_tasks_by_measure_urn(algorithm.system_card, measure_urn) diff --git a/amt/locale/base.pot b/amt/locale/base.pot index 1fc801d2..36de6717 100644 --- a/amt/locale/base.pot +++ b/amt/locale/base.pot @@ -198,33 +198,45 @@ msgstr "" msgid "Organization" msgstr "" -#: amt/api/forms/measure.py:17 -msgid "Status" +#: amt/api/forms/measure.py:24 +msgid "Responsible" msgstr "" #: amt/api/forms/measure.py:32 +msgid "Reviewer" +msgstr "" + +#: amt/api/forms/measure.py:40 +msgid "Accountable" +msgstr "" + +#: amt/api/forms/measure.py:48 +msgid "Status" +msgstr "" + +#: amt/api/forms/measure.py:63 msgid "Information on how this measure is implemented" msgstr "" -#: amt/api/forms/measure.py:39 +#: amt/api/forms/measure.py:70 msgid "" "Select one or more to upload. The files will be saved once you confirm " "changes by pressing the save button." msgstr "" -#: amt/api/forms/measure.py:44 +#: amt/api/forms/measure.py:75 msgid "Add files" msgstr "" -#: amt/api/forms/measure.py:45 +#: amt/api/forms/measure.py:76 msgid "No files selected." msgstr "" -#: amt/api/forms/measure.py:49 +#: amt/api/forms/measure.py:80 msgid "Add URI" msgstr "" -#: amt/api/forms/measure.py:52 +#: amt/api/forms/measure.py:83 msgid "Add links to documents" msgstr "" @@ -458,7 +470,7 @@ msgid "Failed to estimate WOZ value: " msgstr "" #: amt/site/templates/algorithms/details_info.html.j2:16 -#: amt/site/templates/algorithms/details_measure_modal.html.j2:26 +#: amt/site/templates/algorithms/details_measure_modal.html.j2:27 msgid "Description" msgstr "" @@ -488,26 +500,26 @@ msgstr "" msgid "References" msgstr "" -#: amt/site/templates/algorithms/details_measure_modal.html.j2:36 +#: amt/site/templates/algorithms/details_measure_modal.html.j2:37 msgid "Read more on the algoritmekader" msgstr "" -#: amt/site/templates/algorithms/details_measure_modal.html.j2:47 +#: amt/site/templates/algorithms/details_measure_modal.html.j2:63 #: amt/site/templates/macros/editable.html.j2:82 msgid "Save" msgstr "" -#: amt/site/templates/algorithms/details_measure_modal.html.j2:51 +#: amt/site/templates/algorithms/details_measure_modal.html.j2:67 #: amt/site/templates/macros/editable.html.j2:87 #: amt/site/templates/organizations/parts/add_members_modal.html.j2:26 msgid "Cancel" msgstr "" -#: amt/site/templates/algorithms/details_requirements.html.j2:26 +#: amt/site/templates/algorithms/details_requirements.html.j2:27 msgid "measures executed" msgstr "" -#: amt/site/templates/algorithms/details_requirements.html.j2:59 +#: amt/site/templates/algorithms/details_requirements.html.j2:60 #: amt/site/templates/macros/editable.html.j2:24 #: amt/site/templates/macros/editable.html.j2:27 msgid "Edit" @@ -562,7 +574,7 @@ msgstr "" #: amt/site/templates/algorithms/new.html.j2:172 msgid "" "Overview of instruments for the responsible development, deployment, " -"assessment and monitoring of algorithms and AI-systems." +"assessment, and monitoring of algorithms and AI-systems." msgstr "" #: amt/site/templates/algorithms/new.html.j2:180 diff --git a/amt/locale/en_US/LC_MESSAGES/messages.po b/amt/locale/en_US/LC_MESSAGES/messages.po index e939c29b..64b4dda5 100644 --- a/amt/locale/en_US/LC_MESSAGES/messages.po +++ b/amt/locale/en_US/LC_MESSAGES/messages.po @@ -199,33 +199,45 @@ msgstr "" msgid "Organization" msgstr "" -#: amt/api/forms/measure.py:17 -msgid "Status" +#: amt/api/forms/measure.py:24 +msgid "Responsible" msgstr "" #: amt/api/forms/measure.py:32 +msgid "Reviewer" +msgstr "" + +#: amt/api/forms/measure.py:40 +msgid "Accountable" +msgstr "" + +#: amt/api/forms/measure.py:48 +msgid "Status" +msgstr "" + +#: amt/api/forms/measure.py:63 msgid "Information on how this measure is implemented" msgstr "" -#: amt/api/forms/measure.py:39 +#: amt/api/forms/measure.py:70 msgid "" "Select one or more to upload. The files will be saved once you confirm " "changes by pressing the save button." msgstr "" -#: amt/api/forms/measure.py:44 +#: amt/api/forms/measure.py:75 msgid "Add files" msgstr "" -#: amt/api/forms/measure.py:45 +#: amt/api/forms/measure.py:76 msgid "No files selected." msgstr "" -#: amt/api/forms/measure.py:49 +#: amt/api/forms/measure.py:80 msgid "Add URI" msgstr "" -#: amt/api/forms/measure.py:52 +#: amt/api/forms/measure.py:83 msgid "Add links to documents" msgstr "" @@ -459,7 +471,7 @@ msgid "Failed to estimate WOZ value: " msgstr "" #: amt/site/templates/algorithms/details_info.html.j2:16 -#: amt/site/templates/algorithms/details_measure_modal.html.j2:26 +#: amt/site/templates/algorithms/details_measure_modal.html.j2:27 msgid "Description" msgstr "" @@ -489,26 +501,26 @@ msgstr "" msgid "References" msgstr "" -#: amt/site/templates/algorithms/details_measure_modal.html.j2:36 +#: amt/site/templates/algorithms/details_measure_modal.html.j2:37 msgid "Read more on the algoritmekader" msgstr "" -#: amt/site/templates/algorithms/details_measure_modal.html.j2:47 +#: amt/site/templates/algorithms/details_measure_modal.html.j2:63 #: amt/site/templates/macros/editable.html.j2:82 msgid "Save" msgstr "" -#: amt/site/templates/algorithms/details_measure_modal.html.j2:51 +#: amt/site/templates/algorithms/details_measure_modal.html.j2:67 #: amt/site/templates/macros/editable.html.j2:87 #: amt/site/templates/organizations/parts/add_members_modal.html.j2:26 msgid "Cancel" msgstr "" -#: amt/site/templates/algorithms/details_requirements.html.j2:26 +#: amt/site/templates/algorithms/details_requirements.html.j2:27 msgid "measures executed" msgstr "" -#: amt/site/templates/algorithms/details_requirements.html.j2:59 +#: amt/site/templates/algorithms/details_requirements.html.j2:60 #: amt/site/templates/macros/editable.html.j2:24 #: amt/site/templates/macros/editable.html.j2:27 msgid "Edit" @@ -563,7 +575,7 @@ msgstr "" #: amt/site/templates/algorithms/new.html.j2:172 msgid "" "Overview of instruments for the responsible development, deployment, " -"assessment and monitoring of algorithms and AI-systems." +"assessment, and monitoring of algorithms and AI-systems." msgstr "" #: amt/site/templates/algorithms/new.html.j2:180 diff --git a/amt/locale/nl_NL/LC_MESSAGES/messages.mo b/amt/locale/nl_NL/LC_MESSAGES/messages.mo index 52863228f32bd32acbf1de0624d3b032a45f79f2..87b5523a80802362456b24ac2aaa9dfa5028f4f9 100644 GIT binary patch delta 3350 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 None: if state: self.state = state @@ -32,6 +43,10 @@ def update( if new_files: self.files.extend(new_files) + self.responsible_persons = responsible_persons + self.reviewer_persons = reviewer_persons + self.accountable_persons = accountable_persons + class Measure(MeasureBase): name: str diff --git a/amt/site/static/scss/layout.scss b/amt/site/static/scss/layout.scss index dfca1d5b..299be045 100644 --- a/amt/site/static/scss/layout.scss +++ b/amt/site/static/scss/layout.scss @@ -390,13 +390,35 @@ main { visibility: visible; } -.dropdown-content { +.measure-function-circle { + display: flex; + align-items: center; +} + +.measure-function-icon { + width: 25px; + height: 25px; + object-fit: cover; + border-radius: 50%; + z-index: 0; + filter: drop-shadow(0 4px 4px rgb(115 142 171 / 50%)); +} + +.member-container { + position: relative; + display: inline-block; + align-items: center; +} + +.member-container a { display: none; position: absolute; background-color: #f9f9f9; min-width: 160px; box-shadow: 0 8px 16px 0 rgb(0 0 0 / 20%); z-index: 1; + white-space: nowrap; + filter: drop-shadow(0 4px 4px rgb(115 142 171 / 50%)); } .dropdown-content a { @@ -406,6 +428,19 @@ main { display: block; } +.member-container:hover a { + display: block; +} + +.dropdown-content { + display: none; + position: absolute; + background-color: #f9f9f9; + min-width: 160px; + box-shadow: 0 8px 16px 0 rgb(0 0 0 / 20%); + z-index: 1; +} + .dropdown-content a:hover { background-color: #f1f1f1; } diff --git a/amt/site/templates/algorithms/details_measure_modal.html.j2 b/amt/site/templates/algorithms/details_measure_modal.html.j2 index 4f9a7673..9635f392 100644 --- a/amt/site/templates/algorithms/details_measure_modal.html.j2 +++ b/amt/site/templates/algorithms/details_measure_modal.html.j2 @@ -7,50 +7,75 @@ aria-label="Close">
-
-
-
-
-
-
-
- -
{{ measure.description }}
+ +
+
+
+
+
+
+
+ +
{{ measure.description }}
+
+
- -
- {% for form_field in form.fields %}{{ macros.form_field(form.id, form_field) }}{% endfor %} -
+ {% for form_field in form.fields %} + {# Place the measure_state on the side in the modal #} + {% if form_field.name != "measure_state" %} + {# The functions have a specific ordering in horizontal fashion #} + {% if form_field.name == "measure_responsible" %} +
+ {{ macros.form_field(form.id, form_field) }} + {% elif form_field.name == "measure_accountable" %} + {{ macros.form_field(form.id, form_field) }} +
+ {% else %} + {{ macros.form_field(form.id, form_field) }} + {% endif %} + {% endif %} + {% endfor %} + +
+

+ + +

-

- - -

- +
+
+ {% for form_field in form.fields %} + {% if form_field.name == "measure_state" %}{{ macros.form_field(form.id, form_field) }}{% endif %} + {% endfor %} +
- + diff --git a/amt/site/templates/algorithms/details_requirements.html.j2 b/amt/site/templates/algorithms/details_requirements.html.j2 index 485f90d9..f2fe620b 100644 --- a/amt/site/templates/algorithms/details_requirements.html.j2 +++ b/amt/site/templates/algorithms/details_requirements.html.j2 @@ -1,4 +1,5 @@ {% extends 'algorithms/details_base.html.j2' %} +{% import "macros/form_macros.html.j2" as macros with context %} {% block detail_content %}
{% for (requirement, completed_measures_count, measures) in requirements_and_measures %} @@ -58,6 +59,7 @@ aria-label="Home"> {% trans %}Edit{% endtrans %} + {{ macros.user_avatars(measure_task_functions[measure.urn]) }}
diff --git a/amt/site/templates/algorithms/new.html.j2 b/amt/site/templates/algorithms/new.html.j2 index ca719708..d437f55c 100644 --- a/amt/site/templates/algorithms/new.html.j2 +++ b/amt/site/templates/algorithms/new.html.j2 @@ -169,7 +169,7 @@

{% trans %}Instruments{% endtrans %}

- {% trans %}Overview of instruments for the responsible development, deployment, assessment and monitoring of algorithms and AI-systems.{% endtrans %} + {% trans %}Overview of instruments for the responsible development, deployment, assessment, and monitoring of algorithms and AI-systems.{% endtrans %}

diff --git a/resources/system_card_templates/AMT_Template_1.json b/resources/system_card_templates/AMT_Template_1.json index 722ea522..249230d8 100644 --- a/resources/system_card_templates/AMT_Template_1.json +++ b/resources/system_card_templates/AMT_Template_1.json @@ -117,7 +117,10 @@ "urn": "urn:nl:ak:mtr:dat-01", "state": "to do", "version": "", - "value": "" + "value": "", + "accountable_persons": [{ "name": "Default User", "uuid": "1" }], + "reviewer_persons": [{ "name": "Default User", "uuid": "1" }], + "responsible_persons": [{ "name": "Default User", "uuid": "1" }] }, { "urn": "urn:nl:ak:mtr:fur-01", diff --git a/tests/api/routes/test_algorithm.py b/tests/api/routes/test_algorithm.py index e516ea01..246be282 100644 --- a/tests/api/routes/test_algorithm.py +++ b/tests/api/routes/test_algorithm.py @@ -611,6 +611,44 @@ async def test_update_measure_value( assert response.headers["content-type"] == "text/html; charset=utf-8" +@pytest.mark.asyncio +async def test_update_measure_value_with_people( + minio_mock: MockMinioClient, 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) + mocker.patch("amt.api.routes.algorithm.get_user_id_or_error", return_value=default_user().id) + + # Need to make bucket in object store. The Minio class is mocked by minio_mock. + storage_client = Minio( + endpoint=get_settings().OBJECT_STORE_URL, + access_key=get_settings().OBJECT_STORE_USER, + secret_key=get_settings().OBJECT_STORE_PASSWORD, + secure=False, + ) + storage_client.make_bucket(get_settings().OBJECT_STORE_BUCKET_NAME) + + # happy flow + response = await client.post( + "/algorithm/1/measure/urn:nl:ak:mtr:dat-01", + data={ + "measure_state": "done", + "measure_value": "something", + "measure_links": [], + "existing_file_names": [], + "measure_files": [], + "measure_responsible": "Default User", + "measure_accountable": "Default User", + "measure_reviewer": "Default User", + }, + headers={"X-CSRF-Token": "1"}, + ) + 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 From 97e9041cf6508409302b8201100e1f5d69caecc7 Mon Sep 17 00:00:00 2001 From: Laurens Weijs Date: Wed, 18 Dec 2024 15:05:54 +0100 Subject: [PATCH 2/2] Pyright updates --- amt/schema/measure.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/amt/schema/measure.py b/amt/schema/measure.py index f01c60d9..7ff8024d 100644 --- a/amt/schema/measure.py +++ b/amt/schema/measure.py @@ -18,9 +18,9 @@ class MeasureTask(MeasureBase): links: list[str] = Field(default=[]) files: list[str] = Field(default=[]) version: str - accountable_persons: list[Person] = Field(default=[]) - reviewer_persons: list[Person] = Field(default=[]) - responsible_persons: list[Person] = Field(default=[]) + accountable_persons: list[Person] | None = Field(default=[]) + reviewer_persons: list[Person] | None = Field(default=[]) + responsible_persons: list[Person] | None = Field(default=[]) def update( self,