Skip to content

Conversation

monsieurswag
Copy link
Contributor

@monsieurswag monsieurswag commented Aug 26, 2025

Summary by CodeRabbit

  • New Features
    • Introduced Qualifications management: navigation entry, list/detail views, add/edit (enterprise), and form support.
    • Risk scenarios now select multiple Qualifications via a dynamic endpoint.
    • Library pages display imported Qualifications; library imports now include Qualifications.
    • Tables for Qualifications show ref_id, name, description, and abbreviation.
  • Improvements
    • Enabled Add button for Qualifications and refined permission handling across views.
  • Internationalization
    • Added translations for new Qualification labels across many locales; removed an obsolete key.
  • Documentation
    • Updated data-model notes and enterprise backend run instructions.
  • Tests
    • Added end-to-end Qualification import test and expanded test utilities.

Copy link
Contributor

coderabbitai bot commented Aug 26, 2025

Walkthrough

Implements Qualifications as first-class objects linked to libraries and risk scenarios. Migrates RiskScenario.qualifications to ManyToMany, adds import support via QualificationImporter, surfaces qualifications in library dumps/UI, updates serializers/views, adjusts permission checks to use model objects, expands i18n keys, and adds tests. Includes related migrations and documentation tweaks.

Changes

Cohort / File(s) Summary
Core data model & API
backend/core/models.py, backend/core/serializers.py, backend/core/views.py, backend/core/migrations/0093_qualification_library_and_more.py
Adds Qualification.library, is_published, fields_to_check, is_deletable; changes RiskScenario.qualifications to M2M with data migration; serializers expose/write qualifications and library; removes RiskScenarioViewSet qualifications action; domain-import mapping supports qualification_ids.
Library import pipeline
backend/library/utils.py
Adds QualificationImporter; LibraryImporter handles qualifications; relaxes RiskMatrixImporter validation; minor ThreatImporter cleanup.
Resilience model/migration
backend/resilience/models.py, backend/resilience/migrations/0003_alter_escalationthreshold_qualifications.py
Sets related_name="escalation_thresholds" on EscalationThreshold.qualifications; migration updates field.
Backend serdes
backend/serdes/views.py
Removes enterprise_core permission stripping during load; simplifies post() flow.
Frontend model metadata & access control
frontend/src/lib/utils/crud.ts, frontend/src/lib/utils/access-control.ts, frontend/src/lib/utils/modelinfo-router.ts, enterprise/frontend/src/lib/utils/modelinfo-router.ts, enterprise/frontend/src/lib/utils/models.ts, frontend/src/lib/utils/types.ts
Adds actions to ModelMapEntry; getModelInfo delegates to routeToModelInfo; canPerformAction now takes ModelMapEntry, enforces allowed actions; enterprise overrides actions for qualifications; updates URL_MODEL (reactivations/moves).
Frontend forms & schemas
frontend/src/lib/components/Forms/ModelForm.svelte, frontend/src/lib/components/Forms/ModelForm/QualificationForm.svelte, frontend/src/lib/utils/schemas.ts
Adds QualificationForm and route for 'qualifications'; defines QualificationSchema and maps it; adjusts props for some existing forms.
Frontend tables & components
frontend/src/lib/components/ModelTable/ModelTable.svelte, frontend/src/lib/components/ModelTable/Search.svelte, frontend/src/lib/components/ModelTable/LibraryActions.svelte, frontend/src/lib/utils/table.ts, frontend/src/lib/components/TableRowActions/TableRowActions.svelte, frontend/src/lib/components/DetailView/DetailView.svelte
ModelTable uses getModelInfo; adds data-testids; search input testid change; LibraryActions adds testid; qualifications table columns expand; permission checks pass full model objects; boolean guards for edit display.
Frontend routes: libraries & qualifications
frontend/src/routes/(app)/(internal)/loaded-libraries/[id=uuid]/+page.svelte, frontend/src/routes/(app)/(internal)/stored-libraries/[id=uuid]/+page.svelte, enterprise/frontend/src/lib/components/SideBar/navData.ts, frontend/src/routes/(app)/(internal)/[model=urlmodel]/+page.svelte
Shows qualifications sections in library pages with read-only tables; adds sidebar nav for qualifications; enables Add button for qualifications.
Frontend routes: risk scenarios & other pages
frontend/src/routes/(app)/(internal)/risk-scenarios/[id=uuid]/edit/+page.server.ts, frontend/src/routes/(app)/(internal)/risk-scenarios/[id=uuid]/edit/+page.svelte, frontend/src/routes/(app)/(internal)/risk-scenarios/[id=uuid]/+page.svelte, frontend/src/routes/(app)/(internal)/my-profile/+page.svelte, frontend/src/routes/(app)/(internal)/operating-modes/[id=uuid]/+page.svelte, frontend/src/routes/(app)/(internal)/operational-scenarios/[id=uuid]/+page.svelte, frontend/src/routes/(app)/(internal)/risk-assessments/[id=uuid]/+page.svelte, frontend/src/routes/(app)/(internal)/recap/+page.svelte, frontend/src/routes/(app)/(internal)/incidents/[id=uuid]/+page.svelte, frontend/src/routes/(app)/(third-party)/compliance-assessments/[id=uuid]/+page.svelte, frontend/src/routes/(app)/(third-party)/evidences/[id=uuid]/+page.svelte, frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-1/*/+page.svelte
RiskScenario edit switches qualifications select to endpoint; removes qualificationChoices load; renders qualifications via str; multiple pages update canPerformAction calls to pass model object; my-profile uses getModelInfo('users').
i18n
frontend/messages/*.json
Adds translation keys for qualifications-related UI (abbreviation, qualificationOrdering, securityObjectiveOrdering, qualifications, addQualification); removes qualificationsChartTitle in en/fr.
Frontend tests & utilities
frontend/tests/functional/detailed/qualifications.test.ts, frontend/tests/utils/qualification_test_library.yaml, frontend/tests/e2e-tests.sh, frontend/tests/utilsv2/**/*
New E2E test importing a qualifications library and verifying rows; YAML library fixture; migrate check uses SQLITE_FILE; adds/updates test page objects (ModelTable, ModelTableRow, List/Detail views, enterprise test helpers, visibility filters, navigation waits).
Misc docs/CI
documentation/architecture/data-model.md, enterprise/backend/README.md, .github/workflows/functional-tests.yml, frontend/src/routes/(app)/(internal)/[model=urlmodel]/+layout.server.ts
Notes ordering fields have no effect; updates dev-server command; whitespace-only CI tweak; removes an unused TableSource import (type still referenced).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Admin as Admin User
  participant UI as Library UI
  participant Importer as LibraryImporter
  participant QImp as QualificationImporter
  participant DB as DB

  Admin->>UI: Upload YAML (includes qualifications)
  UI->>Importer: init(library_data)
  Importer->>Importer: init_qualifications([...])
  loop each qualification
    Importer->>QImp: is_valid()
    QImp-->>Importer: None (ok)
  end
  UI->>Importer: import_objects(library_object)
  loop each qualification
    Importer->>QImp: import_qualification(library_object)
    QImp->>DB: create Qualification(library=library_object, ...)
  end
  UI->>DB: Fetch library dump (includes qualifications)
  DB-->>UI: qualifications list
Loading
sequenceDiagram
  autonumber
  actor User
  participant RSPage as Risk Scenario Edit Page
  participant API as API
  participant ACL as canPerformAction

  User->>RSPage: Open edit form
  RSPage->>API: GET /api/qualifications?search=...
  API-->>RSPage: { results: [ {id, name, ...}, ... ] }
  RSPage->>User: AutocompleteSelect(multiselect)
  note over RSPage: On save, sends qualification ids

  RSPage->>ACL: canPerformAction({ user, action:'change', model: ModelMapEntry })
  ACL-->>RSPage: boolean
  RSPage->>User: Enable/disable edit actions
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

data-model, db-migration, i18n

Suggested reviewers

  • Mohamed-Hacene

Poem

.·ˆ·. hop hop! .·ˆ·.
I filed new tags upon the shelves,
Qualifications lined themselves.
Libraries hum, imports sing,
Risk scenarios now take wing.
I twitch my nose—permissions check—
Then thump with joy: no gaps, no wreck! 🐇📚✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/custom_qualifications

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/core/models.py (1)

1-1: Apply Ruff formatting to backend/core/models.py

CI indicates formatting issues in this file. Running

ruff format backend/core/models.py

reformats the file (1 file reformatted). Please commit these formatting changes to ensure the build passes.

• File requiring formatting: backend/core/models.py

🧹 Nitpick comments (21)
frontend/messages/el.json (1)

1666-1669: EL: Verify domain terminology for “qualification” vs existing “qualifications” usage

The three additions are grammatically correct. However, earlier in this file “qualifications” is translated as “Κατηγορίες επιπτώσεων,” which conceptually differs from “προσόντα.” If both refer to the same domain object (Qualifications), there may be a terminology mismatch.

  • If “Qualifications” are impact categories in your domain, consider aligning:
- "qualificationOrdering": "Σειρά κατάταξης προσόντων",
+ "qualificationOrdering": "Σειρά κατηγοριών",
  • If they truly are “qualifications” as in labels/abbreviations, consider updating the earlier “qualifications” entry for consistency instead.
    Would you like me to propose a harmonized terminology sweep for EL across the file?
frontend/messages/en.json (2)

1801-1805: Keys added; consider clarifying “showingItemsWith”.

  • The three new keys are good.
  • Minor: “showingItemsWith” may act as a prefix elsewhere. If it’s meant to be a standalone label, consider “Showing items with:” (with a colon). If it’s concatenated in code, current text is fine. Please confirm usage.

477-477: Fix a few visible typos (outside the changed hunk).

These are small, user-facing strings worth correcting:

  • Line 477: "Approuved" → "Approved"
  • Line 531: "Transfered" → "Transferred"
  • Line 1249: "Refence ID" → "Reference ID"

Apply this diff:

-  "ok": "Approuved",
+  "ok": "Approved",
@@
-  "transfer": "Transfered",
+  "transfer": "Transferred",
@@
-  "refIdSemiColon": "Refence ID:",
+  "refIdSemiColon": "Reference ID:",

Also applies to: 531-531, 1249-1249

frontend/messages/de.json (1)

967-971: Nit: Make the German phrasing more idiomatic.

“Angstereignisse” sounds awkward; “Gefürchtete Ereignisse” is the common rendering. If you agree, adjust the string.

Apply:

-"fearedEventAssetHelpText": "Angstereignisse sind in der Regel mit Geschäftswerten (primäre Vermögenswerte) verknüpft. Unterstützende Vermögenswerte sind hier aus Flexibilitätsgründen erlaubt.",
+"fearedEventAssetHelpText": "Gefürchtete Ereignisse sind in der Regel mit Geschäfts-Assets (primären Assets) verknüpft. Unterstützende Assets sind hier aus Flexibilitätsgründen erlaubt.",
frontend/messages/cs.json (1)

950-950: Minor Czech diacritic fix in help text

“Podporná aktiva” should be “Podpůrná aktiva”.

-"fearedEventAssetHelpText": "Strašné události jsou obvykle spojeny s obchodními hodnotami (primární aktiva). Podporná aktiva jsou zde povolena pro větší flexibilitu.",
+"fearedEventAssetHelpText": "Strašné události jsou obvykle spojeny s obchodními hodnotami (primární aktiva). Podpůrná aktiva jsou zde povolena pro větší flexibilitu.",
frontend/messages/hu.json (1)

962-962: Hungarian terminology tweak for naturalness

Prefer “támogató eszközök” over “támogatási eszközök”.

-"fearedEventAssetHelpText": "A félelemkeltő események általában üzleti értékekkel (elsődleges eszközökkel) vannak összekapcsolva. A támogatási eszközök itt is engedélyezettek a rugalmasság érdekében.",
+"fearedEventAssetHelpText": "A félelemkeltő események általában üzleti értékekkel (elsődleges eszközökkel) vannak összekapcsolva. A támogató eszközök itt is engedélyezettek a rugalmasság érdekében.",
frontend/messages/ur.json (1)

930-930: Improve Urdu phrasing for clarity and domain consistency

Use standard terms for “primary assets” and “supporting assets.”

-"fearedEventAssetHelpText": "خوفناک واقعات عام طور پر کاروباری قیمتیں (پہلی جائیدادیں) سے منسلک ہوتے ہیں۔ ان کے لیے اندرونی جائیدادیں بھی ان کے لچکدار ہونے کی وجہ سے اجازت دی جاتی ہیں۔",
+"fearedEventAssetHelpText": "خوف زدہ واقعات عموماً کاروباری اقدار (پرائمری اثاثے) سے منسلک ہوتے ہیں۔ لچک کے لیے یہاں معاون اثاثوں کی بھی اجازت ہے۔",
frontend/messages/sv.json (1)

953-956: LGTM on new i18n keys; minor Swedish wording nitpicks for clarity

Translations are correct and the JSON structure is valid. For more natural Swedish, consider the tweaks below.

- "fearedEventAssetHelpText": "Rädsla för händelser är vanligtvis länkade till företagsvärden (primära tillgångar). Stödjande tillgångar är tillåtna här för flexibilitet.",
+ "fearedEventAssetHelpText": "Fruktade händelser är vanligtvis kopplade till affärsvärden (primära tillgångar). Stödjande tillgångar är tillåtna här för flexibilitet.",

- "qualificationOrdering": "Kvalifikationsordning",
+ "qualificationOrdering": "Kvalificeringsordning",

- "securityObjectiveOrdering": "Säkerhetsmålsordning"
+ "securityObjectiveOrdering": "Säkerhetsmålens ordning"
frontend/src/lib/components/Forms/ModelForm/QualificationForm.svelte (3)

32-38: Clamp ordering to non-negative integers (min/step) to avoid accidental floats/negatives

Prevents invalid values before they reach validation.

 <NumberField
   {form}
   field="qualification_ordering"
   label={m.qualificationOrdering()}
   cacheLock={cacheLocks['qualification_ordering']}
   bind:cachedValue={formDataCache['qualification_ordering']}
+  min={0}
+  step={1}
 />

39-45: Same as above: enforce sensible bounds for security objective ordering

Mirror constraints for consistency with the other ordering field.

 <NumberField
   {form}
   field="security_objective_ordering"
   label={m.securityObjectiveOrdering()}
   cacheLock={cacheLocks['security_objective_ordering']}
   bind:cachedValue={formDataCache['security_objective_ordering']}
+  min={0}
+  step={1}
 />

18-24: Limit the abbreviation input to 20 characters

QualificationSchema caps abbreviation at 20 characters (z.string().max(20)) in frontend/src/lib/utils/schemas.ts:268, so it’s best to reflect this constraint in the UI to avoid unnecessary validation errors.

• Add a maxlength={20} prop to the <TextField> in frontend/src/lib/components/Forms/ModelForm/QualificationForm.svelte:

 <TextField
   {form}
   field="abbreviation"
   label={m.abbreviation()}
   cacheLock={cacheLocks['abbreviation']}
   bind:cachedValue={formDataCache['abbreviation']}
+  maxlength={20}
 />
frontend/src/lib/utils/table.ts (1)

1467-1469: Expose ref_id/name/description/abbreviation for qualifications — good step; consider minor UX polish

Showing ref_id and abbreviation in the list improves scannability. As a small improvement, consider adding meta identifiers (e.g., id, urn) like other library-backed models to help with debugging and deep-links in future.

Suggested tweak (optional):

 qualifications: {
-    head: ['ref_id', 'name', 'description', 'abbreviation'],
-    body: ['ref_id', 'name', 'description', 'abbreviation']
+    head: ['ref_id', 'name', 'description', 'abbreviation'],
+    body: ['ref_id', 'name', 'description', 'abbreviation'],
+    meta: ['id', 'urn']
 },
backend/resilience/migrations/0003_alter_escalationthreshold_qualifications.py (1)

1-20: CI Formatting Issue Resolved

The migration file has been reformatted with Ruff and now passes all checks—no further action is needed to unblock CI.

Optional: improve readability by pluralizing the reverse relation name. If you choose to rename related_name="escalation_threshold" to related_name="escalation_thresholds", update these locations:

  • In backend/resilience/models.py (EscalationThreshold.qualifications field):
    - related_name="escalation_threshold"
    + related_name="escalation_thresholds"
  • In backend/core/models.py (Qualification.is_deletable):
    - self.escalation_threshold.exists()
    + self.escalation_thresholds.exists()
  • In the migration 0003_alter_escalationthreshold_qualifications.py, update related_name accordingly so that historical migrations remain consistent.

No other references to the singular reverse name were found.

frontend/src/lib/utils/schemas.ts (1)

266-273: Align Zod schema with backend model nullability and defaults

Verified in backend/core/models.py that:

  • abbreviation is declared as models.CharField(max_length=20, null=True, blank=True) (lines 1560–1563), so it must be optional and nullable.
  • annotation is declared as models.TextField(null=True, blank=True) (lines 101–102), so it’s already optional/nullable.
  • qualification_ordering and security_objective_ordering are PositiveSmallIntegerField(default=0) (lines 1563–1567) with no nulls, so they default to 0 on the server.

Please update the frontend Zod schema to match:

 export const QualificationSchema = z.object({
   ...NameDescriptionMixin,
-  abbreviation: z.string().max(20),
-  annotation: z.string().optional().nullable(),
-  qualification_ordering: z.number().min(0).max(32767),
-  security_objective_ordering: z.number().min(0).max(32767)
+  abbreviation: z.string().max(20).optional().nullable(),
+  annotation: z.string().optional().nullable(),
+  qualification_ordering: z.coerce.number().min(0).max(32767).default(0),
+  security_objective_ordering: z.coerce.number().min(0).max(32767).default(0),
 });
  • The .optional().nullable() on abbreviation aligns with null=True, blank=True.
  • Keeping annotation unchanged is correct.
  • Using z.coerce.number().default(0) ensures string inputs are cast to numbers and the default of 0 matches the Django model.
frontend/src/routes/(app)/(internal)/loaded-libraries/[id=uuid]/+page.svelte (1)

189-206: Dropdown block for qualifications — consistent with existing blocks

Non-interactive ModelTable and count-based header follow the established pattern. Nice addition.

If you foresee very large qualification sets, consider lazy rendering (e.g., virtualized rows) or pagination in a follow-up to avoid long DOMs. No change needed now.

backend/core/models.py (3)

909-931: reference_count may overcount; use distinct() for M2M joins

Counts on Incident/FearedEvent/EscalationThreshold can duplicate rows when an object has multiple qualifications from the same library. For accuracy and consistency with RA/CA parts already using distinct(), suggest:

-            + Incident.objects.filter(qualifications__library=self).count()
-            + FearedEvent.objects.filter(qualifications__library=self).count()
-            + EscalationThreshold.objects.filter(qualifications__library=self).count()
+            + Incident.objects.filter(qualifications__library=self).distinct().count()
+            + FearedEvent.objects.filter(qualifications__library=self).distinct().count()
+            + EscalationThreshold.objects.filter(qualifications__library=self).distinct().count()

1062-1068: Typo in ReferenceControl.is_deletable docstring

“refence” → “reference”.

-        Returns True if the refence control can be deleted
+        Returns True if the reference control can be deleted

904-912: Optional: Update reference_count docstring for new sources

Consider mentioning incidents/feared events/escalation thresholds to reflect the broadened count.

-        Returns the number of distinct dependent libraries and risk and compliance assessments that reference objects from this library
+        Returns the number of distinct dependent libraries, assessments, and other domain objects (incidents, feared events, escalation thresholds) that reference objects from this library.
backend/library/utils.py (3)

412-437: QualificationImporter — solid baseline; consider light validation

Importer mirrors Threat/ReferenceControl patterns. Optional: validate ordering fields if present to fail-fast on bad data.

 class QualificationImporter:
     REQUIRED_FIELDS = {"ref_id", "urn", "name"}
@@
     def is_valid(self) -> Union[str, None]:
         if missing_fields := self.REQUIRED_FIELDS - set(self.qualification_data.keys()):
             return "Missing the following fields : {}".format(", ".join(missing_fields))
+        qo = self.qualification_data.get("qualification_ordering")
+        so = self.qualification_data.get("security_objective_ordering")
+        if qo is not None and not isinstance(qo, int):
+            return "qualification_ordering must be an integer"
+        if so is not None and not isinstance(so, int):
+            return "security_objective_ordering must be an integer"

728-733: Incorrect log message label (“Threat import error” for qualifications)

Use a qualification-specific label.

-            if (qualification_import_error := self.init_qualifications(qualification_data)) is not None:
-                logger.error("Threat import error", error=qualification_import_error)
+            if (qualification_import_error := self.init_qualifications(qualification_data)) is not None:
+                logger.error("Qualification import error", error=qualification_import_error)

451-458: Unreachable code in RiskMatrixImporter.is_valid

Early return makes the missing_fields check dead code. Either remove it or move the return after validation.

-    def is_valid(self) -> Union[str, None]:
-        return None  # Do not verify anything for now
-
-        # Create function to check if the "JSON definition" of the matrix is wrong or not, this function will be called within this is_valid function and return an error string is an error occured or return None or success exactly like this one.
-        if missing_fields := self.REQUIRED_FIELDS - set(self.risk_matrix_data.keys()):
-            return "Missing the following fields : {}".format(", ".join(missing_fields))
+    def is_valid(self) -> Union[str, None]:
+        # No validation for now
+        return None
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8050582 and 3bdf22e.

📒 Files selected for processing (34)
  • backend/core/migrations/0091_qualification_library_and_more.py (1 hunks)
  • backend/core/models.py (7 hunks)
  • backend/core/serializers.py (1 hunks)
  • backend/library/utils.py (7 hunks)
  • backend/resilience/migrations/0003_alter_escalationthreshold_qualifications.py (1 hunks)
  • backend/resilience/models.py (1 hunks)
  • frontend/messages/ar.json (1 hunks)
  • frontend/messages/cs.json (1 hunks)
  • frontend/messages/da.json (1 hunks)
  • frontend/messages/de.json (1 hunks)
  • frontend/messages/el.json (1 hunks)
  • frontend/messages/en.json (1 hunks)
  • frontend/messages/es.json (1 hunks)
  • frontend/messages/fr.json (1 hunks)
  • frontend/messages/hi.json (1 hunks)
  • frontend/messages/hu.json (1 hunks)
  • frontend/messages/id.json (1 hunks)
  • frontend/messages/it.json (1 hunks)
  • frontend/messages/nl.json (1 hunks)
  • frontend/messages/pl.json (1 hunks)
  • frontend/messages/pt.json (1 hunks)
  • frontend/messages/ro.json (1 hunks)
  • frontend/messages/sv.json (1 hunks)
  • frontend/messages/tr.json (1 hunks)
  • frontend/messages/uk.json (1 hunks)
  • frontend/messages/ur.json (1 hunks)
  • frontend/src/lib/components/Forms/ModelForm.svelte (2 hunks)
  • frontend/src/lib/components/Forms/ModelForm/QualificationForm.svelte (1 hunks)
  • frontend/src/lib/components/SideBar/navData.ts (1 hunks)
  • frontend/src/lib/utils/crud.ts (1 hunks)
  • frontend/src/lib/utils/schemas.ts (2 hunks)
  • frontend/src/lib/utils/table.ts (1 hunks)
  • frontend/src/routes/(app)/(internal)/[model=urlmodel]/+page.svelte (1 hunks)
  • frontend/src/routes/(app)/(internal)/loaded-libraries/[id=uuid]/+page.svelte (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
backend/resilience/migrations/0003_alter_escalationthreshold_qualifications.py (1)
backend/core/migrations/0091_qualification_library_and_more.py (1)
  • Migration (7-24)
backend/core/migrations/0091_qualification_library_and_more.py (1)
backend/resilience/migrations/0003_alter_escalationthreshold_qualifications.py (1)
  • Migration (6-19)
backend/core/serializers.py (2)
backend/core/models.py (17)
  • Meta (107-108)
  • Meta (162-163)
  • Meta (190-191)
  • Meta (195-197)
  • Meta (992-994)
  • Meta (1058-1060)
  • Meta (1189-1191)
  • Meta (1323-1325)
  • Meta (1570-1573)
  • Meta (1626-1628)
  • Meta (1849-1851)
  • Meta (2398-2406)
  • Meta (2432-2434)
  • Meta (2553-2555)
  • Meta (2772-2774)
  • Meta (2912-2914)
  • Qualification (1431-1594)
backend/core/serializer_fields.py (1)
  • FieldsRelatedField (24-86)
frontend/src/lib/utils/schemas.ts (1)
backend/core/base_models.py (1)
  • NameDescriptionMixin (111-124)
backend/core/models.py (3)
backend/library/helpers.py (1)
  • update_translations_in_object (49-75)
backend/ebios_rm/models.py (1)
  • FearedEvent (248-313)
backend/resilience/models.py (1)
  • EscalationThreshold (176-248)
backend/library/utils.py (1)
backend/core/models.py (3)
  • Qualification (1431-1594)
  • LoadedLibrary (849-971)
  • create (2985-2987)
🪛 GitHub Actions: Backend Linters
backend/resilience/migrations/0003_alter_escalationthreshold_qualifications.py

[error] 1-1: Ruff format check detected formatting issues; this file would be reformatted by running 'ruff format'. Command: 'ruff format --check .'

backend/core/migrations/0091_qualification_library_and_more.py

[error] 1-1: Ruff format check detected formatting issues; this file would be reformatted by running 'ruff format'. Command: 'ruff format --check .'

backend/core/models.py

[error] 1-1: Ruff format check detected formatting issues; this file would be reformatted by running 'ruff format'. Command: 'ruff format --check .'

backend/library/utils.py

[error] 1-1: Ruff format check detected formatting issues; this file would be reformatted by running 'ruff format'. Command: 'ruff format --check .'

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: build (3.12)
  • GitHub Check: build_community_frontend
  • GitHub Check: startup-functional-test (3.12)
  • GitHub Check: enterprise-startup-docker-compose-test
  • GitHub Check: test (3.12)
  • GitHub Check: startup-docker-compose-test
🔇 Additional comments (38)
frontend/messages/fr.json (1)

1762-1766: FR i18n keys coverage & usage verified

• All locale JSON files are valid and include the keys
abbreviation, qualificationOrdering, and securityObjectiveOrdering.
• UI references exist across components (e.g., QualificationForm.svelte, table mappings, RiskMatrix stories).
• French strings read correctly.

Optional nitpick: for consistency, you may prefer a plural label

- "qualificationOrdering": "Ordre de qualification",
+ "qualificationOrdering": "Ordre des qualifications",
frontend/messages/id.json (1)

1150-1153: Localization keys added and usage verified

  • Confirmed that frontend/messages/id.json contains the new keys: abbreviation, qualificationOrdering, and securityObjectiveOrdering.
  • Verified these keys are referenced in frontend/src/lib/components/Forms/ModelForm/QualificationForm.svelte at lines 21, 35, and 42.
  • Minor style nit: apply sentence case to the “deselect” translation to align with Indonesian UI conventions.
- "deselect": "Batalkan Pilihan",
+ "deselect": "Batalkan pilihan",

Code changes look good—thanks for adding the translations and ensuring they’re used correctly.

frontend/messages/pt.json (1)

960-963: PT: New qualification-related labels are accurate and consistent

  • “Abreviação”, “Ordenação de qualificações” and “Ordenação dos objetivos de segurança” read well and match the feature intent.
  • Trailing comma before the new keys keeps the JSON valid.
frontend/messages/nl.json (1)

963-966: NL: Added keys read naturally; JSON punctuation is correct

  • “Afkorting”, “Kwalificatievolgorde”, and “Volgorde van beveiligingsdoelen” are clear.
  • The comma adjustment before these keys preserves JSON validity.
frontend/messages/ro.json (1)

959-962: New qualification keys translated correctly; JSON remains valid.

  • "Abreviere", "Ordonarea calificărilor", and "Ordonarea obiectivelor de securitate" read well and match the intended semantics.
  • The preceding fearedEventAssetHelpText now ends with a comma, keeping the JSON valid.
frontend/messages/tr.json (1)

1702-1705: Turkish translations look good and consistent with UI usage.

  • "İlişkili kampanyalar", "Kısaltma", "Nitelik sıralaması", "Güvenlik hedefi sıralaması" are clear and idiomatic.
  • No trailing comma after the last key; JSON stays valid.
frontend/messages/ar.json (1)

930-933: Arabic additions are accurate; RTL text is fine.

  • "اختصار", "ترتيب المؤهلات", and "ترتيب الأهداف الأمنية" correctly convey the intended meanings.
  • Comma added to the previous line keeps JSON valid.
frontend/messages/it.json (1)

1340-1343: Italian translations are clear and consistent.

  • "Abbreviazione", "Ordinamento delle qualifiche", "Ordinamento degli obiettivi di sicurezza" look correct.
  • Placement after "Deseleziona" and commas are correct; JSON valid.
frontend/messages/hi.json (1)

931-935: LGTM: Hindi translations read naturally and align with new fields.

No issues spotted for abbreviation/ordering keys.

frontend/messages/es.json (1)

967-971: All locales updated with new i18n keys

Verified that the three new keys—abbreviation, qualificationOrdering, and securityObjectiveOrdering—are present in every frontend/messages/*.json file (exit code 0, no missing entries). No further action required; ready to merge.

frontend/messages/cs.json (2)

951-953: New qualification keys added correctly

Keys and Czech translations for abbreviation and the two orderings look good and align with the PR’s new UI fields.


950-953: Locale messages consistency and integrity verified

All JSON locale files parse without errors, and each one includes the new keys abbreviation, qualificationOrdering, and securityObjectiveOrdering. No missing entries were detected—no further action is needed.

frontend/messages/hu.json (1)

963-965: New qualification keys and translations look correct

Hungarian translations for the three new keys read well and are consistent with the rest of the file.

frontend/messages/da.json (2)

1246-1248: Danish translations for the new qualification keys are spot on

Good wording and casing; matches existing style.


1245-1245: JSON delimiter fix acknowledged

Adding the trailing comma after "deselect" is necessary and keeps the object valid when appending new keys.

frontend/messages/ur.json (1)

931-933: New qualification keys added with appropriate Urdu translations

The three added labels map correctly to the new fields.

frontend/messages/pl.json (2)

1709-1711: Polish translations for new keys look correct

“Skrót”, “Kolejność kwalifikacji”, and “Kolejność celów bezpieczeństwa” are accurate and consistent.


1707-1707: Delimiter added correctly before new keys

The trailing comma after “recentlyUpdatedAudits” correctly prepares for the appended properties.

frontend/src/lib/components/Forms/ModelForm/QualificationForm.svelte (1)

1-46: LGTM: focused, consistent form component

Component plugs cleanly into existing form controls and i18n. Props and binding patterns match the broader ModelForm ecosystem.

frontend/src/lib/components/Forms/ModelForm.svelte (1)

75-75: Import looks correct and consistent with patterns used for other model-specific forms

frontend/src/routes/(app)/(internal)/[model=urlmodel]/+page.svelte (1)

118-118: LGTM: Add button enabled for qualifications

Removing 'qualifications' from the disabled list correctly exposes the Create flow for this model and matches the new form integration.

frontend/src/lib/utils/crud.ts (2)

724-745: Qualification detail view: format timestamps and render library nicely

  • created_at/updated_at aren’t tagged as datetime, so they’ll render as raw strings. Use type: 'datetime' for consistency with other models.
  • The detail shows library but this entry lacks a foreignKeyFields mapping, so UI may display raw IDs or “[object Object]” depending on the serializer shape. Since backend links Qualification.library -> core.loadedlibrary, wire it to 'loaded-libraries' for proper display and navigation.

Proposed patch:

 qualifications: {
   name: 'qualification',
   localName: 'qualification',
   localNamePlural: 'qualifications',
   verboseName: 'Qualification',
   verboseNamePlural: 'Qualifications',
   detailViewFields: [
     { field: 'name' },
     { field: 'description' },
     { field: 'annotation' },
-    { field: 'created_at' },
-    { field: 'updated_at' },
+    { field: 'created_at', type: 'datetime' },
+    { field: 'updated_at', type: 'datetime' },
     { field: 'urn' },
     { field: 'id' },
     { field: 'provider' },
     { field: 'locale' },
     { field: 'default_locale' },
     { field: 'abbreviation' },
     { field: 'qualification_ordering' },
     { field: 'security_objective_ordering' },
     { field: 'library' }
-  ]
+  ],
+  foreignKeyFields: [
+    { field: 'library', urlModel: 'loaded-libraries' }
+  ]
 }

[ suggest_optional_refactor ]


724-745: The scripts above will locate the ReferentialSerializer and FieldsRelatedField definitions to confirm which fields are exposed by the QualificationReadSerializer. Once we have that context, we can verify if provider, locale, default_locale, and library are included in the API output.

frontend/src/lib/utils/schemas.ts (1)

954-955: Registering QualificationSchema — LGTM

The schema is correctly added to SCHEMA_MAP under qualifications. This will enable form validation for the new model.

frontend/src/routes/(app)/(internal)/loaded-libraries/[id=uuid]/+page.svelte (2)

25-25: Wiring qualifications from library objects — LGTM

Pulling qualifications from data.library.objects keeps this page aligned with the updated library serializer.


69-74: Qualifications table source mirrors list fields — consistent and clear

Head/body mappings align with the new list columns and translations (ref, name, description, abbreviation). Looks good.

backend/core/migrations/0091_qualification_library_and_more.py (3)

14-18: Add Qualification.library FK — consistent with existing referentials

Optional, nullable FK with CASCADE and a clear related_name aligns with Threat/ReferenceControl patterns. Good addition.


19-23: is_published default=True — confirm intended behavior for new rows

Altering the default affects only newly created rows. If you expect existing rows to flip to True, you’ll need a data migration. Otherwise, LGTM.


1-1: Migration file formatting applied

The migration file 0091_qualification_library_and_more.py has been successfully reformatted with Ruff, and the CI formatting check should now pass without issues. No further action is required.

backend/core/serializers.py (2)

1545-1549: QualificationWriteSerializer — minimal and consistent

Excluding only translations matches Threat/ReferenceControl write serializers. No concerns.


1551-1556: Expose library on Qualification reads — consistent with other referentials

library = FieldsRelatedField(["name","id"]) matches Framework/Threat/ReferenceControl. Good.

backend/core/models.py (4)

887-891: Include qualifications in LoadedLibrary._objects export

Matches threats/reference_controls export and front-end usage. Good.


996-1000: Docstring fix for Threat.is_deletable — LGTM

Clearer docstring; behavior unchanged. Good.


1550-1556: Qualification.library/is_published fields — correct modeling

Optional FK with related_name and published flag keeps parity with other referentials. Good addition.


1575-1586: Qualification.is_deletable — sensible guard

Covers incidents, feared events, and escalation thresholds via related_name. Nice.

Please ensure resilience migration updated EscalationThreshold.qualifications with related_name="escalation_threshold" (it appears so). If not, is_deletable would raise.

backend/library/utils.py (3)

568-589: init_qualifications — consistent error aggregation

Matches init_threats/reference_controls patterns. LGTM.


825-827: Wire qualifications into import_objects — LGTM

Objects are created after library creation; consistent with other object types.


1-1: Formatting issues resolved
backend/library/utils.py has been successfully reformatted with ruff format. CI should now pass without errors.

Comment on lines 187 to 191
qualifications = models.ManyToManyField(
Qualification,
blank=True,
related_name="escalation_threshold",
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Prefer plural related_name and verify downstream usages

Using a plural reverse name is clearer for a ManyToMany. Also verify no code still relies on the default escalationthreshold_set.

Suggested rename (requires a migration and code updates):

 qualifications = models.ManyToManyField(
     Qualification,
     blank=True,
-    related_name="escalation_threshold",
+    related_name="escalation_thresholds",
 )

Audit for usages to update:

If you decide to keep the singular name for backward compatibility, consider adding a brief comment explaining the choice to avoid future confusion.


🏁 Script executed:

#!/bin/bash
# Find reverse-relations/old default usages to update
rg -n "escalationthreshold_set\b|\.escalation_threshold\b|\.escalation_thresholds\b" -C2 --type=py --type=ts --type=tsx --type=js --type=svelte

Length of output: 173


🏁 Script executed:

#!/bin/bash
# Re-run search across relevant file extensions using glob patterns
rg -n '\b(escalationthreshold_set|\.escalation_threshold\b|\.escalation_thresholds\b)' -C2 \
  -g '*.py' -g '*.js' -g '*.ts' -g '*.jsx' -g '*.tsx' -g '*.svelte'

Length of output: 871


Rename M2M related_name to plural and update downstream references

A plural reverse name avoids confusion and matches Django conventions for ManyToMany relations. After renaming related_name="escalation_thresholds", you must update every place the old singular name is used:

• backend/core/models.py (around line 1585)

-    self.escalation_threshold.exists(),
+    self.escalation_thresholds.exists(),

• Add a Django migration to rename the relation and update any fixtures or serialized data that reference escalation_threshold.

No other occurrences of .escalation_threshold or the default .escalationthreshold_set were found in the codebase; once this change is in place and migrations are applied, please run your test suite to ensure all reverse-lookups resolve correctly.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
backend/resilience/models.py lines 187-191: rename the ManyToManyField
related_name from "escalation_threshold" to the plural "escalation_thresholds"
and update every downstream reference (e.g., backend/core/models.py around line
1585) to use .escalation_thresholds; add a Django migration that alters the
field to set the new related_name (use an AlterField migration) and include any
data/fixture updates or a small RunPython step if serialized fixtures reference
the old name, then run the test suite to verify all reverse lookups resolve
correctly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (8)
frontend/messages/id.json (1)

1152-1155: Refine Indonesian sentence casing for “deselect”

  • In frontend/messages/id.json (around line 1152), update:
    -  "deselect": "Batalkan Pilihan",
    +  "deselect": "Batalkan pilihan",
frontend/messages/pl.json (1)

1709-1712: Natural Polish phrasing for “recentlyUpdatedAudits”; qualification keys look good.

  • “Niedawno zaktualizowane audyty” is understandable, but “Ostatnio zaktualizowane audyty” is the more idiomatic UI phrase.

Apply this diff:

-  "recentlyUpdatedAudits": "Niedawno zaktualizowane audyty",
+  "recentlyUpdatedAudits": "Ostatnio zaktualizowane audyty",
frontend/src/routes/(app)/(internal)/[model=urlmodel]/+page.svelte (2)

81-93: Avoid list drift: centralize the "no-add" URL models and reuse in both places

The same model list is duplicated in the hotkey handler and the template condition. Extracting it avoids future divergence.

Apply these diffs in-place:

-      if (
-        ![
-          'risk-matrices',
-          'frameworks',
-          'requirement-mapping-sets',
-          'user-groups',
-          'role-assignments'
-        ].includes(URLModel)
-      ) {
+      if (!DISABLE_ADD_URL_MODELS.has(URLModel)) {
         modalCreateForm();
       }
-{#if !['risk-matrices', 'frameworks', 'requirement-mapping-sets', 'user-groups', 'role-assignments'].includes(URLModel)}
+{#if !DISABLE_ADD_URL_MODELS.has(URLModel)}

Add this small helper near the top of the script (outside functions):

// Consolidated list of models where "Add" is disabled in UI
const DISABLE_ADD_URL_MODELS = new Set([
  'risk-matrices',
  'frameworks',
  'requirement-mapping-sets',
  'user-groups',
  'role-assignments'
]);

Also applies to: 119-119


136-147: Confirm whether qualifications should also expose a "Libraries" import action

The PR summary notes qualifications are integrated into the loaded-libraries flow. If users are expected to import qualifications from libraries (like threats/reference-controls), consider adding it here.

If confirmed, minimally extend the check and keep a safe title fallback:

-{#if ['threats', 'reference-controls'].includes(URLModel)}
+{#if ['threats', 'reference-controls', 'qualifications'].includes(URLModel)}
   {@const title =
-    URLModel === 'threats' ? m.importThreats() : m.importReferenceControls()}
+    URLModel === 'threats'
+      ? m.importThreats()
+      : URLModel === 'reference-controls'
+        ? m.importReferenceControls()
+        : m.libraries()}

If a specific translation exists (e.g., m.importQualifications()), swap the fallback m.libraries() accordingly.

backend/core/models.py (2)

1064-1066: Typo in docstring (“refence” → “reference”).

Small wording fix.

-        Returns True if the refence control can be deleted
+        Returns True if the reference control can be deleted

1577-1587: Short-circuit the existence checks to avoid 3 queries when the first is True.

The current any([...]) builds a list, issuing all three .exists() queries every time. Use or-chaining for short-circuit behavior.

-        return not any(
-            [
-                self.incidents.exists(),
-                self.feared_events.exists(),
-                self.escalation_thresholds.exists(),
-            ]
-        )
+        return not (
+            self.incidents.exists()
+            or self.feared_events.exists()
+            or self.escalation_thresholds.exists()
+        )
backend/library/utils.py (2)

414-444: QualificationImporter mirrors model fields; consider update_or_create to be idempotent.

Current create() is fine because StoredLibrary.load prevents re-import; if future flows re-run imports for the same version, switching to update_or_create keyed on (urn, locale) would make this idempotent.

Example:

-        Qualification.objects.create(
+        Qualification.objects.update_or_create(
+            urn=self.qualification_data["urn"].lower(),
+            locale=library_object.locale,
+            defaults={
+                "library": library_object,
+                "ref_id": self.qualification_data["ref_id"],
+                "name": self.qualification_data["name"],
+                "description": self.qualification_data.get("description"),
+                "abbreviation": self.qualification_data.get("abbreviation"),
+                "qualification_ordering": self.qualification_data.get("qualification_ordering", 0),
+                "security_objective_ordering": self.qualification_data.get("security_objective_ordering", 0),
+                "provider": library_object.provider,
+                "is_published": True,
+                "translations": self.qualification_data.get("translations", {}),
+                "default_locale": library_object.default_locale,
+            }
-        )
+        )

735-744: Wrong log label: “Threat import error” for qualifications.

Adjust the logger context for accurate diagnostics.

-                logger.error("Threat import error", error=qualification_import_error)
+                logger.error("Qualification import error", error=qualification_import_error)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3bdf22e and ffb5647.

📒 Files selected for processing (28)
  • backend/core/migrations/0091_qualification_library_and_more.py (1 hunks)
  • backend/core/models.py (7 hunks)
  • backend/core/serializers.py (1 hunks)
  • backend/library/utils.py (7 hunks)
  • backend/resilience/migrations/0003_alter_escalationthreshold_qualifications.py (1 hunks)
  • backend/resilience/models.py (1 hunks)
  • frontend/messages/ar.json (1 hunks)
  • frontend/messages/cs.json (1 hunks)
  • frontend/messages/da.json (1 hunks)
  • frontend/messages/de.json (1 hunks)
  • frontend/messages/en.json (1 hunks)
  • frontend/messages/es.json (1 hunks)
  • frontend/messages/fr.json (1 hunks)
  • frontend/messages/hi.json (1 hunks)
  • frontend/messages/hu.json (1 hunks)
  • frontend/messages/id.json (1 hunks)
  • frontend/messages/it.json (1 hunks)
  • frontend/messages/nl.json (1 hunks)
  • frontend/messages/pl.json (1 hunks)
  • frontend/messages/pt.json (1 hunks)
  • frontend/messages/ro.json (1 hunks)
  • frontend/messages/sv.json (1 hunks)
  • frontend/messages/uk.json (1 hunks)
  • frontend/messages/ur.json (1 hunks)
  • frontend/src/lib/components/Forms/ModelForm.svelte (2 hunks)
  • frontend/src/lib/utils/crud.ts (1 hunks)
  • frontend/src/lib/utils/table.ts (1 hunks)
  • frontend/src/routes/(app)/(internal)/[model=urlmodel]/+page.svelte (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (23)
  • frontend/messages/ro.json
  • frontend/messages/pt.json
  • frontend/messages/hu.json
  • frontend/src/lib/utils/crud.ts
  • frontend/messages/cs.json
  • frontend/messages/de.json
  • frontend/messages/es.json
  • frontend/messages/sv.json
  • frontend/messages/it.json
  • frontend/messages/da.json
  • frontend/src/lib/utils/table.ts
  • frontend/messages/uk.json
  • frontend/messages/ar.json
  • frontend/messages/nl.json
  • backend/resilience/migrations/0003_alter_escalationthreshold_qualifications.py
  • backend/resilience/models.py
  • frontend/messages/fr.json
  • backend/core/serializers.py
  • frontend/messages/ur.json
  • frontend/messages/en.json
  • frontend/messages/hi.json
  • frontend/src/lib/components/Forms/ModelForm.svelte
  • backend/core/migrations/0091_qualification_library_and_more.py
🧰 Additional context used
🧬 Code graph analysis (2)
backend/core/models.py (3)
backend/library/helpers.py (1)
  • update_translations_in_object (49-75)
backend/ebios_rm/models.py (1)
  • FearedEvent (248-313)
backend/resilience/models.py (1)
  • EscalationThreshold (176-248)
backend/library/utils.py (1)
backend/core/models.py (3)
  • Qualification (1431-1596)
  • LoadedLibrary (849-971)
  • create (2987-2989)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: build (3.12)
  • GitHub Check: enterprise-startup-docker-compose-test
  • GitHub Check: build_enterprise_frontend
  • GitHub Check: build_community_frontend
  • GitHub Check: enterprise-startup-functional-test (3.12)
  • GitHub Check: startup-docker-compose-test
  • GitHub Check: test (3.12)
  • GitHub Check: startup-functional-test (3.12)
🔇 Additional comments (11)
frontend/src/routes/(app)/(internal)/[model=urlmodel]/+page.svelte (1)

42-43: i18n key check for add-qualification

This composes a key from model.localName. For "qualification", ensure the key add-qualification exists across locales to avoid showing the raw key in the UI.

You can reuse the script in my first comment; it flags the key if missing. If it’s not present everywhere yet, I can generate a patch for the locale files.

backend/core/models.py (5)

887-891: Good addition: surface qualifications in LoadedLibrary._objects.

Serializing related qualifications with translations keeps parity with threats/reference_controls. Meta.ordering on Qualification will give a stable order here.


909-910: Lazy imports to avoid circular deps are fine here.

Importing FearedEvent/EscalationThreshold within the property keeps module load safe. No action needed.


998-1000: Docstring clarity is fine.

The updated description accurately reflects the method’s intent.


1550-1559: Qualification.library FK and is_published: sensible defaults.

  • CASCADE aligns with library-scoped referentials.
  • null/blank True keeps room for global/default qualifications.

1575-1575: fields_to_check looks appropriate.

Refers to stable identifiers used across the codebase.

backend/library/utils.py (5)

22-23: Expose Qualification in import surface.

Required for the new importer; looks correct.


498-505: Adding "qualifications" to OBJECT_FIELDS is consistent with the new importer.

Keeps backward-compat logic intact for other fields.


516-517: Initialize _qualifications list.

Matches patterns used for other object types.


575-596: init_qualifications: validation pipeline is consistent and error messaging is clear.

Mirrors threats/reference_controls init with indexed error reporting.


836-838: Persisting qualifications during import: LGTM.

Import order (threats → reference_controls → qualifications → risk_matrices → frameworks → mappings) is acceptable; no dependency issues.

Comment on lines +928 to 931
+ Incident.objects.filter(qualifications__library=self).count()
+ FearedEvent.objects.filter(qualifications__library=self).count()
+ EscalationThreshold.objects.filter(qualifications__library=self).count()
+ LoadedLibrary.objects.filter(dependencies=self).distinct().count()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

reference_count may over-count due to M2M duplicates — use distinct().

Counting Incidents/FearedEvents/EscalationThresholds that reference any Qualification from this library via M2M can produce duplicate rows in the join. Align with the earlier distinct() usage in this method.

Apply this diff:

-            + Incident.objects.filter(qualifications__library=self).count()
-            + FearedEvent.objects.filter(qualifications__library=self).count()
-            + EscalationThreshold.objects.filter(qualifications__library=self).count()
+            + Incident.objects.filter(qualifications__library=self).distinct().count()
+            + FearedEvent.objects.filter(qualifications__library=self).distinct().count()
+            + EscalationThreshold.objects.filter(qualifications__library=self).distinct().count()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
+ Incident.objects.filter(qualifications__library=self).count()
+ FearedEvent.objects.filter(qualifications__library=self).count()
+ EscalationThreshold.objects.filter(qualifications__library=self).count()
+ LoadedLibrary.objects.filter(dependencies=self).distinct().count()
Incident.objects.filter(qualifications__library=self).distinct().count()
FearedEvent.objects.filter(qualifications__library=self).distinct().count()
EscalationThreshold.objects.filter(qualifications__library=self).distinct().count()
LoadedLibrary.objects.filter(dependencies=self).distinct().count()
🤖 Prompt for AI Agents
In backend/core/models.py around lines 928 to 931, the reference_count summation
may over-count because the Incident, FearedEvent, and EscalationThreshold
queries join through Qualifications (M2M) and can return duplicate rows; modify
each of those three queryset calls to apply distinct() before count() (matching
the existing LoadedLibrary.distinct().count()) so duplicates from the M2M join
are eliminated and counts are accurate.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
frontend/messages/da.json (1)

1248-1251: New Danish translations look good; consider slightly lighter phrasing for “ordering”

Current compounds are correct but a bit heavy. Optional tweak for readability:

-	"qualificationOrdering": "Kvalifikationsrækkefølge",
-	"securityObjectiveOrdering": "Sikkerhedsmålsrækkefølge",
+	"qualificationOrdering": "Rækkefølge af kvalifikationer",
+	"securityObjectiveOrdering": "Rækkefølge af sikkerhedsmål",
frontend/messages/sv.json (1)

956-959: Align terminology for “qualification” across Swedish strings

Existing key "qualification" (Line 121) is translated as “Kvalificering”, but new strings use “Kvalifikation”. Choose one term and apply consistently.

Option A (align to existing “Kvalificering”):

 "abbreviation": "Förkortning",
-"qualificationOrdering": "Kvalifikationsordning",
-"securityObjectiveOrdering": "Säkerhetsmålsordning",
-"addQualification": "Lägg till kvalifikation"
+"qualificationOrdering": "Kvalificeringsordning",
+"securityObjectiveOrdering": "Säkerhetsmålens ordning",
+"addQualification": "Lägg till kvalificering"

Option B (switch to “Kvalifikation” globally): keep these lines as-is and update Line 121 elsewhere in this file to:

"qualification": "Kvalifikation"

If adopting Option B, confirm no user-facing copy relies on the “process of qualifying” meaning. Otherwise, Option A is safer.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ffb5647 and 4b8d574.

📒 Files selected for processing (25)
  • backend/core/models.py (7 hunks)
  • backend/core/serializers.py (1 hunks)
  • frontend/messages/ar.json (1 hunks)
  • frontend/messages/cs.json (1 hunks)
  • frontend/messages/da.json (1 hunks)
  • frontend/messages/de.json (1 hunks)
  • frontend/messages/el.json (1 hunks)
  • frontend/messages/en.json (1 hunks)
  • frontend/messages/es.json (1 hunks)
  • frontend/messages/fr.json (1 hunks)
  • frontend/messages/hi.json (1 hunks)
  • frontend/messages/hu.json (1 hunks)
  • frontend/messages/id.json (1 hunks)
  • frontend/messages/it.json (1 hunks)
  • frontend/messages/nl.json (1 hunks)
  • frontend/messages/pl.json (1 hunks)
  • frontend/messages/pt.json (1 hunks)
  • frontend/messages/ro.json (1 hunks)
  • frontend/messages/sv.json (1 hunks)
  • frontend/messages/tr.json (1 hunks)
  • frontend/messages/uk.json (1 hunks)
  • frontend/messages/ur.json (1 hunks)
  • frontend/src/lib/utils/crud.ts (1 hunks)
  • frontend/src/lib/utils/schemas.ts (2 hunks)
  • frontend/src/lib/utils/table.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (23)
  • frontend/messages/ro.json
  • frontend/messages/hi.json
  • frontend/src/lib/utils/table.ts
  • frontend/messages/es.json
  • frontend/messages/de.json
  • frontend/messages/nl.json
  • frontend/messages/uk.json
  • frontend/messages/cs.json
  • frontend/messages/id.json
  • frontend/messages/en.json
  • frontend/src/lib/utils/crud.ts
  • frontend/src/lib/utils/schemas.ts
  • frontend/messages/tr.json
  • frontend/messages/hu.json
  • frontend/messages/pl.json
  • backend/core/models.py
  • frontend/messages/ar.json
  • frontend/messages/pt.json
  • backend/core/serializers.py
  • frontend/messages/el.json
  • frontend/messages/fr.json
  • frontend/messages/it.json
  • frontend/messages/ur.json
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: startup-docker-compose-test
  • GitHub Check: enterprise-startup-docker-compose-test
  • GitHub Check: enterprise-startup-functional-test (3.12)
  • GitHub Check: build_community_frontend
  • GitHub Check: build_enterprise_frontend
🔇 Additional comments (2)
frontend/messages/da.json (1)

1247-1247: Comma after deselect is correct; JSON remains valid

No issues. This enables adding the following keys cleanly.

frontend/messages/sv.json (1)

955-955: JSON Syntax Check Passed

All locale JSON files, including frontend/messages/sv.json, parse successfully with no errors. The trailing comma after fearedEventAssetHelpText is already present, so no fix is needed and the file is valid JSON.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
backend/core/migrations/0092_qualification_library_and_more.py (2)

13-23: Confirm cascade semantics; consider SET_NULL to preserve Qualifications on library deletion

Current on_delete=CASCADE will delete Qualifications when a LoadedLibrary is removed. Given the field is nullable (blank=True, null=True) and Qualifications are referenced by EscalationThresholds, verify that cascading deletes are desired. If you prefer to retain Qualifications (and simply break the link), switch to SET_NULL.

-                on_delete=django.db.models.deletion.CASCADE,
+                on_delete=django.db.models.deletion.SET_NULL,

24-28: Make the default DB-backed; optionally backfill if policy changes

If the intent is “new Qualifications default to published,” add db_default=True so inserts outside the ORM (and ORM inserts avoiding explicit values) are consistent and cheaper. If historical rows need flipping, add a data migration; otherwise leave as-is.

-            field=models.BooleanField(default=True, verbose_name="published"),
+            field=models.BooleanField(default=True, db_default=True, verbose_name="published"),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4b8d574 and 5fb360a.

📒 Files selected for processing (2)
  • backend/core/migrations/0092_qualification_library_and_more.py (1 hunks)
  • backend/resilience/migrations/0003_alter_escalationthreshold_qualifications.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/resilience/migrations/0003_alter_escalationthreshold_qualifications.py
🧰 Additional context used
🧬 Code graph analysis (1)
backend/core/migrations/0092_qualification_library_and_more.py (1)
backend/resilience/migrations/0003_alter_escalationthreshold_qualifications.py (1)
  • Migration (6-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: enterprise-startup-functional-test (3.12)
  • GitHub Check: enterprise-startup-docker-compose-test
  • GitHub Check: startup-functional-test (3.12)
  • GitHub Check: startup-docker-compose-test
  • GitHub Check: build_enterprise_frontend
  • GitHub Check: build_community_frontend
  • GitHub Check: build (3.12)
  • GitHub Check: test (3.12)
  • GitHub Check: Analyze (python)
🔇 Additional comments (1)
backend/core/migrations/0092_qualification_library_and_more.py (1)

8-10: Dependencies look correct

Depends on core.0091 and unblocks resilience migration that targets this field. No issues.

@Mohamed-Hacene Mohamed-Hacene changed the title feat: Add custom qualifications feat: add custom qualifications Aug 28, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
.github/workflows/backend-coverage.yaml (2)

49-55: Optional: add Poetry caching to speed up CI.

Cache Poetry downloads and the virtualenv to cut minutes off installs.

       - uses: actions/checkout@v3
+      - name: Cache Poetry and virtualenv
+        uses: actions/cache@v4
+        with:
+          path: |
+            ~/.cache/pypoetry
+            ./.venv
+          key: ${{ runner.os }}-poetry-${{ hashFiles('backend/poetry.lock') }}
+          restore-keys: |
+            ${{ runner.os }}-poetry-
       - name: Install Poetry via pipx
         shell: bash
         run: |
           set -euo pipefail
           python -m pip install --upgrade pip
           python -m pip install --upgrade pipx
           python -m pipx ensurepath
           echo "$HOME/.local/bin" >> "$GITHUB_PATH"
           python -m pipx install poetry
       - name: Install backend requirements
         working-directory: ${{ env.backend-directory }}
-        run: poetry install
+        env:
+          POETRY_VIRTUALENVS_IN_PROJECT: "true"
+        run: poetry install --no-interaction --no-ansi

Also applies to: 67-69


78-83: Optional: generate/upload coverage XML for PR tooling.

If you plan to gate PRs or integrate with Codecov/GitHub summary, emit XML and upload.

       - name: Display coverage report
         working-directory: ${{ env.backend-directory }}
         env:
-          DATABASE_URL: "postgres://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/postgres"
-        run: poetry run coverage report
+          DATABASE_URL: "postgres://postgres:postgres@localhost:${{ job.services.postgres.ports['5432'] }}/postgres"
+        run: |
+          poetry run coverage report
+          poetry run coverage xml
+      - name: Upload coverage artifact
+        uses: actions/upload-artifact@v4
+        with:
+          name: backend-coverage-xml
+          path: backend/coverage.xml
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5fb360a and 54f2337.

📒 Files selected for processing (1)
  • .github/workflows/backend-coverage.yaml (1 hunks)
🧰 Additional context used
🪛 actionlint (1.7.7)
.github/workflows/backend-coverage.yaml

81-81: property access of object must be type of string but got "number"

(expression)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: build_enterprise_frontend
  • GitHub Check: build_community_frontend
  • GitHub Check: build (3.12)
  • GitHub Check: test (3.12)
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (1)
.github/workflows/backend-coverage.yaml (1)

77-77: Good change: run tests via Poetry and simplify coverage commands.

Using poetry run with pytest/coverage is cleaner and leverages default test discovery. No issues spotted.

Also applies to: 82-82

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
frontend/src/routes/(app)/(third-party)/requirement-assessments/[id=uuid]/edit/+page.server.ts (3)

195-213: Do not send noRedirect to the backend; set JSON header; throw redirect.

noRedirect is a frontend-only flag; sending it likely breaks DRF serializers. Also add Content-Type and use throw redirect.

-    const formData = form.data;
+    const { noRedirect, ...payload } = form.data;

     const requestInitOptions: RequestInit = {
       method: 'PUT',
-      body: JSON.stringify(formData)
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify(payload)
     };

     const response = await event.fetch(endpoint, requestInitOptions);

     if (!response.ok) return handleErrorResponse({ event, response, form });

     const object = await response.json();
     const model: string = urlParamModelVerboseName(URLModel);
     setFlash({ type: 'success', message: m.successfullySavedObject({ object: model }) }, event);
-    if (formData.noRedirect) return;
-    redirect(
+    if (noRedirect) return;
+    throw redirect(
       302,
       getSecureRedirect(event.url.searchParams.get('next')) ||
         `/compliance-assessments/${object.compliance_assessment}/`
     );

90-92: Fix undefined identifier: response is not in scope.

The else branch logs response.statusText but response doesn’t exist here.

-        } else {
-          console.error(`Failed to fetch data for ${selectField.field}: ${response.statusText}`);
-        }
+        } else {
+          console.error(`Failed to fetch data for ${selectField.field} from ${url}`);
+        }

48-51: Guard against undefined evidences.

If evidences is null/undefined, .map will throw.

-  object.evidences = object.evidences.map((evidence) => evidence.id);
+  object.evidences = (object.evidences ?? []).map((evidence) => evidence.id);
frontend/src/routes/(app)/(internal)/backup-restore/+page.svelte (1)

71-91: Prevent modal without a selected file and avoid duplicate submissions

if (file) modalStore.trigger(modal); only checks element existence. Gate on an actual selection and focus the input if empty.

-	function modalConfirm(): void {
-		if (isUploading) return; // Prevent multiple clicks
+	function modalConfirm(): void {
+		if (isUploading) return; // Prevent multiple clicks
+		if (!selectedFiles?.length) {
+			file?.focus();
+			return;
+		}
@@
-		if (file) modalStore.trigger(modal);
+		modalStore.trigger(modal);
 	}
frontend/src/lib/components/Forms/AutocompleteSelect.svelte (2)

221-227: Do not unconditionally drop the last path segment.

Unconditional path.pop() removes a legitimate segment when there is no duplicate, causing incorrect breadcrumbs and sorting. Make it conditional against the actual label/name.

Apply:

-                const path: string[] =
-                    pathField && object?.[pathField]
-                        ? object[pathField].map((obj: { str: string; id: string }) => {
-                                return obj.str;
-                            })
-                        : [];
-                path.pop(); // remove duplicate last part if it exists
+                const pathObjs = pathField && Array.isArray(object?.[pathField]) ? object[pathField] : [];
+                const path: string[] = pathObjs.map((obj: { str: string; id: string }) => obj.str);
+                // Remove duplicate last segment only if it matches the label/name
+                const last = path.at(-1);
+                const labelForCheck =
+                    optionsLabelField === 'auto'
+                        ? (object?.name || object?.description || '').toString()
+                        : String(getNestedValue(object, optionsLabelField) ?? '');
+                if (last && last.trim && (last.trim() === labelForCheck.trim() || last.trim() === String(object?.name ?? '').trim())) {
+                    path.pop();
+                }

235-244: Type-safety: avoid spreading optionsInfoFields into infoString.

Spreading adds unknown keys (fields, separator) to infoString, violating the declared Option.infoString shape and likely TS excess property checks. Assign only supported keys.

-                let infoString: { string: string; position: 'suffix' | 'prefix' } | undefined = undefined;
+                let infoString:
+                    | { string: string; position: 'suffix' | 'prefix'; classes?: string }
+                    | undefined = undefined;
                 if (infoFields.length > 0) {
-                    const separator = optionsInfoFields.separator || ' ';
-                    const position = optionsInfoFields.position || 'suffix';
-                    infoString = {
-                        string: infoFields.join(separator),
-                        position: position,
-                        ...optionsInfoFields
-                    };
+                    const separator = optionsInfoFields.separator ?? ' ';
+                    const position = optionsInfoFields.position ?? 'suffix';
+                    infoString = {
+                        string: infoFields.join(separator),
+                        position,
+                        classes: optionsInfoFields.classes
+                    };
                 }
♻️ Duplicate comments (2)
frontend/messages/da.json (1)

1247-1252: Reconfirm usage of addQualification.

Per the earlier verification note, this key was unused across the UI. Either wire it to the intended button/CTA or remove it from all locales if not needed.

.github/workflows/backend-coverage.yaml (1)

76-83: LGTM: fixed port indexing and moved coverage under Poetry.

The ports['5432'] change resolves actionlint; running coverage via Poetry is cleaner.

🧹 Nitpick comments (11)
frontend/messages/pt.json (3)

962-962: Align with existing terminology: use “valores de negócio”

Elsewhere “businessValue” is translated as “Valor de negócio”. Recommend keeping the same noun phrase here.

Apply this diff:

-"fearedEventAssetHelpText": "Os eventos temidos são geralmente vinculados a valores empresariais (ativos primários). Ativos de suporte são permitidos aqui para flexibilidade.",
+"fearedEventAssetHelpText": "Os eventos temidos são geralmente vinculados a valores de negócio (ativos primários). Ativos de suporte são permitidos aqui para flexibilidade.",

965-966: Make wording consistent: use articles in both labels

“securityObjectiveOrdering” uses “dos …”; mirror that for “qualificationOrdering”.

Apply this diff:

-"qualificationOrdering": "Ordenação de qualificações",
+"qualificationOrdering": "Ordenação das qualificações",

964-964: Consider “Sigla” instead of “Abreviação”

“Sigla” is the more common UI label in PT-BR for short forms; optional change.

Apply this diff:

-"abbreviation": "Abreviação",
+"abbreviation": "Sigla",
frontend/src/routes/(app)/(third-party)/requirement-assessments/[id=uuid]/edit/+page.svelte (1)

642-649: Nit: Prefer Svelte’s on:click over onclick.

Keeps code idiomatic and avoids assigning DOM properties directly.

- onclick={() => form.form.update((data) => { return { ...data, noRedirect: true }; })}
+ on:click={() => form.form.update((data) => ({ ...data, noRedirect: true }))}

Apply similarly to the “Save and continue” button.

frontend/src/routes/(app)/(internal)/backup-restore/+page.svelte (4)

46-67: Enhance export: honor server filename, set credentials, minor a11y

Use Content-Disposition filename when present and pass credentials explicitly. Also set a safer default filename on fallback.

-			const response = await fetch('/backup-restore/dump-db/');
+			const response = await fetch('/backup-restore/dump-db/', {
+				method: 'GET',
+				credentials: 'same-origin',
+				headers: { Accept: 'application/octet-stream' }
+			});
@@
-			const blob = await response.blob();
-			const filename = `backup-${new Date().toISOString().split('T')[0]}.bak`;
+			const blob = await response.blob();
+			const cd = response.headers.get('content-disposition') ?? '';
+			const match = cd.match(/filename\*?=(?:UTF-8''|")?([^\";]+)\"?/i);
+			const filename = match?.[1] ?? `backup-${new Date().toISOString().split('T')[0]}.bak`;

Optional: add aria-busy={isExporting} to the export button for accessibility.


108-122: Nit: simplify i18n usage and add aria-busy

m.exporting ? m.exporting() : 'Exporting...' is redundant if the key exists in messages. Also expose busy state for a11y.

-				<button
+				<button
 					type="button"
 					class="btn preset-filled-primary-500 {exportButtonStyles}"
-					disabled={isExporting}
+					disabled={isExporting}
+					aria-busy={isExporting}
 					onclick={handleExport}
 				>
 					{#if isExporting}
 						<i class="fa-solid fa-spinner fa-spin"></i>
-						{m.exporting ? m.exporting() : 'Exporting...'}
+						{m.exporting()}
 					{:else}
 						{m.exportDatabase()}
 					{/if}
 				</button>

130-167: Reset uploading state on failure and set aria-busy on Upload button

Ensure isUploading resets even if the enhance update() throws; expose busy to AT.

-				use:enhance={() => {
-					return async ({ update }) => {
-						await update();
-						isUploading = false;
-					};
-				}}
+				use:enhance={() => {
+					return async ({ update }) => {
+						try {
+							await update();
+						} finally {
+							isUploading = false;
+						}
+					};
+				}}
@@
-					<button
+					<button
 						class="btn preset-filled-secondary-500 {uploadButtonStyles}"
 						type="button"
-						disabled={isUploading}
+						disabled={isUploading || !selectedFiles?.length}
+						aria-busy={isUploading}
 						onclick={modalConfirm}
 					>

96-99: Permission check: prefer truthy flag over hasOwn

Object.hasOwn(user.permissions, 'backup') passes even if backup: false. Use the boolean flag directly.

-const canBackup: boolean = Object.hasOwn(user.permissions, 'backup');
+const canBackup: boolean = !!user.permissions?.backup;
frontend/src/lib/components/Forms/ModelForm/ReferenceControlForm.svelte (1)

68-68: pathField applied to folder selector

Looks good. Optional: the folders endpoint string is duplicated across forms; consider centralizing it in a constants module to prevent drift.

frontend/src/lib/components/Forms/AutocompleteSelect.svelte (2)

269-276: Prefer path-aware sort over contentType check.

Sorting only when both options have contentType can skip correct ordering for data that carries path but no contentType. Sort when both have a non-empty path.

-                // Sort folder by path
-                if (a.contentType && b.contentType) {
-                    const aPath = a.path.join('') + a.label;
-                    const bPath = b.path.join('') + b.label;
+                // Sort by path when both provide it
+                if ((a.path?.length ?? 0) > 0 && (b.path?.length ?? 0) > 0) {
+                    const aPath = a.path.join('/') + '/' + a.label;
+                    const bPath = b.path.join('/') + '/' + b.label;
                     const alphaCompare = aPath.localeCompare(bPath);
-                    return alphaCompare !== 0 ? alphaCompare : aPath.length - bPath.length;
+                    return alphaCompare !== 0 ? alphaCompare : a.path.length - b.path.length;
                 }

17-24: Minor: extract InfoString type for reuse.

Define a reusable type and reference it from Option and local variables to keep shapes in sync.

 interface Option {
   label: string;
   value: string | number;
   suggested?: boolean;
   translatedLabel?: string;
-  path?: string[];
-  infoString?: {
-      string: string;
-      position: 'suffix' | 'prefix';
-      classes?: string;
-  };
+  path?: string[];
+  infoString?: InfoString;
   contentType?: string;
 }
+
+type InfoString = {
+  string: string;
+  position: 'suffix' | 'prefix';
+  classes?: string;
+};
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 54f2337 and 020e35b.

📒 Files selected for processing (45)
  • .github/workflows/backend-coverage.yaml (1 hunks)
  • enterprise/frontend/src/lib/components/Forms/ModelForm/EbiosRmForm.svelte (1 hunks)
  • enterprise/frontend/src/lib/components/Forms/ModelForm/FolderForm.svelte (1 hunks)
  • frontend/messages/ar.json (1 hunks)
  • frontend/messages/cs.json (1 hunks)
  • frontend/messages/da.json (1 hunks)
  • frontend/messages/de.json (1 hunks)
  • frontend/messages/el.json (1 hunks)
  • frontend/messages/en.json (2 hunks)
  • frontend/messages/es.json (1 hunks)
  • frontend/messages/fr.json (3 hunks)
  • frontend/messages/hi.json (1 hunks)
  • frontend/messages/hu.json (1 hunks)
  • frontend/messages/id.json (1 hunks)
  • frontend/messages/it.json (1 hunks)
  • frontend/messages/nl.json (1 hunks)
  • frontend/messages/pl.json (1 hunks)
  • frontend/messages/pt.json (1 hunks)
  • frontend/messages/ro.json (1 hunks)
  • frontend/messages/sv.json (1 hunks)
  • frontend/messages/tr.json (1 hunks)
  • frontend/messages/uk.json (1 hunks)
  • frontend/messages/ur.json (1 hunks)
  • frontend/src/lib/components/Forms/AutocompleteSelect.svelte (5 hunks)
  • frontend/src/lib/components/Forms/ModelForm/AppliedControlPolicyForm.svelte (1 hunks)
  • frontend/src/lib/components/Forms/ModelForm/AssetForm.svelte (1 hunks)
  • frontend/src/lib/components/Forms/ModelForm/CampaignForm.svelte (1 hunks)
  • frontend/src/lib/components/Forms/ModelForm/EbiosRmForm.svelte (1 hunks)
  • frontend/src/lib/components/Forms/ModelForm/ElementaryActionForm.svelte (1 hunks)
  • frontend/src/lib/components/Forms/ModelForm/EntityForm.svelte (1 hunks)
  • frontend/src/lib/components/Forms/ModelForm/EvidenceForm.svelte (1 hunks)
  • frontend/src/lib/components/Forms/ModelForm/FearedEventForm.svelte (1 hunks)
  • frontend/src/lib/components/Forms/ModelForm/IncidentForm.svelte (1 hunks)
  • frontend/src/lib/components/Forms/ModelForm/PerimeterForm.svelte (1 hunks)
  • frontend/src/lib/components/Forms/ModelForm/ProcessingForm.svelte (1 hunks)
  • frontend/src/lib/components/Forms/ModelForm/ReferenceControlForm.svelte (1 hunks)
  • frontend/src/lib/components/Forms/ModelForm/RiskAcceptanceForm.svelte (1 hunks)
  • frontend/src/lib/components/Forms/ModelForm/SecurityExceptionForm.svelte (1 hunks)
  • frontend/src/lib/components/Forms/ModelForm/TaskTemplateForm.svelte (1 hunks)
  • frontend/src/lib/components/Forms/ModelForm/ThreatForm.svelte (1 hunks)
  • frontend/src/lib/components/Forms/ModelForm/VulnerabilitiesForm.svelte (1 hunks)
  • frontend/src/lib/utils/schemas.ts (4 hunks)
  • frontend/src/routes/(app)/(internal)/backup-restore/+page.svelte (3 hunks)
  • frontend/src/routes/(app)/(third-party)/requirement-assessments/[id=uuid]/edit/+page.server.ts (2 hunks)
  • frontend/src/routes/(app)/(third-party)/requirement-assessments/[id=uuid]/edit/+page.svelte (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (17)
  • frontend/messages/hi.json
  • frontend/messages/id.json
  • frontend/messages/es.json
  • frontend/messages/ro.json
  • frontend/messages/pl.json
  • frontend/messages/el.json
  • frontend/messages/fr.json
  • frontend/messages/sv.json
  • frontend/messages/uk.json
  • frontend/messages/hu.json
  • frontend/messages/ur.json
  • frontend/messages/cs.json
  • frontend/messages/it.json
  • frontend/messages/tr.json
  • frontend/messages/nl.json
  • frontend/messages/ar.json
  • frontend/messages/de.json
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/lib/utils/schemas.ts (1)
backend/core/base_models.py (1)
  • NameDescriptionMixin (111-124)
🪛 Checkov (3.2.334)
.github/workflows/backend-coverage.yaml

[MEDIUM] 76-77: Basic Auth Credentials

(CKV_SECRET_4)

🔇 Additional comments (21)
frontend/messages/da.json (1)

1247-1252: Danish translations and JSON update look correct.

"deselect", "saveAndContinue", "abbreviation", "qualificationOrdering", "securityObjectiveOrdering", and "addQualification" read well and match existing naming patterns. JSON structure remains valid.

frontend/messages/en.json (2)

1827-1832: LGTM: new qualification-related strings

daysOfWeekErrorMessage, weeksOfMonthErrorMessage, abbreviation, qualificationOrdering, securityObjectiveOrdering, and addQualification are clear and consistent with existing style.


1805-1806: Keep qualificationsChartTitle: it’s used in frontend/src/routes/(app)/(internal)/analytics/+page.svelte at line 479; no removal needed. saveAndContinue remains valid.

frontend/messages/pt.json (1)

963-963: LGTM — keys added correctly

“saveAndContinue” and “addQualification” read well and match existing action-label style.

Also applies to: 967-967

enterprise/frontend/src/lib/components/Forms/ModelForm/FolderForm.svelte (1)

51-56: Add path field availability check for AutocompleteSelect.

If the folders API doesn’t include a path array by default, this prop will have no effect. Ensure the endpoint returns path for options, or pass the extra field explicitly.

Optionally:

 <AutocompleteSelect
   {form}
   optionsEndpoint="folders?content_type=DO&content_type=GL"
   optionsSelf={object}
   field="parent_folder"
   pathField="path"
+  optionsExtraFields={[['path']]}
   cacheLock={cacheLocks['parent_folder']}
   bind:cachedValue={formDataCache['parent_folder']}
   label={m.parentDomain()}
   hide={initialData.parent_folder}
 />
frontend/src/lib/utils/schemas.ts (1)

881-922: Cross-field validation looks solid.

Guards MONTHLY/YEARLY schedules correctly and allows others to pass.

frontend/src/lib/components/Forms/ModelForm/ThreatForm.svelte (1)

30-31: LGTM: path-aware folder select

Passing pathField="path" aligns with the AutocompleteSelect path support introduced in this PR.

Please confirm:

  • AutocompleteSelect.svelte exposes a typed pathField?: string.
  • The folders endpoint includes a path field in its items; if not, add optionsExtraFields={[['path','str']]} here.
frontend/src/lib/components/Forms/ModelForm/RiskAcceptanceForm.svelte (1)

53-54: LGTM: consistent pathField usage for folders

Consistent with other forms; no functional concerns.

Verify the folders?content_type=DO response includes path; otherwise add optionsExtraFields to fetch it.

frontend/src/lib/components/Forms/ModelForm/VulnerabilitiesForm.svelte (1)

30-31: LGTM: pathField added

Matches the AutocompleteSelect API update.

Confirm the folders options include path by default; if not, append optionsExtraFields={[['path','str']]}.

frontend/src/lib/components/Forms/ModelForm/PerimeterForm.svelte (1)

30-31: LGTM: folder AutocompleteSelect now path-aware

Change is minimal and consistent across the app.

Ensure backend responses for folders include path; otherwise pass optionsExtraFields to fetch it.

frontend/src/lib/components/Forms/ModelForm/TaskTemplateForm.svelte (1)

54-54: pathField wired for folder AutocompleteSelect

Looks correct and consistent with the new AutocompleteSelect API. Please confirm the folders endpoint returns a path array (or that the component maps it accordingly) so the path segments render as expected.

frontend/src/lib/components/Forms/ModelForm/AssetForm.svelte (1)

116-116: Good: pathField added to folder selector

Matches the pattern already used for parent_assets; should enable path-aware rendering for folder too.

frontend/src/lib/components/Forms/ModelForm/FearedEventForm.svelte (1)

40-40: pathField set on folder selector

Looks fine. If AutocompleteSelect doesn’t infer the folders endpoint for this field, consider aligning with other forms by passing optionsEndpoint explicitly.

frontend/src/lib/components/Forms/ModelForm/EvidenceForm.svelte (1)

63-63: pathField added to folder AutocompleteSelect

Change is straightforward and consistent with the broader update.

frontend/src/lib/components/Forms/ModelForm/AppliedControlPolicyForm.svelte (1)

279-279: LGTM: pathField for folder selection is consistent with API changes

No issues. Ensure backend items expose "path" as expected.

frontend/src/lib/components/Forms/ModelForm/SecurityExceptionForm.svelte (1)

88-88: LGTM: pathField added for folder AutocompleteSelect

Matches the new AutocompleteSelect contract. Confirm the endpoint returns a "path" field (array/string as expected by the component).

frontend/src/lib/components/Forms/ModelForm/EbiosRmForm.svelte (1)

70-70: LGTM: pathField="path" for folder mapping

Aligned with the standardized folder selection behavior. Validate the DO-only folders endpoint includes "path".

frontend/src/lib/components/Forms/ModelForm/EntityForm.svelte (1)

45-45: LGTM: pathField parameterization for folder field

Consistent with the AutocompleteSelect update; no further changes needed.

enterprise/frontend/src/lib/components/Forms/ModelForm/EbiosRmForm.svelte (1)

69-69: Consistent path-aware folder selection.

Matches the project-wide shift to pathField="path".

frontend/src/lib/components/Forms/ModelForm/IncidentForm.svelte (1)

67-71: Confirm backend folders endpoint returns path array for DO & GL
In IncidentForm.svelte, pathField="path" and optionsEndpoint="folders?content_type=DO&content_type=GL" are correctly applied. Verify that the folders?content_type=DO&content_type=GL API response includes the path array for both DO and GL entries.

frontend/src/lib/components/Forms/ModelForm/CampaignForm.svelte (1)

113-119: Good: folder AutocompleteSelect now path-aware.

No further changes needed here.

@@ -60,6 +60,7 @@
{form}
optionsEndpoint="folders?content_type=DO&content_type=GL"
field="folder"
pathField="path"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Approve: pathField wiring; verify API shape and call-site coverage

Looks good and consistent with the new AutocompleteSelect API. Please confirm the folders endpoints include a "path" field in the returned option items, and that all folder AutocompleteSelect usages across the repo now pass pathField to avoid inconsistent UX.

Run this to verify the component prop and catch any remaining folder selects missing pathField:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Ensure AutocompleteSelect declares the prop"
rg -nP 'export\s+let\s+pathField' --type svelte

echo "2) Find AutocompleteSelect folder fields missing pathField"
python - <<'PY'
import re, subprocess, sys
files = subprocess.check_output(["fd","-t","f","-e","svelte"], text=True).splitlines()
bad = []
for f in files:
    s = open(f, encoding="utf-8", errors="ignore").read()
    for m in re.finditer(r'<AutocompleteSelect\b[\s\S]*?/>', s, flags=re.S):
        tag = m.group(0)
        if 'field="folder"' in tag and 'pathField=' not in tag:
            bad.append(f); break
if bad:
    print("Missing pathField on folder AutocompleteSelect in:"); [print(" -", f) for f in bad]; sys.exit(1)
print("All folder AutocompleteSelect usages include pathField.")
PY

Length of output: 203


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Ensure AutocompleteSelect declares the prop"
rg -nP 'export\s+let\s+pathField' -g '*.svelte'

echo "2) Find AutocompleteSelect folder usages missing pathField"
python - <<'PY'
import re, subprocess, sys

# find all .svelte files
files = subprocess.check_output(["fd", "-e", "svelte"], text=True).splitlines()
bad = []

for f in files:
    try:
        content = open(f, encoding="utf-8", errors="ignore").read()
    except Exception:
        continue
    for m in re.finditer(r'<AutocompleteSelect\b[\s\S]*?/>', content, flags=re.S):
        tag = m.group(0)
        if 'field="folder"' in tag and 'pathField=' not in tag:
            bad.append(f)
            break

if bad:
    echo="Missing pathField on folder AutocompleteSelect in:"
    print(echo)
    for f in bad:
        print(" -", f)
    sys.exit(1)

print("All folder AutocompleteSelect usages include pathField.")
PY

Length of output: 172


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Locate AutocompleteSelect component file"
component_file=$(fd -e svelte -I | grep -i 'AutocompleteSelect\.svelte' || true)
if [ -z "$component_file" ]; then
  echo "ERROR: AutocompleteSelect.svelte not found"; exit 1
fi
echo "Found: $component_file"

echo "2) Verify 'export let pathField' is declared"
if ! rg -nP 'export\s+let\s+pathField' "$component_file"; then
  echo "ERROR: 'pathField' prop not declared in $component_file"; exit 1
fi

echo "3) Identify all AutocompleteSelect usages with field=\"folder\""
usages=$(rg -l '<AutocompleteSelect[^>]*field="folder"' -g '*.svelte')
if [ -z "$usages" ]; then
  echo "No folder-field AutocompleteSelect usages found"
else
  missing=()
  for f in $usages; do
    if ! rg -q 'pathField=' "$f"; then
      missing+=("$f")
    fi
  done
  if [ ${#missing[@]} -gt 0 ]; then
    echo "Missing pathField in the following files:"
    printf ' - %s\n' "${missing[@]}"
    exit 1
  else
    echo "All folder-field AutocompleteSelect usages include pathField"
  fi
fi

Length of output: 893


Add missing export let pathField prop in AutocompleteSelect.svelte

In frontend/src/lib/components/Forms/AutocompleteSelect.svelte, declare export let pathField so that folder-field <AutocompleteSelect> usages can bind the pathField prop. After adding it, verify all <AutocompleteSelect field="folder" …> instances include pathField.

🤖 Prompt for AI Agents
In frontend/src/lib/components/Forms/AutocompleteSelect.svelte (near the top of
the file, lines ~1–5) add a prop declaration "export let pathField" so the
component accepts a pathField prop; then scan the codebase for all
<AutocompleteSelect field="folder" …> usages and ensure they pass the pathField
prop (e.g., pathField="path" or bind:pathField where appropriate), specifically
confirm frontend/src/lib/components/Forms/ModelForm/ProcessingForm.svelte line
63 includes pathField="path".

Comment on lines 93 to 95
let uploadButtonStyles = $derived(file && !isUploading ? '' : 'chip-disabled');
let exportButtonStyles = $derived(isExporting ? 'chip-disabled' : '');

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Correct the upload button gating to check for an actual selected file

Current logic enables the button if the input element exists, not if a file is selected.

-let uploadButtonStyles = $derived(file && !isUploading ? '' : 'chip-disabled');
+let uploadButtonStyles = $derived(!!(selectedFiles?.length) && !isUploading ? '' : 'chip-disabled');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let uploadButtonStyles = $derived(file && !isUploading ? '' : 'chip-disabled');
let exportButtonStyles = $derived(isExporting ? 'chip-disabled' : '');
let uploadButtonStyles = $derived(!!(selectedFiles?.length) && !isUploading ? '' : 'chip-disabled');
let exportButtonStyles = $derived(isExporting ? 'chip-disabled' : '');
🤖 Prompt for AI Agents
In frontend/src/routes/(app)/(internal)/backup-restore/+page.svelte around lines
93–95, the upload button gating currently enables based on the input element
existence instead of whether a file is actually selected; change the derived
condition to check the selected file (e.g., file && (file.length ?? 1) > 0 or
file instanceof File depending on how `file` is represented) so the button is
only enabled when a real file is present, and update the derived store
expression accordingly to depend on the correct file property.

Comment on lines 130 to 151
<form
enctype="multipart/form-data"
method="post"
use:enhance={() => {
return async ({ update }) => {
await update();
isUploading = false;
};
}}
bind:this={form}
>
<div class="flex flex-col sm:flex-row sm:items-end gap-3">
<div class="flex-1">
<input
id="file"
type="file"
name="file"
class="input"
accept={authorizedExtensions.join(',')}
required
bind:value={file}
/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix: file input is mis-bound; use bind:files and/or bind:this, not bind:value to an HTMLInputElement

bind:value={file} on an <input type="file"> is incorrect and currently binds a string path, not the selected FileList nor the element. This breaks the upload gating logic downstream.

Apply this diff within the form to correctly bind the element and selected files:

-							accept={authorizedExtensions.join(',')}
-							required
-							bind:value={file}
+							accept={authorizedExtensions.join(',')}
+							required
+							bind:this={file}
+							bind:files={selectedFiles}

And add this state near the other state declarations (outside the shown hunk):

let selectedFiles: FileList | null = $state(null);
🤖 Prompt for AI Agents
In frontend/src/routes/(app)/(internal)/backup-restore/+page.svelte around lines
130-151, the file input incorrectly uses bind:value which binds a string path
instead of the FileList or element; replace bind:value with bind:files to
capture the selected FileList and optionally add bind:this if you need a
reference to the input element, then update any upload-gating logic to use that
selected FileList (not a string). Also add a state variable near the other state
declarations such as let selectedFiles: FileList | null = null so the bind:files
target exists and downstream checks use selectedFiles.

Comment on lines 642 to 653
data-testid="save-no-continue-button"
type="submit"
onclick={() =>
form.form.update((data) => {
return { ...data, noRedirect: true };
})}>{m.save()}</button
>
<button
class="btn preset-filled-secondary-500 font-semibold w-full"
data-testid="save-button"
type="submit">{m.save()}</button
type="submit">{m.saveAndContinue()}</button
>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Prevent sticky noRedirect across subsequent submits.

noRedirect stays true after the first “Save” and will also block redirect on the next “Save and continue.” Explicitly reset it on that button.

 <button
   class="btn preset-filled-primary-500 font-semibold w-full"
   data-testid="save-no-continue-button"
   type="submit"
   onclick={() =>
     form.form.update((data) => {
       return { ...data, noRedirect: true };
     })}>{m.save()}</button
 >
 <button
   class="btn preset-filled-secondary-500 font-semibold w-full"
   data-testid="save-button"
-  type="submit">{m.saveAndContinue()}</button
+  type="submit"
+  onclick={() =>
+    form.form.update((data) => {
+      return { ...data, noRedirect: false };
+    })
+  }
+>{m.saveAndContinue()}</button
 >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
data-testid="save-no-continue-button"
type="submit"
onclick={() =>
form.form.update((data) => {
return { ...data, noRedirect: true };
})}>{m.save()}</button
>
<button
class="btn preset-filled-secondary-500 font-semibold w-full"
data-testid="save-button"
type="submit">{m.save()}</button
type="submit">{m.saveAndContinue()}</button
>
<button
class="btn preset-filled-primary-500 font-semibold w-full"
data-testid="save-no-continue-button"
type="submit"
onclick={() =>
form.form.update((data) => {
return { ...data, noRedirect: true };
})
}
>
{m.save()}
</button>
<button
class="btn preset-filled-secondary-500 font-semibold w-full"
data-testid="save-button"
type="submit"
onclick={() =>
form.form.update((data) => {
return { ...data, noRedirect: false };
})
}
>
{m.saveAndContinue()}
</button>
🤖 Prompt for AI Agents
frontend/src/routes/(app)/(third-party)/requirement-assessments/[id=uuid]/edit/+page.svelte
around lines 642 to 653: the "Save and continue" submit button does not reset
form.form.noRedirect, so if a previous "Save" set noRedirect=true that value
persists and prevents redirect on subsequent submits; add an onclick handler to
the save-and-continue button that updates form.form to explicitly set
noRedirect:false (similar to the no-continue button which sets true) so each
submit uses the intended redirect behavior.

@monsieurswag monsieurswag marked this pull request as draft August 28, 2025 11:18
@monsieurswag monsieurswag marked this pull request as draft August 29, 2025 22:48
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/core/serializers.py (1)

598-632: Define explicit qualifications and drop deprecated existing_controls

  • In RiskScenarioImportExportSerializer, add:
    qualifications = HashSlugRelatedField(slug_field="pk", read_only=True, many=True)
    (to mirror the model’s ManyToManyField at core/models.py:2526)
  • Remove "existing_controls" from Meta.fields in the same serializer (currently at serializers.py:619), relying on existing_applied_controls instead.
🧹 Nitpick comments (2)
backend/core/serializers.py (2)

1582-1586: Make library read-only on write to avoid polluting loaded libraries

End-users creating custom qualifications shouldn’t set library. Keep it null; importer code can set it internally if needed.

 class QualificationWriteSerializer(BaseModelSerializer):
+    library = serializers.PrimaryKeyRelatedField(read_only=True)
     class Meta:
         model = Qualification
         exclude = ["translations"]

574-576: Standardize qualifications and add prefetch in RiskScenarioViewSet

  • Replace
    qualifications = FieldsRelatedField(many=True)
    with
    qualifications = FieldsRelatedField(["id", "name"], many=True) in backend/core/serializers.py
  • In RiskScenarioViewSet.get_queryset() (backend/core/views.py:~2230), chain .prefetch_related("qualifications") to avoid N+1 on list endpoints
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 20c167f and 30a974d.

📒 Files selected for processing (2)
  • backend/core/serializers.py (2 hunks)
  • frontend/src/routes/(app)/(internal)/risk-scenarios/[id=uuid]/+page.svelte (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/routes/(app)/(internal)/risk-scenarios/[id=uuid]/+page.svelte
🧰 Additional context used
🧬 Code graph analysis (1)
backend/core/serializers.py (2)
backend/core/serializer_fields.py (1)
  • FieldsRelatedField (24-86)
backend/core/models.py (17)
  • Meta (107-108)
  • Meta (162-163)
  • Meta (190-191)
  • Meta (195-197)
  • Meta (992-994)
  • Meta (1058-1060)
  • Meta (1189-1191)
  • Meta (1323-1325)
  • Meta (1570-1573)
  • Meta (1628-1630)
  • Meta (1851-1853)
  • Meta (2400-2408)
  • Meta (2434-2436)
  • Meta (2555-2557)
  • Meta (2781-2783)
  • Meta (2926-2928)
  • Qualification (1431-1596)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: build_enterprise_frontend
  • GitHub Check: build_community_frontend
  • GitHub Check: build (3.12)
  • GitHub Check: test (3.12)
  • GitHub Check: enterprise-startup-docker-compose-test
  • GitHub Check: enterprise-startup-functional-test (3.12)
  • GitHub Check: startup-docker-compose-test
  • GitHub Check: startup-functional-test (3.12)
  • GitHub Check: Analyze (python)
🔇 Additional comments (1)
backend/core/serializers.py (1)

1588-1594: LGTM: Dedicated read serializer with library relation is consistent

Read path mirrors other referentials (Threat, ReferenceControl) and excludes translations.

@monsieurswag
Copy link
Contributor Author

monsieurswag commented Aug 29, 2025

There are 3 remains things to do :

Adding translations for the default qualifications (by adding a translation dict as a "translations" field for each qualification defined in core.models.Qualification.DEFAULT_QUALIFICATIONS).
Modify the 0092_qualification_library_and_more.py migration to migrate the old JsonField qualifications to the new ManyToManyField ones.
Fix the "load demo data" (the RiskScenario.qualifications migration from a JSONField to a ManyToManyField broke it), the demo data must be updated accordingly.

qualifications are managed similarly now for risk scenarios as for other usecases
remove old field
migrate qualification field content
for objectives and for qualifications
@eric-intuitem eric-intuitem force-pushed the feat/custom_qualifications branch from fb8579c to 0c0ce56 Compare August 31, 2025 22:21
update for new qualifications format in risk scenarios
@eric-intuitem eric-intuitem marked this pull request as ready for review August 31, 2025 23:13
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/src/routes/(app)/(internal)/risk-scenarios/[id=uuid]/edit/+page.svelte (1)

419-427: Residual risk level bound to the wrong form field

RiskLevel in the residual block writes to current_risk_level instead of residual_risk_level.

- <RiskLevel
-   form={_form}
-   field="current_risk_level"
-   label={m.residualRiskLevel()}
-   riskMatrix={data.riskMatrix}
-   probabilityField="residual_proba"
-   impactField="residual_impact"
-   helpText={m.residualRiskLevelHelpText()}
- />
+ <RiskLevel
+   form={_form}
+   field="residual_risk_level"
+   label={m.residualRiskLevel()}
+   riskMatrix={data.riskMatrix}
+   probabilityField="residual_proba"
+   impactField="residual_impact"
+   helpText={m.residualRiskLevelHelpText()}
+ />
backend/core/serializers.py (1)

610-630: Unknown field in RiskScenarioImportExportSerializer.fields

fields includes "existing_controls" which is not defined on the serializer/model (the defined field is existing_applied_controls). This will break serialization.

         fields = [
             "ref_id",
             "name",
             "description",
             "risk_assessment",
             "treatment",
             "threats",
             "vulnerabilities",
             "assets",
-            "existing_controls",
             "existing_applied_controls",
             "applied_controls",
             "current_proba",
             "current_impact",
             "residual_proba",
             "residual_impact",
             "strength_of_knowledge",
             "justification",
             "created_at",
             "updated_at",
             "qualifications",
         ]
♻️ Duplicate comments (2)
frontend/src/lib/utils/types.ts (1)

36-113: Duplicate entries in URL_MODEL (‘roles’, ‘processings’)

roles appears twice (Lines 50 and 110) and processings appears twice (Lines 80 and 86). This can cause duplicate routes/menus.

   'user-groups',
-  'roles',
   'role-assignments',
   'compliance-assessments',
@@
   'kill-chains',
-  'processings',
   'processing-natures',
   'security-exceptions',
@@
-  // privacy,
-  'processings',
+  // privacy,
+  'processings',
@@
-  // roles,
-  'roles',
+  // roles,
+  'roles',
   'permissions',
   'qualifications'

If you prefer keeping the first roles near user management, remove the later one instead; just ensure uniqueness across the array.

backend/core/models.py (1)

928-930: reference_count may over-count due to M2M duplicates — use distinct().

Keeps counts accurate when the join returns duplicate rows.

-            + Incident.objects.filter(qualifications__library=self).count()
-            + FearedEvent.objects.filter(qualifications__library=self).count()
-            + EscalationThreshold.objects.filter(qualifications__library=self).count()
+            + Incident.objects.filter(qualifications__library=self).distinct().count()
+            + FearedEvent.objects.filter(qualifications__library=self).distinct().count()
+            + EscalationThreshold.objects.filter(qualifications__library=self).distinct().count()
🧹 Nitpick comments (11)
frontend/messages/cs.json (2)

954-956: Czech grammar: use genitive plural for “ordering” labels.

Prefer “Pořadí kvalifikací” and “Pořadí bezpečnostních cílů” (ordering of multiple items).

Apply:

-"qualificationOrdering": "Pořadí kvalifikace",
-"securityObjectiveOrdering": "Pořadí bezpečnostního cíle",
+"qualificationOrdering": "Pořadí kvalifikací",
+"securityObjectiveOrdering": "Pořadí bezpečnostních cílů",

957-957: Capitalize list label to match surrounding UI.

Most section/list labels are capitalized. Use “Kvalifikace”.

-"qualifications": "kvalifikace",
+"qualifications": "Kvalifikace",
frontend/messages/ro.json (1)

962-968: Minor consistency: capitalize plural label

Most top-level entity labels are capitalized in RO (e.g., “Politici”, “Biblioteci”). Consider “Calificări” instead of “calificări” for UI consistency. Optional.

- "qualifications": "calificări",
+ "qualifications": "Calificări",
backend/core/views.py (1)

3270-3303: Add documentation for domain import M2M relationships

The _create_model_objects and _create_batch methods handle domain import with M2M relationships. Consider adding docstring details about the new objectives and qualifications relationships being processed.

 def _create_model_objects(self, model, objects, link_dump_database_ids):
-    """Create all objects for a model after validation during domain import."""
+    """Create all objects for a model after validation during domain import.
+    
+    Handles various M2M relationships including:
+    - AppliedControl.objectives
+    - RiskScenario.qualifications
+    """
     logger.debug("Creating objects for model", model=model)
backend/core/migrations/0093_qualification_library_and_more.py (1)

56-67: Remove temporary help text from production migration

The help text "(temporary during migration)" should not be visible in production as it might confuse users if the migration fails partway through.

         migrations.AddField(
             model_name="riskscenario",
             name="qualifications_tmp",
             field=models.ManyToManyField(
                 blank=True,
                 to="core.qualification",
                 related_name="risk_scenarios_tmp",
-                help_text="(temporary during migration)",
-                verbose_name="Qualifications (tmp)",
             ),
         ),
backend/core/models.py (6)

887-891: Stabilize library dump for qualifications (ordering + publish filter).

Return a predictable, filtered list to avoid churn in dumps and exclude unpublished items.

-            res["qualifications"] = [
-                update_translations_in_object(model_to_dict(qualification))
-                for qualification in self.qualifications.all()
-            ]
+            res["qualifications"] = [
+                update_translations_in_object(model_to_dict(qualification))
+                for qualification in self.qualifications
+                .filter(is_published=True)
+                .order_by("qualification_ordering", "id")
+            ]

905-907: Docstring out of sync with implementation.

It now also counts incidents, feared events, and escalation thresholds. Consider updating the wording.

-        Returns the number of distinct dependent libraries and risk and compliance assessments that reference objects from this library
+        Returns the number of distinct dependent libraries, risk/compliance assessments, and other domain objects (incidents, feared events, escalation thresholds) that reference objects from this library

1064-1066: Typo in docstring (“refence” → “reference”).

-        Returns True if the refence control can be deleted
+        Returns True if the reference control can be deleted

1577-1587: Block deletion when a Qualification is used by RiskScenarios.

Currently only incidents/feared_events/escalation_thresholds are checked; consider also guarding on risk_scenarios to avoid surprising data loss.

         return not any(
             [
                 self.incidents.exists(),
                 self.feared_events.exists(),
                 self.escalation_thresholds.exists(),
+                self.risk_scenarios.exists(),
             ]
         )

1433-1460: Add translations to DEFAULT_QUALIFICATIONS (per PR TODO).

Populate the translations field so UI/lookups can localize names/abbrs. Example pattern below; replicate for the rest.

         {
             "abbreviation": "C",
             "qualification_ordering": 1,
             "security_objective_ordering": 1,
             "name": "Confidentiality",
             "urn": "urn:intuitem:risk:qualification:confidentiality",
+            "translations": {
+                "en": {"name": "Confidentiality", "abbreviation": "C"}
+            },
         },
         {
             "abbreviation": "I",
             "qualification_ordering": 2,
             "security_objective_ordering": 2,
             "name": "Integrity",
             "urn": "urn:intuitem:risk:qualification:integrity",
+            "translations": {
+                "en": {"name": "Integrity", "abbreviation": "I"}
+            },
         },

Would you like me to generate a complete patch adding translations for all defaults (and FR placeholders)?

Also applies to: 1441-1446


5352-5355: Audit log: include new qualifications M2M where relevant.

Capture changes to RiskScenario/Incident qualifications like other M2Ms.

 auditlog.register(
     RiskScenario,
-    m2m_fields={"owner", "applied_controls", "existing_applied_controls"},
+    m2m_fields={"owner", "applied_controls", "existing_applied_controls", "qualifications"},
     exclude_fields=common_exclude,
 )
@@
-auditlog.register(
-    Incident,
-    exclude_fields=common_exclude,
-)
+auditlog.register(
+    Incident,
+    m2m_fields={"qualifications"},
+    exclude_fields=common_exclude,
+)

Also applies to: 5401-5406

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 30a974d and 34403fe.

📒 Files selected for processing (35)
  • backend/core/migrations/0093_qualification_library_and_more.py (1 hunks)
  • backend/core/models.py (8 hunks)
  • backend/core/serializers.py (3 hunks)
  • backend/core/views.py (8 hunks)
  • backend/resilience/migrations/0003_alter_escalationthreshold_qualifications.py (1 hunks)
  • frontend/messages/ar.json (1 hunks)
  • frontend/messages/cs.json (1 hunks)
  • frontend/messages/da.json (1 hunks)
  • frontend/messages/de.json (1 hunks)
  • frontend/messages/el.json (1 hunks)
  • frontend/messages/en.json (1 hunks)
  • frontend/messages/es.json (1 hunks)
  • frontend/messages/fr.json (1 hunks)
  • frontend/messages/hi.json (1 hunks)
  • frontend/messages/hu.json (1 hunks)
  • frontend/messages/id.json (1 hunks)
  • frontend/messages/it.json (1 hunks)
  • frontend/messages/nl.json (1 hunks)
  • frontend/messages/pl.json (1 hunks)
  • frontend/messages/pt.json (1 hunks)
  • frontend/messages/ro.json (1 hunks)
  • frontend/messages/sv.json (1 hunks)
  • frontend/messages/tr.json (1 hunks)
  • frontend/messages/uk.json (1 hunks)
  • frontend/messages/ur.json (1 hunks)
  • frontend/src/lib/components/Forms/ModelForm.svelte (2 hunks)
  • frontend/src/lib/components/ModelTable/ModelTable.svelte (2 hunks)
  • frontend/src/lib/utils/crud.ts (1 hunks)
  • frontend/src/lib/utils/schemas.ts (2 hunks)
  • frontend/src/lib/utils/table.ts (1 hunks)
  • frontend/src/lib/utils/types.ts (1 hunks)
  • frontend/src/routes/(app)/(internal)/risk-scenarios/[id=uuid]/+page.svelte (1 hunks)
  • frontend/src/routes/(app)/(internal)/risk-scenarios/[id=uuid]/edit/+page.server.ts (1 hunks)
  • frontend/src/routes/(app)/(internal)/risk-scenarios/[id=uuid]/edit/+page.svelte (1 hunks)
  • frontend/src/routes/(app)/(internal)/stored-libraries/[id=uuid]/+page.svelte (4 hunks)
✅ Files skipped from review due to trivial changes (3)
  • frontend/src/lib/components/ModelTable/ModelTable.svelte
  • frontend/messages/pt.json
  • frontend/messages/hu.json
🚧 Files skipped from review as they are similar to previous changes (24)
  • frontend/messages/pl.json
  • frontend/messages/en.json
  • frontend/messages/da.json
  • frontend/messages/de.json
  • frontend/messages/el.json
  • frontend/messages/ar.json
  • frontend/src/lib/utils/table.ts
  • frontend/messages/es.json
  • frontend/src/routes/(app)/(internal)/risk-scenarios/[id=uuid]/+page.svelte
  • frontend/messages/ur.json
  • frontend/messages/hi.json
  • frontend/messages/id.json
  • frontend/messages/fr.json
  • frontend/messages/tr.json
  • frontend/messages/uk.json
  • frontend/messages/nl.json
  • frontend/src/lib/utils/crud.ts
  • backend/resilience/migrations/0003_alter_escalationthreshold_qualifications.py
  • frontend/src/routes/(app)/(internal)/stored-libraries/[id=uuid]/+page.svelte
  • frontend/messages/sv.json
  • frontend/src/lib/components/Forms/ModelForm.svelte
  • frontend/src/routes/(app)/(internal)/risk-scenarios/[id=uuid]/edit/+page.server.ts
  • frontend/src/lib/utils/schemas.ts
  • frontend/messages/it.json
🧰 Additional context used
🧬 Code graph analysis (4)
backend/core/migrations/0093_qualification_library_and_more.py (2)
backend/core/models.py (1)
  • Qualification (1431-1596)
backend/resilience/migrations/0003_alter_escalationthreshold_qualifications.py (1)
  • Migration (6-22)
backend/core/models.py (3)
backend/library/helpers.py (1)
  • update_translations_in_object (49-75)
backend/ebios_rm/models.py (1)
  • FearedEvent (249-314)
backend/resilience/models.py (1)
  • EscalationThreshold (176-248)
backend/core/views.py (1)
backend/core/models.py (1)
  • Qualification (1431-1596)
backend/core/serializers.py (2)
backend/core/serializer_fields.py (2)
  • FieldsRelatedField (24-86)
  • HashSlugRelatedField (10-21)
backend/core/models.py (7)
  • Meta (107-108)
  • Meta (162-163)
  • Meta (190-191)
  • Meta (195-197)
  • Meta (992-994)
  • Meta (1058-1060)
  • Qualification (1431-1596)
🔇 Additional comments (14)
frontend/messages/cs.json (2)

954-954: LGTM: “abbreviation” translation is correct.


953-958: i18n coverage verified across all locale files
All JSON files in frontend/messages parsed without errors and include abbreviation, qualificationOrdering, securityObjectiveOrdering, qualifications, and addQualification in every locale.

frontend/src/routes/(app)/(internal)/risk-scenarios/[id=uuid]/edit/+page.svelte (1)

439-446: Endpoint-backed qualifications select looks correct; confirm payload shape

Using optionsEndpoint="qualifications" with optionsLabelField="name" and optionsValueField="id" matches QualificationReadSerializer output and RiskScenario write expectations. LGTM; just ensure the API returns id and name in list responses.

backend/core/serializers.py (3)

572-574: Expose qualifications on RiskScenario read: looks good

FieldsRelatedField(many=True) aligns with the M2M and the frontend AutocompleteSelect. No issues.


605-606: Exporting qualifications by URN hash: OK

HashSlugRelatedField(slug_field="urn", many=True, read_only=True) matches existing import/export patterns for related entities. LGTM.


1581-1593: Qualification serializers separation (read/write) is clean

Dedicated write serializer and read serializer with library field are appropriate and match other referential entities. Excluding translations on write is consistent. LGTM.

backend/core/views.py (3)

3412-3415: LGTM! AppliedControl objectives handling added correctly

The new objective_ids mapping for AppliedControl's objectives relationship is properly extracted and will be set via M2M relations.


3599-3600: LGTM! AppliedControl objectives M2M assignment implemented

The new objectives field is correctly assigned using the mapped IDs during domain import.


3630-3631: LGTM! RiskScenario qualifications M2M assignment implemented

The qualifications field is properly assigned to RiskScenario using the Qualification model filter.

backend/core/migrations/0093_qualification_library_and_more.py (1)

1-4: The script above will reveal whether the qualification defaults and RunPython calls are present. Once we have those details, we can confirm if the migration needs an ensure_default_qualifications step or if it’s already handled.

backend/core/models.py (4)

909-911: Local imports to avoid circular deps — OK.

Importing FearedEvent/EscalationThreshold inside the property is fine here.


913-917: Should risk assessments that reference qualifications via risk scenarios be counted?

If yes, include this predicate in the existing RiskAssessment queryset ORs (keeps it one distinct() count rather than a separate summand).

             RiskAssessment.objects.filter(
-                Q(risk_scenarios__threats__library=self)
+                Q(risk_scenarios__threats__library=self)
+                | Q(risk_scenarios__qualifications__library=self)
                 | Q(risk_matrix__library=self)
                 | Q(risk_scenarios__applied_controls__reference_control__library=self)
             )

996-1002: Threat.is_deletable docstring tweak — OK.

Wording improvement looks good.


1550-1559: Qualification now library-linked and publishable — OK.

FK to LoadedLibrary with CASCADE and published flag both make sense.

Comment on lines +7 to +30
def migrate_qualifications(apps, schema_editor):
"""
Copy old qualification values (list of names) into the new ManyToMany field.
Deterministic: assumes RiskScenario.qualifications (old field) was always
a list of strings corresponding exactly to Qualification.name values.
"""
RiskScenario = apps.get_model("core", "RiskScenario")
Qualification = apps.get_model("core", "Qualification")

# Build strict mapping name -> id
name_to_id = dict(Qualification.objects.values_list("name", "id"))

for rs in RiskScenario.objects.all():
old_names = getattr(rs, "qualifications", None)
if not old_names:
continue
ids = []
for name in old_names:
if name not in name_to_id:
raise RuntimeError(f"Unknown qualification name: {name}")
ids.append(name_to_id[name])
if ids:
rs.qualifications_tmp.add(*ids)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling and logging to data migration

The data migration should handle edge cases more gracefully and provide better debugging information. Consider:

  1. Logging unknown qualification names before raising the error
  2. Handling potential database integrity issues with transaction management
  3. Adding a reverse migration for rollback scenarios
 def migrate_qualifications(apps, schema_editor):
     """
     Copy old qualification values (list of names) into the new ManyToMany field.
     Deterministic: assumes RiskScenario.qualifications (old field) was always
     a list of strings corresponding exactly to Qualification.name values.
     """
+    from django.db import transaction
     RiskScenario = apps.get_model("core", "RiskScenario")
     Qualification = apps.get_model("core", "Qualification")
 
     # Build strict mapping name -> id
     name_to_id = dict(Qualification.objects.values_list("name", "id"))
 
-    for rs in RiskScenario.objects.all():
-        old_names = getattr(rs, "qualifications", None)
-        if not old_names:
-            continue
-        ids = []
-        for name in old_names:
-            if name not in name_to_id:
-                raise RuntimeError(f"Unknown qualification name: {name}")
-            ids.append(name_to_id[name])
-        if ids:
-            rs.qualifications_tmp.add(*ids)
+    with transaction.atomic():
+        for rs in RiskScenario.objects.all():
+            old_names = getattr(rs, "qualifications", None)
+            if not old_names:
+                continue
+            ids = []
+            unknown_names = []
+            for name in old_names:
+                if name not in name_to_id:
+                    unknown_names.append(name)
+                else:
+                    ids.append(name_to_id[name])
+            
+            if unknown_names:
+                # Log the problematic scenario for debugging
+                print(f"RiskScenario {rs.id} has unknown qualifications: {unknown_names}")
+                print(f"Available qualifications: {list(name_to_id.keys())}")
+                raise RuntimeError(
+                    f"Unknown qualification names in RiskScenario {rs.id}: {unknown_names}"
+                )
+            
+            if ids:
+                rs.qualifications_tmp.add(*ids)
+
+def reverse_migrate_qualifications(apps, schema_editor):
+    """Reverse migration to restore original data structure."""
+    RiskScenario = apps.get_model("core", "RiskScenario")
+    
+    for rs in RiskScenario.objects.all():
+        qualification_names = list(
+            rs.qualifications_tmp.values_list("name", flat=True)
+        )
+        if qualification_names:
+            rs.qualifications = qualification_names
+            rs.save(update_fields=["qualifications"])

Then update the RunPython operation:

-        migrations.RunPython(migrate_qualifications, migrations.RunPython.noop),
+        migrations.RunPython(migrate_qualifications, reverse_migrate_qualifications),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def migrate_qualifications(apps, schema_editor):
"""
Copy old qualification values (list of names) into the new ManyToMany field.
Deterministic: assumes RiskScenario.qualifications (old field) was always
a list of strings corresponding exactly to Qualification.name values.
"""
RiskScenario = apps.get_model("core", "RiskScenario")
Qualification = apps.get_model("core", "Qualification")
# Build strict mapping name -> id
name_to_id = dict(Qualification.objects.values_list("name", "id"))
for rs in RiskScenario.objects.all():
old_names = getattr(rs, "qualifications", None)
if not old_names:
continue
ids = []
for name in old_names:
if name not in name_to_id:
raise RuntimeError(f"Unknown qualification name: {name}")
ids.append(name_to_id[name])
if ids:
rs.qualifications_tmp.add(*ids)
def migrate_qualifications(apps, schema_editor):
"""
Copy old qualification values (list of names) into the new ManyToMany field.
Deterministic: assumes RiskScenario.qualifications (old field) was always
a list of strings corresponding exactly to Qualification.name values.
"""
from django.db import transaction
RiskScenario = apps.get_model("core", "RiskScenario")
Qualification = apps.get_model("core", "Qualification")
# Build strict mapping name -> id
name_to_id = dict(Qualification.objects.values_list("name", "id"))
with transaction.atomic():
for rs in RiskScenario.objects.all():
old_names = getattr(rs, "qualifications", None)
if not old_names:
continue
ids = []
unknown_names = []
for name in old_names:
if name not in name_to_id:
unknown_names.append(name)
else:
ids.append(name_to_id[name])
if unknown_names:
# Log the problematic scenario for debugging
print(f"RiskScenario {rs.id} has unknown qualifications: {unknown_names}")
print(f"Available qualifications: {list(name_to_id.keys())}")
raise RuntimeError(
f"Unknown qualification names in RiskScenario {rs.id}: {unknown_names}"
)
if ids:
rs.qualifications_tmp.add(*ids)
def reverse_migrate_qualifications(apps, schema_editor):
"""Reverse migration to restore original data structure."""
RiskScenario = apps.get_model("core", "RiskScenario")
for rs in RiskScenario.objects.all():
qualification_names = list(
rs.qualifications_tmp.values_list("name", flat=True)
)
if qualification_names:
rs.qualifications = qualification_names
rs.save(update_fields=["qualifications"])
# And in the migration operations list:
# before:
# migrations.RunPython(migrate_qualifications, migrations.RunPython.noop),
# after:
# migrations.RunPython(migrate_qualifications, reverse_migrate_qualifications),

Comment on lines +3659 to +3665
qualifications = models.ManyToManyField(
Qualification,
blank=True,
verbose_name=_("Qualifications"),
related_name="risk_scenarios",
help_text=_("Qualifications carried by the risk scenario"),
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

JSON→M2M backfill for RiskScenario.qualifications is required.

The schema change is present; ensure migration 0092 includes a reversible RunPython that copies legacy JSON values into the M2M through table.

Example migration snippet (adjust legacy field name if needed):

def forwards(apps, schema_editor):
    RiskScenario = apps.get_model("core", "RiskScenario")
    Qualification = apps.get_model("core", "Qualification")
    db = schema_editor.connection.alias
    for rs in RiskScenario.objects.using(db).all():
        legacy = getattr(rs, "_qualifications_legacy", None) or rs.__dict__.get("qualifications", [])
        if not legacy:
            continue
        to_add = []
        for item in legacy:
            urn = item if isinstance(item, str) else (item.get("urn") if isinstance(item, dict) else None)
            if urn:
                q = Qualification.objects.using(db).filter(urn__iexact=urn).first()
                if q:
                    to_add.append(q)
        if to_add:
            rs.qualifications.add(*to_add)

def backwards(apps, schema_editor):
    RiskScenario = apps.get_model("core", "RiskScenario")
    db = schema_editor.connection.alias
    for rs in RiskScenario.objects.using(db).all():
        setattr(rs, "_qualifications_legacy", [q.urn for q in rs.qualifications.all()])
        rs.save(update_fields=["_qualifications_legacy"])

I can open a follow-up PR with the exact migration once you confirm the legacy field name used in 0091/0092.


🏁 Script executed:

#!/bin/bash
# Verify a data-migration exists that backfills RiskScenario.qualifications
fd -t f -e py 'backend/**/migrations' | xargs rg -nP '0092|RiskScenario.*qualifications|RunPython|JSONField'

Length of output: 292382


Make the qualifications migration reversible and backfill legacy JSON→M2M.

  • Migration exists in backend/core/migrations/0093_qualification_library_and_more.py and currently calls RunPython(migrate_qualifications, migrations.RunPython.noop).
  • Replace with a reversible RunPython: forwards must copy the legacy JSON (introduced in backend/core/migrations/0022_riskscenario_qualifications.py) into the new ManyToMany (map strings/dicts to Qualification by urn and rs.qualifications.add(*qs)); backwards should restore the legacy JSON (e.g., set legacy field to [q.urn for q in rs.qualifications.all()]).
  • I can open a follow-up PR with the exact migration once you confirm the legacy field name.
🤖 Prompt for AI Agents
In backend/core/models.py around lines 3659 to 3665, the new ManyToManyField
"qualifications" needs a reversible migration that backfills from the legacy
JSON field and restores it on reverse; update
backend/core/migrations/0093_qualification_library_and_more.py to replace
RunPython(migrate_qualifications, migrations.RunPython.noop) with a reversible
RunPython function pair: forward should iterate RiskScenario objects, read the
legacy JSON field (use the legacy field name I'll confirm), normalize entries
(strings or dicts) to URNs, look up Qualification by urn and call
rs.qualifications.add(*found_qs) (skip missing quals, handle null/empty), and
save; reverse should iterate RiskScenario objects, build a list of qualification
urns from rs.qualifications.all() and set the legacy JSON field to that list (or
[]), saving each object; ensure the migration uses atomic operations and is
idempotent.

Comment on lines +3472 to +3474
many_to_many_map_ids["qualification_ids"] = get_mapped_ids(
_fields.pop("qualifications", []), link_dump_database_ids
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix indentation issue in qualification_ids mapping

The qualification_ids mapping for RiskScenario is incorrectly indented inside the loop for related fields. It should be at the same level as other field mappings.

                 for field in related__fields:
                     map_key = (
                         f"{field.rstrip('s')}_ids"
                         if not field.endswith("controls")
                         else f"{field}_ids"
                     )
                     many_to_many_map_ids[map_key] = get_mapped_ids(
                         _fields.pop(field, []), link_dump_database_ids
                     )
-                    many_to_many_map_ids["qualification_ids"] = get_mapped_ids(
-                        _fields.pop("qualifications", []), link_dump_database_ids
-                    )
+                many_to_many_map_ids["qualification_ids"] = get_mapped_ids(
+                    _fields.pop("qualifications", []), link_dump_database_ids
+                )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
many_to_many_map_ids["qualification_ids"] = get_mapped_ids(
_fields.pop("qualifications", []), link_dump_database_ids
)
for field in related__fields:
map_key = (
f"{field.rstrip('s')}_ids"
if not field.endswith("controls")
else f"{field}_ids"
)
many_to_many_map_ids[map_key] = get_mapped_ids(
_fields.pop(field, []), link_dump_database_ids
)
many_to_many_map_ids["qualification_ids"] = get_mapped_ids(
_fields.pop("qualifications", []), link_dump_database_ids
)
🤖 Prompt for AI Agents
In backend/core/views.py around lines 3472 to 3474, the assignment adding
"qualification_ids" to many_to_many_map_ids is incorrectly indented inside the
related-fields loop; move that block so the qualification_ids mapping (calling
get_mapped_ids on _fields.pop("qualifications", []), link_dump_database_ids)
sits at the same indentation level as the other top-level field mappings (i.e.,
outside the loop) so it is executed once per RiskScenario instead of per related
field.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
backend/library/utils.py (2)

755-764: Gate qualifications to enterprise and fix log label

Only process qualifications when enterprise is installed; also correct the logger label.

Apply:

-        if "qualifications" in library_objects:
-            qualification_data = library_objects["qualifications"]
-            if (
-                qualification_import_error := self.init_qualifications(
-                    qualification_data
-                )
-            ) is not None:
-                logger.error("Threat import error", error=qualification_import_error)
-                return qualification_import_error
+        if apps.is_installed("enterprise_core") and "qualifications" in library_objects:
+            qualification_data = library_objects["qualifications"]
+            if (
+                qualification_import_error := self.init_qualifications(
+                    qualification_data
+                )
+            ) is not None:
+                logger.error("Qualification import error", error=qualification_import_error)
+                return qualification_import_error

410-441: Make qualification import idempotent and validate abbreviation length

Use update_or_create keyed by (urn, locale) to avoid duplicate rows on re-imports, and enforce abbreviation max length (20) before hitting DB constraints.

Apply:

 class QualificationImporter:
@@
     def import_qualification(self, library_object: LoadedLibrary):
-        Qualification.objects.create(
-            library=library_object,
-            urn=self.qualification_data["urn"].lower(),
-            ref_id=self.qualification_data["ref_id"],
-            name=self.qualification_data["name"],
-            description=self.qualification_data.get("description"),
-            abbreviation=self.qualification_data.get("abbreviation"),
-            qualification_ordering=self.qualification_data.get(
-                "qualification_ordering", 0
-            ),
-            security_objective_ordering=self.qualification_data.get(
-                "security_objective_ordering", 0
-            ),
-            provider=library_object.provider,
-            is_published=True,
-            locale=library_object.locale,
-            translations=self.qualification_data.get("translations", {}),
-            default_locale=library_object.default_locale,  # Change this in the future ?
-        )
+        abbr = self.qualification_data.get("abbreviation")
+        if abbr is not None and len(abbr) > 20:
+            raise ValueError("abbreviation must be <= 20 characters")
+
+        Qualification.objects.update_or_create(
+            urn=self.qualification_data["urn"].lower(),
+            locale=library_object.locale,
+            defaults=dict(
+                library=library_object,
+                ref_id=self.qualification_data["ref_id"],
+                name=self.qualification_data["name"],
+                description=self.qualification_data.get("description"),
+                abbreviation=abbr,
+                qualification_ordering=self.qualification_data.get(
+                    "qualification_ordering", 0
+                ),
+                security_objective_ordering=self.qualification_data.get(
+                    "security_objective_ordering", 0
+                ),
+                provider=library_object.provider,
+                is_published=True,
+                translations=self.qualification_data.get("translations", {}),
+                default_locale=library_object.default_locale,  # Change this in the future ?
+            ),
+        )
🧹 Nitpick comments (10)
backend/library/utils.py (1)

5-5: Remove unused import or use it for enterprise gating

from django.apps import apps is unused unless you gate qualifications to enterprise; either remove it or apply the gating below.

enterprise/frontend/src/lib/utils/models.ts (1)

1-5: Make override type-safe and freeze action literals

Use literal types to prevent drift and ease downstream narrowing.

-export const MODEL_MAP_OVERRIDE = {
-	qualifications: {
-		actions: ['add', 'view', 'change', 'delete']
-	},
-};
+export const MODEL_MAP_OVERRIDE = {
+	qualifications: {
+		actions: ['add', 'view', 'change', 'delete'] as const,
+	},
+} as const;
frontend/src/lib/components/DetailView/DetailView.svelte (1)

210-214: Consider extracting domain resolution into a helper

The folder-domain calculation is repeated across views; a small util (e.g., getDomainId(model, data, user)) would reduce drift.

frontend/src/routes/(app)/(internal)/risk-scenarios/[id=uuid]/+page.svelte (2)

26-31: Switch to model object for permissions — LGTM

Minor note: for consistency with newer helpers, consider getModelInfo('risk-scenarios') instead of URL_MODEL_MAP here.


363-366: Simplify qualifications rendering and avoid per-item fallback commas

Current loop can emit leading/trailing commas and multiple “undefined”. Join once with a single fallback.

Apply this diff:

-					{#each data.scenario.qualifications as qualification, i}
-						{#if i > 0},{/if}
-						{qualification.str || m.undefined()}
-					{/each}
+					{#if data.scenario.qualifications?.length}
+						{data.scenario.qualifications.map((q) => q.str).filter(Boolean).join(', ')}
+					{:else}
+						{m.undefined()}
+					{/if}
frontend/src/routes/(app)/(third-party)/compliance-assessments/[id=uuid]/+page.svelte (1)

51-56: Pass full ModelMapEntry to canPerformAction—use getModelInfo to honor overrides

Using URL_MODEL_MAP works but may miss enterprise overrides/actions. Switch to getModelInfo for consistency with the rest of the app.

Apply:

@@
- import { URL_MODEL_MAP } from '$lib/utils/crud';
+ import { URL_MODEL_MAP, getModelInfo } from '$lib/utils/crud';
@@
- const model = URL_MODEL_MAP['compliance-assessments'];
+ const model = getModelInfo('compliance-assessments');
@@
- const requirementAssessmentModel = URL_MODEL_MAP['requirement-assessments'];
+ const requirementAssessmentModel = getModelInfo('requirement-assessments');

Ensure both models have .name populated after this change.

Also applies to: 60-65

frontend/src/lib/utils/access-control.ts (1)

1-16: Guard on disallowed actions first; tiny readability win

Early-return if the action isn’t allowed by the model before checking domain permissions.

Apply:

 export function canPerformAction({ user, action, model, domain }: CanPerformActionParams): boolean {
-  return (
-    (user.domain_permissions[domain] || []).includes(`${action}_${model.name}`) &&
-    !(model?.actions && !model.actions.includes(action))
-  );
+  if (model?.actions && !model.actions.includes(action)) return false;
+  return (user.domain_permissions[domain] || []).includes(`${action}_${model.name}`);
 }
frontend/src/lib/utils/crud.ts (3)

732-749: Qualifications detail view: mark datetime fields; optionally link library

  • created_at and updated_at should use type: 'datetime' for consistent rendering (used elsewhere).
  • If the detail renderer supports linking FKs via urlModel, consider marking library as urlModel: 'libraries' so it’s clickable (verify typings).

Apply:

       detailViewFields: [
         { field: 'name' },
         { field: 'description' },
         { field: 'annotation' },
-        { field: 'created_at' },
-        { field: 'updated_at' },
+        { field: 'created_at', type: 'datetime' },
+        { field: 'updated_at', type: 'datetime' },
         { field: 'urn' },
         { field: 'id' },
         { field: 'provider' },
         { field: 'locale' },
         { field: 'default_locale' },
         { field: 'abbreviation' },
         { field: 'qualification_ordering' },
         { field: 'security_objective_ordering' },
-        { field: 'library' }
+        { field: 'library' }
       ],
       actions: ['view']

If linking is desired, and supported by your renderer types, change the last line to:
{ field: 'library', urlModel: 'libraries' }


1619-1622: getModelInfo refactor is cleaner; propagate the router usage

Nice guard for undefined and centralizing resolution in routeToModelInfo.

To keep behavior consistent (e.g., for model variants like *_duplicate), update helpers below to use getModelInfo instead of direct URL_MODEL_MAP access:

-export const urlParamModelForeignKeyFields = (model: string): ForeignKeyField[] => {
-  return URL_MODEL_MAP[model]?.foreignKeyFields || [];
-};
+export const urlParamModelForeignKeyFields = (model: string): ForeignKeyField[] => {
+  const info = getModelInfo(model);
+  return info?.foreignKeyFields || [];
+};

-export const urlParamModelSelectFields = (model: string): SelectField[] => {
-  return URL_MODEL_MAP[model]?.selectFields || [];
-};
+export const urlParamModelSelectFields = (model: string): SelectField[] => {
+  const info = getModelInfo(model);
+  return info?.selectFields || [];
+};

Please verify no callers rely on the previous direct-lookup fallback semantics.


7-7: Decouple URL_MODEL_MAP to eliminate the ES module cycle
We have a circular import:

  • frontend/src/lib/utils/crud.ts imports routeToModelInfo (line 1621)
  • frontend/src/lib/utils/modelinfo-router.ts imports URL_MODEL_MAP (line 1)

To break this cycle, consider:

  • Extracting URL_MODEL_MAP into its own module (e.g. utils/model-map.ts) that both files import
  • Or changing routeToModelInfo to accept the map as an argument (e.g. routeToModelInfo(model, URL_MODEL_MAP)) so modelinfo-router.ts no longer imports from crud.ts
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 34403fe and 28c41bf.

📒 Files selected for processing (25)
  • .github/workflows/functional-tests.yml (1 hunks)
  • backend/library/utils.py (8 hunks)
  • backend/serdes/views.py (0 hunks)
  • enterprise/frontend/src/lib/utils/modelinfo-router.ts (1 hunks)
  • enterprise/frontend/src/lib/utils/models.ts (1 hunks)
  • frontend/src/lib/components/DetailView/DetailView.svelte (1 hunks)
  • frontend/src/lib/components/ModelTable/ModelTable.svelte (6 hunks)
  • frontend/src/lib/components/TableRowActions/TableRowActions.svelte (3 hunks)
  • frontend/src/lib/utils/access-control.ts (1 hunks)
  • frontend/src/lib/utils/crud.ts (4 hunks)
  • frontend/src/lib/utils/modelinfo-router.ts (1 hunks)
  • frontend/src/routes/(app)/(internal)/[model=urlmodel]/+layout.server.ts (0 hunks)
  • frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-1/baseline/+page.svelte (1 hunks)
  • frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-1/ebios-rm-study/+page.svelte (1 hunks)
  • frontend/src/routes/(app)/(internal)/incidents/[id=uuid]/+page.svelte (1 hunks)
  • frontend/src/routes/(app)/(internal)/my-profile/+page.svelte (2 hunks)
  • frontend/src/routes/(app)/(internal)/operating-modes/[id=uuid]/+page.svelte (1 hunks)
  • frontend/src/routes/(app)/(internal)/operational-scenarios/[id=uuid]/+page.svelte (1 hunks)
  • frontend/src/routes/(app)/(internal)/recap/+page.svelte (1 hunks)
  • frontend/src/routes/(app)/(internal)/risk-assessments/[id=uuid]/+page.svelte (1 hunks)
  • frontend/src/routes/(app)/(internal)/risk-scenarios/[id=uuid]/+page.svelte (2 hunks)
  • frontend/src/routes/(app)/(internal)/ro-to/[id=uuid]/+page.svelte (1 hunks)
  • frontend/src/routes/(app)/(internal)/stored-libraries/[id=uuid]/+page.svelte (3 hunks)
  • frontend/src/routes/(app)/(third-party)/compliance-assessments/[id=uuid]/+page.svelte (2 hunks)
  • frontend/src/routes/(app)/(third-party)/evidences/[id=uuid]/+page.svelte (1 hunks)
💤 Files with no reviewable changes (2)
  • backend/serdes/views.py
  • frontend/src/routes/(app)/(internal)/[model=urlmodel]/+layout.server.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • .github/workflows/functional-tests.yml
  • frontend/src/routes/(app)/(internal)/stored-libraries/[id=uuid]/+page.svelte
🧰 Additional context used
🧬 Code graph analysis (5)
frontend/src/lib/utils/modelinfo-router.ts (2)
enterprise/frontend/src/lib/utils/modelinfo-router.ts (1)
  • routeToModelInfo (4-11)
frontend/src/lib/utils/crud.ts (1)
  • URL_MODEL_MAP (147-1468)
enterprise/frontend/src/lib/utils/modelinfo-router.ts (2)
frontend/src/lib/utils/crud.ts (1)
  • URL_MODEL_MAP (147-1468)
enterprise/frontend/src/lib/utils/models.ts (1)
  • MODEL_MAP_OVERRIDE (1-5)
frontend/src/lib/utils/access-control.ts (2)
frontend/src/lib/utils/types.ts (1)
  • User (5-24)
frontend/src/lib/utils/crud.ts (1)
  • ModelMapEntry (122-141)
frontend/src/lib/utils/crud.ts (2)
frontend/src/lib/utils/types.ts (1)
  • urlModel (117-117)
frontend/src/lib/utils/modelinfo-router.ts (1)
  • routeToModelInfo (3-9)
backend/library/utils.py (1)
backend/core/models.py (3)
  • Qualification (1431-1596)
  • LoadedLibrary (849-971)
  • create (3001-3003)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: startup-docker-compose-test
  • GitHub Check: enterprise-startup-docker-compose-test
  • GitHub Check: enterprise-startup-functional-test (3.12)
  • GitHub Check: startup-functional-test (3.12)
  • GitHub Check: build_enterprise_frontend
  • GitHub Check: build_community_frontend
  • GitHub Check: test (3.12)
  • GitHub Check: build (3.12)
🔇 Additional comments (19)
frontend/src/routes/(app)/(internal)/risk-assessments/[id=uuid]/+page.svelte (1)

36-41: Verified: all canPerformAction calls switch to full model
No remaining instances of model.name in call sites.

frontend/src/routes/(app)/(internal)/recap/+page.svelte (1)

28-34: LGTM: permission refactor consistency

Using the full model object with canPerformAction is consistent with the new API.

frontend/src/routes/(app)/(internal)/ro-to/[id=uuid]/+page.svelte (1)

42-48: LGTM: switched to full model for ACL

model: model aligns with the updated access-control helper.

frontend/src/routes/(app)/(internal)/operational-scenarios/[id=uuid]/+page.svelte (1)

50-55: LGTM: updated canPerformAction signature

Passing the full model object is correct here.

backend/library/utils.py (3)

571-592: Good validation scaffold for qualifications

Validation and error aggregation mirror other importers and look consistent.


494-513: LGTM: wire qualifications into object lists and state

Adding "qualifications" to OBJECT_FIELDS and _qualifications state is correct.


841-843: LGTM: import loop

Delegating to each importer is consistent with other object types.

frontend/src/routes/(app)/(internal)/operating-modes/[id=uuid]/+page.svelte (1)

69-78: Approve: passing full model object to canPerformAction is correct; no legacy string-based usages found.

frontend/src/routes/(app)/(internal)/incidents/[id=uuid]/+page.svelte (1)

172-181: Permission check update LGTM

Switching to model: data.model aligns with ModelMapEntry-based permissions.

frontend/src/routes/(app)/(third-party)/evidences/[id=uuid]/+page.svelte (1)

70-79: Consistent use of full model object for permissions

Change is correct and matches the new access-control signature.

frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-1/baseline/+page.svelte (1)

65-73: Pass full model object to canPerformAction — LGTM

Change aligns with the updated access-control API. Domain resolution looks consistent with other views.

frontend/src/lib/components/DetailView/DetailView.svelte (1)

205-215: Pass full model object to canPerformAction — LGTM

Matches the project-wide shift to object-based permissions.

frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-1/ebios-rm-study/+page.svelte (1)

87-95: Pass full model object to canPerformAction — LGTM

Consistent with the new access-control signature.

frontend/src/lib/components/ModelTable/ModelTable.svelte (3)

9-14: Model info now resolved via getModelInfo—LGTM

This aligns with the new routing and supports enterprise overrides.

Also applies to: 139-139


341-351: Permission checks now pass the model object—LGTM

Using ModelMapEntry matches access-control.ts and enables per-model action gating.

Confirm that getModelInfo(URLModel) always returns an entry with a valid name for all tables rendered here.

Also applies to: 357-371


412-412: Added data-testid hooks—nice for E2E stability

data-testid="model-table" and per-row ids will simplify selectors.

Also applies to: 510-510

frontend/src/lib/components/TableRowActions/TableRowActions.svelte (2)

129-144: Switched to ModelMapEntry for auth checks—LGTM

Domain resolution and fallbacks remain unchanged.

Also applies to: 146-160


162-169: Explicit Boolean coercion for displayEdit—LGTM

Makes intent clear with no behavior change.

frontend/src/lib/utils/crud.ts (1)

140-141: actions metadata addition looks good

Adding actions?: Array<'add' | 'view' | 'change' | 'delete'> enables per-model UI capabilities control.

Comment on lines +4 to +11
export function routeToModelInfo(model: string) {
const baseModel = model.split('_')[0];
const map = URL_MODEL_MAP[model] || URL_MODEL_MAP[baseModel] || {};
// The urlmodel of {model}_duplicate must be {model}
map['urlModel'] = baseModel;
const override = MODEL_MAP_OVERRIDE[baseModel] || {};
return {...map, ...override};
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Bug: mutates URL_MODEL_MAP entries in place

Assigning map['urlModel'] mutates the shared object; side effects can leak globally. Clone before setting urlModel.

Apply:

-export function routeToModelInfo(model: string) {
-  const baseModel = model.split('_')[0];
-  const map = URL_MODEL_MAP[model] || URL_MODEL_MAP[baseModel] || {};
-  // The urlmodel of {model}_duplicate must be {model}
-  map['urlModel'] = baseModel;
-  const override = MODEL_MAP_OVERRIDE[baseModel] || {};
-  return {...map, ...override};
-}
+export function routeToModelInfo(model: string) {
+  const baseModel = model.split('_')[0];
+  const base = URL_MODEL_MAP[model] ?? URL_MODEL_MAP[baseModel] ?? {};
+  const map = { ...base, urlModel: baseModel };
+  const override = MODEL_MAP_OVERRIDE[baseModel] ?? {};
+  return { ...map, ...override };
+}

Search for other places that might mutate URL_MODEL_MAP entries.


🏁 Script executed:

#!/bin/bash
rg -nP "URL_MODEL_MAP\\[[^\\]]+\\]\\s*\\[|Object\\.assign\\(\\s*URL_MODEL_MAP" -C2

Length of output: 3743


Shallow-clone before setting urlModel

Apply the following change to avoid mutating shared URL_MODEL_MAP:

 export function routeToModelInfo(model: string) {
-  const baseModel = model.split('_')[0];
-  const map = URL_MODEL_MAP[model] || URL_MODEL_MAP[baseModel] || {};
-  // The urlmodel of {model}_duplicate must be {model}
-  map['urlModel'] = baseModel;
-  const override = MODEL_MAP_OVERRIDE[baseModel] || {};
-  return {...map, ...override};
+  const baseModel = model.split('_')[0];
+  const base = URL_MODEL_MAP[model] ?? URL_MODEL_MAP[baseModel] ?? {};
+  const map = { ...base, urlModel: baseModel };
+  const override = MODEL_MAP_OVERRIDE[baseModel] ?? {};
+  return { ...map, ...override };
 }

No other write-operations on URL_MODEL_MAP entries were found in the codebase.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function routeToModelInfo(model: string) {
const baseModel = model.split('_')[0];
const map = URL_MODEL_MAP[model] || URL_MODEL_MAP[baseModel] || {};
// The urlmodel of {model}_duplicate must be {model}
map['urlModel'] = baseModel;
const override = MODEL_MAP_OVERRIDE[baseModel] || {};
return {...map, ...override};
}
export function routeToModelInfo(model: string) {
const baseModel = model.split('_')[0];
const base = URL_MODEL_MAP[model] ?? URL_MODEL_MAP[baseModel] ?? {};
const map = { ...base, urlModel: baseModel };
const override = MODEL_MAP_OVERRIDE[baseModel] ?? {};
return { ...map, ...override };
}
🤖 Prompt for AI Agents
In enterprise/frontend/src/lib/utils/modelinfo-router.ts around lines 4 to 11,
the function mutates the object returned from URL_MODEL_MAP by setting
map['urlModel'], which can mutate shared map entries; fix by shallow-cloning the
resolved map before modifying it (e.g., const mapCopy = {...map}), set
mapCopy['urlModel'] = baseModel, then return the merged result using mapCopy and
the override instead of mutating the original.

Comment on lines +3 to +9
export function routeToModelInfo(model: string) {
const baseModel = model.split('_')[0];
const map = URL_MODEL_MAP[model] || URL_MODEL_MAP[baseModel] || {};
// The urlmodel of {model}_duplicate must be {model}
map['urlModel'] = baseModel;
return map;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid mutating URL_MODEL_MAP entries

Setting map['urlModel'] mutates shared config. Return a new object to prevent side effects.

Apply this diff:

 export function routeToModelInfo(model: string) {
 	const baseModel = model.split('_')[0];
-	const map = URL_MODEL_MAP[model] || URL_MODEL_MAP[baseModel] || {};
-	// The urlmodel of {model}_duplicate must be {model}
-	map['urlModel'] = baseModel;
-	return map;
+	const entry = URL_MODEL_MAP[model] || URL_MODEL_MAP[baseModel] || {};
+	// The urlmodel of {model}_duplicate must be {model}
+	return { ...entry, urlModel: baseModel };
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function routeToModelInfo(model: string) {
const baseModel = model.split('_')[0];
const map = URL_MODEL_MAP[model] || URL_MODEL_MAP[baseModel] || {};
// The urlmodel of {model}_duplicate must be {model}
map['urlModel'] = baseModel;
return map;
}
export function routeToModelInfo(model: string) {
const baseModel = model.split('_')[0];
const entry = URL_MODEL_MAP[model] || URL_MODEL_MAP[baseModel] || {};
// The urlmodel of {model}_duplicate must be {model}
return { ...entry, urlModel: baseModel };
}
🤖 Prompt for AI Agents
In frontend/src/lib/utils/modelinfo-router.ts around lines 3 to 9, the code
mutates shared URL_MODEL_MAP by assigning map['urlModel'] = baseModel; instead
return a new object so you don't modify the shared config. Replace the mutation
with creating and returning a copy (e.g. using the object spread or
Object.assign) that includes all existing map properties plus urlModel:
baseModel, so return { ...map, urlModel: baseModel } instead of mutating map.

@@ -6,6 +6,7 @@
import { getLocale } from '$paraglide/runtime';
import Anchor from '$lib/components/Anchor/Anchor.svelte';
import { canPerformAction } from '$lib/utils/access-control';
import { getModelInfo } from '$lib/utils/crud';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Guard against undefined from getModelInfo('users') to avoid runtime/type errors

getModelInfo may return undefined per the new typing; add a small guard (or non-null assertion) before passing to canPerformAction.

 const user = page.data.user;
-const canEditObject: boolean = canPerformAction({
+const userModel = getModelInfo('users');
+const canEditObject: boolean = userModel
+  ? canPerformAction({
     user,
     action: 'change',
-    model: getModelInfo('users'),
+    model: userModel,
     domain: user.root_folder_id
-  });
+  })
+  : false;

Optional check to ensure the model entry exists:

Also applies to: 28-34


🏁 Script executed:

#!/bin/bash
# Verify "users" exists in the URL model map
rg -nP "URL_MODEL_MAP\s*=\s*{(?s).*['\"]users['\"]\s*:" -C2 frontend

Length of output: 74


🏁 Script executed:

#!/bin/bash
# Locate definition and usage of getModelInfo
rg -nR "getModelInfo" -C3 frontend

# Search for any model map definitions that might include "users"
rg -nR "URL_MODEL_MAP" -C3 frontend || rg -nR "MODEL_MAP" -C3 frontend

Length of output: 1317


🏁 Script executed:

#!/bin/bash
# Locate the TypeScript definition of getModelInfo
rg -n "export function getModelInfo" -C5 -g "*.ts" .

# Locate the URL_MODEL_MAP declaration to verify 'users' entry
rg -n "URL_MODEL_MAP\s*=" -C5 -g "*.ts" .

Length of output: 98


🏁 Script executed:

#!/bin/bash
# List files in the utils directory containing crud
ls frontend/src/lib/utils || echo "Directory not found"

# Display the first 200 lines of any crud.* files
for f in frontend/src/lib/utils/crud.*; do
  [ -f "$f" ] && {
    echo "=== $f ==="
    sed -n '1,200p' "$f"
  }
done

Length of output: 5876


🏁 Script executed:

#!/bin/bash
# Find all occurrences of getModelInfo to locate its implementation
rg -n "getModelInfo" -C3 -g "*.ts"

# Show content of modelinfo-router.ts around the definition of getModelInfo
grep -R "getModelInfo" -n frontend/src/lib/utils/modelinfo-router.ts | cut -d: -f1 | while read -r ln; do
  sed -n "$((ln-5)),$((ln+20))p" frontend/src/lib/utils/modelinfo-router.ts
done

# Verify 'users' key exists in URL_MODEL_MAP
grep -R "URL_MODEL_MAP" -n frontend/src/lib/utils/crud.ts | cut -d: -f1 | head -1 | while read -r start; do
  sed -n "$start, $((start+500))p" frontend/src/lib/utils/crud.ts | grep -C2 "'users'"
done

Length of output: 91723


Use non-null assertion for getModelInfo('users')

Replace the unguarded call with a non-null assertion to satisfy the return type and avoid passing undefined:

-const canEditObject = canPerformAction({
-  user,
-  action: 'change',
-  model: getModelInfo('users'),
-  domain: user.root_folder_id
-});
+const canEditObject = canPerformAction({
+  user,
+  action: 'change',
+  model: getModelInfo('users')!,
+  domain: user.root_folder_id
+});

Apply the same ! assertion for the getModelInfo('users') call at lines 28–34.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { getModelInfo } from '$lib/utils/crud';
const canEditObject = canPerformAction({
user,
action: 'change',
model: getModelInfo('users')!,
domain: user.root_folder_id
});
🤖 Prompt for AI Agents
In frontend/src/routes/(app)/(internal)/my-profile/+page.svelte at line 9 and
especially around lines 28–34, the calls to getModelInfo('users') are unguarded
and may be inferred as possibly undefined; update those calls by appending a
non-null assertion (!) to getModelInfo('users') so the expressions become
getModelInfo('users')! (apply the same change to every occurrence on lines
28–34) to satisfy the return type and avoid passing undefined.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants