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-пакета admin/jira-sync: служебные колонки → issue_note; links.csv — сверочный набор
|
||||||
(ожидаемые связи против зеркала), не команды. Повтор обновляет заметки, не дублирует."""
|
(ожидаемые связи против зеркала), не команды. Повтор обновляет заметки, не дублирует.
|
||||||
|
Пустая ячейка CSV не меняет существующее поле заметки (правки, сделанные в хабе, сохраняются)."""
|
||||||
import csv
|
import csv
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from . import mirror, settings
|
from . import db, mirror, settings
|
||||||
|
|
||||||
|
|
||||||
def _rows(path: Path) -> list[dict]:
|
def _rows(path: Path) -> list[dict]:
|
||||||
@ -18,8 +19,20 @@ def _clean(value) -> str | None:
|
|||||||
return value or 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:
|
def import_package(conn, folder) -> dict:
|
||||||
folder = Path(folder)
|
folder = Path(folder)
|
||||||
|
with db.transaction(conn):
|
||||||
|
return _import(conn, folder)
|
||||||
|
|
||||||
|
|
||||||
|
def _import(conn, folder: Path) -> dict:
|
||||||
notes = 0
|
notes = 0
|
||||||
csv_keys = []
|
csv_keys = []
|
||||||
|
|
||||||
@ -27,10 +40,10 @@ def import_package(conn, folder) -> dict:
|
|||||||
key = _clean(r.get("Код"))
|
key = _clean(r.get("Код"))
|
||||||
if not key:
|
if not key:
|
||||||
continue
|
continue
|
||||||
mirror.set_note(conn, key, {
|
_note(conn, key, {
|
||||||
"fact_status": _clean(r.get("Статус по факту")), "work_kind": _clean(r.get("Вид работы")),
|
"fact_status": _clean(r.get("Статус по факту")), "work_kind": _clean(r.get("Вид работы")),
|
||||||
"done_text": _clean(r.get("Что сделано по факту (внутр.)")), "rest_text": _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
|
notes += 1
|
||||||
csv_keys.append(key)
|
csv_keys.append(key)
|
||||||
|
|
||||||
@ -41,17 +54,17 @@ def import_package(conn, folder) -> dict:
|
|||||||
if real:
|
if real:
|
||||||
new_keys[f"new:{i}"] = real
|
new_keys[f"new:{i}"] = real
|
||||||
csv_keys.append(real)
|
csv_keys.append(real)
|
||||||
mirror.set_note(conn, key, {
|
_note(conn, key, {
|
||||||
"fact_status": _clean(r.get("Статус по факту")), "work_kind": _clean(r.get("Вид работы")),
|
"fact_status": _clean(r.get("Статус по факту")), "work_kind": _clean(r.get("Вид работы")),
|
||||||
"rest_text": _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
|
notes += 1
|
||||||
|
|
||||||
for r in _rows(folder / "epics.csv"):
|
for r in _rows(folder / "epics.csv"):
|
||||||
key = _clean(r.get("Ключ"))
|
key = _clean(r.get("Ключ"))
|
||||||
if not key:
|
if not key:
|
||||||
continue
|
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
|
notes += 1
|
||||||
csv_keys.append(key)
|
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 rep["missing_links"] == [{"source": "AURORA-1533", "target": "AURORA-1416", "link_type": "ASUPR_Блокер_Начала"}]
|
||||||
assert importer.import_package(conn, tmp_path)["notes"] == 6
|
assert importer.import_package(conn, tmp_path)["notes"] == 6
|
||||||
assert conn.execute("SELECT COUNT(*) AS n FROM issue_note").fetchone()["n"] == 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