feat(backend): drop dictionary dialect columns, add allowed_languages

- Removes source_dialect and target_dialect from TerminologyDictionary model,
  schemas, routes, helper serialization, and all tests
- Adds allowed_languages config field (BCP-47 language codes) to GlobalSettings
  with validation, consolidated settings response, and API endpoint
- Adds alembic migration f1a2b3c4d5e6 to drop the two columns
This commit is contained in:
2026-06-03 11:48:03 +03:00
parent 236dadb914
commit 1e6ec34c2b
15 changed files with 129 additions and 67 deletions

View File

@@ -63,8 +63,6 @@ def _create_dict(db_session, name="Test Dict") -> TerminologyDictionary:
d = TerminologyDictionary(
id=str(uuid.uuid4()),
name=name,
source_dialect="ru",
target_dialect="en",
)
db_session.add(d)
db_session.flush()

View File

@@ -44,7 +44,7 @@ class TestContextCapture:
# region test_context_capture_in_correction [C:2] [TYPE Function]
def test_context_capture_in_correction(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Test Dict", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Test Dict")
job = TranslationJob(name="Ctx Job", source_dialect="a", target_dialect="b", status="DRAFT")
db_session.add(job)
db_session.flush()

View File

@@ -46,13 +46,10 @@ class TestDictionaryCRUD:
def test_create_dictionary(self, db_session: Session):
d = DictionaryManager.create_dictionary(
db_session, name="Finance Terms",
source_dialect="postgresql", target_dialect="clickhouse",
created_by="test_user", description="Finance-related term mappings",
)
assert d.id is not None
assert d.name == "Finance Terms"
assert d.source_dialect == "postgresql"
assert d.target_dialect == "clickhouse"
assert d.created_by == "test_user"
assert d.is_active is True
@@ -64,7 +61,7 @@ class TestDictionaryCRUD:
# region test_update_dictionary [C:2] [TYPE Function]
# @BRIEF Verify dictionary metadata update.
def test_update_dictionary(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Old Name", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Old Name")
updated = DictionaryManager.update_dictionary(
db_session, d.id, name="New Name", description="Updated desc", is_active=False,
)
@@ -76,7 +73,7 @@ class TestDictionaryCRUD:
# region test_delete_dictionary [C:2] [TYPE Function]
# @BRIEF Verify dictionary deletion also removes entries.
def test_delete_dictionary(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="To Delete", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="To Delete")
entry = DictionaryManager.add_entry(db_session, d.id, "hello", "hola", source_language="en", target_language="es")
assert entry.id is not None
DictionaryManager.delete_dictionary(db_session, d.id)
@@ -88,7 +85,7 @@ class TestDictionaryCRUD:
# @BRIEF Verify paginated dictionary listing.
def test_list_dictionaries(self, db_session: Session):
for i in range(5):
DictionaryManager.create_dictionary(db_session, name=f"Dict {i}", source_dialect="a", target_dialect="b")
DictionaryManager.create_dictionary(db_session, name=f"Dict {i}")
dicts, total = DictionaryManager.list_dictionaries(db_session, page=1, page_size=2)
assert total == 5
assert len(dicts) == 2
@@ -97,7 +94,7 @@ class TestDictionaryCRUD:
# region test_delete_dictionary_blocked_by_active_job [C:2] [TYPE Function]
# @BRIEF Verify deletion is blocked when attached to active/scheduled jobs.
def test_delete_dictionary_blocked_by_active_job(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Test", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Test")
job = TranslationJob(name="Active Job", source_dialect="a", target_dialect="b", status="ACTIVE", created_by="test_user")
db_session.add(job)
db_session.flush()
@@ -111,7 +108,7 @@ class TestDictionaryCRUD:
# region test_delete_dictionary_allowed_with_completed_job [C:2] [TYPE Function]
# @BRIEF Verify deletion is allowed when only completed/failed jobs reference the dictionary.
def test_delete_dictionary_allowed_with_completed_job(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Test", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Test")
job = TranslationJob(name="Completed Job", source_dialect="a", target_dialect="b", status="COMPLETED", created_by="test_user")
db_session.add(job)
db_session.flush()
@@ -130,7 +127,7 @@ class TestDictionaryEntryCRUD:
# region test_add_entry_duplicate [C:2] [TYPE Function]
# @BRIEF Verify duplicate entry raises ValueError.
def test_add_entry_duplicate(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Test", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Test")
DictionaryManager.add_entry(db_session, d.id, "Hello", "Hola", source_language="en", target_language="es")
with pytest.raises(ValueError, match="already exists"):
DictionaryManager.add_entry(db_session, d.id, "hello", "Bonjour", source_language="en", target_language="es")
@@ -141,8 +138,8 @@ class TestDictionaryEntryCRUD:
# region test_add_entry_duplicate_per_dictionary [C:2] [TYPE Function]
# @BRIEF Verify duplicate is per-dictionary (same term in different dicts is OK).
def test_add_entry_duplicate_per_dictionary(self, db_session: Session):
d1 = DictionaryManager.create_dictionary(db_session, name="Dict1", source_dialect="a", target_dialect="b")
d2 = DictionaryManager.create_dictionary(db_session, name="Dict2", source_dialect="a", target_dialect="b")
d1 = DictionaryManager.create_dictionary(db_session, name="Dict1")
d2 = DictionaryManager.create_dictionary(db_session, name="Dict2")
DictionaryManager.add_entry(db_session, d1.id, "hello", "hola", source_language="en", target_language="es")
entry = DictionaryManager.add_entry(db_session, d2.id, "hello", "bonjour", source_language="en", target_language="es")
assert entry.id is not None
@@ -151,7 +148,7 @@ class TestDictionaryEntryCRUD:
# region test_edit_entry [C:2] [TYPE Function]
# @BRIEF Verify entry edit updates fields and enforces uniqueness.
def test_edit_entry(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Test", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Test")
entry = DictionaryManager.add_entry(db_session, d.id, "hello", "hola", source_language="en", target_language="es")
updated = DictionaryManager.edit_entry(db_session, entry.id, target_term="HOLA!")
assert updated.target_term == "HOLA!"
@@ -163,7 +160,7 @@ class TestDictionaryEntryCRUD:
# region test_delete_entry [C:2] [TYPE Function]
# @BRIEF Verify entry deletion.
def test_delete_entry(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Test", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Test")
entry = DictionaryManager.add_entry(db_session, d.id, "hello", "hola", source_language="en", target_language="es")
DictionaryManager.delete_entry(db_session, entry.id)
entries, total = DictionaryManager.list_entries(db_session, d.id)
@@ -173,7 +170,7 @@ class TestDictionaryEntryCRUD:
# region test_clear_entries [C:2] [TYPE Function]
# @BRIEF Verify clearing all entries for a dictionary.
def test_clear_entries(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Test", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Test")
DictionaryManager.add_entry(db_session, d.id, "hello", "hola", source_language="en", target_language="es")
DictionaryManager.add_entry(db_session, d.id, "world", "mundo", source_language="en", target_language="es")
deleted = DictionaryManager.clear_entries(db_session, d.id)
@@ -185,7 +182,7 @@ class TestDictionaryEntryCRUD:
# region test_add_entry_with_language_pair [C:2] [TYPE Function]
# @BRIEF Verify creating entry with language pair stores correctly.
def test_add_entry_with_language_pair(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Lang Test", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Lang Test")
entry = DictionaryManager.add_entry(db_session, d.id, "hello", "привет", source_language="en", target_language="ru")
assert entry.source_language == "en"
assert entry.target_language == "ru"
@@ -198,7 +195,7 @@ class TestDictionaryEntryCRUD:
# region test_duplicate_same_language_pair [C:2] [TYPE Function]
# @BRIEF Verify duplicate with same language pair raises conflict.
def test_duplicate_same_language_pair(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Dup Test", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Dup Test")
DictionaryManager.add_entry(db_session, d.id, "hello", "привет", source_language="en", target_language="ru")
with pytest.raises(ValueError, match="already exists"):
DictionaryManager.add_entry(db_session, d.id, "hello", "hallo", source_language="en", target_language="ru")
@@ -207,7 +204,7 @@ class TestDictionaryEntryCRUD:
# region test_same_term_different_language_pair [C:2] [TYPE Function]
# @BRIEF Verify same term with different language pair is allowed.
def test_same_term_different_language_pair(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Multi Lang", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Multi Lang")
entry1 = DictionaryManager.add_entry(db_session, d.id, "hello", "привет", source_language="en", target_language="ru")
entry2 = DictionaryManager.add_entry(db_session, d.id, "hello", "hallo", source_language="en", target_language="de")
assert entry1.id != entry2.id

View File

@@ -50,7 +50,7 @@ class TestFilterBasic:
# region test_filter_for_batch_matches [C:2] [TYPE Function]
def test_filter_for_batch_matches(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Test Dict", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Test Dict")
DictionaryManager.add_entry(db_session, d.id, "hello", "hola", source_language="en", target_language="es")
DictionaryManager.add_entry(db_session, d.id, "world", "mundo", source_language="en", target_language="es")
DictionaryManager.add_entry(db_session, d.id, "foo", "bar", source_language="en", target_language="es")
@@ -76,7 +76,7 @@ class TestFilterBasic:
# region test_filter_for_batch_case_insensitive [C:2] [TYPE Function]
def test_filter_for_batch_case_insensitive(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Test Dict", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Test Dict")
DictionaryManager.add_entry(db_session, d.id, "Hello World", "Hola Mundo", source_language="en", target_language="es")
job = TranslationJob(name="Test Job", source_dialect="a", target_dialect="b", status="DRAFT")
@@ -94,7 +94,7 @@ class TestFilterBasic:
# region test_filter_for_batch_word_boundary [C:2] [TYPE Function]
def test_filter_for_batch_word_boundary(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Test Dict", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Test Dict")
DictionaryManager.add_entry(db_session, d.id, "cat", "gato", source_language="en", target_language="es")
job = TranslationJob(name="Test Job", source_dialect="a", target_dialect="b", status="DRAFT")
@@ -117,8 +117,8 @@ class TestFilterPriority:
# region test_filter_for_batch_multi_dictionary_priority [C:2] [TYPE Function]
def test_filter_for_batch_multi_dictionary_priority(self, db_session: Session):
d1 = DictionaryManager.create_dictionary(db_session, name="Priority1", source_dialect="a", target_dialect="b")
d2 = DictionaryManager.create_dictionary(db_session, name="Priority2", source_dialect="a", target_dialect="b")
d1 = DictionaryManager.create_dictionary(db_session, name="Priority1")
d2 = DictionaryManager.create_dictionary(db_session, name="Priority2")
DictionaryManager.add_entry(db_session, d1.id, "hello", "hola", source_language="en", target_language="es")
DictionaryManager.add_entry(db_session, d2.id, "hello", "bonjour", source_language="en", target_language="es")
@@ -145,7 +145,7 @@ class TestFilterLanguagePair:
# region test_filter_for_batch_with_language_pair [C:2] [TYPE Function]
def test_filter_for_batch_with_language_pair(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Lang Filter", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Lang Filter")
DictionaryManager.add_entry(db_session, d.id, "hello", "привет", source_language="en", target_language="ru")
DictionaryManager.add_entry(db_session, d.id, "world", "мир", source_language="en", target_language="ru")
DictionaryManager.add_entry(db_session, d.id, "hello", "hallo", source_language="en", target_language="de")
@@ -169,7 +169,7 @@ class TestFilterLanguagePair:
# region test_filter_for_batch_target_language_only [C:2] [TYPE Function]
def test_filter_for_batch_target_language_only(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Target Only", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Target Only")
DictionaryManager.add_entry(db_session, d.id, "hello", "привет", source_language="en", target_language="ru")
DictionaryManager.add_entry(db_session, d.id, "hello", "hallo", source_language="en", target_language="de")
DictionaryManager.add_entry(db_session, d.id, "bonjour", "hallo", source_language="fr", target_language="de")
@@ -196,7 +196,7 @@ class TestMigration:
# region test_migrate_old_entries [C:2] [TYPE Function]
def test_migrate_old_entries(self, db_session: Session):
d = TerminologyDictionary(name="Old Dict", source_dialect="a", target_dialect="b")
d = TerminologyDictionary(name="Old Dict")
db_session.add(d)
db_session.flush()

View File

@@ -38,7 +38,7 @@ class TestImportOverwrite:
# region test_import_csv_overwrite [C:2] [TYPE Function]
def test_import_csv_overwrite(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Test", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Test")
DictionaryManager.add_entry(db_session, d.id, "hello", "hola", source_language="en", target_language="es")
csv_content = "source_term,target_term,context_notes,source_language,target_language\nhello,HELLO,,en,es\nworld,mundo,,en,es"
@@ -59,7 +59,7 @@ class TestImportKeepExisting:
# region test_import_csv_keep_existing [C:2] [TYPE Function]
def test_import_csv_keep_existing(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Test", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Test")
DictionaryManager.add_entry(db_session, d.id, "hello", "hola", source_language="en", target_language="es")
csv_content = "source_term,target_term,source_language,target_language\nhello,HELLO,en,es\nworld,mundo,en,es"
@@ -79,7 +79,7 @@ class TestImportCancel:
# region test_import_csv_cancel_on_conflict [C:2] [TYPE Function]
def test_import_csv_cancel_on_conflict(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Test", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Test")
DictionaryManager.add_entry(db_session, d.id, "hello", "hola", source_language="en", target_language="es")
csv_content = "source_term,target_term,source_language,target_language\nhello,HELLO,en,es\nworld,mundo,en,es"
@@ -97,7 +97,7 @@ class TestImportFormat:
# region test_import_tsv [C:2] [TYPE Function]
def test_import_tsv(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Test", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Test")
tsv_content = "source_term\ttarget_term\tsource_language\ttarget_language\nhello\thola\ten\tes\nworld\tmundo\ten\tes"
result = DictionaryManager.import_entries(db_session, d.id, tsv_content, delimiter="\t", on_conflict="overwrite")
assert result["created"] == 2
@@ -106,14 +106,14 @@ class TestImportFormat:
# region test_import_invalid_format [C:2] [TYPE Function]
def test_import_invalid_format(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Test", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Test")
with pytest.raises(ValueError, match="source_term"):
DictionaryManager.import_entries(db_session, d.id, "name,value\nhello,hola", delimiter=",", on_conflict="overwrite")
# endregion test_import_invalid_format
# region test_import_empty_rows [C:2] [TYPE Function]
def test_import_empty_rows(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Test", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Test")
csv_content = "source_term,target_term\nhello,hola\n,world\nfoo,"
result = DictionaryManager.import_entries(db_session, d.id, csv_content, delimiter=",", on_conflict="overwrite")
assert result["created"] == 1
@@ -122,7 +122,7 @@ class TestImportFormat:
# region test_import_preview [C:2] [TYPE Function]
def test_import_preview(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Test", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Test")
DictionaryManager.add_entry(db_session, d.id, "hello", "hola", source_language="en", target_language="es")
csv_content = "source_term,target_term,source_language,target_language\nhello,HELLO,en,es\nworld,mundo,en,es"
@@ -142,7 +142,7 @@ class TestExportEntries:
# region test_export_entries [C:2] [TYPE Function]
def test_export_entries(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Export Test", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Export Test")
DictionaryManager.add_entry(db_session, d.id, "hello", "привет", source_language="en", target_language="ru")
DictionaryManager.add_entry(db_session, d.id, "world", "мир", source_language="en", target_language="ru")
@@ -161,7 +161,7 @@ class TestExportEntries:
# region test_import_with_default_language [C:2] [TYPE Function]
def test_import_with_default_language(self, db_session: Session):
d = DictionaryManager.create_dictionary(db_session, name="Default Lang Import", source_dialect="a", target_dialect="b")
d = DictionaryManager.create_dictionary(db_session, name="Default Lang Import")
csv_content = "source_term,target_term\nhello,привет\nworld,мир"
result = DictionaryManager.import_entries(
db_session, d.id, csv_content, delimiter=",", on_conflict="overwrite",

View File

@@ -45,8 +45,8 @@ class DictionaryCorrectionService:
effective_context_source = "manual"
normalized = _normalize_term(source_term)
entry_src_lang = dictionary.source_dialect or "und"
entry_tgt_lang = dictionary.target_dialect or "und"
entry_src_lang = "und"
entry_tgt_lang = "und"
existing = (
db.query(DictionaryEntry)
@@ -139,8 +139,8 @@ class DictionaryCorrectionService:
continue
normalized = _normalize_term(source_term)
bulk_src_lang = dictionary.source_dialect or "und"
bulk_tgt_lang = dictionary.target_dialect or "und"
bulk_src_lang = "und"
bulk_tgt_lang = "und"
existing = (
db.query(DictionaryEntry)

View File

@@ -24,15 +24,13 @@ class DictionaryCRUD:
@staticmethod
def create_dictionary(
db: Session, name: str,
source_dialect: str = "", target_dialect: str = "",
created_by: str | None = None, description: str | None = None,
is_active: bool = True,
) -> TerminologyDictionary:
with belief_scope("DictionaryCRUD.create_dictionary"):
logger.reason("Creating dictionary", {"name": name, "source": source_dialect, "target": target_dialect})
logger.reason("Creating dictionary", {"name": name})
dictionary = TerminologyDictionary(
name=name, description=description,
source_dialect=source_dialect or "", target_dialect=target_dialect or "",
is_active=is_active, created_by=created_by,
)
db.add(dictionary)
@@ -47,8 +45,7 @@ class DictionaryCRUD:
@staticmethod
def update_dictionary(
db: Session, dict_id: str, name: str | None = None,
description: str | None = None, source_dialect: str | None = None,
target_dialect: str | None = None, is_active: bool | None = None,
description: str | None = None, is_active: bool | None = None,
) -> TerminologyDictionary:
with belief_scope("DictionaryCRUD.update_dictionary"):
dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()
@@ -59,10 +56,6 @@ class DictionaryCRUD:
dictionary.name = name
if description is not None:
dictionary.description = description
if source_dialect is not None:
dictionary.source_dialect = source_dialect
if target_dialect is not None:
dictionary.target_dialect = target_dialect
if is_active is not None:
dictionary.is_active = is_active
db.commit()