diff --git a/backend/core/migrations/0046_riskassessment_ebios_rm_study.py b/backend/core/migrations/0046_riskassessment_ebios_rm_study.py
new file mode 100644
index 000000000..36cc5a92c
--- /dev/null
+++ b/backend/core/migrations/0046_riskassessment_ebios_rm_study.py
@@ -0,0 +1,26 @@
+# Generated by Django 5.1.4 on 2024-12-11 11:07
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("core", "0045_alter_appliedcontrol_category_and_more"),
+ ("ebios_rm", "0003_remove_ebiosrmstudy_risk_assessments"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="riskassessment",
+ name="ebios_rm_study",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="risk_assessments",
+ to="ebios_rm.ebiosrmstudy",
+ verbose_name="EBIOS RM study",
+ ),
+ ),
+ ]
diff --git a/backend/core/models.py b/backend/core/models.py
index d201af8b3..cc08c0d35 100644
--- a/backend/core/models.py
+++ b/backend/core/models.py
@@ -1997,6 +1997,14 @@ class RiskAssessment(Assessment):
ref_id = models.CharField(
max_length=100, null=True, blank=True, verbose_name=_("reference id")
)
+ ebios_rm_study = models.ForeignKey(
+ "ebios_rm.EbiosRMStudy",
+ verbose_name=_("EBIOS RM study"),
+ on_delete=models.CASCADE,
+ null=True,
+ blank=True,
+ related_name="risk_assessments",
+ )
class Meta:
verbose_name = _("Risk assessment")
diff --git a/backend/core/serializers.py b/backend/core/serializers.py
index e55df2c53..0e3c95ae7 100644
--- a/backend/core/serializers.py
+++ b/backend/core/serializers.py
@@ -212,7 +212,7 @@ class RiskAssessmentReadSerializer(AssessmentReadSerializer):
class Meta:
model = RiskAssessment
- fields = "__all__"
+ exclude = ["ebios_rm_study"]
class AssetWriteSerializer(BaseModelSerializer):
diff --git a/backend/core/views.py b/backend/core/views.py
index ad510618f..562e5f3ba 100644
--- a/backend/core/views.py
+++ b/backend/core/views.py
@@ -575,6 +575,7 @@ class RiskAssessmentViewSet(BaseModelViewSet):
"authors",
"risk_matrix",
"status",
+ "ebios_rm_study",
]
@action(detail=False, name="Risk assessments per status")
diff --git a/backend/ebios_rm/migrations/0003_remove_ebiosrmstudy_risk_assessments.py b/backend/ebios_rm/migrations/0003_remove_ebiosrmstudy_risk_assessments.py
new file mode 100644
index 000000000..d02c92ad2
--- /dev/null
+++ b/backend/ebios_rm/migrations/0003_remove_ebiosrmstudy_risk_assessments.py
@@ -0,0 +1,16 @@
+# Generated by Django 5.1.4 on 2024-12-11 11:07
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("ebios_rm", "0002_alter_roto_target_objective"),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name="ebiosrmstudy",
+ name="risk_assessments",
+ ),
+ ]
diff --git a/backend/ebios_rm/models.py b/backend/ebios_rm/models.py
index 7b8853494..83ad19ea1 100644
--- a/backend/ebios_rm/models.py
+++ b/backend/ebios_rm/models.py
@@ -7,7 +7,6 @@
Asset,
ComplianceAssessment,
Qualification,
- RiskAssessment,
RiskMatrix,
Threat,
)
@@ -49,13 +48,6 @@ class Status(models.TextChoices):
"Compliance assessments established as security baseline during workshop 1.4"
),
)
- risk_assessments = models.ManyToManyField(
- RiskAssessment,
- blank=True,
- verbose_name=_("Risk assessments"),
- related_name="ebios_rm_studies",
- help_text=_("Risk assessments generated at the end of workshop 4"),
- )
reference_entity = models.ForeignKey(
Entity,
on_delete=models.PROTECT,
diff --git a/backend/ebios_rm/views.py b/backend/ebios_rm/views.py
index bf5ec10b4..71ddb5d7d 100644
--- a/backend/ebios_rm/views.py
+++ b/backend/ebios_rm/views.py
@@ -33,6 +33,11 @@ class EbiosRMStudyViewSet(BaseModelViewSet):
def status(self, request):
return Response(dict(EbiosRMStudy.Status.choices))
+ @action(detail=True, name="Get risk matrix", url_path="risk-matrix")
+ def risk_matrix(self, request, pk=None):
+ ebios_rm_study = self.get_object()
+ return Response(RiskMatrixReadSerializer(ebios_rm_study.risk_matrix).data)
+
@method_decorator(cache_page(60 * LONG_CACHE_TTL))
@action(detail=True, name="Get gravity choices")
def gravity(self, request, pk):
diff --git a/frontend/src/lib/components/Forms/ModelForm/RiskAssessmentForm.svelte b/frontend/src/lib/components/Forms/ModelForm/RiskAssessmentForm.svelte
index 1a5f2d850..9c623be76 100644
--- a/frontend/src/lib/components/Forms/ModelForm/RiskAssessmentForm.svelte
+++ b/frontend/src/lib/components/Forms/ModelForm/RiskAssessmentForm.svelte
@@ -65,6 +65,7 @@
bind:cachedValue={formDataCache['risk_matrix']}
label={m.riskMatrix()}
helpText={m.riskAssessmentMatrixHelpText()}
+ hidden={initialData.risk_matrix}
/>
+ {#if initialData.ebios_rm_study}
+
+ {/if}
{/if}
diff --git a/frontend/src/lib/utils/crud.ts b/frontend/src/lib/utils/crud.ts
index c53282e6c..24e7cba8f 100644
--- a/frontend/src/lib/utils/crud.ts
+++ b/frontend/src/lib/utils/crud.ts
@@ -5,7 +5,7 @@ import LanguageDisplay from '$lib/components/ModelTable/LanguageDisplay.svelte';
import LibraryActions from '$lib/components/ModelTable/LibraryActions.svelte';
import UserGroupNameDisplay from '$lib/components/ModelTable/UserGroupNameDisplay.svelte';
import { BASE_API_URL } from './constants';
-import { URL_MODEL, type urlModel } from './types';
+import { type urlModel } from './types';
type GetOptionsParams = {
objects: any[];
@@ -191,7 +191,8 @@ export const URL_MODEL_MAP: ModelMap = {
{ field: 'authors', urlModel: 'users' },
{ field: 'reviewers', urlModel: 'users', urlParams: 'is_third_party=false' },
{ field: 'risk_matrix', urlModel: 'risk-matrices' },
- { field: 'risk_scenarios', urlModel: 'risk-scenarios' }
+ { field: 'risk_scenarios', urlModel: 'risk-scenarios' },
+ { field: 'ebios_rm_study', urlModel: 'ebios-rm' }
],
reverseForeignKeyFields: [{ field: 'risk_assessment', urlModel: 'risk-scenarios' }],
selectFields: [{ field: 'status' }],
@@ -672,24 +673,6 @@ export const URL_MODEL_MAP: ModelMap = {
{ field: 'folder', urlModel: 'folders', urlParams: 'content_type=DO' }
]
},
- 'ro-to': {
- endpointUrl: 'ebios-rm/ro-to',
- name: 'roto',
- localName: 'roto',
- localNamePlural: 'roto',
- verboseName: 'Ro to',
- verboseNamePlural: 'Ro to',
- foreignKeyFields: [
- { field: 'ebios_rm_study', urlModel: 'ebios-rm' },
- { field: 'feared_events', urlModel: 'feared-events' }
- ],
- selectFields: [
- { field: 'risk-origin' },
- { field: 'motivation' },
- { field: 'resources' },
- { field: 'pertinence' }
- ]
- },
attack_paths: {
endpointUrl: 'ebios-rm/attack-paths',
name: 'attackpath',
diff --git a/frontend/src/lib/utils/schemas.ts b/frontend/src/lib/utils/schemas.ts
index a7429aa00..c3aab8653 100644
--- a/frontend/src/lib/utils/schemas.ts
+++ b/frontend/src/lib/utils/schemas.ts
@@ -96,7 +96,8 @@ export const RiskAssessmentSchema = z.object({
due_date: z.union([z.literal('').transform(() => null), z.string().date()]).nullish(),
authors: z.array(z.string().optional()).optional(),
reviewers: z.array(z.string().optional()).optional(),
- observation: z.string().optional().nullable()
+ observation: z.string().optional().nullable(),
+ ebios_rm_study: z.string().uuid().optional()
});
export const ThreatSchema = z.object({
diff --git a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/+layout.server.ts b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/+layout.server.ts
new file mode 100644
index 000000000..cb361f306
--- /dev/null
+++ b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/+layout.server.ts
@@ -0,0 +1,7 @@
+import { loadDetail } from '$lib/utils/load';
+import type { PageServerLoad } from './$types';
+import { getModelInfo } from '$lib/utils/crud';
+
+export const load: PageServerLoad = async (event) => {
+ return await loadDetail({ event, model: getModelInfo('ebios-rm'), id: event.params.id });
+};
diff --git a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/+page.server.ts b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/+page.server.ts
index cb361f306..06293b581 100644
--- a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/+page.server.ts
+++ b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/+page.server.ts
@@ -1,7 +1,44 @@
-import { loadDetail } from '$lib/utils/load';
-import type { PageServerLoad } from './$types';
+import { defaultWriteFormAction } from '$lib/utils/actions';
+import { BASE_API_URL } from '$lib/utils/constants';
import { getModelInfo } from '$lib/utils/crud';
+import { modelSchema } from '$lib/utils/schemas';
+import type { ModelInfo } from '$lib/utils/types';
+import { type Actions } from '@sveltejs/kit';
+import { superValidate } from 'sveltekit-superforms';
+import { zod } from 'sveltekit-superforms/adapters';
+import type { PageServerLoad } from './$types';
+
+export const load: PageServerLoad = async ({ params, fetch }) => {
+ const URLModel = 'ebios-rm';
+ const model: ModelInfo = getModelInfo(URLModel);
+
+ const endpoint = model.endpointUrl
+ ? `${BASE_API_URL}/${model.endpointUrl}/${params.id}/`
+ : `${BASE_API_URL}/${model.urlModel}/${params.id}/`;
+ const res = await fetch(endpoint);
+ const data = await res.json();
+
+ const initialData = {
+ risk_matrix: data.risk_matrix.id,
+ ebios_rm_study: params.id
+ };
+
+ const createSchema = modelSchema('risk-assessments');
+ const createRiskAnalysisForm = await superValidate(initialData, zod(createSchema), {
+ errors: false
+ });
+
+ return { createRiskAnalysisForm, model: getModelInfo('risk-assessments') };
+};
-export const load: PageServerLoad = async (event) => {
- return await loadDetail({ event, model: getModelInfo('ebios-rm'), id: event.params.id });
+export const actions: Actions = {
+ create: async (event) => {
+ // const redirectToWrittenObject = Boolean(event.params.model === 'entity-assessments');
+ return defaultWriteFormAction({
+ event,
+ urlModel: 'risk-assessments',
+ action: 'create'
+ // redirectToWrittenObject: redirectToWrittenObject
+ });
+ }
};
diff --git a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/+page.svelte b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/+page.svelte
index 59ef62bad..12a02f6b9 100644
--- a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/+page.svelte
+++ b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/+page.svelte
@@ -5,11 +5,19 @@
import { page } from '$app/stores';
import type { PageData } from './$types';
import { breadcrumbObject } from '$lib/utils/stores';
+ import type { ModalComponent, ModalSettings, ModalStore } from '@skeletonlabs/skeleton';
+ import { getModalStore } from '@skeletonlabs/skeleton';
+ import CreateModal from '$lib/components/Modals/CreateModal.svelte';
+ import MissingConstraintsModal from '$lib/components/Modals/MissingConstraintsModal.svelte';
+ import { checkConstraints } from '$lib/utils/crud';
+ const modalStore: ModalStore = getModalStore();
export let data: PageData;
$: breadcrumbObject.set(data.data);
+ const riskAnalysisCreated: boolean = data.data.risk_assessments.length > 0;
+
const dummydata = {
ws1: [
{
@@ -80,13 +88,64 @@
}
],
ws5: [
- { title: safeTranslate(m.ebiosWs5_1()), status: 'done', href: '#' },
- { title: safeTranslate(m.ebiosWs5_2()), status: 'done', href: '#' },
- { title: safeTranslate(m.ebiosWs5_3()), status: 'to_do', href: '#' },
- { title: safeTranslate(m.ebiosWs5_4()), status: 'to_do', href: '#' },
- { title: safeTranslate(m.ebiosWs5_5()), status: 'done', href: '#' }
+ {
+ title: safeTranslate(m.ebiosWs5_1()),
+ status: riskAnalysisCreated ? 'done' : 'to_do',
+ href: '#'
+ },
+ {
+ title: safeTranslate(m.ebiosWs5_2()),
+ status: 'done',
+ href: `${$page.url.pathname}/workshop-five/risk-analyses?next=${$page.url.pathname}`
+ },
+ {
+ title: safeTranslate(m.ebiosWs5_3()),
+ status: 'to_do',
+ href: `${$page.url.pathname}/workshop-five/risk-analyses?next=${$page.url.pathname}`
+ },
+ {
+ title: safeTranslate(m.ebiosWs5_4()),
+ status: 'to_do',
+ href: `${$page.url.pathname}/workshop-five/risk-analyses?next=${$page.url.pathname}`
+ },
+ {
+ title: safeTranslate(m.ebiosWs5_5()),
+ status: 'done',
+ href: `${$page.url.pathname}/workshop-five/risk-analyses?next=${$page.url.pathname}`
+ }
]
};
+
+ function modalCreateForm(): void {
+ let modalComponent: ModalComponent = {
+ ref: CreateModal,
+ props: {
+ form: data.createRiskAnalysisForm,
+ model: data.model
+ }
+ };
+ let modal: ModalSettings = {
+ type: 'component',
+ component: modalComponent,
+ // Data
+ title: safeTranslate('add-' + data.model.localName)
+ };
+ if (
+ checkConstraints(data.createRiskAnalysisForm.constraints, data.model.foreignKeys).length > 0
+ ) {
+ modalComponent = {
+ ref: MissingConstraintsModal
+ };
+ modal = {
+ type: 'component',
+ component: modalComponent,
+ title: m.warning(),
+ body: safeTranslate('add-' + data.model.localName).toLowerCase(),
+ value: checkConstraints(data.createRiskAnalysisForm.constraints, data.model.foreignKeys)
+ };
+ }
+ modalStore.trigger(modal);
+ }
@@ -97,7 +156,25 @@
-
+
+
+
+
+
diff --git a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/Tile.svelte b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/Tile.svelte
index 7887f0a11..e21cee13d 100644
--- a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/Tile.svelte
+++ b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/Tile.svelte
@@ -6,6 +6,7 @@
export let status = '';
export let meta = null;
export let accent_color = '';
+ export let createRiskAnalysis = false;
@@ -29,27 +30,35 @@
{#each meta as step, i}
{#if step.status == 'done'}
-
-
-
-
- {m.activity()} {i + 1}
- {step.title}
-
+ {#if createRiskAnalysis && i == 0}
+
+ {:else}
+
+
+
+
+ {m.activity()} {i + 1}
+ {step.title}
+
+ {/if}
{:else}
-
-
-
-
- {m.activity()} {i + 1}
- {step.title}
-
+ {#if createRiskAnalysis && i == 0}
+
+ {:else}
+
+
+
+
+ {m.activity()} {i + 1}
+ {step.title}
+
+ {/if}
{/if}
{/each}
diff --git a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-five/risk-analyses/+page.server.ts b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-five/risk-analyses/+page.server.ts
new file mode 100644
index 000000000..0fac9be87
--- /dev/null
+++ b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-five/risk-analyses/+page.server.ts
@@ -0,0 +1,113 @@
+import { defaultDeleteFormAction, defaultWriteFormAction } from '$lib/utils/actions';
+import { BASE_API_URL } from '$lib/utils/constants';
+import {
+ getModelInfo,
+ urlParamModelForeignKeyFields,
+ urlParamModelSelectFields
+} from '$lib/utils/crud';
+import { modelSchema } from '$lib/utils/schemas';
+import type { ModelInfo, urlModel } from '$lib/utils/types';
+import { type Actions } from '@sveltejs/kit';
+import { superValidate } from 'sveltekit-superforms';
+import { zod } from 'sveltekit-superforms/adapters';
+import { z } from 'zod';
+import type { PageServerLoad } from './$types';
+import { listViewFields } from '$lib/utils/table';
+import { tableSourceMapper, type TableSource } from '@skeletonlabs/skeleton';
+
+export const load: PageServerLoad = async ({ params, fetch }) => {
+ const schema = z.object({ id: z.string().uuid() });
+ const deleteForm = await superValidate(zod(schema));
+ const URLModel = 'risk-assessments';
+ const createSchema = modelSchema(URLModel);
+ const ebiosMatrixRes = await fetch(`${BASE_API_URL}/ebios-rm/studies/${params.id}/risk-matrix/`);
+ const risk_matrix_id = await ebiosMatrixRes.json().then((res) => res.id);
+ const initialData = {
+ ebios_rm_study: params.id,
+ risk_matrix: risk_matrix_id
+ };
+ const createForm = await superValidate(initialData, zod(createSchema), { errors: false });
+ const model: ModelInfo = getModelInfo(URLModel);
+ const foreignKeyFields = urlParamModelForeignKeyFields(URLModel);
+ const selectFields = model.selectFields;
+
+ const foreignKeys: Record
= {};
+
+ for (const keyField of foreignKeyFields) {
+ const model = getModelInfo(keyField.urlModel);
+ const queryParams = keyField.urlParams ? `?${keyField.urlParams}` : '';
+ const url = model.endpointUrl
+ ? `${BASE_API_URL}/${model.endpointUrl}/${queryParams}`
+ : `${BASE_API_URL}/${model.urlModel}/${queryParams}`;
+ const response = await fetch(url);
+ if (response.ok) {
+ foreignKeys[keyField.field] = await response.json().then((data) => data.results);
+ } else {
+ console.error(`Failed to fetch data for ${keyField.field}: ${response.statusText}`);
+ }
+ }
+
+ model['foreignKeys'] = foreignKeys;
+
+ const selectOptions: Record = {};
+
+ if (selectFields) {
+ for (const selectField of selectFields) {
+ const url = `${BASE_API_URL}/${URLModel}/${
+ selectField.detail ? params.id + '/' : ''
+ }${selectField.field}/`;
+ const response = await fetch(url);
+ if (response.ok) {
+ selectOptions[selectField.field] = await response.json().then((data) =>
+ Object.entries(data).map(([key, value]) => ({
+ label: value,
+ value: key
+ }))
+ );
+ } else {
+ console.error(`Failed to fetch data for ${selectField.field}: ${response.statusText}`);
+ }
+ }
+ }
+
+ model['selectOptions'] = selectOptions;
+
+ const endpoint = model.endpointUrl
+ ? `${BASE_API_URL}/${model.endpointUrl}?ebios_rm_study=${params.id}`
+ : `${BASE_API_URL}/${model.urlModel}?ebios_rm_study=${params.id}`;
+ const res = await fetch(endpoint);
+ const data = await res.json().then((res) => res.results);
+
+ const bodyData = tableSourceMapper(data, listViewFields[URLModel as urlModel].body);
+
+ const headData: Record = listViewFields[URLModel as urlModel].body.reduce(
+ (obj, key, index) => {
+ obj[key] = listViewFields[URLModel as urlModel].head[index];
+ return obj;
+ },
+ {}
+ );
+
+ const table: TableSource = {
+ head: headData,
+ body: bodyData,
+ meta: data // metaData
+ };
+
+ return { createForm, deleteForm, model, URLModel, table };
+};
+
+export const actions: Actions = {
+ create: async (event) => {
+ // const redirectToWrittenObject = Boolean(event.params.model === 'entity-assessments');
+ return defaultWriteFormAction({
+ event,
+ urlModel: 'risk-assessments',
+ action: 'create'
+ // redirectToWrittenObject: redirectToWrittenObject
+ });
+ },
+ delete: async (event) => {
+ return defaultDeleteFormAction({ event, urlModel: 'risk-assessments' });
+ }
+};
diff --git a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-five/risk-analyses/+page.svelte b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-five/risk-analyses/+page.svelte
new file mode 100644
index 000000000..725a099fc
--- /dev/null
+++ b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-five/risk-analyses/+page.svelte
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+