Skip to content
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
3bdf22e
feat: Add custom qualifications
monsieurswag Aug 26, 2025
085ea7b
rename escalation_threshold related_name and ruff format
monsieurswag Aug 26, 2025
05f1c0c
Fix translation typo
monsieurswag Aug 26, 2025
12f8eeb
Remove some unused props
monsieurswag Aug 26, 2025
ffb5647
Fix conflicts
monsieurswag Aug 26, 2025
21e778f
Merge branch 'main' into feat/custom_qualifications
monsieurswag Aug 27, 2025
41abf75
Add missing translation
monsieurswag Aug 27, 2025
4b8d574
Fix conflicts
monsieurswag Aug 27, 2025
0338930
Fix migration conflicts
monsieurswag Aug 27, 2025
5fb360a
Merge branch 'main' into feat/custom_qualifications
monsieurswag Aug 27, 2025
54f2337
build(ci): run coverage with poetry in backend-coverage
Mohamed-Hacene Aug 28, 2025
1d30d82
Fix conflicts
monsieurswag Aug 28, 2025
020e35b
fix(ci): add string keys for databse url ports
Mohamed-Hacene Aug 28, 2025
4ccdbb6
Fix conflicts
monsieurswag Aug 28, 2025
69c498b
Prevent floats from being passed as ordering values
monsieurswag Aug 28, 2025
d46f0de
Make custom qualifications only available in enterprise version
monsieurswag Aug 28, 2025
fe38f72
Make qualifications only accessible in enterprise version
monsieurswag Aug 28, 2025
37e5490
Add warning when storing a libary containing qualifications in the co…
monsieurswag Aug 28, 2025
27a50d0
Ordering field note in data-model
monsieurswag Aug 28, 2025
1bedf83
Fix e2e-tests database snapshot usage
monsieurswag Aug 28, 2025
79d1d49
Add qualification test
monsieurswag Aug 28, 2025
c32e231
Fix conflicts
monsieurswag Aug 29, 2025
e6d5ab4
Formatter
monsieurswag Aug 29, 2025
530b0f7
Step qualification fix
monsieurswag Aug 29, 2025
028e4cb
Fix CI
monsieurswag Aug 29, 2025
96edf33
Remove debug line
monsieurswag Aug 29, 2025
69a335e
Fix custom qualifications not showing in risk scenarios
monsieurswag Aug 29, 2025
4ae0c28
Remove unecessary API endpoint
monsieurswag Aug 29, 2025
78cc138
Fix conflicts
monsieurswag Aug 29, 2025
20c167f
Make RiskScenario qualifications a ManyToManyField
monsieurswag Aug 29, 2025
30a974d
Fix qualification translation in risk scenario detail view
monsieurswag Aug 29, 2025
6a05f75
Formatter
monsieurswag Aug 29, 2025
a88d51e
Remove debug line
monsieurswag Aug 29, 2025
a70e1ab
remove EE warning in CE
eric-intuitem Aug 31, 2025
5c4a46c
Merge branch 'main' into feat/custom_qualifications
eric-intuitem Aug 31, 2025
4cf66c5
fix migrations
eric-intuitem Aug 31, 2025
3bf4cb6
simplify/harmonize code
eric-intuitem Aug 31, 2025
673bafc
Update +page.server.ts
eric-intuitem Aug 31, 2025
5b93e9c
Update 0093_qualification_library_and_more.py
eric-intuitem Aug 31, 2025
2f5bec4
fix migration
eric-intuitem Aug 31, 2025
0c0ce56
Fix domain import/export
eric-intuitem Aug 31, 2025
97d693b
Update dummy-domain.bak
eric-intuitem Aug 31, 2025
34403fe
ruff+prettier
eric-intuitem Aug 31, 2025
e40b2e4
Merge branch 'main' into feat/custom_qualifications
nas-tabchiche Sep 2, 2025
28d6a10
remove is_enterprise flag
nas-tabchiche Sep 2, 2025
0ae96a7
add `actions` entry to ModelMap
nas-tabchiche Sep 2, 2025
7c824ea
udpate canPerformAction function signature
nas-tabchiche Sep 2, 2025
c7dee95
write canPerformAction predicate in a way that does not make ESLint f…
nas-tabchiche Sep 2, 2025
3033b1c
allow for enterprise overrides on modelInfo objects
nas-tabchiche Sep 2, 2025
28c41bf
remove last references to is_enterprise
nas-tabchiche Sep 2, 2025
5c565b4
remove isEnterpriseMode call
nas-tabchiche Sep 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/backend-coverage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ jobs:
- name: Run coverage
working-directory: ${{ env.backend-directory }}
env:
DATABASE_URL: "postgres://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/postgres"
run: find . -path '*/tests/*' -and -name 'test*.py' -and -not -path "./venv/*" | xargs coverage run -m pytest
DATABASE_URL: "postgres://postgres:postgres@localhost:${{ job.services.postgres.ports['5432'] }}/postgres"
run: poetry run coverage run -m pytest
- name: Display coverage report
working-directory: ${{ env.backend-directory }}
env:
DATABASE_URL: "postgres://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/postgres"
run: coverage report
DATABASE_URL: "postgres://postgres:postgres@localhost:${{ job.services.postgres.ports['5432'] }}/postgres"
run: poetry run coverage report
29 changes: 29 additions & 0 deletions backend/core/migrations/0092_qualification_library_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 5.1.10 on 2025-08-25 21:14

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("core", "0091_appliedcontrol_objectives_and_more"),
]

operations = [
migrations.AddField(
model_name="qualification",
name="library",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="qualifications",
to="core.loadedlibrary",
),
),
migrations.AlterField(
model_name="qualification",
name="is_published",
field=models.BooleanField(default=True, verbose_name="published"),
),
]
40 changes: 38 additions & 2 deletions backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,11 @@ def _objects(self):
update_translations_in_object(model_to_dict(reference_control))
for reference_control in self.reference_controls.all()
]
if self.qualifications.exists():
res["qualifications"] = [
update_translations_in_object(model_to_dict(qualification))
for qualification in self.qualifications.all()
]
if self.risk_matrices.exists():
matrix = self.risk_matrices.first()
res["risk_matrix"] = update_translations_in_object(model_to_dict(matrix))
Expand All @@ -900,6 +905,10 @@ def reference_count(self) -> int:
"""
Returns the number of distinct dependent libraries and risk and compliance assessments that reference objects from this library
"""

from ebios_rm.models import FearedEvent
from resilience.models import EscalationThreshold

return (
RiskAssessment.objects.filter(
Q(risk_scenarios__threats__library=self)
Expand All @@ -916,6 +925,9 @@ def reference_count(self) -> int:
)
.distinct()
.count()
+ 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()
Comment on lines +928 to 931
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.

)

Expand Down Expand Up @@ -983,7 +995,7 @@ class Meta:

def is_deletable(self) -> bool:
"""
Returns True if the framework can be deleted
Returns True if the threat can be deleted
"""
if self.requirements.exists():
return False
Expand Down Expand Up @@ -1049,7 +1061,7 @@ class Meta:

def is_deletable(self) -> bool:
"""
Returns True if the framework can be deleted
Returns True if the refence control can be deleted
"""
if self.requirements.exists() or self.appliedcontrol_set.exists():
return False
Expand Down Expand Up @@ -1535,6 +1547,16 @@ class Qualification(ReferentialObjectMixin, I18nObjectMixin, PublishInRootFolder
},
]

library = models.ForeignKey(
LoadedLibrary,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="qualifications",
)

is_published = models.BooleanField(_("published"), default=True)

abbreviation = models.CharField(
max_length=20, null=True, blank=True, verbose_name=_("Abbreviation")
)
Expand All @@ -1550,6 +1572,20 @@ class Meta:
verbose_name_plural = _("Qualifications")
ordering = ["qualification_ordering"]

fields_to_check = ["ref_id", "name"]

def is_deletable(self) -> bool:
"""
Returns True if the qualification can be deleted
"""
return not any(
[
self.incidents.exists(),
self.feared_events.exists(),
self.escalation_thresholds.exists(),
]
)

@classmethod
def create_default_qualifications(cls):
for qualification in cls.DEFAULT_QUALIFICATIONS:
Expand Down
10 changes: 7 additions & 3 deletions backend/core/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1564,14 +1564,18 @@ class Meta:
exclude = ["folder", "is_published"]


class QualificationReadSerializer(ReferentialSerializer):
class QualificationWriteSerializer(BaseModelSerializer):
class Meta:
model = Qualification
exclude = ["translations"]


class QualificationWriteSerializer(QualificationReadSerializer):
pass
class QualificationReadSerializer(ReferentialSerializer):
library = FieldsRelatedField(["name", "id"])

class Meta:
model = Qualification
exclude = ["translations"]


class SecurityExceptionWriteSerializer(BaseModelSerializer):
Expand Down
71 changes: 70 additions & 1 deletion backend/library/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
RiskMatrix,
ReferenceControl,
Threat,
Qualification,
)
from django.db import transaction
from iam.models import Folder
Expand Down Expand Up @@ -341,7 +342,6 @@ class ThreatImporter:

def __init__(self, threat_data: dict):
self.threat_data = threat_data
self._object = None

def is_valid(self) -> Union[str, None]:
if missing_fields := self.REQUIRED_FIELDS - set(self.threat_data.keys()):
Expand Down Expand Up @@ -411,6 +411,38 @@ def import_reference_control(self, library_object: LoadedLibrary):
)


class QualificationImporter:
REQUIRED_FIELDS = {"ref_id", "urn", "name"}

def __init__(self, qualification_data: dict):
self.qualification_data = qualification_data

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))

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 ?
)

Comment on lines +410 to +440
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

Make qualification import idempotent and add basic field validation.

  • Re-importing the same library may duplicate rows. Prefer update_or_create keyed by (urn, locale).
  • Validate abbreviation length (<= 20) to fail fast before DB error.
 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 ?
+            ),
+        )

If other importers intentionally create fresh rows (non-idempotent) and rely on a cleanup step, keep consistency. Do you want me to align Threat/ReferenceControl importers to the same idempotent pattern?


🏁 Script executed:

#!/bin/bash
# Inspect Qualification model definition
rg -n "class Qualification" -C3 backend/library/models.py

# List all Importer classes in utils
rg -n "class .*Importer" -C3 backend/library/utils.py

# Inspect import methods implementation in utils
rg -n -C3 "def import_" backend/library/utils.py

Length of output: 4966


🏁 Script executed:

#!/bin/bash
# Locate Qualification model definition
rg -n "class Qualification" -C3 .

# Find abbreviation field definitions across models
rg -n "abbreviation" -C3 .

Length of output: 133386


QualificationImporter: Use update_or_create & validate abbreviation length

  • Replace Qualification.objects.create(…) with
    Qualification.objects.update_or_create(urn=…, locale=…, defaults={…})
    to prevent duplicate rows on re-import.
  • Before saving, raise ValueError if abbreviation exceeds the model’s max_length=20.

Also, ThreatImporter and ReferenceControlImporter still use create() despite the “(URN, locale) is unique” comment—consider aligning them to this idempotent pattern for consistency.


# The couple (URN, locale) is unique. ===> Check this in the future
class RiskMatrixImporter:
REQUIRED_FIELDS = {"ref_id", "urn", "json_definition"}
Expand Down Expand Up @@ -463,6 +495,7 @@ class LibraryImporter:
OBJECT_FIELDS = [
"threats",
"reference_controls",
"qualifications",
"risk_matrix", # This field name is deprecated
"risk_matrices",
"framework", # This field name is deprecated
Expand All @@ -480,6 +513,7 @@ def __init__(self, library: StoredLibrary):
self._library = library
self._frameworks = []
self._threats = []
self._qualifications = []
self._reference_controls = []
self._risk_matrices = []
self._requirement_mapping_sets = []
Expand Down Expand Up @@ -538,6 +572,28 @@ def init_reference_controls(
invalid_reference_control_error,
)

def init_qualifications(self, qualifications: List[dict]) -> Union[str, None]:
qualification_importers = []
import_errors = []
for index, qualification_data in enumerate(qualifications):
qualification_importer = QualificationImporter(qualification_data)
qualification_importers.append(qualification_importer)
if (qualification_error := qualification_importer.is_valid()) is not None:
import_errors.append((index, qualification_error))

self._qualifications = qualification_importers

if import_errors:
# We will have to think about error message internationalization later
invalid_qualification_index, invalid_qualification_error = import_errors[0]
return "[QUALIFICATION_ERROR] {} invalid qualification{} detected, the {}{} qualification has the following error : {}".format(
len(import_errors),
"s" if len(import_errors) > 1 else "",
invalid_qualification_index + 1,
{1: "st", 2: "nd", 3: "rd"}.get(invalid_qualification_index + 1, "th"),
invalid_qualification_error,
)

def init_risk_matrices(self, risk_matrices: List[dict]) -> Union[str, None]:
risk_matrix_importers = []
import_errors = []
Expand Down Expand Up @@ -676,6 +732,16 @@ def init(self) -> Union[str, None]:
logger.error("Threat import error", error=threat_import_error)
return threat_import_error

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 "risk_matrix" in library_objects and "risk_matrices" in library_objects:
return "A library can't have both 'risk_matrix' and 'risk_matrices' objects fields."

Expand Down Expand Up @@ -767,6 +833,9 @@ def import_objects(self, library_object: LoadedLibrary):
for reference_control in self._reference_controls:
reference_control.import_reference_control(library_object)

for qualification in self._qualifications:
qualification.import_qualification(library_object)

for risk_matrix in self._risk_matrices:
risk_matrix.import_risk_matrix(library_object)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 5.1.10 on 2025-08-25 21:14

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("core", "0092_qualification_library_and_more"),
("resilience", "0002_businessimpactanalysis_is_locked"),
]

operations = [
migrations.AlterField(
model_name="escalationthreshold",
name="qualifications",
field=models.ManyToManyField(
blank=True,
related_name="escalation_thresholds",
to="core.qualification",
),
),
]
1 change: 1 addition & 0 deletions backend/resilience/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ class EscalationThreshold(AbstractBaseModel, FolderMixin):
qualifications = models.ManyToManyField(
Qualification,
blank=True,
related_name="escalation_thresholds",
)
quanti_impact = FloatField(default=0)
quanti_impact_unit = CharField(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
{form}
optionsEndpoint="folders?content_type=DO"
field="folder"
pathField="path"
cacheLock={cacheLocks['folder']}
bind:cachedValue={formDataCache['folder']}
label={m.domain()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
optionsEndpoint="folders?content_type=DO&content_type=GL"
optionsSelf={object}
field="parent_folder"
pathField="path"
cacheLock={cacheLocks['parent_folder']}
bind:cachedValue={formDataCache['parent_folder']}
label={m.parentDomain()}
Expand Down
7 changes: 6 additions & 1 deletion frontend/messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -929,5 +929,10 @@
"matchingScenarios": "السيناريوهات المطابقة",
"select": "تحديد",
"deselect": "إلغاء التحديد",
"fearedEventAssetHelpText": "الأحداث المخيفة عادة ما تكون مرتبطة بقيم الأعمال (الأصول الأساسية). يُسمح بالأصول الداعمة هنا من أجل المرونة"
"fearedEventAssetHelpText": "الأحداث المخيفة عادة ما تكون مرتبطة بقيم الأعمال (الأصول الأساسية). يُسمح بالأصول الداعمة هنا من أجل المرونة",
"saveAndContinue": "حفظ ومتابعة",
"abbreviation": "اختصار",
"qualificationOrdering": "ترتيب المؤهلات",
"securityObjectiveOrdering": "ترتيب الأهداف الأمنية",
"addQualification": "إضافة تأهيل"
}
7 changes: 6 additions & 1 deletion frontend/messages/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -949,5 +949,10 @@
"matchingScenarios": "Shodné scénáře",
"select": "Vybrat",
"deselect": "Zrušit výběr",
"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). Podporná aktiva jsou zde povolena pro větší flexibilitu.",
"saveAndContinue": "Uložit a pokračovat",
"abbreviation": "Zkratka",
"qualificationOrdering": "Pořadí kvalifikace",
"securityObjectiveOrdering": "Pořadí bezpečnostního cíle",
"addQualification": "Přidat kvalifikaci"
}
7 changes: 6 additions & 1 deletion frontend/messages/da.json
Original file line number Diff line number Diff line change
Expand Up @@ -1244,5 +1244,10 @@
],
"matchingScenarios": "Matchende scenarier",
"select": "Vælg",
"deselect": "Fravælg"
"deselect": "Fravælg",
"saveAndContinue": "Gem og fortsæt",
"abbreviation": "Forkortelse",
"qualificationOrdering": "Kvalifikationsrækkefølge",
"securityObjectiveOrdering": "Sikkerhedsmålsrækkefølge",
"addQualification": "Tilføj kvalifikation"
}
7 changes: 6 additions & 1 deletion frontend/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -966,5 +966,10 @@
"matchingScenarios": "Übereinstimmende Szenarien",
"select": "Auswählen",
"deselect": "Abwählen",
"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": "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.",
"saveAndContinue": "Speichern und fortfahren",
"abbreviation": "Abkürzung",
"qualificationOrdering": "Qualifikationsreihenfolge",
"securityObjectiveOrdering": "Reihenfolge der Sicherheitsziele",
"addQualification": "Qualifikation hinzufügen"
}
7 changes: 6 additions & 1 deletion frontend/messages/el.json
Original file line number Diff line number Diff line change
Expand Up @@ -1663,5 +1663,10 @@
"dataWizardSelectFramework": "Επιλέξτε Πλαίσιο",
"dataWizardParsingResults": "Αποτελέσματα ανάλυσης:",
"dataWizardTemplatesAndGuidelines": "Πρότυπα και οδηγίες",
"dataWizardLoadExcelData": "Φόρτωση δεδομένων Excel"
"dataWizardLoadExcelData": "Φόρτωση δεδομένων Excel",
"saveAndContinue": "Αποθήκευση και συνέχιση",
"abbreviation": "Συντομογραφία",
"qualificationOrdering": "Σειρά κατάταξης προσόντων",
"securityObjectiveOrdering": "Σειρά στόχων ασφαλείας",
"addQualification": "Προσθήκη προσόντος"
}
Loading