From 8bb987f4ed2f7df50979385403998cd560a5e1b1 Mon Sep 17 00:00:00 2001 From: eric-intuitem <71850047+eric-intuitem@users.noreply.github.com> Date: Sun, 8 Sep 2024 19:16:40 +0200 Subject: [PATCH] Add cost field to applied controls Currency not yet managed. NumberField needs improvement --- .../api/test_api_applied_controls.py | 6 + backend/app_tests/api/test_utils.py | 1 + .../migrations/0025_appliedcontrol_cost.py | 21 +++ backend/core/models.py | 18 +++ backend/core/serializers.py | 1 + .../core/templates/core/action_plan_pdf.html | 2 + backend/core/templates/snippets/mp_data.html | 2 + backend/core/views.py | 10 +- documentation/architecture/data-model.md | 5 + .../src/lib/components/Forms/ModelForm.svelte | 8 + frontend/messages/ar.json | 3 + frontend/messages/de.json | 3 + frontend/messages/en.json | 3 + frontend/messages/es.json | 3 + frontend/messages/fr.json | 3 + frontend/messages/hi.json | 3 + frontend/messages/it.json | 5 +- frontend/messages/nl.json | 3 + frontend/messages/pl.json | 3 + frontend/messages/pt.json | 3 + frontend/messages/ro.json | 3 + frontend/messages/ur.json | 3 + .../src/lib/components/Forms/ModelForm.svelte | 9 ++ .../lib/components/Forms/NumberField.svelte | 75 +++++++++ .../src/lib/components/Forms/TextField.svelte | 1 + frontend/src/lib/utils/crud.ts | 1 + frontend/src/lib/utils/locales.ts | 2 + frontend/src/lib/utils/schemas.ts | 1 + .../[id=uuid]/action-plan/+page.svelte | 144 ++++-------------- .../[id=uuid]/remediation-plan/+page.svelte | 2 + frontend/tests/functional/user-route.test.ts | 1 + frontend/tests/utils/test-utils.ts | 1 + 32 files changed, 235 insertions(+), 114 deletions(-) create mode 100644 backend/core/migrations/0025_appliedcontrol_cost.py create mode 100644 frontend/src/lib/components/Forms/NumberField.svelte diff --git a/backend/app_tests/api/test_api_applied_controls.py b/backend/app_tests/api/test_api_applied_controls.py index b7705cfc5..782f1dc4e 100644 --- a/backend/app_tests/api/test_api_applied_controls.py +++ b/backend/app_tests/api/test_api_applied_controls.py @@ -16,6 +16,8 @@ APPLIED_CONTROL_EFFORT2 = ("M", "Medium") APPLIED_CONTROL_LINK = "https://example.com" APPLIED_CONTROL_ETA = "2024-01-01" +APPLIED_CONTROL_COST = 24.42 +APPLIED_CONTROL_COST2 = 25.43 @pytest.mark.django_db @@ -104,6 +106,7 @@ def test_get_applied_controls(self, test): "link": APPLIED_CONTROL_LINK, "eta": APPLIED_CONTROL_ETA, "effort": APPLIED_CONTROL_EFFORT[0], + "cost": APPLIED_CONTROL_COST, "folder": test.folder, }, { @@ -135,6 +138,7 @@ def test_create_applied_controls(self, test): "link": APPLIED_CONTROL_LINK, "eta": APPLIED_CONTROL_ETA, "effort": APPLIED_CONTROL_EFFORT[0], + "cost": APPLIED_CONTROL_COST, "folder": str(test.folder.id), }, { @@ -167,6 +171,7 @@ def test_update_applied_controls(self, test): "link": APPLIED_CONTROL_LINK, "eta": APPLIED_CONTROL_ETA, "effort": APPLIED_CONTROL_EFFORT[0], + "cost": APPLIED_CONTROL_COST, "folder": test.folder, }, { @@ -177,6 +182,7 @@ def test_update_applied_controls(self, test): "link": "new " + APPLIED_CONTROL_LINK, "eta": "2025-01-01", "effort": APPLIED_CONTROL_EFFORT2[0], + "cost": APPLIED_CONTROL_COST2, "folder": str(folder.id), }, { diff --git a/backend/app_tests/api/test_utils.py b/backend/app_tests/api/test_utils.py index 448c5073b..bf691cbb6 100644 --- a/backend/app_tests/api/test_utils.py +++ b/backend/app_tests/api/test_utils.py @@ -494,6 +494,7 @@ def get_object( json.loads(response_item[key]) == value ), f"{verbose_name} {key.replace('_', ' ')} queried from the API don't match {verbose_name.lower()} {key.replace('_', ' ')} in the database" else: + print("coucou", type(value)) assert ( response_item[key] == value ), f"{verbose_name} {key.replace('_', ' ')} queried from the API don't match {verbose_name.lower()} {key.replace('_', ' ')} in the database" diff --git a/backend/core/migrations/0025_appliedcontrol_cost.py b/backend/core/migrations/0025_appliedcontrol_cost.py new file mode 100644 index 000000000..b008363b8 --- /dev/null +++ b/backend/core/migrations/0025_appliedcontrol_cost.py @@ -0,0 +1,21 @@ +# Generated by Django 5.1 on 2024-09-08 20:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0024_appliedcontrol_owner"), + ] + + operations = [ + migrations.AddField( + model_name="appliedcontrol", + name="cost", + field=models.FloatField( + help_text="Cost of the measure (using globally-chosen currency)", + null=True, + verbose_name="Cost", + ), + ), + ] diff --git a/backend/core/models.py b/backend/core/models.py index 64e3a6506..f052de87f 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -1274,6 +1274,11 @@ class Status(models.TextChoices): help_text=_("Relative effort of the measure (using T-Shirt sizing)"), verbose_name=_("Effort"), ) + cost = models.FloatField( + null=True, + help_text=_("Cost of the measure (using globally-chosen currency)"), + verbose_name=_("Cost"), + ) fields_to_check = ["name"] @@ -1628,6 +1633,19 @@ def quality_check(self) -> dict: } ) + if not mtg["cost"]: + warnings_lst.append( + { + "msg": _( + "{} does not have an estimated cost. This will help you for prioritization" + ).format(mtg["name"]), + "msgid": "appliedControlNoCost", + "link": f"applied-controls/{mtg['id']}", + "obj_type": "appliedcontrol", + "object": {"name": mtg["name"], "id": mtg["id"]}, + } + ) + if not mtg["link"]: info_lst.append( { diff --git a/backend/core/serializers.py b/backend/core/serializers.py index fac6fff35..3ae5d1912 100644 --- a/backend/core/serializers.py +++ b/backend/core/serializers.py @@ -310,6 +310,7 @@ class AppliedControlReadSerializer(AppliedControlWriteSerializer): ) # type : get_type_display evidences = FieldsRelatedField(many=True) effort = serializers.CharField(source="get_effort_display") + cost = serializers.FloatField() ranking_score = serializers.IntegerField(source="get_ranking_score") owner = FieldsRelatedField(many=True) diff --git a/backend/core/templates/core/action_plan_pdf.html b/backend/core/templates/core/action_plan_pdf.html index 3397130c3..e46c31fdb 100644 --- a/backend/core/templates/core/action_plan_pdf.html +++ b/backend/core/templates/core/action_plan_pdf.html @@ -46,6 +46,7 @@

{% trans "Action plan" %}

{% trans "ETA" %} {% trans "Expiry date" %} {% trans "Effort" %} + {% trans "Cost" %} {% trans "Matching requirements" %} @@ -59,6 +60,7 @@

{% trans "Action plan" %}

{{ applied_control.eta|default:"--" }} {{ applied_control.expiry_date|default:"--" }} {{ applied_control.get_effort_display|default:"--" }} + {{ applied_control.cost|default:"--" }} {% get_requirements_count applied_control compliance_assessment %} {% empty %} diff --git a/backend/core/templates/snippets/mp_data.html b/backend/core/templates/snippets/mp_data.html index ab5d70457..207186e9c 100644 --- a/backend/core/templates/snippets/mp_data.html +++ b/backend/core/templates/snippets/mp_data.html @@ -61,6 +61,7 @@ {% trans "Reference control" %} {% trans "ETA" %} {% trans "Effort" %} + {% trans "Cost" %} {% trans "Link" %} {% trans "Status" %} @@ -74,6 +75,7 @@ {% if appliedcontrol.reference_control %}{{ appliedcontrol.reference_control }}{% else %}--{% endif %} {% if appliedcontrol.eta %}{{ appliedcontrol.eta }}{% else %}--{%endif%} {% if appliedcontrol.effort %}{{ appliedcontrol.effort }}{% else %}--{%endif%} + {% if appliedcontrol.cost %}{{ appliedcontrol.cost }}{% else %}--{%endif%} {% if appliedcontrol.link %}{% else %}--{% endif %} diff --git a/backend/core/views.py b/backend/core/views.py index baf9e8417..911acb72b 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -431,6 +431,7 @@ def treatment_plan_csv(self, request, pk): "reference_control", "eta", "effort", + "cost", "link", "status", ] @@ -457,6 +458,7 @@ def treatment_plan_csv(self, request, pk): mtg.reference_control, mtg.eta, mtg.effort, + mtg.cost, mtg.link, mtg.status, ] @@ -615,6 +617,7 @@ class AppliedControlViewSet(BaseModelViewSet): "status", "reference_control", "effort", + "cost", "risk_scenarios", "requirement_assessments", "evidences", @@ -670,7 +673,7 @@ def todo(self, request): """measures = [{ key: getattr(mtg,key) for key in [ - "id","folder","reference_control","type","status","effort","name","description","eta","link","created_at","updated_at" + "id","folder","reference_control","type","status","effort", "cost", "name","description","eta","link","created_at","updated_at" ] } for mtg in measures] for i in range(len(measures)) : @@ -1432,6 +1435,7 @@ def action_plan(self, request, pk): "expiry_date": applied_control.expiry_date, "link": applied_control.link, "effort": applied_control.effort, + "cost": applied_control.cost, "owners": [ { "id": owner.id, @@ -1784,7 +1788,7 @@ def todo(self, request): """measures = [{ key: getattr(mtg,key) for key in [ - "id","folder","reference_control","type","status","effort","name","description","eta","link","created_at","updated_at" + "id","folder","reference_control","type","status","effort","cost","name","description","eta","link","created_at","updated_at" ] } for mtg in measures] for i in range(len(measures)) : @@ -1979,6 +1983,7 @@ def export_mp_csv(request): "reference_control", "eta", "effort", + "cost", "link", "status", ] @@ -2000,6 +2005,7 @@ def export_mp_csv(request): mtg.reference_control, mtg.eta, mtg.effort, + mtg.cost, mtg.link, mtg.status, ] diff --git a/documentation/architecture/data-model.md b/documentation/architecture/data-model.md index 67e7474bf..84734fbca 100644 --- a/documentation/architecture/data-model.md +++ b/documentation/architecture/data-model.md @@ -227,6 +227,7 @@ erDiagram date expiration url link string effort + float cost string[] tags } @@ -598,6 +599,7 @@ namespace DomainObjects { +DateField expiry_date +CharField link +CharField effort + +Decimal cost +RiskScenario[] risk_scenarios() +RiskAssessments[] risk_assessments() @@ -777,11 +779,14 @@ A applied control has the following specific fields: - an Estimated Time of Arrival date - a validity date (expiration field) - an effort (--/S/M/L/XL) +- a cost (--/float value) - a url link - a list of user-defined tags When a applied control derives from a reference control, the same category and csf_function are proposed, but this can be changed. +Costs are measured in a global currency/multiple that is defined in global settings. + ## Compliance and risk assessments Both types of assessments have common fields: diff --git a/enterprise/frontend/src/lib/components/Forms/ModelForm.svelte b/enterprise/frontend/src/lib/components/Forms/ModelForm.svelte index e481fea7b..c92890ac2 100644 --- a/enterprise/frontend/src/lib/components/Forms/ModelForm.svelte +++ b/enterprise/frontend/src/lib/components/Forms/ModelForm.svelte @@ -400,6 +400,14 @@ cacheLock={cacheLocks['effort']} bind:cachedValue={formDataCache['effort']} /> + + + {#if helpText} +

{helpText}

+ {/if} + diff --git a/frontend/src/lib/components/Forms/TextField.svelte b/frontend/src/lib/components/Forms/TextField.svelte index ff39bb115..503e9296b 100644 --- a/frontend/src/lib/components/Forms/TextField.svelte +++ b/frontend/src/lib/components/Forms/TextField.svelte @@ -1,6 +1,7 @@ diff --git a/frontend/src/routes/(app)/risk-assessments/[id=uuid]/remediation-plan/+page.svelte b/frontend/src/routes/(app)/risk-assessments/[id=uuid]/remediation-plan/+page.svelte index f33d1a983..9c651cc61 100644 --- a/frontend/src/routes/(app)/risk-assessments/[id=uuid]/remediation-plan/+page.svelte +++ b/frontend/src/routes/(app)/risk-assessments/[id=uuid]/remediation-plan/+page.svelte @@ -94,6 +94,7 @@ {m.referenceControl()} {m.eta()} {m.effort()} + {m.cost()} {m.link()} {m.status()} @@ -112,6 +113,7 @@ > {measure.eta ?? '--'} {measure.effort ?? '--'} + {measure.cost ?? '--'} {measure.link ?? '--'} ({ { name: 'expiry_date', type: type.DATE }, { name: 'link', type: type.TEXT }, { name: 'effort', type: type.SELECT }, + { name: 'cost', type: type.TEXT }, { name: 'folder', type: type.SELECT_AUTOCOMPLETE }, { name: 'reference_control', type: type.SELECT_AUTOCOMPLETE } ]);