"""安全每日一题 按员工语言出题:``secquiz_questions.localized``。 同一条反馈的另一半。#607 修的是「英文界面右下角的安全学习气泡是中文 的」—— 那次的结论是链路没坏、**内容缺语言**,于是给素材补了译文。 安全每日一题是同一个问题里没人提的那一半:题干、四个选项、答完之后的 解析都存在**只装一种语言**的平面列里,所以一个读英文界面的员工点开 「安全每日一题」,拿到的是中文题目 + 中文选项 + 中文解析 —— 而他答错 与否会进排行榜、进部门擂台、进安全意识参与度那份合规证据。 ## 本迁移做一件事 ``secquiz_questions`` 加 ``localized JSON NULL``: {"en": {"prompt": "…", "options": [{"key": "A", "text": "…"}, …], "explanation": "…"}, "ja": {…}} **中文不在里面。** 与 ``secaw_materials.localized``(三种语言都在 JSON 里)不同,这里 ``prompt`` / ``options`` / ``explanation`` 三个平面列继续 充当中文基线。两个原因: * 这一列是加在**已有 309 行**上的,它们的中文本来就在平面列里 —— 搬 一次等于重写每一行 + 每一个读者(排行榜、擂台、素材小测、MCP、 ``me_universe`` 装备墙都读这张表)。 * ``correct_key`` 与语言无关,它必须紧挨着**唯一一份权威选项表**,而那 份表就是基线的。 **选项 key 是契约,文案不是。** ``correct_key`` 记的是选项 key (``"A"`` / ``"ABD"`` / ``"T"``),而读者是从我们渲染给他的那份列表里 选的。所以一个 key 集合与基线不一致的 block 不是这道题的译文,是另一 道题 —— ``localization.block_for`` 会拒掉它并回退到基线(而不是拿读者 没见过的选项去判他对错),写入路径 ``localization.validate_block`` 把 同一条规则前移成编辑器里的 422。 回填:不需要。``localized`` 为 NULL 的历史行含义正确 —— 这道题目前只有 中文。译文由 `scripts/load_security_content.py` 从仓库外的导出灌入,读取面在缺语言时回报 ``is_fallback_lang``,页面 据此提示「今天这道题还没有英文版」,而不是把一道中文题默默摆在英文界面 上(那和坏页面无法区分)。 """ from __future__ import annotations import sqlalchemy as sa from alembic import op revision = "628" down_revision = "627" branch_labels = None depends_on = None QUESTIONS = "secquiz_questions" def _has_table(bind, name: str) -> bool: return bool(bind.execute( sa.text( "SELECT 1 FROM information_schema.tables " "WHERE table_schema = DATABASE() AND table_name = :t LIMIT 1", ), {"t": name}, ).scalar()) def _has_column(bind, table: str, column: str) -> bool: return bool(bind.execute( sa.text( "SELECT 1 FROM information_schema.columns " "WHERE table_schema = DATABASE() AND table_name = :t " "AND column_name = :c LIMIT 1", ), {"t": table, "c": column}, ).scalar()) def upgrade() -> None: bind = op.get_bind() if _has_table(bind, QUESTIONS) and not _has_column( bind, QUESTIONS, "localized", ): op.add_column( QUESTIONS, sa.Column("localized", sa.JSON(), nullable=True), ) def downgrade() -> None: bind = op.get_bind() if _has_table(bind, QUESTIONS) and _has_column( bind, QUESTIONS, "localized", ): op.drop_column(QUESTIONS, "localized")