Files
app-autotest/site/db/scenario_defs.py
T

236 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
CRUD для таблицы scenario_definitions — определения сценариев.
Сценарии редактируются через UI (модальный редактор), хранятся в БД.
Это позволяет менять сценарии БЕЗ редеплоя приложения.
ОПТИМИСТИЧНАЯ БЛОКИРОВКА (version):
update_definition проверяет version — если другой пользователь уже изменил
сценарий, возвращает conflict=true → UI показывает "Конфликт версий".
МЯГКОЕ УДАЛЕНИЕ:
delete_definition ставит is_active=FALSE — не удаляет физически.
Это позволяет восстановить случайно удалённый сценарий.
SEED-СЦЕНАРИИ:
Сценарии с client_id="" и stand="" — seed (дефолтные, из YAML).
Они доступны всем пользователям но не могут быть удалены через UI.
Функции:
list_definitions(client_id, stand) — список активных (+seed)
get_definition(def_id, cid, stand) — один (по id)
create_definition(...) → {id, version}
update_definition(...) → {ok, version} | {conflict: true}
delete_definition(...) → bool
lock_check(cid, stand) — есть ли RUNNING → False
"""
import json
from db.pool import get_conn, put_conn
def list_definitions(client_id, stand):
"""Список ВСЕХ активных определений: пользовательские + seed.
Seed-сценарии (client_id="" AND stand="") видны всем.
Пользовательские — только для текущего пользователя и стенда.
Returns:
list[dict] с ключами: id, name, steps, version, is_active,
created_at, updated_at, client_id, stand, is_seed (bool)."""
conn = get_conn()
if not conn:
return []
try:
cur = conn.cursor()
# Два условия через OR: пользовательские + seed
cur.execute("""
SELECT id, name, steps, version, is_active, created_at, updated_at,
client_id, stand
FROM scenario_definitions
WHERE is_active = TRUE
AND ((client_id = %s AND stand = %s) OR (client_id = '' AND stand = ''))
ORDER BY name
""", (client_id, stand))
rows = cur.fetchall()
cols = [d[0] for d in cur.description]
result = []
for r in rows:
d = dict(zip(cols, r))
# Конвертируем datetime → ISO-строка (JSON-совместимо)
d["created_at"] = d["created_at"].isoformat() if d["created_at"] else None
d["updated_at"] = d["updated_at"].isoformat() if d["updated_at"] else None
# is_seed: client_id пустой → seed (из YAML)
d["is_seed"] = (d.get("client_id") == "" and d.get("stand") == "")
result.append(d)
return result
except Exception as e:
print(f"[DEFS] list error: {e}", flush=True)
return []
finally:
put_conn(conn)
def get_definition(def_id, client_id, stand):
"""Получить ОДНО определение по id.
Доступ: пользовательские (client_id+stand) ИЛИ seed (пустые).
Returns:
dict или None если не найдено."""
conn = get_conn()
if not conn:
return None
try:
cur = conn.cursor()
cur.execute("""
SELECT id, name, steps, version, is_active, created_at, updated_at, updated_by
FROM scenario_definitions
WHERE id = %s AND ((client_id = %s AND stand = %s) OR (client_id = '' AND stand = ''))
""", (def_id, client_id, stand))
row = cur.fetchone()
if not row:
return None
cols = [d[0] for d in cur.description]
d = dict(zip(cols, row))
d["created_at"] = d["created_at"].isoformat() if d["created_at"] else None
d["updated_at"] = d["updated_at"].isoformat() if d["updated_at"] else None
return d
except Exception as e:
print(f"[DEFS] get error: {e}", flush=True)
return None
finally:
put_conn(conn)
def create_definition(client_id, stand, name, steps, updated_by=""):
"""Создать ИЛИ перезаписать определение (upsert).
ON CONFLICT (client_id, stand, LOWER(name)):
Если сценарий с таким именем уже существует → UPDATE (перезапись steps, version+1).
Если нет → INSERT.
Это позволяет «сохранить» без проверки существования.
Returns:
{id: int, version: int} или None при ошибке."""
conn = get_conn()
if not conn:
return None
try:
cur = conn.cursor()
cur.execute("""
INSERT INTO scenario_definitions (client_id, stand, name, steps, updated_by)
VALUES (%s, %s, %s, %s, %s)
ON CONFLICT (client_id, stand, LOWER(name))
DO UPDATE SET steps = EXCLUDED.steps, version = scenario_definitions.version + 1,
is_active = TRUE, updated_at = NOW(), updated_by = EXCLUDED.updated_by
RETURNING id, version
""", (client_id, stand, name, json.dumps(steps), updated_by))
row = cur.fetchone()
conn.commit()
cur.close()
return {"id": row[0], "version": row[1]}
except Exception as e:
print(f"[DEFS] create error: {e}", flush=True)
conn.rollback()
return None
finally:
put_conn(conn)
def update_definition(def_id, client_id, stand, name, steps, version, updated_by=""):
"""Обновить определение с ОПТИМИСТИЧНОЙ БЛОКИРОВКОЙ.
WHERE version = %s — если версия изменилась (другой пользователь/вкладка),
UPDATE затронет 0 строк → возвращаем {conflict: true}.
Returns:
{ok: True, version: int} — успех
{conflict: True} — версия устарела (409 Conflict)
{error: str} — ошибка БД"""
conn = get_conn()
if not conn:
return {"error": "no db"}
try:
cur = conn.cursor()
cur.execute("""
UPDATE scenario_definitions
SET name = %s, steps = %s, version = version + 1,
updated_at = NOW(), updated_by = %s
WHERE id = %s AND client_id = %s AND stand = %s AND version = %s
RETURNING version
""", (name, json.dumps(steps), updated_by, def_id, client_id, stand, version))
row = cur.fetchone()
conn.commit()
cur.close()
if row:
return {"ok": True, "version": row[0]}
# 0 строк — версия не совпала (кто-то уже изменил)
return {"conflict": True}
except Exception as e:
print(f"[DEFS] update error: {e}", flush=True)
conn.rollback()
return {"error": str(e)}
finally:
put_conn(conn)
def delete_definition(def_id, client_id, stand):
"""Мягкое удаление: is_active = FALSE.
Только для пользовательских сценариев (client_id не пустой).
Seed-сценарии не могут быть удалены через этот метод.
Returns:
bool — True если удалён, False если не найден."""
conn = get_conn()
if not conn:
return False
try:
cur = conn.cursor()
cur.execute("""
UPDATE scenario_definitions
SET is_active = FALSE, updated_at = NOW()
WHERE id = %s AND client_id = %s AND stand = %s
""", (def_id, client_id, stand))
ok = cur.rowcount > 0
conn.commit()
cur.close()
return ok
except Exception as e:
print(f"[DEFS] delete error: {e}", flush=True)
conn.rollback()
return False
finally:
put_conn(conn)
def lock_check(client_id, stand):
"""Проверить что нет активного RUNNING-запуска сценария.
Используется перед запуском нового сценария — предотвращает
одновременный запуск двух сценариев одним пользователем.
Returns:
True — можно запускать (нет RUNNING)
False — нельзя (уже есть RUNNING) → 409 Conflict"""
conn = get_conn()
if not conn:
return True # без БД — разрешаем (fallback)
try:
cur = conn.cursor()
cur.execute("""
SELECT id FROM scenario_runs
WHERE client_id = %s AND stand = %s AND status = 'RUNNING'
LIMIT 1
""", (client_id, stand))
row = cur.fetchone()
cur.close()
return row is None # None = нет RUNNING = можно запускать
except Exception as e:
print(f"[DEFS] lock_check error: {e}", flush=True)
return True
finally:
put_conn(conn)