diff --git a/backend/core/base_models.py b/backend/core/base_models.py index 264567870..5e1240797 100644 --- a/backend/core/base_models.py +++ b/backend/core/base_models.py @@ -46,13 +46,19 @@ def is_unique_in_scope(self, scope: models.QuerySet, fields_to_check: list) -> b # to avoid false positives as a result of the object being compared to itself if self.pk: scope = scope.exclude(pk=self.pk) - return not scope.filter( - **{ - f"{field}__iexact": getattr(self, field) - for field in fields_to_check - if hasattr(self, field) - } - ).exists() + filters = {} + for field in fields_to_check: + if hasattr(self, field): + field_value = getattr(self, field) + model_field = self._meta.get_field(field) + + # Use the appropriate lookup based on the field type + if isinstance(model_field, models.ForeignKey): + filters[f"{field}__exact"] = field_value + else: + filters[f"{field}__iexact"] = field_value + + return not scope.filter(**filters).exists() def display_path(self): pass diff --git a/backend/ebios_rm/models.py b/backend/ebios_rm/models.py index 35361e0db..af9277a9c 100644 --- a/backend/ebios_rm/models.py +++ b/backend/ebios_rm/models.py @@ -126,6 +126,8 @@ class FearedEvent(NameDescriptionMixin, FolderMixin): gravity = models.SmallIntegerField(default=-1, verbose_name=_("Gravity")) is_selected = models.BooleanField(verbose_name=_("Is selected"), default=False) justification = models.TextField(verbose_name=_("Justification"), blank=True) + + fields_to_check = ["name", "ref_id"] class Meta: verbose_name = _("Feared event") @@ -223,6 +225,8 @@ class Pertinence(models.IntegerChoices): ) is_selected = models.BooleanField(verbose_name=_("Is selected"), default=False) justification = models.TextField(verbose_name=_("Justification"), blank=True) + + fields_to_check = ["target_objective", "risk_origin"] def __str__(self) -> str: return f"{self.get_risk_origin_display()} - {self.target_objective}" @@ -334,11 +338,16 @@ class Category(models.TextChoices): is_selected = models.BooleanField(verbose_name=_("Is selected"), default=False) justification = models.TextField(verbose_name=_("Justification"), blank=True) + + fields_to_check = ["entity", "category"] class Meta: verbose_name = _("Stakeholder") verbose_name_plural = _("Stakeholders") ordering = ["created_at"] + + def get_scope(self): + return self.__class__.objects.filter(ebios_rm_study=self.ebios_rm_study) def __str__(self): return f"{self.entity.name} - {self.get_category_display()}" @@ -388,11 +397,16 @@ class StrategicScenario(NameDescriptionMixin, FolderMixin): help_text=_("RO/TO couple from which the attach path is derived"), ) ref_id = models.CharField(max_length=100, blank=True) + + fields_to_check = ["name", "ref_id"] class Meta: verbose_name = _("Strategic Scenario") verbose_name_plural = _("Strategic Scenarios") ordering = ["created_at"] + + def get_scope(self): + return self.__class__.objects.filter(ebios_rm_study=self.ebios_rm_study) def save(self, *args, **kwargs): self.folder = self.ebios_rm_study.folder @@ -423,11 +437,16 @@ class AttackPath(NameDescriptionMixin, FolderMixin): ref_id = models.CharField(max_length=100, blank=True) is_selected = models.BooleanField(verbose_name=_("Is selected"), default=False) justification = models.TextField(verbose_name=_("Justification"), blank=True) + + fields_to_check = ["name", "ref_id"] class Meta: verbose_name = _("Attack path") verbose_name_plural = _("Attack paths") ordering = ["created_at"] + + def get_scope(self): + return self.__class__.objects.filter(ebios_rm_study=self.ebios_rm_study) def save(self, *args, **kwargs): self.ebios_rm_study = self.strategic_scenario.ebios_rm_study diff --git a/frontend/src/lib/utils/schemas.ts b/frontend/src/lib/utils/schemas.ts index 2d09b8766..ab3c76d21 100644 --- a/frontend/src/lib/utils/schemas.ts +++ b/frontend/src/lib/utils/schemas.ts @@ -447,13 +447,13 @@ export const StakeholderSchema = z.object({ export const StrategicScenarioSchema = z.object({ ...NameDescriptionMixin, + ebios_rm_study: z.string(), ro_to_couple: z.string().uuid(), ref_id: z.string().optional() }); export const AttackPathSchema = z.object({ ...NameDescriptionMixin, - ebios_rm_study: z.string(), strategic_scenario: z.string().uuid(), stakeholders: z.string().uuid().optional().array().optional(), is_selected: z.boolean().optional(), diff --git a/frontend/src/routes/(app)/(internal)/operational-scenarios/[id=uuid]/+page.svelte b/frontend/src/routes/(app)/(internal)/operational-scenarios/[id=uuid]/+page.svelte index 0a14446a3..cbd6b572f 100644 --- a/frontend/src/routes/(app)/(internal)/operational-scenarios/[id=uuid]/+page.svelte +++ b/frontend/src/routes/(app)/(internal)/operational-scenarios/[id=uuid]/+page.svelte @@ -122,7 +122,7 @@ >

{:else} -

{m.noStakeholders()}

+

{m.noStakeholders()}

{/each}