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']}
/>
+
+
+ import { formFieldProxy } from 'sveltekit-superforms';
+ import { onMount } from 'svelte';
+ import type { CacheLock } from '$lib/utils/types';
+
+ let _class = '';
+
+ export { _class as class };
+ export let label: string | undefined = undefined;
+ export let field: string;
+ export let helpText: string | undefined = undefined;
+ export let cachedValue: string | undefined; // = '';
+ export let cacheLock: CacheLock = {
+ promise: new Promise((res) => res(null)),
+ resolve: (x) => x
+ };
+
+ export let form;
+
+ label = label ?? field;
+
+ const { value, errors, constraints } = formFieldProxy(form, field);
+ // $: value.set(cachedValue);
+ // $value = cachedValue;
+ $: cachedValue = $value;
+
+ $: if ($$restProps.type === 'date' && $value === '') {
+ $value = null;
+ }
+
+ onMount(async () => {
+ const cacheResult = await cacheLock.promise;
+ if (cacheResult) $value = cacheResult;
+ });
+
+ $: classesTextField = (errors: string[] | undefined) => (errors ? 'input-error' : '');
+ $: classesDisabled = (disabled: boolean) => (disabled ? 'opacity-50' : '');
+
+
+
+
+ {#if label !== undefined && !$$props.hidden}
+ {#if $constraints?.required || $$props.required}
+
+ {:else}
+
+ {/if}
+ {/if}
+ {#if $errors}
+
+ {#each $errors as error}
+ {error}
+ {/each}
+
+ {/if}
+
+
+
+
+ {#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 }
]);
| |