fix(hub): импорт не затирает правки хаба пустыми ячейками CSV, весь импорт в транзакции
Пустая ячейка CSV не меняет существующее поле заметки; заметка без непустых полей не пишется, но учитывается в счётчике как раньше. Импорт обёрнут в db.transaction (set_note — SAVEPOINT). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
This commit is contained in:
parent
6216039526
commit
acbc19c7eb
@ -1,9 +1,10 @@
|
||||
"""Разовый импорт CSV-пакета admin/jira-sync: служебные колонки → issue_note; links.csv — сверочный набор
|
||||
(ожидаемые связи против зеркала), не команды. Повтор обновляет заметки, не дублирует."""
|
||||
(ожидаемые связи против зеркала), не команды. Повтор обновляет заметки, не дублирует.
|
||||
Пустая ячейка CSV не меняет существующее поле заметки (правки, сделанные в хабе, сохраняются)."""
|
||||
import csv
|
||||
from pathlib import Path
|
||||
|
||||
from . import mirror, settings
|
||||
from . import db, mirror, settings
|
||||
|
||||
|
||||
def _rows(path: Path) -> list[dict]:
|
||||
@ -18,8 +19,20 @@ def _clean(value) -> str | None:
|
||||
return value or None
|
||||
|
||||
|
||||
def _note(conn, key, fields: dict) -> None:
|
||||
"""Записать только непустые поля; если их нет — заметку не трогаем."""
|
||||
fields = {k: v for k, v in fields.items() if v is not None}
|
||||
if fields:
|
||||
mirror.set_note(conn, key, fields, "import")
|
||||
|
||||
|
||||
def import_package(conn, folder) -> dict:
|
||||
folder = Path(folder)
|
||||
with db.transaction(conn):
|
||||
return _import(conn, folder)
|
||||
|
||||
|
||||
def _import(conn, folder: Path) -> dict:
|
||||
notes = 0
|
||||
csv_keys = []
|
||||
|
||||
@ -27,10 +40,10 @@ def import_package(conn, folder) -> dict:
|
||||
key = _clean(r.get("Код"))
|
||||
if not key:
|
||||
continue
|
||||
mirror.set_note(conn, key, {
|
||||
_note(conn, key, {
|
||||
"fact_status": _clean(r.get("Статус по факту")), "work_kind": _clean(r.get("Вид работы")),
|
||||
"done_text": _clean(r.get("Что сделано по факту (внутр.)")), "rest_text": _clean(r.get("Что осталось (внутр.)")),
|
||||
"owner_note": _clean(r.get("Комментарий актуализации"))}, "import")
|
||||
"owner_note": _clean(r.get("Комментарий актуализации"))})
|
||||
notes += 1
|
||||
csv_keys.append(key)
|
||||
|
||||
@ -41,17 +54,17 @@ def import_package(conn, folder) -> dict:
|
||||
if real:
|
||||
new_keys[f"new:{i}"] = real
|
||||
csv_keys.append(real)
|
||||
mirror.set_note(conn, key, {
|
||||
_note(conn, key, {
|
||||
"fact_status": _clean(r.get("Статус по факту")), "work_kind": _clean(r.get("Вид работы")),
|
||||
"rest_text": _clean(r.get("Основание (внутр.)")),
|
||||
"owner_note": None if real else _clean(r.get("Тема"))}, "import")
|
||||
"owner_note": None if real else _clean(r.get("Тема"))})
|
||||
notes += 1
|
||||
|
||||
for r in _rows(folder / "epics.csv"):
|
||||
key = _clean(r.get("Ключ"))
|
||||
if not key:
|
||||
continue
|
||||
mirror.set_note(conn, key, {"work_kind": "эпик", "owner_note": _clean(r.get("Комментарий"))}, "import")
|
||||
_note(conn, key, {"work_kind": "эпик", "owner_note": _clean(r.get("Комментарий"))})
|
||||
notes += 1
|
||||
csv_keys.append(key)
|
||||
|
||||
|
||||
@ -50,3 +50,52 @@ def test_import_notes_and_report(tmp_path):
|
||||
assert rep["missing_links"] == [{"source": "AURORA-1533", "target": "AURORA-1416", "link_type": "ASUPR_Блокер_Начала"}]
|
||||
assert importer.import_package(conn, tmp_path)["notes"] == 6
|
||||
assert conn.execute("SELECT COUNT(*) AS n FROM issue_note").fetchone()["n"] == 6
|
||||
|
||||
|
||||
def write_tasks(folder, comment):
|
||||
write_csv(folder / "tasks.csv",
|
||||
["Код", "Тема", "Вид работы", "Статус по факту", "Комментарий актуализации",
|
||||
"Что сделано по факту (внутр.)", "Что осталось (внутр.)", "Спринт"],
|
||||
[["AURORA-1533", "Тест ОСВ", "тестирование", "Ожидание разработки", comment, "", "Прогнать", "Спринт 2"]])
|
||||
|
||||
|
||||
def test_reimport_keeps_hub_edit_when_cell_empty(tmp_path):
|
||||
write_tasks(tmp_path, "Актуализация 10.10.2026")
|
||||
conn = db.connect(":memory:")
|
||||
settings.ensure_defaults(conn)
|
||||
importer.import_package(conn, tmp_path)
|
||||
mirror.set_note(conn, "AURORA-1533", {"owner_note": "правка в хабе"}, "hub")
|
||||
write_tasks(tmp_path, "") # пустая ячейка в CSV
|
||||
importer.import_package(conn, tmp_path)
|
||||
n = mirror.get_note(conn, "AURORA-1533")
|
||||
assert n["owner_note"] == "правка в хабе"
|
||||
assert n["rest_text"] == "Прогнать"
|
||||
|
||||
|
||||
def test_reimport_updates_nonempty_cell(tmp_path):
|
||||
write_tasks(tmp_path, "Первая версия")
|
||||
conn = db.connect(":memory:")
|
||||
settings.ensure_defaults(conn)
|
||||
importer.import_package(conn, tmp_path)
|
||||
write_tasks(tmp_path, "Вторая версия")
|
||||
importer.import_package(conn, tmp_path)
|
||||
assert mirror.get_note(conn, "AURORA-1533")["owner_note"] == "Вторая версия"
|
||||
|
||||
|
||||
def test_import_does_not_touch_issue_tables(tmp_path):
|
||||
make_package(tmp_path)
|
||||
conn = db.connect(":memory:")
|
||||
settings.ensure_defaults(conn)
|
||||
mirror.apply_state(conn, [{"key": "AURORA-1533", "summary": "т", "labels": []}],
|
||||
[{"source_key": "AURORA-1533", "target_key": "AURORA-1416", "link_type": "x"}])
|
||||
count = lambda t: conn.execute(f"SELECT COUNT(*) AS n FROM {t}").fetchone()["n"]
|
||||
before = (count("issue"), count("issue_link"))
|
||||
importer.import_package(conn, tmp_path)
|
||||
assert (count("issue"), count("issue_link")) == before
|
||||
|
||||
|
||||
def test_missing_csv_files_give_empty_report(tmp_path):
|
||||
conn = db.connect(":memory:")
|
||||
settings.ensure_defaults(conn)
|
||||
rep = importer.import_package(conn, tmp_path)
|
||||
assert rep == {"notes": 0, "expected_links": 0, "missing_keys": [], "missing_links": []}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user