Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/flash mode #481

Merged
merged 16 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1451,7 +1451,25 @@ def get_selected_implementation_groups(self):
]

def get_requirement_assessments(self):
return RequirementAssessment.objects.filter(compliance_assessment=self)
"""
Returns sorted assessable requirement assessments based on the selected implementation groups
"""
if not self.selected_implementation_groups:
return RequirementAssessment.objects.filter(
compliance_assessment=self, requirement__assessable=True
).order_by("requirement__order_id")
selected_implementation_groups_set = set(self.selected_implementation_groups)
filtered_requirements = RequirementAssessment.objects.filter(
compliance_assessment=self, requirement__assessable=True
).order_by("requirement__order_id")
requirement_assessments_list = []
for requirement in filtered_requirements:
if selected_implementation_groups_set & set(
requirement.requirement.implementation_groups
):
requirement_assessments_list.append(requirement)

return requirement_assessments_list

def get_requirements_status_count(self):
requirements_status_count = []
Expand Down Expand Up @@ -1692,6 +1710,9 @@ class Status(models.TextChoices):
def __str__(self) -> str:
return self.requirement.display_short

def get_requirement_description(self) -> str:
return self.requirement.description

class Meta:
verbose_name = _("Requirement assessment")
verbose_name_plural = _("Requirement assessments")
Expand Down
1 change: 1 addition & 0 deletions backend/core/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@ class Meta:

class RequirementAssessmentReadSerializer(BaseModelSerializer):
name = serializers.CharField(source="__str__")
description = serializers.CharField(source="get_requirement_description")
compliance_assessment = FieldsRelatedField()
folder = FieldsRelatedField()

Expand Down
21 changes: 21 additions & 0 deletions backend/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,27 @@ def tree(self, request, pk):
filter_graph_by_implementation_groups(tree, implementation_groups)
)

@action(detail=True, methods=["get"])
def flash_mode(self, request, pk):
"""Returns the list of requirement assessments for flash mode"""
requirement_assessments_objects = (
self.get_object().get_requirement_assessments()
)
requirements_objects = RequirementNode.objects.filter(
framework=self.get_object().framework
)
requirement_assessments = RequirementAssessmentReadSerializer(
requirement_assessments_objects, many=True
).data
requirements = RequirementNodeReadSerializer(
requirements_objects, many=True
).data
flash_mode = {
"requirements": requirements,
"requirement_assessments": requirement_assessments,
}
return Response(flash_mode, status=status.HTTP_200_OK)

@action(detail=True)
def export(self, request, pk):
(object_ids_view, _, _) = RoleAssignment.get_accessible_object_ids(
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@
"asZIP": "als ZIP",
"incoming": "Eingehend",
"outdated": "Veraltet",
"goBackToAudit": "Zurück zum Audit",
"exportBackupDescription": "Dies wird die Datenbank serialisieren und ein Backup erstellen, einschließlich Benutzer und RBAC. Beweise und andere Dateien sind im Backup nicht enthalten.",
"importBackupDescription": "Dies wird die Datenbank aus einem Backup deserialisieren und wiederherstellen. Dies wird alle vorhandenen Daten, einschließlich Benutzer und RBAC, überschreiben und kann nicht rückgängig gemacht werden."
}
2 changes: 2 additions & 0 deletions frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,8 @@
"asZIP": "as ZIP",
"incoming": "Incoming",
"outdated": "Outdated",
"flashMode": "Flash mode",
"goBackToAudit": "Go back to the audit",
"exportBackupDescription": "This will serialize and create a backup of the database, including users and RBAC. Evidences and other files are not included in the backup.",
"importBackupDescription": "This will deserialize and restore the database from a backup. This will overwrite all existing data, including users and RBAC and cannot be undone."
}
5 changes: 4 additions & 1 deletion frontend/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -528,5 +528,8 @@
"matchingRequirements": "Requisitos coincidentes",
"asZIP": "como ZIP",
"incoming": "Entrante",
"outdated": "Desactualizado"
"outdated": "Desactualizado",
"goBackToAudit": "Volver a la auditoría",
"exportBackupDescription": "Esto serializará y creará una copia de seguridad de la base de datos, incluidos los usuarios y RBAC. Las pruebas y otros archivos no se incluyen en la copia de seguridad.",
"importBackupDescription": "Esto deserializará y restaurará la base de datos desde una copia de seguridad. Esto sobrescribirá todos los datos existentes, incluidos los usuarios y RBAC, y no se puede deshacer."
}
1 change: 1 addition & 0 deletions frontend/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@
"asZIP": "en ZIP",
"incoming": "En approche",
"outdated": "Dépassé",
"goBackToAudit": "Retour à l'audit",
"exportBackupDescription": "Cela va sérialiser et créer une sauvegarde de la base de données, y compris les utilisateurs et RBAC. Les preuves et autres fichiers ne sont pas inclus dans la sauvegarde.",
"importBackupDescription": "Cela va désérialiser et restaurer la base de données à partir d'une sauvegarde. Cela va écraser toutes les données existantes, y compris les utilisateurs et RBAC. Cette action est irréversible."
}
1 change: 1 addition & 0 deletions frontend/messages/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@
"asZIP": "come ZIP",
"incoming": "In arrivo",
"outdated": "Obsoleto",
"goBackToAudit": "Torniamo all'audit",
"exportBackupDescription": "Questo serializzerà e creerà un backup del database, inclusi utenti e RBAC. Le prove e altri file non sono inclusi nel backup.",
"importBackupDescription": "Questo deserializzerà e ripristinerà il database da un backup. Questo sovrascriverà tutti i dati esistenti, inclusi utenti e RBAC, e non può essere annullato."
}
1 change: 1 addition & 0 deletions frontend/messages/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@
"asZIP": "als ZIP",
"incoming": "Binnenkomend",
"outdated": "Verouderd",
"goBackToAudit": "Ga terug naar de controle",
"exportBackupDescription": "Dit zal de database serialiseren en een back-up maken, inclusief gebruikers en RBAC. Bewijzen en andere bestanden zijn niet inbegrepen in de back-up.",
"importBackupDescription": "Dit zal de database deserialiseren en herstellen vanaf een back-up. Dit zal alle bestaande gegevens, inclusief gebruikers en RBAC, overschrijven en kan niet ongedaan worden gemaakt."
}
1 change: 1 addition & 0 deletions frontend/messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@
"asZIP": "em ZIP",
"incoming": "aproximação",
"outdated": "Desatualizado",
"goBackToAudit": "Volte para a auditoria",
"exportBackupDescription": "Isso irá serializar e criar um backup do banco de dados, incluindo usuários e RBAC. Evidências e outros arquivos não estão incluídos no backup.",
"importBackupDescription": "Isso irá desserializar e restaurar o banco de dados a partir de um backup. Isso substituirá todos os dados existentes, incluindo usuários e RBAC, e não poderá ser desfeito."
}
3 changes: 2 additions & 1 deletion frontend/src/lib/utils/locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,8 @@ export function localItems(languageTag: string): LocalItems {
remediationPlan: m.remediationPlan({ languageTag: languageTag }),
incoming: m.incoming({ languageTag: languageTag }),
today: m.today({ languageTag: languageTag }),
outdated: m.outdated({ languageTag: languageTag })
outdated: m.outdated({ languageTag: languageTag }),
flashMode: m.flashMode({ languageTag: languageTag })
};
return LOCAL_ITEMS;
}
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@
data-popup="popupDownload"
>
<p class="block px-4 py-2 text-sm text-gray-800">{m.complianceAssessment()}</p>

<a
href="/compliance-assessments/{data.compliance_assessment.id}/export"
class="block px-4 py-2 text-sm text-gray-800 hover:bg-gray-200">... {m.asZIP()}</a
Expand All @@ -219,6 +220,9 @@
<a href={`${$page.url.pathname}/action-plan`} class="btn variant-filled-primary h-fit"
><i class="fa-solid fa-heart-pulse mr-2" />{m.actionPlan()}</a
>
<a href={`${$page.url.pathname}/flash-mode`} class="btn variant-filled-surface h-fit"
><i class="fa-solid fa-forward-fast mr-2" /> {m.flashMode()}</a
>
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { BASE_API_URL } from '$lib/utils/constants';
import type { PageServerLoad } from './$types';
import type { Actions } from '@sveltejs/kit';

export const load = (async ({ fetch, params }) => {
const URLModel = 'compliance-assessments';
const endpoint = `${BASE_API_URL}/${URLModel}/${params.id}/`;

const res = await fetch(endpoint);
const compliance_assessment = await res.json();

const flashMode = await fetch(`${endpoint}flash_mode/`).then((res) => res.json());

const requirement_assessments = flashMode.requirement_assessments;
const requirements = flashMode.requirements;

return {
URLModel,
compliance_assessment,
requirement_assessments,
requirements
};
}) satisfies PageServerLoad;

export const actions: Actions = {
updateRequirementAssessment: async (event) => {
const formData = await event.request.formData();
const values: { id: string; status: string } = { id: '', status: '' };
for (const entry of formData.entries()) {
values[entry[0]] = entry[1];
}
const URLModel = 'requirement-assessments';
const endpoint = `${BASE_API_URL}/${URLModel}/${values.id}/`;

const requestInitOptions: RequestInit = {
method: 'PATCH',
body: JSON.stringify(values)
};

await event.fetch(endpoint, requestInitOptions);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<script lang="ts">
import type { PageData } from './$types';
import { RadioGroup, RadioItem } from '@skeletonlabs/skeleton';
import * as m from '$paraglide/messages';
import { breadcrumbObject } from '$lib/utils/stores';
import { COMPLIANCE_COLOR_MAP } from '$lib/utils/constants';
import { getRequirementTitle } from '$lib/utils/helpers';

export let data: PageData;

breadcrumbObject.set(data.compliance_assessment);

let possible_options = [
{ id: 'to_do', label: m.toDo() },
{ id: 'in_progress', label: m.inProgress() },
{ id: 'non_compliant', label: m.nonCompliant() },
{ id: 'partially_compliant', label: m.partiallyCompliant() },
{ id: 'compliant', label: m.compliant() },
{ id: 'not_applicable', label: m.notApplicable() }
];

// Reactive variable to keep track of the current item index
let currentIndex = 0;

$: color = COMPLIANCE_COLOR_MAP[data.requirement_assessments[currentIndex].status];

$: requirement = data.requirements.find(
(req) => req.id === data.requirement_assessments[currentIndex].requirement
);
$: parent = data.requirements.find((req) => req.urn === requirement.parent_urn);

$: title = requirement.display_short
? requirement.display_short
: parent.display_short
? parent.display_short
: parent.description;

// Function to handle the "Next" button click
function nextItem() {
if (currentIndex < data.requirement_assessments.length - 1) {
currentIndex += 1;
} else {
currentIndex = 0;
}
}

// Function to handle the "Back" button click
function previousItem() {
if (currentIndex > 0) {
currentIndex -= 1;
} else {
currentIndex = data.requirement_assessments.length - 1;
}
}

// Function to update the status of the current item
function updateStatus(event) {
data.requirement_assessments[currentIndex].status = event.target.value;
const form = document.getElementById('flashModeForm');
const formData = new FormData(form);
fetch(form.action, {
method: 'POST',
body: formData
});
}
</script>

<div class="flex flex-col h-full justify-center items-center">
<div
style="border-color: {color}"
class="flex flex-col bg-white w-3/4 h-3/4 rounded-xl shadow-xl p-4 border-4"
>
{#if data.requirement_assessments[currentIndex]}
<div class="flex flex-col w-full h-full space-y-4">
<div class="flex justify-between h-1/6">
<div class="">
<a
href="/compliance-assessments/{data.compliance_assessment.id}"
class="flex items-center space-x-2 text-primary-800 hover:text-primary-600"
>
<i class="fa-solid fa-arrow-left" />
<p class="">{m.goBackToAudit()}</p>
</a>
</div>
<div class="font-semibold">{currentIndex + 1}/{data.requirement_assessments.length}</div>
</div>
<div class="flex flex-col h-1/2 items-center text-center justify-center">
<p class="font-semibold">{title}</p>
{#if data.requirement_assessments[currentIndex].description}
{data.requirement_assessments[currentIndex].description}
{/if}
</div>
<div class="items-center">
<div class="">
<h3 class="mb-4 font-semibold text-gray-900 dark:text-white">{m.status()}</h3>
<form id="flashModeForm" action="?/updateRequirementAssessment" method="post">
<ul
class="items-center w-full text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg sm:flex dark:bg-gray-700 dark:border-gray-600 dark:text-white"
>
<input hidden name="id" value={data.requirement_assessments[currentIndex].id} />
{#each possible_options as option}
<li
class="w-full border-b border-gray-200 sm:border-b-0 sm:border-r dark:border-gray-600"
>
<div class="flex items-center ps-3">
<input
id={option.id}
type="radio"
value={option.id}
name="status"
checked={option.id === data.requirement_assessments[currentIndex].status}
on:change={updateStatus}
class="w-4 h-4 text-primary-500 bg-gray-100 border-gray-300 focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-700 dark:focus:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"
/>
<label
for={option.id}
class="w-full py-3 ms-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>{option.label}
</label>
</div>
</li>
{/each}
</ul>
</form>
</div>
</div>
</div>
<div class="flex justify-between">
<button class="bg-gray-400 text-white px-4 py-2 rounded" on:click={previousItem}>
{m.previous()}
</button>
<button class="variant-filled-primary px-4 py-2 rounded" on:click={nextItem}>
{m.next()}
</button>
</div>
{/if}
</div>
</div>
Loading