Skip to content

Commit

Permalink
Merge pull request #481 from intuitem/feat/flash_mode
Browse files Browse the repository at this point in the history
Feat/flash mode
  • Loading branch information
ab-smith committed May 24, 2024
2 parents e5853da + 5ba31b9 commit bf11daf
Show file tree
Hide file tree
Showing 14 changed files with 238 additions and 2 deletions.
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.",
"requirementAppliedControlHelpText": "Mit den ausgewählten Maßnahmen verknüpfte Nachweise werden automatisch der Anforderung zugeordnet.",
Expand Down
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.",
"requirementAppliedControlHelpText": "Evidences linked to the selected measures will be automatically associated with the requirement.",
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@
"asZIP": "como ZIP",
"incoming": "Entrante",
"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.",
"requirementAppliedControlHelpText": "Las evidencias vinculadas a las medidas seleccionadas se asociarán automáticamente al requisito.",
Expand Down
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.",
"requirementAppliedControlHelpText": "Les preuves liées aux mesures sélectionnées seront automatiquement associées à l'exigence.",
Expand Down
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.",
"requirementAppliedControlHelpText": "Le evidenze legate alle misure selezionate verranno automaticamente associate al requisito.",
Expand Down
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.",
"requirementAppliedControlHelpText": "Bewijsstukken die verband houden met de geselecteerde maatregelen worden automatisch aan de eis gekoppeld.",
Expand Down
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.",
"requirementAppliedControlHelpText": "As evidências vinculadas às medidas selecionadas serão automaticamente associadas ao requisito.",
Expand Down
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>

0 comments on commit bf11daf

Please sign in to comment.