From 2da47d8536d232adaeda2b7431f4d55762d6df1a Mon Sep 17 00:00:00 2001 From: Mohamed-Hacene <90701924+Mohamed-Hacene@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:48:05 +0100 Subject: [PATCH] Workshop 2 UX (#1180) Co-authored-by: eric-intuitem <71850047+eric-intuitem@users.noreply.github.com> --- ...oto_pertinence_alter_roto_feared_events.py | 26 +++ backend/ebios_rm/models.py | 24 ++- backend/ebios_rm/serializers.py | 2 +- backend/ebios_rm/views.py | 8 +- documentation/architecture/data-model.md | 7 +- frontend/messages/en.json | 6 +- .../src/lib/components/Forms/ModelForm.svelte | 2 +- .../Forms/ModelForm/RoToForm.svelte | 184 +++++++++++------- .../components/ModelTable/ModelTable.svelte | 3 +- frontend/src/lib/utils/crud.ts | 14 +- frontend/src/lib/utils/load.ts | 4 +- frontend/src/lib/utils/schemas.ts | 3 +- .../ebios-rm/[id=uuid]/+page.svelte | 6 +- .../workshop-one/ebios-rm-study/+page.svelte | 8 +- .../feared-events/+page.server.ts | 8 +- .../workshop-two/ro-to/+page.server.ts | 4 +- .../[id=uuid]/workshop-two/ro-to/+page.svelte | 19 +- .../ro-to/[id=uuid]/+page.server.ts | 37 ++++ .../(internal)/ro-to/[id=uuid]/+page.svelte | 165 ++++++++++++++++ .../ro-to/[id=uuid]/edit/+page.server.ts | 67 +++++++ .../ro-to/[id=uuid]/edit/+page.svelte | 17 ++ 21 files changed, 507 insertions(+), 107 deletions(-) create mode 100644 backend/ebios_rm/migrations/0004_remove_roto_pertinence_alter_roto_feared_events.py create mode 100644 frontend/src/routes/(app)/(internal)/ro-to/[id=uuid]/+page.server.ts create mode 100644 frontend/src/routes/(app)/(internal)/ro-to/[id=uuid]/+page.svelte create mode 100644 frontend/src/routes/(app)/(internal)/ro-to/[id=uuid]/edit/+page.server.ts create mode 100644 frontend/src/routes/(app)/(internal)/ro-to/[id=uuid]/edit/+page.svelte diff --git a/backend/ebios_rm/migrations/0004_remove_roto_pertinence_alter_roto_feared_events.py b/backend/ebios_rm/migrations/0004_remove_roto_pertinence_alter_roto_feared_events.py new file mode 100644 index 000000000..954fe19f8 --- /dev/null +++ b/backend/ebios_rm/migrations/0004_remove_roto_pertinence_alter_roto_feared_events.py @@ -0,0 +1,26 @@ +# Generated by Django 5.1.4 on 2024-12-12 18:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("ebios_rm", "0003_remove_ebiosrmstudy_risk_assessments"), + ] + + operations = [ + migrations.RemoveField( + model_name="roto", + name="pertinence", + ), + migrations.AlterField( + model_name="roto", + name="feared_events", + field=models.ManyToManyField( + blank=True, + related_name="ro_to_couples", + to="ebios_rm.fearedevent", + verbose_name="Feared events", + ), + ), + ] diff --git a/backend/ebios_rm/models.py b/backend/ebios_rm/models.py index 83ad19ea1..dbe56bfb8 100644 --- a/backend/ebios_rm/models.py +++ b/backend/ebios_rm/models.py @@ -194,7 +194,10 @@ class Pertinence(models.IntegerChoices): on_delete=models.CASCADE, ) feared_events = models.ManyToManyField( - FearedEvent, verbose_name=_("Feared events"), related_name="ro_to_couples" + FearedEvent, + verbose_name=_("Feared events"), + related_name="ro_to_couples", + blank=True, ) risk_origin = models.CharField( @@ -211,11 +214,6 @@ class Pertinence(models.IntegerChoices): choices=Resources.choices, default=Resources.UNDEFINED, ) - pertinence = models.PositiveSmallIntegerField( - verbose_name=_("Pertinence"), - choices=Pertinence.choices, - default=Pertinence.UNDEFINED, - ) activity = models.PositiveSmallIntegerField( verbose_name=_("Activity"), default=0, validators=[MaxValueValidator(4)] ) @@ -234,6 +232,20 @@ def save(self, *args, **kwargs): self.folder = self.ebios_rm_study.folder super().save(*args, **kwargs) + @property + def get_pertinence(self): + PERTINENCE_MATRIX = [ + [1, 1, 2, 2], + [1, 2, 3, 3], + [2, 3, 3, 4], + [2, 3, 4, 4], + ] + if self.motivation == 0 or self.resources == 0: + return self.Pertinence(self.Pertinence.UNDEFINED).label + return self.Pertinence( + PERTINENCE_MATRIX[self.motivation - 1][self.resources - 1] + ).label + class Stakeholder(AbstractBaseModel, FolderMixin): class Category(models.TextChoices): diff --git a/backend/ebios_rm/serializers.py b/backend/ebios_rm/serializers.py index 2d81ac5c9..3eed45bf0 100644 --- a/backend/ebios_rm/serializers.py +++ b/backend/ebios_rm/serializers.py @@ -94,9 +94,9 @@ class RoToReadSerializer(BaseModelSerializer): folder = FieldsRelatedField() 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") + pertinence = serializers.CharField(source="get_pertinence") class Meta: model = RoTo diff --git a/backend/ebios_rm/views.py b/backend/ebios_rm/views.py index 71ddb5d7d..6d35d48a1 100644 --- a/backend/ebios_rm/views.py +++ b/backend/ebios_rm/views.py @@ -69,9 +69,7 @@ def likelihood(self, request, pk): class FearedEventViewSet(BaseModelViewSet): model = FearedEvent - filterset_fields = [ - "ebios_rm_study", - ] + filterset_fields = ["ebios_rm_study", "ro_to_couples"] @action(detail=True, name="Get risk matrix", url_path="risk-matrix") def risk_matrix(self, request, pk=None): @@ -112,10 +110,6 @@ def motivation(self, request): def resources(self, request): return Response(dict(RoTo.Resources.choices)) - @action(detail=False, name="Get pertinence choices") - def pertinence(self, request): - return Response(dict(RoTo.Pertinence.choices)) - class StakeholderViewSet(BaseModelViewSet): model = Stakeholder diff --git a/documentation/architecture/data-model.md b/documentation/architecture/data-model.md index 20766a703..59d68c962 100644 --- a/documentation/architecture/data-model.md +++ b/documentation/architecture/data-model.md @@ -1210,7 +1210,7 @@ The object risk_origin_target_objective (workshop 2) contains the following fiel - target objective (text) - motivation (--/1 very low/2 low/3 significant/4 strong) (--/très peu/peu/assez/fortement motivé) - resources (--/1 limited/2 significant/3 important/4 unlimited) (--/limitées/significatives/importantes/illimitées) -- pertinence (--/1 Irrelevant/2 partially relevant/3 fairly relevant/4 highly relevant) (--/peu pertinent/moyennement pertient/plutôt pertinent/très pertinent) +- pertinence (--/1 Irrelevant/2 partially relevant/3 fairly relevant/4 highly relevant) (--/peu pertinent/moyennement pertient/plutôt pertinent/très pertinent) -> calculated - activity (--/1/2/3/4) - selected - justification @@ -1267,7 +1267,7 @@ The frontend for risk study shall propose the following steps: ```mermaid erDiagram DOMAIN ||--o{ EBIOS_RM_STUDY : contains - DOMAIN ||--o{ STAKEHOLDER : contains + DOMAIN ||--o{ STAKEHOLDER : contains DOMAIN ||--o{ OPERATIONAL_SCENARIO : contains DOMAIN ||--o{ FEARED_EVENT : contains DOMAIN ||--o{ RO_TO : contains @@ -1289,7 +1289,7 @@ erDiagram EBIOS_RM_STUDY }o--o| ENTITY : studies EBIOS_RM_STUDY }o--o{ COMPLIANCE_ASSESSMENT: leverages EBIOS_RM_STUDY }o--|| RISK_MATRIX : leverages - EBIOS_RM_STUDY }o--o{ RISK_ASSESSMENT : generates + EBIOS_RM_STUDY |o--o{ RISK_ASSESSMENT : generates ATTACK_PATH }o--|| RO_TO : derives RO_TO }o--o{ FEARED_EVENT : corresponds_to OPERATIONAL_SCENARIO }o--|{ ATTACK_PATH : derives @@ -1326,7 +1326,6 @@ erDiagram string target_objective int motivation int resources - int pertinence int activity bool selected string justification diff --git a/frontend/messages/en.json b/frontend/messages/en.json index dc6f3a23e..3e3526fbd 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -926,6 +926,7 @@ "ebiosRmMatrixHelpText": "Risk matrix used as a reference for the study. Defaults to `urn:intuitem:risk:library:risk-matrix-4x4-ebios-rm`", "activityOne": "Activity 1", "activityTwo": "Activity 2", + "activityThree": "Activity 3", "ebiosRmStudy": "Ebios RM study", "qualifications": "Qualifications", "impacts": "Impacts", @@ -980,7 +981,10 @@ "attackPaths": "Attack paths", "currentCriticality": "Current criticality", "residualCriticality": "Residual criticality", - "errorAssetGraphMustNotContainCycles": "The asset graph must not contain cycles.", + "notSelected": "Not selected", + "identifyRoTo": "Identify RO/TO", + "evaluateRoTo": "Evaluate RO/TO", + "selectRoTo": "Select RO/TO", "resetPasswordHere": "You can reset your password here.", "resetPassword": "Reset password", "ebiosRm": "Ebios RM", diff --git a/frontend/src/lib/components/Forms/ModelForm.svelte b/frontend/src/lib/components/Forms/ModelForm.svelte index e30978a25..8ca71675b 100644 --- a/frontend/src/lib/components/Forms/ModelForm.svelte +++ b/frontend/src/lib/components/Forms/ModelForm.svelte @@ -267,7 +267,7 @@ {:else if URLModel === 'feared-events'} <FearedEventForm {form} {model} {cacheLocks} {formDataCache} {initialData} /> {:else if URLModel === 'ro-to'} - <RoToForm {form} {model} {cacheLocks} {formDataCache} {initialData} /> + <RoToForm {form} {model} {cacheLocks} {formDataCache} {initialData} {context} /> {:else if URLModel === 'stakeholders'} <StakeholderForm {form} {model} {cacheLocks} {formDataCache} {context} /> {:else if URLModel === 'attack-paths'} diff --git a/frontend/src/lib/components/Forms/ModelForm/RoToForm.svelte b/frontend/src/lib/components/Forms/ModelForm/RoToForm.svelte index eee24fba6..a78db0a56 100644 --- a/frontend/src/lib/components/Forms/ModelForm/RoToForm.svelte +++ b/frontend/src/lib/components/Forms/ModelForm/RoToForm.svelte @@ -8,12 +8,27 @@ import { getOptions } from '$lib/utils/crud'; import TextArea from '../TextArea.svelte'; import NumberField from '../NumberField.svelte'; + import { page } from '$app/stores'; export let form: SuperValidated<any>; export let model: ModelInfo; export let cacheLocks: Record<string, CacheLock> = {}; export let formDataCache: Record<string, any> = {}; export let initialData: Record<string, any> = {}; + export let context: string; + + const activityBackground = context === 'edit' ? 'bg-white' : 'bg-surface-100-800-token'; + + let activeActivity: string | null = null; + $page.url.searchParams.forEach((value, key) => { + if (key === 'activity' && value === 'one') { + activeActivity = 'one'; + } else if (key === 'activity' && value === 'two') { + activeActivity = 'two'; + } else if (key === 'activity' && value === 'three') { + activeActivity = 'three'; + } + }); </script> <AutocompleteSelect @@ -25,68 +40,107 @@ label={m.ebiosRmStudy()} hidden={initialData.ebios_rm_study} /> -<AutocompleteSelect - multiple - {form} - options={getOptions({ - objects: model.foreignKeys['feared_events'], - extra_fields: [['folder', 'str']], - label: 'auto' - })} - field="feared_events" - label={m.fearedEvents()} -/> -<Select - {form} - options={model.selectOptions['risk-origin']} - field="risk_origin" - label={m.riskOrigin()} - cacheLock={cacheLocks['risk_origin']} - bind:cachedValue={formDataCache['risk_origin']} -/> -<TextArea - {form} - field="target_objective" - label={m.targetObjective()} - cacheLock={cacheLocks['target_objective']} - bind:cachedValue={formDataCache['target_objective']} -/> -<Select - {form} - options={model.selectOptions['motivation']} - field="motivation" - label={m.motivation()} - cacheLock={cacheLocks['motivation']} - bind:cachedValue={formDataCache['motivation']} -/> -<Select - {form} - options={model.selectOptions['resources']} - field="resources" - label={m.resources()} - cacheLock={cacheLocks['resources']} - bind:cachedValue={formDataCache['resources']} -/> -<Select - {form} - options={model.selectOptions['pertinence']} - field="pertinence" - label={m.pertinence()} - cacheLock={cacheLocks['pertinence']} - bind:cachedValue={formDataCache['pertinence']} -/> -<NumberField - {form} - field="activity" - label={m.activity()} - cacheLock={cacheLocks['activity']} - bind:cachedValue={formDataCache['activity']} -/> -<Checkbox {form} field="is_selected" label={m.isSelected()} /> -<TextArea - {form} - field="justification" - label={m.justification()} - cacheLock={cacheLocks['justification']} - bind:cachedValue={formDataCache['justification']} -/> +<div + class="relative p-2 space-y-2 rounded-md {activeActivity === 'one' + ? 'border-2 border-primary-500' + : 'border-2 border-gray-300 border-dashed'}" +> + <p + class="absolute -top-3 {activityBackground} font-bold {activeActivity === 'one' + ? 'text-primary-500' + : 'text-gray-500'}" + > + {m.activityOne()} + </p> + <Select + {form} + options={model.selectOptions['risk-origin']} + field="risk_origin" + label={m.riskOrigin()} + cacheLock={cacheLocks['risk_origin']} + bind:cachedValue={formDataCache['risk_origin']} + /> + <TextArea + {form} + field="target_objective" + label={m.targetObjective()} + cacheLock={cacheLocks['target_objective']} + bind:cachedValue={formDataCache['target_objective']} + /> +</div> +<div + class="relative p-2 space-y-2 rounded-md {activeActivity === 'two' + ? 'border-2 border-primary-500' + : 'border-2 border-gray-300 border-dashed'}" +> + <p + class="absolute -top-3 {activityBackground} font-bold {activeActivity === 'two' + ? 'text-primary-500' + : 'text-gray-500'}" + > + {m.activityTwo()} + </p> + <Select + {form} + options={model.selectOptions['motivation']} + field="motivation" + label={m.motivation()} + cacheLock={cacheLocks['motivation']} + bind:cachedValue={formDataCache['motivation']} + /> + <Select + {form} + options={model.selectOptions['resources']} + field="resources" + label={m.resources()} + cacheLock={cacheLocks['resources']} + bind:cachedValue={formDataCache['resources']} + /> + <!-- <Select + {form} + options={model.selectOptions['pertinence']} + field="pertinence" + label={m.pertinence()} + cacheLock={cacheLocks['pertinence']} + bind:cachedValue={formDataCache['pertinence']} + /> --> + <NumberField + {form} + field="activity" + label={m.activity()} + cacheLock={cacheLocks['activity']} + bind:cachedValue={formDataCache['activity']} + /> +</div> +<div + class="relative p-2 space-y-2 rounded-md {activeActivity === 'three' + ? 'border-2 border-primary-500' + : 'border-2 border-gray-300 border-dashed'}" +> + <p + class="absolute -top-3 {activityBackground} font-bold {activeActivity === 'three' + ? 'text-primary-500' + : 'text-gray-500'}" + > + {m.activityThree()} + </p> + <Checkbox {form} field="is_selected" label={m.isSelected()} /> + <AutocompleteSelect + multiple + {form} + options={getOptions({ + objects: model.foreignKeys['feared_events'], + extra_fields: [['folder', 'str']], + label: 'auto' + })} + field="feared_events" + label={m.fearedEvents()} + /> + <TextArea + {form} + field="justification" + label={m.justification()} + cacheLock={cacheLocks['justification']} + bind:cachedValue={formDataCache['justification']} + /> +</div> diff --git a/frontend/src/lib/components/ModelTable/ModelTable.svelte b/frontend/src/lib/components/ModelTable/ModelTable.svelte index 6cd242326..5b8a25b8c 100644 --- a/frontend/src/lib/components/ModelTable/ModelTable.svelte +++ b/frontend/src/lib/components/ModelTable/ModelTable.svelte @@ -42,6 +42,7 @@ // Props (styles) export let element: CssClasses = 'table'; export let text: CssClasses = 'text-xs'; + export let backgroundColor: CssClasses = 'bg-white'; export let color: CssClasses = ''; export let regionHead: CssClasses = ''; export let regionHeadCell: CssClasses = 'uppercase bg-white text-gray-700'; @@ -94,7 +95,7 @@ // Replace $$props.class with classProp for compatibility let classProp = ''; // Replacing $$props.class - $: classesBase = `${classProp || 'bg-white'}`; + $: classesBase = `${classProp || backgroundColor}`; $: classesTable = `${element} ${text} ${color}`; import { goto } from '$app/navigation'; diff --git a/frontend/src/lib/utils/crud.ts b/frontend/src/lib/utils/crud.ts index 6bbdd4862..b9a6d9357 100644 --- a/frontend/src/lib/utils/crud.ts +++ b/frontend/src/lib/utils/crud.ts @@ -105,6 +105,7 @@ interface ForeignKeyField { urlModel: urlModel; endpointUrl?: string; urlParams?: string; + detail?: boolean; } interface Field { @@ -624,7 +625,7 @@ export const URL_MODEL_MAP: ModelMap = { verboseNamePlural: 'Feared events', foreignKeyFields: [ { field: 'ebios_rm_study', urlModel: 'ebios-rm', endpointUrl: 'ebios-rm/studies' }, - { field: 'assets', urlModel: 'assets', urlParams: 'ebios_rm_studies=' }, + { field: 'assets', urlModel: 'assets', urlParams: 'ebios_rm_studies=', detail: true }, { field: 'qualifications', urlModel: 'qualifications' } ], selectFields: [{ field: 'gravity', valueType: 'number', detail: true }] @@ -638,13 +639,18 @@ export const URL_MODEL_MAP: ModelMap = { verboseNamePlural: 'Ro to', foreignKeyFields: [ { field: 'ebios_rm_study', urlModel: 'ebios-rm', endpointUrl: 'ebios-rm/studies' }, - { field: 'feared_events', urlModel: 'feared-events', endpointUrl: 'ebios-rm/feared-events' } + { + field: 'feared_events', + urlModel: 'feared-events', + endpointUrl: 'ebios-rm/feared-events', + urlParams: 'ebios_rm_study=', + detail: true + } ], selectFields: [ { field: 'risk-origin' }, { field: 'motivation', valueType: 'number' }, - { field: 'resources', valueType: 'number' }, - { field: 'pertinence', valueType: 'number' } + { field: 'resources', valueType: 'number' } ] }, stakeholders: { diff --git a/frontend/src/lib/utils/load.ts b/frontend/src/lib/utils/load.ts index bc4b2838d..893b34068 100644 --- a/frontend/src/lib/utils/load.ts +++ b/frontend/src/lib/utils/load.ts @@ -43,7 +43,7 @@ export const loadDetail = async ({ event, model, id }) => { const initialData = {}; await Promise.all( model.reverseForeignKeyFields.map(async (e) => { - const relEndpoint = `${BASE_API_URL}/${e.urlModel}/?${e.field}=${event.params.id}`; + const relEndpoint = `${BASE_API_URL}/${e.endpointUrl || e.urlModel}/?${e.field}=${event.params.id}`; const res = await event.fetch(relEndpoint); const revData = await res.json().then((res) => res.results); @@ -103,7 +103,7 @@ export const loadDetail = async ({ event, model, id }) => { if (info.selectFields) { await Promise.all( info.selectFields.map(async (selectField) => { - const url = `${BASE_API_URL}/${urlModel}/${selectField.field}/`; + const url = `${BASE_API_URL}/${e.endpointUrl || e.urlModel}/${selectField.field}/`; const response = await event.fetch(url); if (response.ok) { selectOptions[selectField.field] = await response.json().then((data) => diff --git a/frontend/src/lib/utils/schemas.ts b/frontend/src/lib/utils/schemas.ts index c26536b1e..7872fa503 100644 --- a/frontend/src/lib/utils/schemas.ts +++ b/frontend/src/lib/utils/schemas.ts @@ -418,12 +418,11 @@ export const fearedEventsSchema = z.object({ export const roToSchema = z.object({ ebios_rm_study: z.string(), - feared_events: z.string().uuid().array(), + feared_events: z.string().uuid().optional().array().optional(), 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() 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 105e5989c..a39ec4d84 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 @@ -45,17 +45,17 @@ { title: safeTranslate(m.ebiosWs2_1()), status: 'to_do', - href: `${$page.url.pathname}/workshop-two/ro-to?next=${$page.url.pathname}` + href: `${$page.url.pathname}/workshop-two/ro-to?activity=one&next=${$page.url.pathname}` }, { title: safeTranslate(m.ebiosWs2_2()), status: 'to_do', - href: `${$page.url.pathname}/workshop-two/ro-to?next=${$page.url.pathname}` + href: `${$page.url.pathname}/workshop-two/ro-to?activity=two&next=${$page.url.pathname}` }, { title: safeTranslate(m.ebiosWs2_3()), status: 'to_do', - href: `${$page.url.pathname}/workshop-two/ro-to?next=${$page.url.pathname}` + href: `${$page.url.pathname}/workshop-two/ro-to?activity=three&next=${$page.url.pathname}` } ], ws3: [ diff --git a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-one/ebios-rm-study/+page.svelte b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-one/ebios-rm-study/+page.svelte index aef2b1594..55c599eed 100644 --- a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-one/ebios-rm-study/+page.svelte +++ b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-one/ebios-rm-study/+page.svelte @@ -105,7 +105,9 @@ : 'text-gray-500'}">{m.activityOne()}</span > {#if ebiosRmStudy.description} - <p class="text-gray-600">{ebiosRmStudy.description}</p> + <p class="text-gray-600 whitespace-pre-wrap text-justify w-full"> + {ebiosRmStudy.description} + </p> {:else} <p class="text-gray-600">{m.noDescription()}</p> {/if} @@ -117,7 +119,7 @@ <ul class="list-disc list-inside text-gray-600"> {#if ebiosRmStudy.authors?.length} {#each ebiosRmStudy.authors as author} - <li>{author.str}</li> + <li><a class="anchor" href="/users/{author.id}">{author.str}</a></li> {/each} {:else} <li>{m.noAuthor()}</li> @@ -132,7 +134,7 @@ <ul class="list-disc list-inside text-gray-600"> {#if ebiosRmStudy.reviewers?.length} {#each ebiosRmStudy.reviewers as reviewer} - <li>{reviewer.str}</li> + <li><a class="anchor" href="/users/{reviewer.id}">{reviewer.str}</a></li> {/each} {:else} <li>{m.noReviewer()}</li> diff --git a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-one/feared-events/+page.server.ts b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-one/feared-events/+page.server.ts index 257a7a783..09d7e7047 100644 --- a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-one/feared-events/+page.server.ts +++ b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-one/feared-events/+page.server.ts @@ -49,14 +49,12 @@ export const load: PageServerLoad = async ({ params, fetch }) => { for (const keyField of foreignKeyFields) { const model = getModelInfo(keyField.urlModel); - const queryParams = keyField.urlParams ? `?${keyField.urlParams}` : ''; + const queryParams = keyField.urlParams + ? `?${keyField.urlParams}${keyField.detail ? params.id : ''}` + : ''; let url = model.endpointUrl ? `${BASE_API_URL}/${model.endpointUrl}/${queryParams}` : `${BASE_API_URL}/${model.urlModel}/${queryParams}`; - if (model.urlModel === 'assets') { - url = `${BASE_API_URL}/${model.urlModel}/${queryParams}${params.id}`; - console.log(url); - } const response = await 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]/workshop-two/ro-to/+page.server.ts b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-two/ro-to/+page.server.ts index fdad14f79..8ff2e8aeb 100644 --- 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 @@ -32,7 +32,9 @@ export const load: PageServerLoad = async ({ params, fetch }) => { for (const keyField of foreignKeyFields) { const keyModel = getModelInfo(keyField.urlModel); - const queryParams = keyField.urlParams ? `?${keyField.urlParams}` : ''; + const queryParams = keyField.urlParams + ? `?${keyField.urlParams}${keyField.detail ? params.id : ''}` + : ''; const url = `${BASE_API_URL}/${keyModel.endpointUrl ?? keyModel.urlModel}/${queryParams}`; const response = await fetch(url); if (response.ok) { 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 index 08ebedae8..772da6e6e 100644 --- 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 @@ -8,6 +8,7 @@ import MissingConstraintsModal from '$lib/components/Modals/MissingConstraintsModal.svelte'; import { checkConstraints } from '$lib/utils/crud'; import * as m from '$paraglide/messages.js'; + import { page } from '$app/stores'; const modalStore: ModalStore = getModalStore(); @@ -50,9 +51,25 @@ } modalStore.trigger(modal); } + + let activeActivity: string | null = null; + $page.url.searchParams.forEach((value, key) => { + if (key === 'activity' && value === 'one') { + activeActivity = 'one'; + } else if (key === 'activity' && value === 'two') { + activeActivity = 'two'; + } else if (key === 'activity' && value === 'three') { + activeActivity = 'three'; + } + }); </script> -<ModelTable source={data.table} deleteForm={data.deleteForm} {URLModel}> +<ModelTable + source={data.table} + deleteForm={data.deleteForm} + {URLModel} + detailQueryParameter={`activity=${activeActivity}`} +> <div slot="addButton"> <span class="inline-flex overflow-hidden rounded-md border bg-white shadow-sm"> <button diff --git a/frontend/src/routes/(app)/(internal)/ro-to/[id=uuid]/+page.server.ts b/frontend/src/routes/(app)/(internal)/ro-to/[id=uuid]/+page.server.ts new file mode 100644 index 000000000..e3c9c06ba --- /dev/null +++ b/frontend/src/routes/(app)/(internal)/ro-to/[id=uuid]/+page.server.ts @@ -0,0 +1,37 @@ +import type { PageServerLoad } from './$types'; +import { BASE_API_URL } from '$lib/utils/constants'; +import { listViewFields } from '$lib/utils/table'; +import { tableSourceMapper, type TableSource } from '@skeletonlabs/skeleton'; +import { getModelInfo } from '$lib/utils/crud'; + +export const load: PageServerLoad = async (event) => { + const URLModel = 'ro-to'; + const model = getModelInfo(URLModel); + const endpoint = `${BASE_API_URL}/${model.endpointUrl}/${event.params.id}/`; + const response = await event.fetch(endpoint); + const data = await response.json(); + + const relEndpoint = `${BASE_API_URL}/ebios-rm/feared-events?ro_to_couples=${event.params.id}`; + const res = await event.fetch(relEndpoint); + const revData = await res.json().then((res) => res.results); + + const tableFieldsRef = listViewFields['feared-events']; + const tableFields = { + head: [...tableFieldsRef.head], + body: [...tableFieldsRef.body] + }; + const index = tableFields.body.indexOf('ro_to_couples'); + if (index > -1) { + tableFields.head.splice(index, 1); + tableFields.body.splice(index, 1); + } + const bodyData = tableSourceMapper(revData, tableFields.body); + + const table: TableSource = { + head: tableFields.head, + body: bodyData, + meta: revData + }; + + return { data, table }; +}; diff --git a/frontend/src/routes/(app)/(internal)/ro-to/[id=uuid]/+page.svelte b/frontend/src/routes/(app)/(internal)/ro-to/[id=uuid]/+page.svelte new file mode 100644 index 000000000..6305659e3 --- /dev/null +++ b/frontend/src/routes/(app)/(internal)/ro-to/[id=uuid]/+page.svelte @@ -0,0 +1,165 @@ +<script lang="ts"> + import type { PageData } from './$types'; + import * as m from '$paraglide/messages'; + import { page } from '$app/stores'; + import { pageTitle } from '$lib/utils/stores'; + import { safeTranslate } from '$lib/utils/i18n'; + import ModelTable from '$lib/components/ModelTable/ModelTable.svelte'; + + export let data: PageData; + + const roto = data.data; + + pageTitle.set(roto.risk_origin + ' - ' + roto.target_objective); + + let activeActivity: string | null = null; + $page.url.searchParams.forEach((value, key) => { + if (key === 'activity' && value === 'one') { + activeActivity = 'one'; + } else if (key === 'activity' && value === 'two') { + activeActivity = 'two'; + } else if (key === 'activity' && value === 'three') { + activeActivity = 'three'; + } + }); + + const pertinenceColor = { + undefined: 'bg-gray-200 text-gray-700', + irrelevant: 'bg-green-200 text-green-700', + 'partially relevant': 'bg-yellow-200 text-yellow-700', + fairly_relevant: 'bg-orange-200 text-orange-700', + higly_relevant: 'bg-red-200 text-red-700' + }; +</script> + +<div class="card p-4 bg-white shadow-lg"> + <div class="flex flex-col space-y-4"> + <div + id="activityOne" + class="relative p-4 space-y-4 rounded-md w-full flex flex-col items-center justify-center + {activeActivity === 'one' + ? 'border-2 border-primary-500' + : 'border-2 border-gray-300 border-dashed'}" + > + <span + class="absolute -top-3 bg-white font-bold {activeActivity === 'one' + ? 'text-primary-500' + : 'text-gray-500'}">{m.activityOne()}</span + > + <h1 + class="font-bold text-xl {activeActivity === 'one' ? 'text-primary-500' : 'text-gray-500'}" + > + {m.identifyRoTo()} + </h1> + <div class="flex flex-row space-x-1"> + <p class="flex flex-col items-center"> + <span class="text-xs text-gray-500">{m.riskOrigin()}</span> + <span class="font-bold">{roto.risk_origin} /</span> + </p> + <p class="flex flex-col items-center"> + <span class="text-xs text-gray-500">{m.targetObjective()}</span> + <span class="font-bold">{roto.target_objective}</span> + </p> + </div> + <a + href={`${$page.url.pathname}/edit?activity=${activeActivity}&next=${$page.url.pathname}?activity=${activeActivity}`} + class="btn variant-filled-primary h-fit absolute top-2 right-4" + > + <i class="fa-solid fa-pen-to-square mr-2" data-testid="edit-button" /> + {m.edit()} + </a> + </div> + <div + id="activityTwo" + class="relative p-4 space-y-4 rounded-md w-full flex flex-col items-center + {activeActivity === 'two' + ? 'border-2 border-primary-500' + : 'border-2 border-gray-300 border-dashed'}" + > + <span + class="absolute -top-3 bg-white font-bold {activeActivity === 'two' + ? 'text-primary-500' + : 'text-gray-500'}">{m.activityTwo()}</span + > + <h1 + class="font-bold text-xl {activeActivity === 'two' ? 'text-primary-500' : 'text-gray-500'}" + > + {m.evaluateRoTo()} + </h1> + <div class="flex space-x-6"> + <p class="flex flex-col items-center"> + <span class="text-xs text-gray-500">{m.motivation()}</span> + <span class="badge text-sm font-bold">{safeTranslate(roto.motivation)}</span> + </p> + <span>x</span> + <p class="flex flex-col items-center"> + <span class="text-xs text-gray-500">{m.resources()}</span> + <span class="badge text-sm font-bold">{safeTranslate(roto.resources)}</span> + </p> + <span>=</span> + <p class="flex flex-col items-center"> + <span class="text-xs text-gray-500">{m.pertinence()}</span> + <span class="badge text-sm font-bold {pertinenceColor[roto.pertinence]}" + >{safeTranslate(roto.pertinence)}</span + > + </p> + </div> + <p> + <span class="badge bg-violet-200 text-violet-700">{m.activity()}</span> + <span>=</span> + <span class="font-bold">{roto.activity}</span> + </p> + </div> + <div + id="activityThree" + class="relative p-4 space-y-4 rounded-md w-full flex flex-col items-center + {activeActivity === 'three' + ? 'border-2 border-primary-500' + : 'border-2 border-gray-300 border-dashed'}" + > + <span + class="absolute -top-3 bg-white font-bold {activeActivity === 'three' + ? 'text-primary-500' + : 'text-gray-500'}">{m.activityThree()}</span + > + <h1 + class="font-bold text-xl {activeActivity === 'three' + ? 'text-primary-500' + : 'text-gray-500'}" + > + {m.selectRoTo()} + </h1> + <p> + {#if roto.is_selected} + <span class="badge bg-green-200 text-green-700">{m.selected()}</span> + {:else} + <span class="badge bg-red-200 text-red-700">{m.notSelected()}</span> + {/if} + </p> + <div class="w-full p-4 bg-gray-50 border rounded-md shadow-sm"> + <h3 class="font-semibold text-lg text-gray-700 flex items-center space-x-2"> + <i class="fa-solid fa-table text-gray-500 opacity-75"></i> + <span>{m.fearedEvents()}</span> + </h3> + <ModelTable + backgroundColor="bg-gray-50" + regionBody="bg-gray-50" + regionHeadCell="uppercase bg-gray-50 text-gray-700" + source={data.table} + URLModel="feared-events" + ></ModelTable> + </div> + <div class="w-full p-4 bg-gray-50 border rounded-md shadow-sm"> + <h3 class="font-semibold text-lg text-gray-700 flex items-center space-x-2"> + <i class="fa-solid fa-eye text-gray-500 opacity-75"></i> + <span>{m.justification()}</span> + </h3> + {#if roto.justification} + <p class="text-gray-600">{roto.justification}</p> + {:else} + <p class="text-gray-600">{m.noJustification()}</p> + {/if} + </div> + </div> + </div> +</div> diff --git a/frontend/src/routes/(app)/(internal)/ro-to/[id=uuid]/edit/+page.server.ts b/frontend/src/routes/(app)/(internal)/ro-to/[id=uuid]/edit/+page.server.ts new file mode 100644 index 000000000..0c9e15c11 --- /dev/null +++ b/frontend/src/routes/(app)/(internal)/ro-to/[id=uuid]/edit/+page.server.ts @@ -0,0 +1,67 @@ +import { BASE_API_URL } from '$lib/utils/constants'; +import { getModelInfo } from '$lib/utils/crud'; +import { modelSchema } from '$lib/utils/schemas'; +import { superValidate } from 'sveltekit-superforms'; +import { zod } from 'sveltekit-superforms/adapters'; +import type { PageServerLoad, Actions } from '../$types'; +import { defaultWriteFormAction } from '$lib/utils/actions'; + +export const load: PageServerLoad = async (event) => { + const URLModel = 'ro-to'; + const model = getModelInfo(URLModel); + const schema = modelSchema(URLModel); + const objectEndpoint = `${BASE_API_URL}/${model.endpointUrl}/${event.params.id}/object/`; + const objectResponse = await event.fetch(objectEndpoint); + const object = await objectResponse.json(); + + const form = await superValidate(object, zod(schema), { errors: false }); + const foreignKeyFields = model.foreignKeyFields; + const selectFields = model.selectFields; + + const foreignKeys: Record<string, any> = {}; + + if (foreignKeyFields) { + for (const keyField of foreignKeyFields) { + const queryParams = keyField.urlParams + ? `?${keyField.urlParams}${keyField.detail ? object.ebios_rm_study : ''}` + : ''; + const url = `${BASE_API_URL}/${keyField.endpointUrl || keyField.urlModel}/${queryParams}`; + const response = await event.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}`); + } + } + } + + const selectOptions: Record<string, any> = {}; + + if (selectFields) { + for (const selectField of selectFields) { + const url = `${BASE_API_URL}/${model.endpointUrl}/${ + selectField.detail ? event.params.id + '/' : '' + }${selectField.field}/`; + const response = await event.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.foreignKeys = foreignKeys; + model.selectOptions = selectOptions; + return { form, model, object, foreignKeys, selectOptions, URLModel }; +}; + +export const actions: Actions = { + default: async (event) => { + return defaultWriteFormAction({ event, urlModel: 'ro-to', action: 'edit' }); + } +}; diff --git a/frontend/src/routes/(app)/(internal)/ro-to/[id=uuid]/edit/+page.svelte b/frontend/src/routes/(app)/(internal)/ro-to/[id=uuid]/edit/+page.svelte new file mode 100644 index 000000000..ec44e5c21 --- /dev/null +++ b/frontend/src/routes/(app)/(internal)/ro-to/[id=uuid]/edit/+page.svelte @@ -0,0 +1,17 @@ +<script lang="ts"> + import type { PageData } from './$types'; + import ModelForm from '$lib/components/Forms/ModelForm.svelte'; + export let data: PageData; +</script> + +<div class="card p-4 bg-white shadow-lg"> + <ModelForm + customNameDescription + form={data.form} + object={data.object} + selectOptions={data.selectOptions} + foreignKeys={data.foreignKeys} + model={data.model} + context="edit" + /> +</div>