Skip to content

Commit

Permalink
Allow audit creation when creating entity assessment
Browse files Browse the repository at this point in the history
  • Loading branch information
nas-tabchiche committed Sep 6, 2024
1 parent 14b3179 commit 019b258
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 72 deletions.
35 changes: 0 additions & 35 deletions backend/core/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1154,39 +1154,4 @@ def handle(exc, context):
return drf_exception_handler(exc, context)


def transform_question_to_answer(json_data):
"""
Used during Requirement Assessment creation to create a questionnaire base on
the Requirement Node question JSON field
Args:
json_data (json): JSON describing a questionnaire from a Requirement Node
Returns:
json: JSON formatted for the frontend to display a form
"""
question_type = json_data.get("question_type", "")
question_choices = json_data.get("question_choices", [])
questions = json_data.get("questions", [])

form_fields = []

for question in questions:
field = {}
field["urn"] = question.get("urn", "")
field["text"] = question.get("text", "")

if question_type == "unique_choice":
field["type"] = "unique_choice"
field["options"] = question_choices
elif question_type == "date":
field["type"] = "date"
else:
field["type"] = "text"

field["answer"] = ""

form_fields.append(field)

form_json = {"questions": form_fields}
return form_json
73 changes: 72 additions & 1 deletion backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from django.utils.html import format_html
from django.utils.translation import get_language
from django.utils.translation import gettext_lazy as _
from iam.models import FolderMixin, PublishInRootFolderMixin
from iam.models import Folder, FolderMixin, PublishInRootFolderMixin
from library.helpers import update_translations, update_translations_in_object
from structlog import get_logger

Expand Down Expand Up @@ -937,6 +937,45 @@ class Meta:
verbose_name = _("RequirementNode")
verbose_name_plural = _("RequirementNodes")

def format_answer(self) -> dict:
"""
Used during Requirement Assessment creation to create a questionnaire based on
the Requirement Node question JSON field
Args:
json_data (json): JSON describing a questionnaire from a Requirement Node
Returns:
json: JSON formatted for the frontend to display a form
"""
if not self.question:
return {}
json_data = self.question
_type = json_data.get("question_type", "")
choices = json_data.get("question_choices", [])
questions = json_data.get("questions", [])

form_fields = []

for question in questions:
field = {}
field["urn"] = question.get("urn", "")
field["text"] = question.get("text", "")

if _type == "unique_choice":
field["type"] = "unique_choice"
field["options"] = choices
elif _type == "date":
field["type"] = "date"
else:
field["type"] = "text"

field["answer"] = ""

form_fields.append(field)

return {"questions": form_fields}


class RequirementMappingSet(ReferentialObjectMixin):
library = models.ForeignKey(
Expand Down Expand Up @@ -2001,6 +2040,38 @@ def save(self, *args, **kwargs) -> None:
self.scores_definition = self.framework.scores_definition
super().save(*args, **kwargs)

def create_requirement_assessments(self, baseline: Self | None = None):
requirements = RequirementNode.objects.filter(framework=self.framework)
requirement_assessments = []
for requirement in requirements:
requirement_assessment = RequirementAssessment.objects.create(
compliance_assessment=self,
requirement=requirement,
folder=Folder.objects.get(id=self.project.folder.id),
answer=transform_question_to_answer(requirement.question)
if requirement.question
else {},
)
if baseline and baseline.framework == self.framework:
baseline_requirement_assessment = RequirementAssessment.objects.get(
compliance_assessment=baseline, requirement=requirement
)
requirement_assessment.result = baseline_requirement_assessment.result
requirement_assessment.status = baseline_requirement_assessment.status
requirement_assessment.score = baseline_requirement_assessment.score
requirement_assessment.is_scored = (
baseline_requirement_assessment.is_scored
)
requirement_assessment.evidences.set(
baseline_requirement_assessment.evidences.all()
)
requirement_assessment.applied_controls.set(
baseline_requirement_assessment.applied_controls.all()
)
requirement_assessment.save()
requirement_assessments.append(requirement_assessment)
return requirement_assessments

def get_global_score(self):
requirement_assessments_scored = (
RequirementAssessment.objects.filter(compliance_assessment=self)
Expand Down
30 changes: 2 additions & 28 deletions backend/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1482,34 +1482,8 @@ def perform_create(self, serializer):
Create RequirementAssessment objects for the newly created ComplianceAssessment
"""
baseline = serializer.validated_data.pop("baseline", None)
instance = serializer.save()
requirements = RequirementNode.objects.filter(framework=instance.framework)
for requirement in requirements:
requirement_assessment = RequirementAssessment.objects.create(
compliance_assessment=instance,
requirement=requirement,
folder=Folder.objects.get(id=instance.project.folder.id),
answer=transform_question_to_answer(requirement.question)
if requirement.question
else {},
)
if baseline and baseline.framework == instance.framework:
baseline_requirement_assessment = RequirementAssessment.objects.get(
compliance_assessment=baseline, requirement=requirement
)
requirement_assessment.result = baseline_requirement_assessment.result
requirement_assessment.status = baseline_requirement_assessment.status
requirement_assessment.score = baseline_requirement_assessment.score
requirement_assessment.is_scored = (
baseline_requirement_assessment.is_scored
)
requirement_assessment.evidences.set(
baseline_requirement_assessment.evidences.all()
)
requirement_assessment.applied_controls.set(
baseline_requirement_assessment.applied_controls.all()
)
requirement_assessment.save()
instance: ComplianceAssessment = serializer.save()
instance.create_requirement_assessments(baseline)
if baseline and baseline.framework != instance.framework:
mapping_set = RequirementMappingSet.objects.get(
target_framework=serializer.validated_data["framework"],
Expand Down
43 changes: 39 additions & 4 deletions backend/tprm/serializers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from rest_framework import serializers
from core.models import ComplianceAssessment, Framework

from core.serializer_fields import FieldsRelatedField
from core.serializers import BaseModelSerializer
from iam.models import Folder
from tprm.models import Entity, Representative, Solution, EntityAssessment
from rest_framework import serializers
from django.utils.translation import gettext_lazy as _
from tprm.models import Entity, EntityAssessment, Representative, Solution


class EntityReadSerializer(BaseModelSerializer):
Expand Down Expand Up @@ -38,6 +38,41 @@ class Meta:
exclude = []


class EntityAssessmentCreateSerializer(BaseModelSerializer):
create_audit = serializers.BooleanField(default=True)
framework = serializers.PrimaryKeyRelatedField(
queryset=Framework.objects.all(), required=False
)
selected_implementation_groups = serializers.ListField(
child=serializers.CharField(), required=False
)

def create(self, validated_data):
create_audit = validated_data.pop("create_audit")
_framework = validated_data.pop("framework", None)
_selected_implementation_groups = validated_data.pop(
"selected_implementation_groups", None
)
instance = super().create(validated_data)
if create_audit:
if not _framework:
raise serializers.ValidationError("frameworkRequiredToCreateAudit")
audit = ComplianceAssessment.objects.create(
name=validated_data["name"],
framework=_framework,
project=validated_data["project"],
selected_implementation_groups=_selected_implementation_groups,
)
audit.create_requirement_assessments()
instance.compliance_assessment = audit
instance.save()
return instance

class Meta:
model = EntityAssessment
exclude = []


class RepresentativeReadSerializer(BaseModelSerializer):
entity = FieldsRelatedField()

Expand Down
7 changes: 7 additions & 0 deletions backend/tprm/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from tprm.models import Entity, Representative, Solution, EntityAssessment
from rest_framework.decorators import action

from tprm.serializers import EntityAssessmentCreateSerializer


class BaseModelViewSet(AbstractBaseModelViewSet):
serializers_module = "tprm.serializers"
Expand All @@ -25,6 +27,11 @@ class EntityAssessmentViewSet(BaseModelViewSet):
model = EntityAssessment
filterset_fields = ["status", "project", "project__folder", "authors", "entity"]

def get_serializer_class(self, **kwargs):
if self.action == "create":
return EntityAssessmentCreateSerializer
return super().get_serializer_class(**kwargs)

@action(detail=False, name="Get status choices")
def status(self, request):
return Response(dict(EntityAssessment.Status.choices))
Expand Down
36 changes: 36 additions & 0 deletions frontend/src/lib/components/Forms/ModelForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,42 @@
hide={initialData.folder}
/>
{:else if URLModel === 'entity-assessments'}
{#if context === 'create'}
<Checkbox {form} field="create_audit" label="_createAudit" />
<AutocompleteSelect
{form}
disabled={object.id}
options={getOptions({ objects: model.foreignKeys['framework'] })}
field="framework"
cacheLock={cacheLocks['framework']}
bind:cachedValue={formDataCache['framework']}
label={m.framework()}
on:change={async (e) => {
if (e.detail) {
await fetch(`/frameworks/${e.detail}`)
.then((r) => r.json())
.then((r) => {
const implementation_groups = r['implementation_groups_definition'] || [];
model.selectOptions['selected_implementation_groups'] = implementation_groups.map(
(group) => ({ label: group.name, value: group.ref_id })
);
});
}
}}
/>
{#if model.selectOptions['selected_implementation_groups'] && model.selectOptions['selected_implementation_groups'].length}
<AutocompleteSelect
multiple
translateOptions={false}
{form}
options={model.selectOptions['selected_implementation_groups']}
field="selected_implementation_groups"
cacheLock={cacheLocks['selected_implementation_groups']}
bind:cachedValue={formDataCache['selected_implementation_groups']}
label={m.selectedImplementationGroups()}
/>
{/if}
{/if}
<AutocompleteSelect
{form}
options={getOptions({ objects: model.foreignKeys['project'] })}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/components/Modals/CreateModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
{parent}
{model}
{closeModal}
{context}
context="create"
{riskAssessmentDuplication}
caching={true}
action="?/{formAction}"
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/lib/utils/crud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,12 +475,13 @@ export const URL_MODEL_MAP: ModelMap = {
foreignKeyFields: [
{ field: 'project', urlModel: 'projects' },
{ field: 'entity', urlModel: 'entities' },
{ field: 'framework', urlModel: 'frameworks' },
{ field: 'authors', urlModel: 'users' },
{ field: 'reviewers', urlModel: 'users' },
{ field: 'evidence', urlModel: 'evidences' },
{ field: 'compliance_assessment', urlModel: 'compliance-assessments' }
],
selectFields: [{ field: 'status' }],
selectFields: [{ field: 'status' }, { field: 'selected_implementation_groups', detail: true }],
filters: [{ field: 'status' }]
},
solutions: {
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/lib/utils/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,10 @@ export const EntitiesSchema = baseNamedObject({
reference_link: z.string().optional()
});

export const entityAssessmentsSchema = baseNamedObject({
export const EntityAssessmentSchema = baseNamedObject({
create_audit: z.boolean().optional().default(true),
framework: z.string().optional(),
selected_implementation_groups: z.array(z.string().optional()).optional(),
version: z.string().optional().default('0.1'),
project: z.string(),
status: z.string().optional().nullable(),
Expand Down Expand Up @@ -333,7 +336,7 @@ const SCHEMA_MAP: Record<string, AnyZodObject> = {
users: UserCreateSchema,
'sso-settings': SSOSettingsSchema,
entities: EntitiesSchema,
'entity-assessments': entityAssessmentsSchema,
'entity-assessments': EntityAssessmentSchema,
representatives: representativeSchema,
solutions: solutionSchema
};
Expand Down

0 comments on commit 019b258

Please sign in to comment.