Skip to content

Commit

Permalink
Add cost field to applied controls
Browse files Browse the repository at this point in the history
Currency not yet managed.
NumberField needs improvement
  • Loading branch information
eric-intuitem committed Sep 8, 2024
1 parent 3b8c5ab commit 8bb987f
Show file tree
Hide file tree
Showing 32 changed files with 235 additions and 114 deletions.
6 changes: 6 additions & 0 deletions backend/app_tests/api/test_api_applied_controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
},
{
Expand Down Expand Up @@ -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),
},
{
Expand Down Expand Up @@ -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,
},
{
Expand All @@ -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),
},
{
Expand Down
1 change: 1 addition & 0 deletions backend/app_tests/api/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
21 changes: 21 additions & 0 deletions backend/core/migrations/0025_appliedcontrol_cost.py
Original file line number Diff line number Diff line change
@@ -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",
),
),
]
18 changes: 18 additions & 0 deletions backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down Expand Up @@ -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(
{
Expand Down
1 change: 1 addition & 0 deletions backend/core/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions backend/core/templates/core/action_plan_pdf.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ <h1 class="flex justify-center">{% trans "Action plan" %}</h1>
<th class="text-md p-2 text-center">{% trans "ETA" %}</th>
<th class="text-md p-2 text-center">{% trans "Expiry date" %}</th>
<th class="text-md p-2 text-center">{% trans "Effort" %}</th>
<th class="text-md p-2 text-center">{% trans "Cost" %}</th>
<th class="text-md p-2 text-center">{% trans "Matching requirements" %}</th>
</tr>
</thead>
Expand All @@ -59,6 +60,7 @@ <h1 class="flex justify-center">{% trans "Action plan" %}</h1>
<td class="text-md p-2 text-center">{{ applied_control.eta|default:"--" }}</td>
<td class="text-md p-2 text-center">{{ applied_control.expiry_date|default:"--" }}</td>
<td class="text-md p-2 text-center">{{ applied_control.get_effort_display|default:"--" }}</td>
<td class="text-md p-2 text-center">{{ applied_control.cost|default:"--" }}</td>
<td class="text-md p-2 text-center">{% get_requirements_count applied_control compliance_assessment %}</td>
</tr>
{% empty %}
Expand Down
2 changes: 2 additions & 0 deletions backend/core/templates/snippets/mp_data.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
<td class="px-2 font-semibold">{% trans "Reference control" %}</td>
<td class="px-2 font-semibold">{% trans "ETA" %}</td>
<td class="px-2 font-semibold">{% trans "Effort" %}</td>
<td class="px-2 font-semibold">{% trans "Cost" %}</td>
<td class="px-2 font-semibold text-center">{% trans "Link" %}</td>
<td class="px-2 font-semibold text-center">{% trans "Status" %}</td>
</tr>
Expand All @@ -74,6 +75,7 @@
<td class="px-2 py-3">{% if appliedcontrol.reference_control %}{{ appliedcontrol.reference_control }}{% else %}--{% endif %}</td>
<td class="px-2 py-3">{% if appliedcontrol.eta %}{{ appliedcontrol.eta }}{% else %}--{%endif%}</td>
<td class="px-2 py-3">{% if appliedcontrol.effort %}{{ appliedcontrol.effort }}{% else %}--{%endif%}</td>
<td class="px-2 py-3">{% if appliedcontrol.cost %}{{ appliedcontrol.cost }}{% else %}--{%endif%}</td>
<td class="px-2 py-3 text-center">{% if appliedcontrol.link %}<a onclick="event.stopPropagation();" href="{{ appliedcontrol.link }}"
class="hover:text-blue-400"><i
class="fas fa-external-link-square-alt"></i></a>{% else %}--{% endif %}
Expand Down
10 changes: 8 additions & 2 deletions backend/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ def treatment_plan_csv(self, request, pk):
"reference_control",
"eta",
"effort",
"cost",
"link",
"status",
]
Expand All @@ -457,6 +458,7 @@ def treatment_plan_csv(self, request, pk):
mtg.reference_control,
mtg.eta,
mtg.effort,
mtg.cost,
mtg.link,
mtg.status,
]
Expand Down Expand Up @@ -615,6 +617,7 @@ class AppliedControlViewSet(BaseModelViewSet):
"status",
"reference_control",
"effort",
"cost",
"risk_scenarios",
"requirement_assessments",
"evidences",
Expand Down Expand Up @@ -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)) :
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)) :
Expand Down Expand Up @@ -1979,6 +1983,7 @@ def export_mp_csv(request):
"reference_control",
"eta",
"effort",
"cost",
"link",
"status",
]
Expand All @@ -2000,6 +2005,7 @@ def export_mp_csv(request):
mtg.reference_control,
mtg.eta,
mtg.effort,
mtg.cost,
mtg.link,
mtg.status,
]
Expand Down
5 changes: 5 additions & 0 deletions documentation/architecture/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ erDiagram
date expiration
url link
string effort
float cost
string[] tags
}
Expand Down Expand Up @@ -598,6 +599,7 @@ namespace DomainObjects {
+DateField expiry_date
+CharField link
+CharField effort
+Decimal cost
+RiskScenario[] risk_scenarios()
+RiskAssessments[] risk_assessments()
Expand Down Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions enterprise/frontend/src/lib/components/Forms/ModelForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,14 @@
cacheLock={cacheLocks['effort']}
bind:cachedValue={formDataCache['effort']}
/>
<Select
{form}
field="cost"
label={m.cost()}
helpText={m.costHelpText()}
cacheLock={cacheLocks['cost']}
bind:cachedValue={formDataCache['cost']}
/>
<AutocompleteSelect
{form}
options={getOptions({ objects: model.foreignKeys['folder'] })}
Expand Down
3 changes: 3 additions & 0 deletions frontend/messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
"rowCount": "عرض {start} إلى {end} من {total}",
"status": "الحالة",
"effort": "الجهد",
"cost": "يكلف",
"impact": "التأثير",
"expiryDate": "تاريخ الانتهاء",
"link": "الرابط",
Expand Down Expand Up @@ -411,6 +412,7 @@
"expiryDateHelpText": "التاريخ الذي لم يعد فيه الكائن صالحًا",
"linkHelpText": "عنوان URL خارجي لمتابعة الإجراءات (مثل تذكرة Jira)",
"effortHelpText": "الجهد المطلوب لتنفيذ التحكم المطبق",
"costHelpText": "التكلفة المطلوبة لتنفيذ الرقابة المطبقة",
"riskAcceptanceJusitficationHelpText": "تبرير قبول المخاطر. يمكن فقط للموافق تحرير هذا الحقل.",
"approverHelpText": "هوية مالك المخاطر والموافق",
"riskAcceptanceRiskScenariosHelpText": "سيناريوهات المخاطر التي تم قبولها",
Expand Down Expand Up @@ -558,6 +560,7 @@
"appliedControlNoETA": "لا يوجد وقت متوقع للوصول",
"appliedControlETAInPast": "الوقت المتوقع للوصول في الماضي. فكر في تحديث حالته أو تاريخه",
"appliedControlNoEffort": "لا يوجد جهد مقدر. سيساعدك هذا في تحديد الأولويات",
"appliedControlNoCost": "ليس لها تكلفة تقديرية. هذا سوف يساعدك على تحديد الأولويات",
"appliedControlNoLink": "التحكم المطبق لا يحتوي على رابط خارجي مرفق. سيساعدك هذا في متابعة الإجراءات",
"riskAcceptanceNoExpiryDate": "القبول لا يحتوي على تاريخ انتهاء الصلاحية",
"riskAcceptanceExpired": "القبول قد انتهت صلاحيته. فكر في تحديث حالته أو تاريخه",
Expand Down
3 changes: 3 additions & 0 deletions frontend/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
"status": "Status",
"result": "Ergebnis",
"effort": "Aufwand",
"cost": "Kosten",
"impact": "Auswirkung",
"expiryDate": "Ablaufdatum",
"link": "Link",
Expand Down Expand Up @@ -421,6 +422,7 @@
"expiryDateHelpText": "Datum, ab dem das Objekt nicht mehr gültig ist",
"linkHelpText": "Externe URL zur Aktionsverfolgung (z. B. Jira-Ticket)",
"effortHelpText": "Der Aufwand, der für die Implementierung der angewendeten Kontrolle erforderlich ist",
"costHelpText": "Die für die Implementierung der angewandten Kontrolle erforderlichen Kosten",
"riskAcceptanceJusitficationHelpText": "Begründung für die Risikoakzeptanz. Nur der Genehmiger kann dieses Feld bearbeiten.",
"approverHelpText": "Identität des Risikoeigners und Genehmigers",
"riskAcceptanceRiskScenariosHelpText": "Die akzeptierten Risikoszenarien",
Expand Down Expand Up @@ -572,6 +574,7 @@
"appliedControlNoETA": "Hat keine ETA",
"appliedControlETAInPast": "Die voraussichtliche Ankunftszeit liegt nun in der Vergangenheit. Aktualisieren Sie den Status oder das Datum.",
"appliedControlNoEffort": "Hat keinen geschätzten Aufwand. Dies hilft Ihnen bei der Priorisierung",
"appliedControlNoCost": "Es gibt keinen geschätzten Kosten. Dies hilft Ihnen bei der Priorisierung",
"appliedControlNoLink": "Angewandte Kontrolle hat keinen externen Link angehängt. Dies hilft Ihnen bei der Nachverfolgung",
"riskAcceptanceNoExpiryDate": "Die Annahme hat kein Ablaufdatum",
"riskAcceptanceExpired": "Die Annahme ist abgelaufen. Erwägen Sie eine Aktualisierung des Status oder des Datums",
Expand Down
3 changes: 3 additions & 0 deletions frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
"status": "Status",
"result": "Result",
"effort": "Effort",
"cost": "Cost",
"impact": "Impact",
"expiryDate": "Expiry date",
"link": "Link",
Expand Down Expand Up @@ -422,6 +423,7 @@
"expiryDateHelpText": "Date by which the object is no longer valid",
"linkHelpText": "External URL for action follow-up (eg. Jira ticket)",
"effortHelpText": "The effort required to implement the applied control",
"costHelpText": "The cost required to implement the applied control",
"riskAcceptanceJusitficationHelpText": "Justification for the risk acceptance. Only the approver can edit this field.",
"approverHelpText": "Risk owner and approver identity",
"riskAcceptanceRiskScenariosHelpText": "The risk scenarios that are accepted",
Expand Down Expand Up @@ -574,6 +576,7 @@
"appliedControlNoETA": "Does not have an ETA",
"appliedControlETAInPast": "ETA is in the past now. Consider updating its status or the date",
"appliedControlNoEffort": "Does not have an estimated effort. This will help you for prioritization",
"appliedControlNoCost": "Does not have an estimated cost. This will help you for prioritization",
"appliedControlNoLink": "Applied control does not have an external link attached. This will help you for follow-up",
"riskAcceptanceNoExpiryDate": "Acceptance has no expiry date",
"riskAcceptanceExpired": "Acceptance has expired. Consider updating the status or the date",
Expand Down
3 changes: 3 additions & 0 deletions frontend/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
"status": "Estado",
"result": "Resultado",
"effort": "Esfuerzo",
"cost": "Costo",
"impact": "Impacto",
"expiryDate": "Fecha de caducidad",
"link": "Enlace",
Expand Down Expand Up @@ -421,6 +422,7 @@
"expiryDateHelpText": "Fecha en la que el objeto ya no es válido",
"linkHelpText": "URL externa para el seguimiento de acciones (por ejemplo, ticket de Jira)",
"effortHelpText": "El esfuerzo necesario para implementar el control aplicado",
"costHelpText": "El costo requerido para implementar el control aplicado.",
"riskAcceptanceJusitficationHelpText": "Justificación para la aceptación de riesgos. Solo el aprobador puede editar este campo.",
"approverHelpText": "Identidad del propietario del riesgo y aprobador",
"riskAcceptanceRiskScenariosHelpText": "Los escenarios de riesgo que se aceptan",
Expand Down Expand Up @@ -572,6 +574,7 @@
"appliedControlNoETA": "No tiene ETA",
"appliedControlETAInPast": "ETA ya es cosa del pasado. Considere actualizar su estado o la fecha",
"appliedControlNoEffort": "No tiene un esfuerzo estimado. Esto le ayudará a priorizar",
"appliedControlNoCost": "No tiene un costo estimado. Esto le ayudará a priorizar",
"appliedControlNoLink": "El control aplicado no tiene enlace externo adjunto. Esto le ayudará para el seguimiento.",
"riskAcceptanceNoExpiryDate": "La aceptación no tiene fecha de caducidad.",
"riskAcceptanceExpired": "La aceptación ha caducado. Considere actualizar el estado o la fecha",
Expand Down
3 changes: 3 additions & 0 deletions frontend/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
"status": "Statut",
"result": "Résultat",
"effort": "Effort",
"cost": "Coût",
"impact": "Impact",
"expiryDate": "Date d'expiration",
"link": "Lien",
Expand Down Expand Up @@ -421,6 +422,7 @@
"expiryDateHelpText": "Date à laquelle l'objet n'est plus valide",
"linkHelpText": "URL externe pour le suivi des actions (ex. ticket Jira)",
"effortHelpText": "L'effort requis pour mettre en œuvre la mesure appliquée",
"costHelpText": "Le coût requis pour mettre en œuvre la mesure appliquée",
"riskAcceptanceJusitficationHelpText": "Justification de l'acceptation du risque. Seul l'approbateur peut modifier ce champ.",
"approverHelpText": "Identité du propriétaire du risque et de l’approbateur",
"riskAcceptanceRiskScenariosHelpText": "Les scénarios de risques acceptés",
Expand Down Expand Up @@ -572,6 +574,7 @@
"appliedControlNoETA": "N'a pas d'ETA",
"appliedControlETAInPast": "L’ETA appartient désormais au passé. Pensez à mettre à jour son statut ou la date",
"appliedControlNoEffort": "N'a pas d'effort estimé. Cela vous aidera pour la priorisation",
"appliedControlNoCost": "N'a pas de coût estimé. Cela vous aidera pour la priorisation",
"appliedControlNoLink": "La mesure appliquée n’a pas de lien externe attaché. Cela vous aidera pour le suivi",
"riskAcceptanceNoExpiryDate": "L'acceptation n'a pas de date d'expiration",
"riskAcceptanceExpired": "L'acceptation a expiré. Pensez à mettre à jour le statut ou la date",
Expand Down
Loading

0 comments on commit 8bb987f

Please sign in to comment.