diff --git a/backend/ebios_rm/migrations/0002_alter_roto_target_objective.py b/backend/ebios_rm/migrations/0002_alter_roto_target_objective.py
new file mode 100644
index 000000000..2af2cfbd7
--- /dev/null
+++ b/backend/ebios_rm/migrations/0002_alter_roto_target_objective.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.1.1 on 2024-12-06 14:51
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("ebios_rm", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="roto",
+ name="target_objective",
+ field=models.TextField(verbose_name="Target objective"),
+ ),
+ ]
diff --git a/backend/ebios_rm/models.py b/backend/ebios_rm/models.py
index 30543cba9..044c6bc49 100644
--- a/backend/ebios_rm/models.py
+++ b/backend/ebios_rm/models.py
@@ -208,9 +208,7 @@ class Pertinence(models.IntegerChoices):
risk_origin = models.CharField(
max_length=32, verbose_name=_("Risk origin"), choices=RiskOrigin.choices
)
- target_objective = models.CharField(
- max_length=200, verbose_name=_("Target objective")
- )
+ target_objective = models.TextField(verbose_name=_("Target objective"))
motivation = models.PositiveSmallIntegerField(
verbose_name=_("Motivation"),
choices=Motivation.choices,
diff --git a/backend/ebios_rm/serializers.py b/backend/ebios_rm/serializers.py
index 15da633fc..4b5a0d9d2 100644
--- a/backend/ebios_rm/serializers.py
+++ b/backend/ebios_rm/serializers.py
@@ -92,7 +92,11 @@ class RoToReadSerializer(BaseModelSerializer):
str = serializers.CharField(source="__str__")
ebios_rm_study = FieldsRelatedField()
folder = FieldsRelatedField()
- feared_events = FieldsRelatedField(many=True)
+ feared_events = FieldsRelatedField(["folder", "id"], many=True)
+
+ pertinence = serializers.CharField(source="get_pertinence_display")
+ motivation = serializers.CharField(source="get_motivation_display")
+ resources = serializers.CharField(source="get_resources_display")
class Meta:
model = RoTo
diff --git a/backend/ebios_rm/views.py b/backend/ebios_rm/views.py
index af0360603..8fcaf6ce9 100644
--- a/backend/ebios_rm/views.py
+++ b/backend/ebios_rm/views.py
@@ -77,6 +77,10 @@ def gravity(self, request, pk):
class RoToViewSet(BaseModelViewSet):
model = RoTo
+ filterset_fields = [
+ "ebios_rm_study",
+ ]
+
@action(detail=False, name="Get risk origin choices", url_path="risk-origin")
def risk_origin(self, request):
return Response(dict(RoTo.RiskOrigin.choices))
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index 11b1f8bf3..a78f1bc0d 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -384,7 +384,7 @@
"exportDatabase": "Export database",
"upload": "Upload",
"add": "Add",
- "undefined": "--",
+ "undefined": "undefined",
"production": "Production",
"design": "Design",
"development": "Development",
@@ -937,5 +937,28 @@
"fearedEvent": "Feared event",
"fearedEvents": "Feared events",
"isSelected": "Is selected",
- "ebiosRM": "Ebios RM"
+ "ebiosRM": "Ebios RM",
+ "riskOrigin": "Risk origin",
+ "targetObjective": "Target objective",
+ "motivation": "Motivation",
+ "resources": "Resources",
+ "pertinence": "Pertinence",
+ "limited": "Limited",
+ "significant": "Significant",
+ "important": "Important",
+ "unlimited": "Unlimited",
+ "strong": "Strong",
+ "irrelevant": "Irrelevant",
+ "partiallyRelevant": "Partially relevant",
+ "fairlyRelevant": "Fairly relevant",
+ "highlyRelevant": "Highly relevant",
+ "roTo": "RO/TO",
+ "addRoto": "Add RO/TO couple",
+ "organizedCrime": "Organized crime",
+ "terrorist": "Terrorist",
+ "activist": "Activist",
+ "professional": "Professional",
+ "amateur": "Amateur",
+ "avenger": "Avenger",
+ "pathological": "Pathological"
}
diff --git a/frontend/src/lib/components/Forms/ModelForm.svelte b/frontend/src/lib/components/Forms/ModelForm.svelte
index 7f30a2a71..3866c9d84 100644
--- a/frontend/src/lib/components/Forms/ModelForm.svelte
+++ b/frontend/src/lib/components/Forms/ModelForm.svelte
@@ -28,6 +28,7 @@
import GeneralSettingsForm from './ModelForm/GeneralSettingForm.svelte';
import EbiosRmForm from './ModelForm/EbiosRmForm.svelte';
import FearedEventForm from './ModelForm/FearedEventForm.svelte';
+ import RoToForm from './ModelForm/RoToForm.svelte';
import AutocompleteSelect from './AutocompleteSelect.svelte';
@@ -261,6 +262,8 @@
{:else if URLModel === 'feared-events'}
+ {:else if URLModel === 'ro-to'}
+
{/if}
{#if closeModal}
diff --git a/frontend/src/lib/components/Forms/ModelForm/RoToForm.svelte b/frontend/src/lib/components/Forms/ModelForm/RoToForm.svelte
new file mode 100644
index 000000000..683e6accd
--- /dev/null
+++ b/frontend/src/lib/components/Forms/ModelForm/RoToForm.svelte
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/lib/utils/crud.ts b/frontend/src/lib/utils/crud.ts
index 49f30e7ab..b0e762ebe 100644
--- a/frontend/src/lib/utils/crud.ts
+++ b/frontend/src/lib/utils/crud.ts
@@ -623,6 +623,24 @@ export const URL_MODEL_MAP: ModelMap = {
{ field: 'qualifications', urlModel: 'qualifications' }
],
selectFields: [{ field: 'gravity', valueType: 'number', detail: true }]
+ },
+ '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', valueType: 'number' },
+ { field: 'resources', valueType: 'number' },
+ { field: 'pertinence', valueType: 'number' }
+ ]
}
};
diff --git a/frontend/src/lib/utils/schemas.ts b/frontend/src/lib/utils/schemas.ts
index 413d7c45c..ed65b698d 100644
--- a/frontend/src/lib/utils/schemas.ts
+++ b/frontend/src/lib/utils/schemas.ts
@@ -413,6 +413,19 @@ export const fearedEventsSchema = z.object({
qualifications: z.string().optional().array().optional()
});
+export const roToSchema = z.object({
+ ebios_rm_study: z.string(),
+ feared_events: z.string().uuid().array(),
+ risk_origin: z.string(),
+ target_objective: z.string(),
+ motivation: z.number().default(0).optional(),
+ resources: z.number().default(0).optional(),
+ pertinence: z.number().default(0).optional(),
+ activity: z.number().min(0).max(4).optional().default(0),
+ is_selected: z.boolean().optional().default(false),
+ justification: z.string().optional()
+});
+
const SCHEMA_MAP: Record
= {
folders: FolderSchema,
projects: ProjectSchema,
@@ -439,7 +452,8 @@ const SCHEMA_MAP: Record = {
vulnerabilities: vulnerabilitySchema,
'filtering-labels': FilteringLabelSchema,
'ebios-rm': ebiosRMSchema,
- 'feared-events': fearedEventsSchema
+ 'feared-events': fearedEventsSchema,
+ 'ro-to': roToSchema
};
export const modelSchema = (model: string) => {
diff --git a/frontend/src/lib/utils/table.ts b/frontend/src/lib/utils/table.ts
index a02b1c270..07e8f9bc3 100644
--- a/frontend/src/lib/utils/table.ts
+++ b/frontend/src/lib/utils/table.ts
@@ -555,5 +555,23 @@ export const listViewFields: ListViewFieldsConfig = {
'feared-events': {
head: ['selected', 'assets', 'fearedEvent', 'qualifications', 'gravity'],
body: ['is_selected', 'assets', 'description', 'qualifications', 'gravity']
+ },
+ 'ro-to': {
+ head: [
+ 'isSelected',
+ 'riskOrigin',
+ 'targetObjective',
+ 'motivation',
+ 'fearedEvents',
+ 'pertinence'
+ ],
+ body: [
+ 'is_selected',
+ 'risk_origin',
+ 'target_objective',
+ 'motivation',
+ 'feared_events',
+ 'pertinence'
+ ]
}
};
diff --git a/frontend/src/lib/utils/types.ts b/frontend/src/lib/utils/types.ts
index 14c7c0356..0e1dde450 100644
--- a/frontend/src/lib/utils/types.ts
+++ b/frontend/src/lib/utils/types.ts
@@ -52,7 +52,8 @@ export const URL_MODEL = [
'representatives',
'vulnerabilities',
'filtering-labels',
- 'feared-events'
+ 'feared-events',
+ 'ro-to'
// 'ebios-rm',
] as const;
diff --git a/frontend/src/routes/(app)/(internal)/[model=urlmodel]/+page.server.ts b/frontend/src/routes/(app)/(internal)/[model=urlmodel]/+page.server.ts
index fe10f5bf2..2b5f03185 100644
--- a/frontend/src/routes/(app)/(internal)/[model=urlmodel]/+page.server.ts
+++ b/frontend/src/routes/(app)/(internal)/[model=urlmodel]/+page.server.ts
@@ -27,7 +27,10 @@ export const load: PageServerLoad = async ({ params, fetch }) => {
for (const keyField of foreignKeyFields) {
const queryParams = keyField.urlParams ? `?${keyField.urlParams}` : '';
- const url = `${BASE_API_URL}/${keyField.urlModel}/${queryParams}`;
+ const keyModel = getModelInfo(keyField.urlModel);
+ const url = keyModel.endpointUrl
+ ? `${BASE_API_URL}/${keyModel.endpointUrl}/${queryParams}`
+ : `${BASE_API_URL}/${keyField.urlModel}/${queryParams}`;
const response = await fetch(url);
if (response.ok) {
foreignKeys[keyField.field] = await response.json().then((data) => data.results);
@@ -42,7 +45,9 @@ export const load: PageServerLoad = async ({ params, fetch }) => {
for (const selectField of selectFields) {
if (selectField.detail) continue;
- const url = `${BASE_API_URL}/${params.model}/${selectField.field}/`;
+ const url = model.endpointUrl
+ ? `${BASE_API_URL}/${model.endpointUrl}/${selectField.field}/`
+ : `${BASE_API_URL}/${params.model}/${selectField.field}/`;
const response = await fetch(url);
if (response.ok) {
selectOptions[selectField.field] = await response.json().then((data) =>
diff --git a/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/+page.server.ts b/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/+page.server.ts
index 699a98520..a1d709195 100644
--- a/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/+page.server.ts
+++ b/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/+page.server.ts
@@ -26,8 +26,11 @@ export const load: PageServerLoad = async (event) => {
if (modelInfo.foreignKeyFields) {
await Promise.all(
modelInfo.foreignKeyFields.map(async (keyField) => {
+ const keyModel = getModelInfo(keyField.urlModel);
const queryParams = keyField.urlParams ? `?${keyField.urlParams}` : '';
- const url = `${BASE_API_URL}/${keyField.urlModel}/${queryParams}`;
+ const url = keyModel.endpointUrl
+ ? `${BASE_API_URL}/${keyModel.endpointUrl}/${queryParams}`
+ : `${BASE_API_URL}/${keyField.urlModel}/${queryParams}`;
const response = await event.fetch(url);
if (response.ok) {
foreignKeys[keyField.field] = await response.json().then((data) => data.results);
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 21df4f60f..4929466a5 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
@@ -34,9 +34,21 @@
}
],
ws2: [
- { title: safeTranslate(m.ebiosWs2_1()), status: 'to_do', href: '#' },
- { title: safeTranslate(m.ebiosWs2_2()), status: 'to_do', href: '#' },
- { title: safeTranslate(m.ebiosWs2_3()), status: 'to_do', href: '#' }
+ {
+ title: safeTranslate(m.ebiosWs2_1()),
+ status: 'to_do',
+ href: `${$page.url.pathname}/workshop-two/ro-to?next=${$page.url.pathname}`
+ },
+ {
+ title: safeTranslate(m.ebiosWs2_2()),
+ status: 'to_do',
+ href: `${$page.url.pathname}/workshop-two/ro-to?next=${$page.url.pathname}`
+ },
+ {
+ title: safeTranslate(m.ebiosWs2_3()),
+ status: 'to_do',
+ href: `${$page.url.pathname}/workshop-two/ro-to?next=${$page.url.pathname}`
+ }
],
ws3: [
{ title: safeTranslate(m.ebiosWs3_1()), status: 'to_do', href: '#' },
diff --git a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-two/ro-to/+page.server.ts b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-two/ro-to/+page.server.ts
new file mode 100644
index 000000000..fdad14f79
--- /dev/null
+++ b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-two/ro-to/+page.server.ts
@@ -0,0 +1,103 @@
+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 = 'ro-to';
+ const createSchema = modelSchema(URLModel);
+ const initialData = {
+ ebios_rm_study: params.id
+ };
+ const createForm = await superValidate(initialData, zod(createSchema), { errors: false });
+ const model: ModelInfo = getModelInfo(URLModel);
+ const foreignKeyFields = urlParamModelForeignKeyFields(URLModel);
+ const selectFields = urlParamModelSelectFields(URLModel);
+
+ const foreignKeys: Record = {};
+
+ for (const keyField of foreignKeyFields) {
+ const keyModel = getModelInfo(keyField.urlModel);
+ const queryParams = keyField.urlParams ? `?${keyField.urlParams}` : '';
+ const url = `${BASE_API_URL}/${keyModel.endpointUrl ?? keyModel.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 = {};
+
+ for (const selectField of selectFields) {
+ if (selectField.detail) continue;
+ const url = `${BASE_API_URL}/${model.endpointUrl ?? model.urlModel}/${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: selectField.valueType === 'number' ? parseInt(key) : key
+ }))
+ );
+ } else {
+ console.error(`Failed to fetch data for ${selectField.field}: ${response.statusText}`);
+ }
+ }
+
+ model['selectOptions'] = selectOptions;
+
+ const endpoint = `${BASE_API_URL}/${model.endpointUrl}?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: 'ro-to',
+ action: 'create'
+ // redirectToWrittenObject: redirectToWrittenObject
+ });
+ },
+ delete: async (event) => {
+ return defaultDeleteFormAction({ event, urlModel: 'ro-to' });
+ }
+};
diff --git a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-two/ro-to/+page.svelte b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-two/ro-to/+page.svelte
new file mode 100644
index 000000000..08ebedae8
--- /dev/null
+++ b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-two/ro-to/+page.svelte
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+