diff --git a/backend/app_tests/api/test_api_compliance_assessments.py b/backend/app_tests/api/test_api_compliance_assessments.py index 0ff2003d7..543bf048e 100644 --- a/backend/app_tests/api/test_api_compliance_assessments.py +++ b/backend/app_tests/api/test_api_compliance_assessments.py @@ -122,6 +122,8 @@ def test_get_compliance_assessments(self, test): "framework": { "id": str(Framework.objects.all()[0].id), "str": str(Framework.objects.all()[0]), + "min_score": 0, + "max_score": 100 }, }, user_group=test.user_group, @@ -150,6 +152,8 @@ def test_create_compliance_assessments(self, test): "framework": { "id": str(Framework.objects.all()[0].id), "str": str(Framework.objects.all()[0]), + "min_score": Framework.objects.all()[0].min_score, + "max_score": Framework.objects.all()[0].max_score }, }, user_group=test.user_group, @@ -190,6 +194,8 @@ def test_update_compliance_assessments(self, test): "framework": { "id": str(Framework.objects.all()[0].id), "str": str(Framework.objects.all()[0]), + "min_score": Framework.objects.all()[0].min_score, + "max_score": Framework.objects.all()[0].max_score }, }, user_group=test.user_group, diff --git a/backend/core/helpers.py b/backend/core/helpers.py index 34a63f808..4f3785134 100644 --- a/backend/core/helpers.py +++ b/backend/core/helpers.py @@ -238,6 +238,8 @@ def get_sorted_requirement_nodes_rec( "name": node.name, "ra_id": str(req_as.id) if requirements_assessed else None, "status": req_as.status if requirements_assessed else None, + "score": req_as.score if requirements_assessed else None, + "max_score": req_as.compliance_assessment.framework.max_score if requirements_assessed else None, "status_display": req_as.get_status_display() if requirements_assessed else None, "status_i18n": camel_case(req_as.status) if requirements_assessed else None, "node_content": node.display_long, @@ -266,6 +268,8 @@ def get_sorted_requirement_nodes_rec( "description": req.description, "ra_id": str(req_as.id), "status": req_as.status, + "score": req_as.score, + "max_score": req_as.compliance_assessment.framework.max_score, "status_display": req_as.get_status_display(), "status_i18n": camel_case(req_as.status), "style": "leaf", diff --git a/backend/core/migrations/0009_framework_max_score_framework_min_score_and_more.py b/backend/core/migrations/0009_framework_max_score_framework_min_score_and_more.py new file mode 100644 index 000000000..a605812dc --- /dev/null +++ b/backend/core/migrations/0009_framework_max_score_framework_min_score_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.0.4 on 2024-04-16 14:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0008_alter_complianceassessment_status_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='framework', + name='max_score', + field=models.IntegerField(default=100, verbose_name='Maximum score'), + ), + migrations.AddField( + model_name='framework', + name='min_score', + field=models.IntegerField(default=0, verbose_name='Minimum score'), + ), + migrations.AddField( + model_name='requirementassessment', + name='score', + field=models.IntegerField(blank=True, null=True, verbose_name='Score'), + ), + ] diff --git a/backend/core/models.py b/backend/core/models.py index 04762a1ad..4cd054f10 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -328,6 +328,12 @@ def __str__(self) -> str: class Framework(ReferentialObjectMixin): + min_score = models.IntegerField( + default=0, verbose_name=_("Minimum score") + ) + max_score = models.IntegerField( + default=100, verbose_name=_("Maximum score") + ) library = models.ForeignKey( Library, on_delete=models.CASCADE, @@ -1288,6 +1294,11 @@ class Result(models.TextChoices): class Meta: verbose_name = _("Compliance assessment") verbose_name_plural = _("Compliance assessments") + + def get_global_score(self): + requirement_assessments_scored = RequirementAssessment.objects.filter(compliance_assessment=self).exclude(score=None) + score = requirement_assessments_scored.aggregate(models.Avg('score')) + return score['score__avg'] or -1 def get_requirements_status_count(self): requirements_status_count = [] @@ -1465,6 +1476,7 @@ def quality_check(self) -> dict: class RequirementAssessment(AbstractBaseModel, FolderMixin): + class Status(models.TextChoices): TODO = "to_do", _("To do") IN_PROGRESS = "in_progress", _("In progress") @@ -1473,12 +1485,18 @@ class Status(models.TextChoices): COMPLIANT = "compliant", _("Compliant") NOT_APPLICABLE = "not_applicable", _("Not applicable") + status = models.CharField( max_length=100, choices=Status.choices, default=Status.TODO, verbose_name=_("Status"), ) + score = models.IntegerField( + blank=True, + null=True, + verbose_name=_("Score"), + ) evidences = models.ManyToManyField( Evidence, blank=True, @@ -1501,6 +1519,13 @@ class Status(models.TextChoices): verbose_name=_("Applied controls"), related_name="requirement_assessments", ) + + def get_score_choices(self): + return [(i, i) for i in range(self.compliance_assessment.framework.min_score, self.compliance_assessment.framework.max_score + 1)] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._meta.get_field('score').choices = self.get_score_choices() def __str__(self) -> str: return self.requirement.display_short diff --git a/backend/core/serializers.py b/backend/core/serializers.py index 805eab409..863dab9df 100644 --- a/backend/core/serializers.py +++ b/backend/core/serializers.py @@ -476,7 +476,7 @@ class Meta: class ComplianceAssessmentReadSerializer(AssessmentReadSerializer): - framework = FieldsRelatedField() + framework = FieldsRelatedField(["id", "min_score", "max_score"]) class Meta: model = ComplianceAssessment diff --git a/backend/core/views.py b/backend/core/views.py index b53230040..b49a85c90 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -1223,6 +1223,12 @@ def quality_check(self, request): for a in compliance_assessments ] return Response({"results": res}) + + @action(detail=True, methods=["get"]) + def global_score(self, request, pk): + """Returns the global score of the compliance assessment""" + return Response({"score": self.get_object().get_global_score(), + "max_score": self.get_object().framework.max_score}) @action(detail=True, methods=["get"], url_path="quality_check") def quality_check_detail(self, request, pk): diff --git a/backend/library/utils.py b/backend/library/utils.py index b06b74878..23cef7234 100644 --- a/backend/library/utils.py +++ b/backend/library/utils.py @@ -290,6 +290,8 @@ def import_framework(self, library_object: Library): ref_id=self.framework_data["ref_id"], name=self.framework_data.get("name"), description=self.framework_data.get("description"), + min_score=self.framework_data.get("min_score", 0), + max_score=self.framework_data.get("max_score", 100), provider=library_object.provider, locale=library_object.locale, default_locale=library_object.default_locale, # Change this in the future ? diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 416d05a1c..a6c0c89cb 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -501,6 +501,9 @@ "missingMandatoyObjects2": "Please add them before proceeding", "attemptToDeleteOnlyAdminAccountError": "You can't delete the only admin account of your application.", "attemptToRemoveOnlyAdminUserGroup": "You can't remove the only admin user of the application from the admin user group.", + "scoringHelpText": "Check to enable scoring", + "minScore": "Minimum score", + "maxScore": "Maximum score", "setTemporaryPassword1": "In case the user cannot set their own password, you can", "setTemporaryPassword": "set a temporary password", "setTemporaryPassword2": "Please use a strong one and make sure to inform the user to change it as soon as possible", diff --git a/frontend/messages/fr.json b/frontend/messages/fr.json index 1d605e939..e116b72f3 100644 --- a/frontend/messages/fr.json +++ b/frontend/messages/fr.json @@ -501,6 +501,9 @@ "missingMandatoyObjects2": "Veuillez les ajouter avant de continuer", "attemptToDeleteOnlyAdminAccountError": "Vous ne pouvez pas supprimer votre unique compte administrateur de l'application.", "attemptToRemoveOnlyAdminUserGroup": "Vous ne pouvez pas retirer le seul compte administrateur de l'application du groupe des administrateurs.", + "scoringHelpText": "Cocher pour activer le score", + "minScore": "Score minimum", + "maxScore": "Score maximum", "setTemporaryPassword1": "Si l'utilisateur ne peut pas définir son propre mot de passe, vous pouvez", "setTemporaryPassword": "définir un mot de passe temporaire", "setTemporaryPassword2": "Veuillez en utiliser un solide et assurez-vous d'informer l'utilisateur de le modifier dès que possible.", diff --git a/frontend/src/lib/components/Forms/Score.svelte b/frontend/src/lib/components/Forms/Score.svelte new file mode 100644 index 000000000..ce300b529 --- /dev/null +++ b/frontend/src/lib/components/Forms/Score.svelte @@ -0,0 +1,77 @@ + + +
{error}
+ {/each} +{m.scoringHelpText()}
+