chore: curatare agresiva comentarii — scoatere referinte US/PRD din cod si template-uri
Eliminat zgomotul de trasabilitate (US-xxx, PRD x.x, Rn, OV-x, Tn, decizii/naratiune istorica) din 41 fisiere app/ + template-uri. Pastrate comentariile care documenteaza invarianti si logica ne-evidenta (idempotenta/hash, reconciliere anti-duplicat, RAR 500 esec definitiv, creds per cont, WAF User-Agent, 422 fara echo de parola, scope NULL->1), curatate doar de tokeni. Verificare: pentru cele 27 module .py curatate, structura de cod (tokeni non-comentariu/ non-string) e IDENTICA fata de HEAD -> doar comentarii/docstring-uri schimbate. Singura schimbare de cod e in tests/test_web_responsive.py (scos 3 assert pe markeri US-006/007/008, inlocuite de asertiunile structurale alaturate). 0 tokeni US/PRD reziduali in app/. Regresie: 896 passed, 1 deselected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
"""Dashboard Jinja2 + HTMX (server-rendered, zero build).
|
||||
|
||||
Schelet cu stari explicite: empty (coada goala), banner alerta blocate,
|
||||
worker viu/mort, ultimul login RAR. Editor mapari + browser nomenclator +
|
||||
export CSV + stare "RAR indisponibil" = de adaugat (plan.md sect. 4 + design-review).
|
||||
worker viu/mort, ultimul login RAR. Editor mapari + browser nomenclator.
|
||||
|
||||
U5 — Rute web pentru import fisier (upload → mapare coloane → preview → confirma → trimite).
|
||||
Consuma endpointurile backend din import_router (helper-e interne) fara a le modifica.
|
||||
Toate rutele /_import/* returneaza fragmente HTML targetate pe #import-section prin HTMX.
|
||||
Rute web pentru import fisier (upload → mapare coloane → preview → confirma → trimite).
|
||||
Consuma helper-e interne din import_router fara a le modifica. Toate rutele /_import/*
|
||||
returneaza fragmente HTML targetate pe #import-section prin HTMX.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -83,12 +82,12 @@ from ..mapping import (
|
||||
text_rules_overlap,
|
||||
)
|
||||
|
||||
# Campuri canonice cu eticheta umana pentru dropdown mapare coloane (U5)
|
||||
# Campuri canonice cu eticheta umana pentru dropdown mapare coloane
|
||||
_CANONICAL_FIELDS = [(k, v[0]) for k, v in _CANONICAL_SYNONYMS.items()]
|
||||
|
||||
router = APIRouter(tags=["web"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).resolve().parent / "templates"))
|
||||
# Expune parse_erori in toate template-urile (US-006, PRD 5.4)
|
||||
# Expune parse_erori in toate template-urile
|
||||
templates.env.globals["parse_erori"] = parse_erori
|
||||
|
||||
_BLOCKED = ("error", "needs_data", "needs_mapping")
|
||||
@@ -98,7 +97,7 @@ def _ctx(request: Request, **extra) -> dict:
|
||||
"""Context de baza pentru template-uri cu formulare: include mereu csrf_token.
|
||||
|
||||
Previne lock-out in prod (web_auth_required=True): orice re-randare de eroare
|
||||
trebuie sa includa csrf_token negol altfel urmatorul submit da 403 (task #8).
|
||||
trebuie sa includa csrf_token negol altfel urmatorul submit da 403.
|
||||
"""
|
||||
return {"request": request, "csrf_token": get_csrf_token(request), **extra}
|
||||
|
||||
@@ -161,15 +160,14 @@ def _rar_state(hb, worker_alive: bool) -> str:
|
||||
return "indisponibil?" if age > 108000 else "ok"
|
||||
|
||||
|
||||
# US-002: "import" nu mai e tab separat — importul traieste pe Acasa. ?tab=import
|
||||
# cade pe Acasa (tab invalid -> fallback "acasa" in dashboard()), fara 404.
|
||||
# US-003 (3.6): "coada" (Trimiteri) nu mai e tab — Trimiterile sunt sectiune pe Acasa.
|
||||
# ?tab=coada cade tot pe Acasa (fallback), fara 404, fara fragment orfan.
|
||||
# "import" si "coada" nu mai sunt tab-uri separate — importul si Trimiterile sunt
|
||||
# sectiuni pe Acasa. ?tab=import / ?tab=coada cad pe Acasa (fallback in dashboard()),
|
||||
# fara 404 si fara fragment orfan.
|
||||
_TABS_VALIDE = {"acasa", "mapari", "cont", "nomenclator", "integrare", "jurnal"}
|
||||
|
||||
|
||||
def _get_acasa_context(request: Request, conn, account_id: int) -> dict:
|
||||
"""Calculeaza contextul pentru panoul Acasa (US-005).
|
||||
"""Calculeaza contextul pentru panoul Acasa.
|
||||
|
||||
Scoped pe contul sesiunii — identic cu pattern-ul din restul rutelor (NULL->1).
|
||||
"""
|
||||
@@ -197,8 +195,8 @@ def _get_acasa_context(request: Request, conn, account_id: int) -> dict:
|
||||
).fetchone()
|
||||
are_cheie_folosita = row_key is not None
|
||||
|
||||
# US-003 (3.6): contorul de atentie (blocate) se reflecta in heading-ul
|
||||
# sectiunii "Trimiterile tale" de pe Acasa, nu pe un tab disparut.
|
||||
# Contorul de atentie (blocate) se reflecta in heading-ul sectiunii
|
||||
# "Trimiterile tale" de pe Acasa.
|
||||
counts = _status_counts(conn, account_id)
|
||||
blocate_total = sum(counts.get(s, 0) for s in _BLOCKED)
|
||||
|
||||
@@ -212,7 +210,7 @@ def _get_acasa_context(request: Request, conn, account_id: int) -> dict:
|
||||
"pills_categorii": _pills_categorii(counts),
|
||||
# Semnatura datelor: nudge-ul "Date noi" o compara la fiecare poll usor.
|
||||
"versiune_trimiteri": _trimiteri_versiune(conn, account_id),
|
||||
# US-002: Acasa include caseta de upload -> are nevoie de csrf_token
|
||||
# Acasa include caseta de upload -> are nevoie de csrf_token
|
||||
"csrf_token": get_csrf_token(request),
|
||||
}
|
||||
|
||||
@@ -220,8 +218,8 @@ def _get_acasa_context(request: Request, conn, account_id: int) -> dict:
|
||||
def _render_panel_acasa(request: Request, conn=None, account_id: int = 1, status: str | None = None) -> str:
|
||||
"""Randeaza panoul Acasa ca string HTML.
|
||||
|
||||
`status` (US-014/T13): deep-link `?tab=acasa&status=error` pre-selecteaza filtrul de
|
||||
stare in sectiunea Trimiteri, astfel ca lista se incarca direct filtrata (nu dead-end).
|
||||
`status`: deep-link `?tab=acasa&status=error` pre-selecteaza filtrul de stare in
|
||||
sectiunea Trimiteri, astfel ca lista se incarca direct filtrata (nu dead-end).
|
||||
"""
|
||||
if conn is None:
|
||||
return templates.get_template("_acasa.html").render(
|
||||
@@ -243,8 +241,8 @@ def _render_panel_import(request: Request) -> str:
|
||||
|
||||
|
||||
def _render_panel_coada(request: Request, conn=None, account_id: int = 1) -> str:
|
||||
"""US-003 (3.6): "coada" nu mai e panou propriu — serveste continutul Acasa
|
||||
(Trimiterile sunt sectiune pe Acasa). Pastrat ca alias pentru deep-link/bookmark vechi."""
|
||||
""""coada" nu mai e panou propriu — serveste continutul Acasa (Trimiterile sunt
|
||||
sectiune pe Acasa). Pastrat ca alias pentru deep-link/bookmark vechi."""
|
||||
return _render_panel_acasa(request, conn, account_id)
|
||||
|
||||
|
||||
@@ -334,10 +332,10 @@ def _jurnal_context(
|
||||
data_de: str | None = None, data_pana: str | None = None,
|
||||
cont: str | None = None, page: int = 0,
|
||||
) -> dict:
|
||||
"""Context pentru tab-ul Jurnal (US-006): evenimente paginate + filtre + scope.
|
||||
"""Context pentru tab-ul Jurnal: evenimente paginate + filtre + scope.
|
||||
|
||||
Admin -> vede TOT, cu filtru optional pe cont. Non-admin -> DOAR evenimentele
|
||||
contului sau (regula NULL->cont 1, ca restul UI-ului). Decizie §5.
|
||||
contului sau (regula NULL->cont 1, ca restul UI-ului).
|
||||
"""
|
||||
admin = is_account_admin(conn, account_id)
|
||||
tip = (tip or "").strip() or None
|
||||
@@ -397,7 +395,7 @@ def _jurnal_context(
|
||||
|
||||
|
||||
def _render_panel_jurnal(request: Request, conn, account_id: int) -> str:
|
||||
"""Randeaza panoul Jurnal ca string HTML (US-006)."""
|
||||
"""Randeaza panoul Jurnal ca string HTML."""
|
||||
return templates.get_template("_jurnal.html").render(_jurnal_context(request, conn, account_id))
|
||||
|
||||
|
||||
@@ -424,20 +422,20 @@ def _render_panel_for_tab(request: Request, conn, account_id: int, tab: str, sta
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
def dashboard(request: Request, tab: str = "acasa", status: str | None = None) -> HTMLResponse:
|
||||
"""Dashboard principal cu tab-uri (US-003).
|
||||
"""Dashboard principal cu tab-uri.
|
||||
|
||||
Parametrul ?tab= permite deep-link pe orice sectiune; panoul activ e randat
|
||||
server-side la full load (fara palpaiere la refresh, degradare gratiosa fara JS).
|
||||
Tab invalid -> fallback la 'acasa'. `?status=` (US-014/T13) pre-filtreaza lista
|
||||
Trimiteri de pe Acasa (deep-link din banner-ul "Necesita atentia ta").
|
||||
Tab invalid -> fallback la 'acasa'. `?status=` pre-filtreaza lista Trimiteri de
|
||||
pe Acasa (deep-link din banner-ul "Necesita atentia ta").
|
||||
"""
|
||||
account_id = require_login(request)
|
||||
active_tab = tab if tab in _TABS_VALIDE else "acasa"
|
||||
conn = get_connection()
|
||||
try:
|
||||
panel_html = _render_panel_for_tab(request, conn, account_id, active_tab, status=status)
|
||||
# Badge contoare pe tab-uri (US-011): needs_mapping -> Mapari. Blocatele
|
||||
# (fost badge "coada") se reflecta acum in heading-ul sectiunii Trimiteri (US-003).
|
||||
# Badge contoare pe tab-uri: needs_mapping -> Mapari. Blocatele se reflecta in
|
||||
# heading-ul sectiunii Trimiteri.
|
||||
counts = _status_counts(conn, account_id)
|
||||
badges = {
|
||||
"mapari": counts.get("needs_mapping", 0),
|
||||
@@ -460,7 +458,7 @@ def dashboard(request: Request, tab: str = "acasa", status: str | None = None) -
|
||||
|
||||
@router.get("/_fragments/acasa", response_class=HTMLResponse)
|
||||
def fragment_acasa(request: Request) -> HTMLResponse:
|
||||
"""Fragment HTMX pentru tab-ul Acasa (US-003, US-005)."""
|
||||
"""Fragment HTMX pentru tab-ul Acasa."""
|
||||
account_id = require_login(request)
|
||||
conn = get_connection()
|
||||
try:
|
||||
@@ -472,16 +470,16 @@ def fragment_acasa(request: Request) -> HTMLResponse:
|
||||
|
||||
@router.get("/_fragments/import", response_class=HTMLResponse)
|
||||
def fragment_import(request: Request) -> HTMLResponse:
|
||||
"""Fragment HTMX pentru tab-ul Import — include zona de upload (US-003)."""
|
||||
"""Fragment HTMX pentru tab-ul Import — include zona de upload."""
|
||||
require_login(request)
|
||||
return templates.TemplateResponse("_upload.html", _ctx(request))
|
||||
|
||||
|
||||
@router.get("/_fragments/coada", response_class=HTMLResponse)
|
||||
def fragment_coada(request: Request) -> HTMLResponse:
|
||||
"""US-003 (3.6): "coada" nu mai are fragment propriu. Serveste continutul Acasa
|
||||
(Trimiterile sunt sectiune permanenta pe Acasa) — evita un fragment `_coada.html`
|
||||
orfan din bookmark-uri/HTMX vechi. Nu da 404."""
|
||||
""""coada" nu mai are fragment propriu. Serveste continutul Acasa (Trimiterile sunt
|
||||
sectiune permanenta pe Acasa) — evita un fragment `_coada.html` orfan din
|
||||
bookmark-uri/HTMX vechi. Nu da 404."""
|
||||
account_id = require_login(request)
|
||||
conn = get_connection()
|
||||
try:
|
||||
@@ -528,7 +526,7 @@ def fragment_jurnal(
|
||||
cont: str | None = None,
|
||||
page: int = 0,
|
||||
) -> HTMLResponse:
|
||||
"""Tab Jurnal (US-006): evenimente app_events paginate + filtre, scoped pe cont.
|
||||
"""Tab Jurnal: evenimente app_events paginate + filtre, scoped pe cont.
|
||||
|
||||
Admin vede tot (filtru optional pe cont); non-admin doar evenimentele proprii.
|
||||
"""
|
||||
@@ -563,8 +561,8 @@ def fragment_banner(request: Request) -> HTMLResponse:
|
||||
def _blocate_defalcat(counts: dict[str, int]) -> list[tuple]:
|
||||
"""Construieste lista [(eticheta, n), ...] pentru starile blocate cu n > 0.
|
||||
|
||||
Ordinea: needs_mapping, needs_data, error — aceeasi ca in PRD.
|
||||
Returneaza lista goala daca nu exista nicio stare blocata.
|
||||
Ordinea: needs_mapping, needs_data, error. Returneaza lista goala daca nu
|
||||
exista nicio stare blocata.
|
||||
"""
|
||||
rezultat = []
|
||||
for status in ("needs_mapping", "needs_data", "error"):
|
||||
@@ -575,13 +573,12 @@ def _blocate_defalcat(counts: dict[str, int]) -> list[tuple]:
|
||||
|
||||
|
||||
def _pills_categorii(counts: dict[str, int]) -> list[dict]:
|
||||
"""Pill-uri pentru starile cu problema (US-003 PRD 5.10).
|
||||
"""Pill-uri pentru starile cu problema.
|
||||
|
||||
Inlocuieste _blocate_actionabil (care incarca PII/VIN per rand).
|
||||
Reutilizeaza contoarele deja calculate din _status_counts.
|
||||
Returneza lista goala daca nu exista nicio stare blocata.
|
||||
Reutilizeaza contoarele deja calculate din _status_counts (fara PII/VIN per rand).
|
||||
Returneaza lista goala daca nu exista nicio stare blocata.
|
||||
"""
|
||||
# DESIGN.md §Componente: Lipsa cod = --warn (chihlimbar), celelalte categorii = --err (rosu).
|
||||
# Lipsa cod = --warn (chihlimbar), celelalte categorii = --err (rosu).
|
||||
# Culoarea e CSS variable name (nu clasa), injectata direct in style tag al pill-ului,
|
||||
# pentru ca s-needs_mapping in base.html e tot --err (incorect pentru pill).
|
||||
PILL_DEFS = [
|
||||
@@ -598,7 +595,7 @@ def _pills_categorii(counts: dict[str, int]) -> list[dict]:
|
||||
|
||||
@router.get("/_fragments/status", response_class=HTMLResponse)
|
||||
def fragment_status(request: Request) -> HTMLResponse:
|
||||
"""Bara de status persistenta cu etichete umane (US-002, PRD 3.4).
|
||||
"""Bara de status persistenta cu etichete umane.
|
||||
|
||||
Scoped pe contul sesiunii. Expune starea worker, legatura RAR, ultima
|
||||
autentificare, contorii de coada si defalcarea blocatelor pe motiv.
|
||||
@@ -623,7 +620,7 @@ def fragment_status(request: Request) -> HTMLResponse:
|
||||
"request": request,
|
||||
"worker_lbl": worker_lbl,
|
||||
"rar_lbl": rar_lbl,
|
||||
# Stari binare pentru bife accesibile (US-001 PRD 3.5): glifa + culoare
|
||||
# Stari binare pentru bife accesibile: glifa + culoare
|
||||
"worker_ok": worker_alive,
|
||||
"rar_ok": rar_ok,
|
||||
"eticheta_ultima_auth": ETICHETA_ULTIMA_AUTENTIFICARE_RAR,
|
||||
@@ -657,9 +654,8 @@ def _iso_date_prefix(value: object) -> str | None:
|
||||
|
||||
Permite filtrarea dupa data_prestatie chiar daca valoarea contine ora/minut/secunda
|
||||
(ex. '2026-06-20 14:35:07' sau '2026-06-20T14:35:07') — extrage portiunea de data
|
||||
fara a exclude timestamp-urile (bug-ul fix US-001: _is_iso_date cerea len==10).
|
||||
Valori care nu incep cu o data ISO valida (ex. '05.12.2024') intorc None si
|
||||
sunt excluse din filtru — comportament actual pastrat.
|
||||
fara a exclude timestamp-urile. Valori care nu incep cu o data ISO valida
|
||||
(ex. '05.12.2024') intorc None si sunt excluse din filtru.
|
||||
"""
|
||||
s = str(value or "").strip()
|
||||
if len(s) < 10:
|
||||
@@ -673,16 +669,16 @@ def _iso_date_prefix(value: object) -> str | None:
|
||||
|
||||
|
||||
# Stari care semnaleaza o problema ce necesita atentia operatorului. Eticheta umana
|
||||
# scurta de pe rand (US-001, R1) e ne-goala DOAR pe acestea; pe queued/sending/sent e "".
|
||||
# scurta de pe rand e ne-goala DOAR pe acestea; pe queued/sending/sent e "".
|
||||
_STARI_CU_PROBLEMA = ("error", "needs_data", "needs_mapping")
|
||||
|
||||
|
||||
def _eticheta_problema(status: str, motiv: str) -> str:
|
||||
"""Eticheta umana scurta a problemei pentru randul de tabel (US-001, R1).
|
||||
"""Eticheta umana scurta a problemei pentru randul de tabel.
|
||||
|
||||
Reutilizeaza `motiv` (motiv_uman, deja calculat in randul de view) si cade pe
|
||||
`eticheta_scurta` cand motivul e gol — NU re-parseaza `rar_error` (R1: DRY, fara
|
||||
al 3-lea decoder). Codul BRUT de catalog ramane doar pentru modal, nu pe rand.
|
||||
`eticheta_scurta` cand motivul e gol — NU re-parseaza `rar_error` (DRY, fara al
|
||||
3-lea decoder). Codul BRUT de catalog ramane doar pentru modal, nu pe rand.
|
||||
|
||||
Sir gol pe stari fara problema (queued/sending/sent); ne-gol pe error/needs_*.
|
||||
Defensiv: motiv_uman nu arunca, iar starile cu problema au intotdeauna eticheta
|
||||
@@ -694,13 +690,13 @@ def _eticheta_problema(status: str, motiv: str) -> str:
|
||||
|
||||
|
||||
def _submission_row_view(r) -> dict:
|
||||
"""Imbogateste un rand de submission cu campuri afisabile umane (US-003/US-004)."""
|
||||
"""Imbogateste un rand de submission cu campuri afisabile umane."""
|
||||
eticheta = eticheta_stare(r["status"])
|
||||
motiv = motiv_uman(r["status"], r["rar_error"])
|
||||
return {
|
||||
"id": r["id"],
|
||||
"status": r["status"],
|
||||
# PRD 5.8 US-007/US-006: pill = eticheta scurta; textul lung ramane ca tooltip (title=).
|
||||
# pill = eticheta scurta; textul lung ramane ca tooltip (title=).
|
||||
"stare_scurt": eticheta_scurta(r["status"]),
|
||||
"stare_text": eticheta[0],
|
||||
"stare_css": eticheta[2],
|
||||
@@ -708,15 +704,15 @@ def _submission_row_view(r) -> dict:
|
||||
"id_prezentare": r["id_prezentare"],
|
||||
"updated_at": format_data_rar(r["updated_at"]),
|
||||
"motiv": motiv,
|
||||
# US-001/R1: eticheta umana scurta a problemei sub pill (text, nu cod brut).
|
||||
# eticheta umana scurta a problemei sub pill (text, nu cod brut).
|
||||
"eticheta_problema": _eticheta_problema(r["status"], motiv),
|
||||
# US-011: randurile blocate (error/needs_data/needs_mapping) sunt selectabile
|
||||
# pentru stergere bulk; sent/sending/queued raman read-only (fara checkbox).
|
||||
# randurile blocate (error/needs_data/needs_mapping) sunt selectabile pentru
|
||||
# stergere bulk; sent/sending/queued raman read-only (fara checkbox).
|
||||
"gestionabil": r["status"] in _GESTIONABILE_WEB,
|
||||
}
|
||||
|
||||
|
||||
_PAGE_SIZE = 25 # Marime pagina fixa (US-004 PRD 5.10)
|
||||
_PAGE_SIZE = 25 # Marime pagina fixa
|
||||
|
||||
|
||||
@router.get("/_fragments/submissions", response_class=HTMLResponse)
|
||||
@@ -728,9 +724,9 @@ def fragment_submissions(
|
||||
data_pana: str | None = None,
|
||||
page: int = 1,
|
||||
) -> HTMLResponse:
|
||||
"""Tabel Trimiteri, scoped pe cont, cu filtre optionale si paginare (US-009, US-004).
|
||||
"""Tabel Trimiteri, scoped pe cont, cu filtre optionale si paginare.
|
||||
|
||||
US-004 H1: totalul se calculeaza DIFERIT dupa tipul de filtru:
|
||||
Totalul se calculeaza DIFERIT dupa tipul de filtru:
|
||||
- FARA filtru Python (status-only / niciun filtru): SQL COUNT(*) + LIMIT/OFFSET
|
||||
- CU filtru vehicul/data activ: fetch-all -> filtreaza Python -> total=len -> slice
|
||||
SQL COUNT/LIMIT pe calea cu filtru Python ar da total gresit (taie inainte de filtru).
|
||||
@@ -756,8 +752,8 @@ def fragment_submissions(
|
||||
where_sql = " AND ".join(where)
|
||||
|
||||
if filtru_python:
|
||||
# Calea B: fetch-all, filtreaza in Python, slice (US-004 H1)
|
||||
# FARA LIMIT — altfel paginile >8 ar disparea silentios (bug PRD H1)
|
||||
# Calea B: fetch-all, filtreaza in Python, slice.
|
||||
# FARA LIMIT — altfel paginile >8 ar disparea silentios.
|
||||
rows_db = conn.execute(
|
||||
"SELECT id, status, id_prezentare, rar_status_code, rar_error, retry_count, "
|
||||
f"updated_at, payload_json FROM submissions WHERE {where_sql} ORDER BY id DESC",
|
||||
@@ -773,7 +769,7 @@ def fragment_submissions(
|
||||
if vehicul_q not in hay:
|
||||
continue
|
||||
if data_de or data_pana:
|
||||
# Extragem portiunea YYYY-MM-DD (US-001 fix).
|
||||
# Extragem portiunea YYYY-MM-DD.
|
||||
d_prefix = _iso_date_prefix(prez["data_prestatie"])
|
||||
if d_prefix is None:
|
||||
continue
|
||||
@@ -785,7 +781,7 @@ def fragment_submissions(
|
||||
|
||||
total = len(view_all)
|
||||
pages = max(1, math.ceil(total / _PAGE_SIZE)) if total > 0 else 1
|
||||
page = max(1, min(page, pages)) # clamp H2
|
||||
page = max(1, min(page, pages)) # clamp la nr. de pagini
|
||||
offset = (page - 1) * _PAGE_SIZE
|
||||
view = view_all[offset:offset + _PAGE_SIZE]
|
||||
|
||||
@@ -796,7 +792,7 @@ def fragment_submissions(
|
||||
).fetchone()[0]
|
||||
|
||||
pages = max(1, math.ceil(total / _PAGE_SIZE)) if total > 0 else 1
|
||||
page = max(1, min(page, pages)) # clamp H2
|
||||
page = max(1, min(page, pages)) # clamp la nr. de pagini
|
||||
offset = (page - 1) * _PAGE_SIZE
|
||||
|
||||
rows_db = conn.execute(
|
||||
@@ -815,13 +811,13 @@ def fragment_submissions(
|
||||
"rows": view,
|
||||
"filtru_activ": filtru_activ,
|
||||
"csrf_token": get_csrf_token(request),
|
||||
# Paginare (US-004)
|
||||
# Paginare
|
||||
"total": total,
|
||||
"page": page,
|
||||
"pages": pages,
|
||||
"page_start": page_start,
|
||||
"page_end": page_end,
|
||||
# Filtre curente pentru linkurile de paginare (pastreaza filtrele, H2)
|
||||
# Filtre curente pentru linkurile de paginare (pastreaza filtrele)
|
||||
"f_status": status or "",
|
||||
"f_vehicul": vehicul_q or "",
|
||||
"f_data_de": data_de or "",
|
||||
@@ -835,17 +831,17 @@ def fragment_submissions(
|
||||
conn.close()
|
||||
|
||||
|
||||
# Stari ne-trimise blocate pe care le putem corecta inline (US-010).
|
||||
# Stari ne-trimise blocate pe care le putem corecta inline.
|
||||
_CORECTABILE = ("needs_data", "needs_mapping")
|
||||
# US-006b: stari cu select editabil cod_prestatie (superset al _CORECTABILE: error
|
||||
# primeste select in formularul /repune, nu in /corecteaza — fara schimbare de vehicle fields).
|
||||
# Stari cu select editabil cod_prestatie (superset al _CORECTABILE: error primeste
|
||||
# select in formularul /repune, nu in /corecteaza — fara schimbare de vehicle fields).
|
||||
_EDITABILE_OP = ("needs_data", "needs_mapping", "error")
|
||||
# Stari gestionabile prin lifecycle web (US-011): sterge / re-pune in coada.
|
||||
# Stari gestionabile prin lifecycle web: sterge / re-pune in coada.
|
||||
_GESTIONABILE_WEB = ("error", "needs_data", "needs_mapping")
|
||||
|
||||
|
||||
def _render_submissions(request: Request, conn, account_id: int) -> HTMLResponse:
|
||||
"""Re-randeaza lista Trimiteri (fara filtre) — folosit dupa actiuni bulk (US-011)."""
|
||||
"""Re-randeaza lista Trimiteri (fara filtre) — folosit dupa actiuni bulk."""
|
||||
scope_sql, scope_params = account_scope_clause(account_id)
|
||||
rows = conn.execute(
|
||||
"SELECT id, status, id_prezentare, rar_status_code, rar_error, retry_count, "
|
||||
@@ -865,7 +861,7 @@ def _render_submissions(request: Request, conn, account_id: int) -> HTMLResponse
|
||||
|
||||
|
||||
def _payload_form_values(payload_json) -> dict:
|
||||
"""Valori brute pentru prefill-ul formularului de corectie (US-010)."""
|
||||
"""Valori brute pentru prefill-ul formularului de corectie."""
|
||||
try:
|
||||
data = json.loads(payload_json) if payload_json else {}
|
||||
if not isinstance(data, dict):
|
||||
@@ -882,7 +878,7 @@ def _payload_form_values(payload_json) -> dict:
|
||||
|
||||
|
||||
def _nemapate_pentru_submission(row, nomenclator: list[dict]) -> list[dict]:
|
||||
"""Operatiile nemapate ale UNUI submission needs_mapping, cu sugestii fuzzy (PRD 5.7).
|
||||
"""Operatiile nemapate ale UNUI submission needs_mapping, cu sugestii fuzzy.
|
||||
|
||||
Echivalentul `pending_unmapped` restrans la un singur rand: parseaza payload_json,
|
||||
aduna prestatiile fara cod_prestatie (cu cod_op_service) si ataseaza sugestii din
|
||||
@@ -921,12 +917,12 @@ def _detaliu_ctx(request: Request, row, *, message: str | None = None,
|
||||
"""Context pentru _trimitere_detaliu.html dintr-un rand de submission.
|
||||
|
||||
`conn`+`account_id` (optional): cand sunt date si randul e needs_mapping, expune
|
||||
`nemapate_inline` + `nomenclator` pentru maparea inline din panou (PRD 5.7).
|
||||
`nemapate_inline` + `nomenclator` pentru maparea inline din panou.
|
||||
"""
|
||||
eticheta = eticheta_stare(row["status"])
|
||||
nemapate_inline: list[dict] = []
|
||||
nomenclator: list[dict] = []
|
||||
# Variabila interna: nomenclatorul complet (incarcat pentru needs_mapping, refolosit pt US-006)
|
||||
# Nomenclatorul complet, incarcat pentru needs_mapping si refolosit mai jos.
|
||||
_nomenclator_complet: list[dict] = []
|
||||
if conn is not None and row["status"] == "needs_mapping":
|
||||
# Un singur SELECT pe nomenclator: il refolosim si pentru sugestii si pentru dropdown.
|
||||
@@ -934,14 +930,14 @@ def _detaliu_ctx(request: Request, row, *, message: str | None = None,
|
||||
nemapate_inline = _nemapate_pentru_submission(row, _nomenclator_complet)
|
||||
nomenclator = _nomenclator_complet if nemapate_inline else []
|
||||
|
||||
# US-006/US-006b: nomenclator pentru selectul cod_prestatie — needs_data/needs_mapping (in
|
||||
# formularul /corecteaza) + error (in formularul /repune). Refoloseste _nomenclator_complet
|
||||
# daca e deja incarcat (needs_mapping), altfel incarca fresh.
|
||||
# Nomenclator pentru selectul cod_prestatie — needs_data/needs_mapping (in formularul
|
||||
# /corecteaza) + error (in formularul /repune). Refoloseste _nomenclator_complet daca
|
||||
# e deja incarcat (needs_mapping), altfel incarca fresh.
|
||||
nomenclator_rar: list[dict] = []
|
||||
if conn is not None and row["status"] in _EDITABILE_OP:
|
||||
nomenclator_rar = _nomenclator_complet if _nomenclator_complet else load_nomenclator(conn)
|
||||
|
||||
# US-006: cod_prestatie curent din prima prestatie (pentru pre-selectare in select)
|
||||
# cod_prestatie curent din prima prestatie (pentru pre-selectare in select)
|
||||
cod_prestatie_curent = ""
|
||||
try:
|
||||
_pd = json.loads(row["payload_json"] or "{}")
|
||||
@@ -969,14 +965,14 @@ def _detaliu_ctx(request: Request, row, *, message: str | None = None,
|
||||
"created_at": format_data_rar(row["created_at"]),
|
||||
"updated_at": format_data_rar(row["updated_at"]),
|
||||
"next_attempt_at": format_data_rar(row["next_attempt_at"]),
|
||||
# randuri ne-trimise blocate sunt corectabile (US-010); sent/sending nu
|
||||
# randuri ne-trimise blocate sunt corectabile; sent/sending nu
|
||||
"editabil": row["status"] in _CORECTABILE,
|
||||
# US-011: error/needs_data/needs_mapping pot fi sterse / re-puse in coada
|
||||
# error/needs_data/needs_mapping pot fi sterse / re-puse in coada
|
||||
"gestionabil": row["status"] in _GESTIONABILE_WEB,
|
||||
# PRD 5.7: mapare inline (operatii nemapate ale acestui rand + nomenclator)
|
||||
# mapare inline (operatii nemapate ale acestui rand + nomenclator)
|
||||
"nemapate_inline": nemapate_inline,
|
||||
"nomenclator": nomenclator,
|
||||
# US-006: select cod_prestatie pentru stari editabile
|
||||
# select cod_prestatie pentru stari editabile
|
||||
"nomenclator_rar": nomenclator_rar,
|
||||
"cod_prestatie_curent": cod_prestatie_curent,
|
||||
"corectie_msg": message,
|
||||
@@ -988,7 +984,7 @@ def _detaliu_ctx(request: Request, row, *, message: str | None = None,
|
||||
|
||||
|
||||
def _fetch_submission_scoped(conn, account_id: int, submission_id: int):
|
||||
"""Randul scoped pe cont sau None (404 cross-account, nu confirmam existenta — B3)."""
|
||||
"""Randul scoped pe cont sau None (404 cross-account, nu confirmam existenta)."""
|
||||
scope_sql, scope_params = account_scope_clause(account_id)
|
||||
return conn.execute(
|
||||
f"SELECT * FROM submissions WHERE id=? AND {scope_sql}",
|
||||
@@ -996,14 +992,14 @@ def _fetch_submission_scoped(conn, account_id: int, submission_id: int):
|
||||
).fetchone()
|
||||
|
||||
|
||||
# Campuri afisate in detaliul trimiterii (panou dedicat US-004). payload_json e
|
||||
# plaintext si se foloseste doar pentru campurile derivate (prezentare_din_payload).
|
||||
# Campuri afisate in detaliul trimiterii. payload_json e plaintext si se foloseste
|
||||
# doar pentru campurile derivate (prezentare_din_payload).
|
||||
@router.get("/_fragments/trimitere/{submission_id}", response_class=HTMLResponse)
|
||||
def fragment_trimitere_detaliu(request: Request, submission_id: int) -> HTMLResponse:
|
||||
"""Detaliu complet al unei trimiteri, in panou dedicat (#trimitere-detaliu).
|
||||
|
||||
Scoped pe contul sesiunii: 404 daca randul nu exista SAU apartine altui cont
|
||||
(acelasi mesaj, nu confirmam existenta — vezi B3/router.py).
|
||||
(acelasi mesaj, nu confirmam existenta).
|
||||
"""
|
||||
account_id = require_login(request)
|
||||
conn = get_connection()
|
||||
@@ -1021,7 +1017,7 @@ def fragment_trimitere_detaliu(request: Request, submission_id: int) -> HTMLResp
|
||||
|
||||
@router.post("/trimitere/{submission_id}/mapeaza", response_class=HTMLResponse)
|
||||
async def post_mapeaza_inline(request: Request, submission_id: int) -> HTMLResponse:
|
||||
"""Mapare inline din panoul de detaliu (PRD 5.7): alege cod RAR pentru o operatie nemapata.
|
||||
"""Mapare inline din panoul de detaliu: alege cod RAR pentru o operatie nemapata.
|
||||
|
||||
Reutilizeaza EXACT save_mapping + reresolve_account (ca tab-ul Mapari) — fara logica
|
||||
noua de clasificare. Re-rezolva scoped pe batch-ul randului (canal API batch_id IS NULL
|
||||
@@ -1108,10 +1104,9 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
|
||||
if isinstance(val, str) and val.strip() != "":
|
||||
content[camp] = val.strip()
|
||||
|
||||
# US-006: injectare cod_prestatie din form INAINTE de resolve_prestatii.
|
||||
# Oglindeste validarea din post_mapeaza_inline (nomenclator check). Codul nou
|
||||
# e injectat in prima prestatie (index 0); build_key il include in hash (CLAUDE.md
|
||||
# invariant "build_key hashuieste cod_prestatie, idempotency.py:34").
|
||||
# Injectare cod_prestatie din form INAINTE de resolve_prestatii. Oglindeste
|
||||
# validarea din post_mapeaza_inline (nomenclator check). Codul nou e injectat in
|
||||
# prima prestatie (index 0); build_key il include in hash.
|
||||
_cod_raw = form.get("cod_prestatie")
|
||||
cod_prestatie_form = (_cod_raw.strip().upper() if isinstance(_cod_raw, str) else "")
|
||||
if cod_prestatie_form:
|
||||
@@ -1143,7 +1138,7 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
|
||||
resolved, unmapped = resolve_prestatii(content.get("prestatii"), mapping, valid_codes, text_rules)
|
||||
content["prestatii"] = resolved
|
||||
|
||||
# US-010: telemetrie pentru itemii rezolvati prin regula text (calea corectie web).
|
||||
# telemetrie pentru itemii rezolvati prin regula text (calea corectie web).
|
||||
_emite_text_rule_hits(conn, account_id, row["id"], resolved)
|
||||
|
||||
# Canonicalizare (strip ".0" odometru, VIN/nr upper) INAINTE de validare si cheie.
|
||||
@@ -1238,8 +1233,8 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
|
||||
"_trimitere_detaliu.html",
|
||||
_detaliu_ctx(request, row2, message="Corectat — randul a fost re-pus in coada."),
|
||||
)
|
||||
# PRD 5.9 US-003 (R5): pe succes, lista se reincarca (trimiteriChanged) si modalul
|
||||
# se inchide (inchideModal). After-settle ca inchiderea sa urmeze swap-ul fragmentului.
|
||||
# Pe succes, lista se reincarca (trimiteriChanged) si modalul se inchide
|
||||
# (inchideModal). After-settle ca inchiderea sa urmeze swap-ul fragmentului.
|
||||
resp.headers["HX-Trigger-After-Settle"] = "trimiteriChanged, inchideModal"
|
||||
return resp
|
||||
finally:
|
||||
@@ -1247,26 +1242,26 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
|
||||
|
||||
|
||||
# =========================================================================== #
|
||||
# US-011 — Lifecycle trimiteri blocate din dashboard: sterge / re-pune in coada #
|
||||
# Peste helper-ul US-009 (submissions_admin). CSRF enforce; scoped pe sesiune. #
|
||||
# Lifecycle trimiteri blocate din dashboard: sterge / re-pune in coada. #
|
||||
# Peste helper-ul submissions_admin. CSRF enforce; scoped pe sesiune. #
|
||||
# =========================================================================== #
|
||||
|
||||
@router.post("/trimitere/{submission_id}/repune", response_class=HTMLResponse)
|
||||
async def post_repune_trimitere(request: Request, submission_id: int) -> HTMLResponse:
|
||||
"""Re-pune in coada un rand blocat (error/needs_data/needs_mapping) din dashboard.
|
||||
|
||||
US-006b: daca randul e in starea `error` si formularul contine `cod_prestatie`,
|
||||
actualizeaza codul in payload, recalculeaza cheia de idempotency si re-pun in coada
|
||||
direct (fara `requeue_submission`, care nu actualizeaza cheia). Scoped pe sesiune
|
||||
(404 cross-account/inexistent, 409 sent/sending). Re-randeaza panoul de detaliu cu
|
||||
starea noua + nudge `trimiteriChanged` pentru lista.
|
||||
Daca randul e in starea `error` si formularul contine `cod_prestatie`, actualizeaza
|
||||
codul in payload, recalculeaza cheia de idempotency si re-pune in coada direct (fara
|
||||
`requeue_submission`, care nu actualizeaza cheia). Scoped pe sesiune (404
|
||||
cross-account/inexistent, 409 sent/sending). Re-randeaza panoul de detaliu cu starea
|
||||
noua + nudge `trimiteriChanged` pentru lista.
|
||||
"""
|
||||
account_id = require_login(request)
|
||||
form = await request.form()
|
||||
verify_csrf(request, str(form.get("csrf_token") or ""))
|
||||
conn = get_connection()
|
||||
try:
|
||||
# US-006b: prelucrare cod_prestatie pentru starea error (inaintea requeue_submission
|
||||
# Prelucrare cod_prestatie pentru starea error (inaintea requeue_submission
|
||||
# standard, care nu actualizeaza cheia de idempotency).
|
||||
_cod_raw = form.get("cod_prestatie")
|
||||
cod_prestatie_form = (_cod_raw.strip().upper() if isinstance(_cod_raw, str) else "")
|
||||
@@ -1398,7 +1393,7 @@ async def post_sterge_trimitere(request: Request, submission_id: int) -> HTMLRes
|
||||
resp = HTMLResponse(
|
||||
'<div class="flash" style="margin:0;">Trimitere stearsa.</div>'
|
||||
)
|
||||
# PRD 5.9 US-003 (R5): pe succes, lista se reincarca + modalul se inchide.
|
||||
# Pe succes, lista se reincarca + modalul se inchide.
|
||||
resp.headers["HX-Trigger-After-Settle"] = "trimiteriChanged, inchideModal"
|
||||
return resp
|
||||
finally:
|
||||
@@ -1410,7 +1405,7 @@ async def post_sterge_bulk(request: Request) -> HTMLResponse:
|
||||
"""Sterge in bloc trimiterile selectate (doar blocate, scoped pe sesiune).
|
||||
|
||||
Sare peste randuri sent/sending (read-only) si cross-account (inexistente) fara a
|
||||
opri operatia — pe modelul panoului admin (PRD 5.5). Re-randeaza lista Trimiteri.
|
||||
opri operatia — pe modelul panoului admin. Re-randeaza lista Trimiteri.
|
||||
"""
|
||||
account_id = require_login(request)
|
||||
form = await request.form()
|
||||
@@ -1436,7 +1431,7 @@ async def post_sterge_bulk(request: Request) -> HTMLResponse:
|
||||
|
||||
def _load_saved_op_mappings(conn, account_id: int) -> list[dict]:
|
||||
"""Mapari operatie->cod salvate (operations_mapping) ale contului, cu numele
|
||||
prestatiei jonctionat din nomenclator (US-005). Scoped pe cont (NOT NULL → simplu)."""
|
||||
prestatiei jonctionat din nomenclator. Scoped pe cont (NOT NULL → simplu)."""
|
||||
acct = account_or_default(account_id)
|
||||
rows = conn.execute(
|
||||
"SELECT o.id, o.cod_op_service, o.cod_prestatie, o.auto_send, n.nume_prestatie "
|
||||
@@ -1458,7 +1453,7 @@ def _load_saved_op_mappings(conn, account_id: int) -> list[dict]:
|
||||
|
||||
|
||||
def _load_column_formats(conn, account_id: int) -> list[dict]:
|
||||
"""Formate de coloane salvate (column_mappings) ale contului (US-006).
|
||||
"""Formate de coloane salvate (column_mappings) ale contului.
|
||||
|
||||
Coloanele afisate = cheile din json_mapare (campurile recunoscute). Scoped pe cont.
|
||||
"""
|
||||
@@ -1507,7 +1502,7 @@ def _render_mapari(
|
||||
def fragment_mapari(request: Request) -> HTMLResponse:
|
||||
"""Editor mapari: operatii ROAAUTO nemapate + sugestii fuzzy pe nomenclator RAR.
|
||||
|
||||
Scoped pe contul sesiunii (C6/task#7): pending_unmapped primeste account_id explicit.
|
||||
Scoped pe contul sesiunii: pending_unmapped primeste account_id explicit.
|
||||
"""
|
||||
account_id = require_login(request)
|
||||
conn = get_connection()
|
||||
@@ -1547,7 +1542,7 @@ def post_mapare(
|
||||
|
||||
|
||||
# =========================================================================== #
|
||||
# US-005 — Mapari operatii salvate: editare cod/auto-send + stergere #
|
||||
# Mapari operatii salvate: editare cod/auto-send + stergere. #
|
||||
# CRUD pe operations_mapping scoped pe sesiune; re-rezolva blocatele la edit. #
|
||||
# =========================================================================== #
|
||||
|
||||
@@ -1611,7 +1606,7 @@ def post_sterge_mapare_salvata(
|
||||
|
||||
|
||||
# =========================================================================== #
|
||||
# US-004 (5.8) — Reguli automate (text): substring -> cod RAR #
|
||||
# Reguli automate (text): substring -> cod RAR. #
|
||||
# Adaugare/stergere reguli text scoped pe sesiune; salvarea re-rezolva blocajele.#
|
||||
# =========================================================================== #
|
||||
|
||||
@@ -1628,7 +1623,7 @@ def post_salveaza_regula_text(
|
||||
Scoped pe contul sesiunii (save_text_rule foloseste account_or_default(sesiune)).
|
||||
Valideaza cod_prestatie fata de nomenclator INAINTE de save (cod necunoscut ->
|
||||
respins inline, fara salvare). La succes: mesaj „Regula salvata. Deblocate: N"
|
||||
+ trigger trimiteriChanged (refresh lista), ca maparea inline (5.7).
|
||||
+ trigger trimiteriChanged (refresh lista), ca maparea inline.
|
||||
"""
|
||||
account_id = require_login(request)
|
||||
verify_csrf(request, csrf_token)
|
||||
@@ -1647,7 +1642,7 @@ def post_salveaza_regula_text(
|
||||
request, conn, account_id,
|
||||
message=f"Cod RAR necunoscut in nomenclator: {cod}.",
|
||||
)
|
||||
# US-011: avertisment neblocant daca regula noua se suprapune (substring,
|
||||
# avertisment neblocant daca regula noua se suprapune (substring,
|
||||
# oricare directie) cu una existenta. Calculam INAINTE de save, fata de
|
||||
# regulile curente, ca pattern-ul nou sa nu se compare cu sine.
|
||||
overlap = text_rules_overlap(pat, load_text_rules(conn, account_id))
|
||||
@@ -1696,7 +1691,7 @@ def post_preview_regula_text(
|
||||
pattern: str = Form(""),
|
||||
csrf_token: str | None = Form(None),
|
||||
) -> HTMLResponse:
|
||||
"""Preview pre-salvare (US-009): cate operatii nemapate ar potrivi regula.
|
||||
"""Preview pre-salvare: cate operatii nemapate ar potrivi regula.
|
||||
|
||||
NU salveaza nimic (zero scriere DB). Normalizeaza pattern-ul cu
|
||||
normalize_for_match, numara operatiile DISTINCTE nemapate ale contului
|
||||
@@ -1743,7 +1738,7 @@ def post_preview_regula_text(
|
||||
|
||||
|
||||
# =========================================================================== #
|
||||
# US-006 — Formate de coloane salvate: editare format data + stergere #
|
||||
# Formate de coloane salvate: editare format data + stergere. #
|
||||
# CRUD pe column_mappings scoped pe sesiune (prin id, verificat pe account). #
|
||||
# =========================================================================== #
|
||||
|
||||
@@ -1879,7 +1874,7 @@ def _web_compute_preview(
|
||||
if not raw_rows_db:
|
||||
return "Niciun rand in batch."
|
||||
|
||||
# Decripteaza randurile + override-urile editate (3.6)
|
||||
# Decripteaza randurile + override-urile editate
|
||||
rows: list[dict[str, Any]] = []
|
||||
overrides: list[dict[str, Any]] = []
|
||||
for r in raw_rows_db:
|
||||
@@ -1907,12 +1902,12 @@ def _web_compute_preview(
|
||||
json_mapare: dict[str, str] = json.loads(mapping_row["json_mapare"])
|
||||
format_data: str | None = mapping_row["format_data"]
|
||||
|
||||
# Mapare operatii (o singura incarcare — Eng#5)
|
||||
# Mapare operatii (o singura incarcare)
|
||||
mapping_meta = load_mapping_meta(conn, acct)
|
||||
mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
|
||||
# US-010/US-003 (paritate): preview-ul web trebuie sa aplice ACELEASI reguli text +
|
||||
# validare nomenclator ca si commit-ul (2426), altfel un rand rezolvabil doar prin
|
||||
# regula text ar fi marcat needs_mapping si exclus din commit. Incarcate o data.
|
||||
# Paritate cu commit-ul: preview-ul web trebuie sa aplice ACELEASI reguli text +
|
||||
# validare nomenclator, altfel un rand rezolvabil doar prin regula text ar fi marcat
|
||||
# needs_mapping si exclus din commit. Incarcate o data.
|
||||
valid_codes = load_nomenclator_codes(conn) or None
|
||||
text_rules = load_text_rules(conn, acct)
|
||||
|
||||
@@ -1980,7 +1975,7 @@ def _web_compute_preview(
|
||||
"idempotency_key": key,
|
||||
})
|
||||
|
||||
# Already_sent: batch lookup (Eng#5 — fara N+1)
|
||||
# Already_sent: batch lookup (fara N+1)
|
||||
unique_keys = list(set(keys_for_lookup))
|
||||
already_sent_map = _already_sent_lookup(conn, account_id, unique_keys)
|
||||
|
||||
@@ -2091,7 +2086,7 @@ async def web_upload_import(
|
||||
try:
|
||||
sig = _signature(parsed.columns)
|
||||
|
||||
# Stagingul in DB (tranzactie explicita — Issue 6)
|
||||
# Stagingul in DB (tranzactie explicita)
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
try:
|
||||
cur = conn.execute(
|
||||
@@ -2330,15 +2325,15 @@ def web_preview_import(
|
||||
|
||||
|
||||
# =========================================================================== #
|
||||
# US-002 (3.6) — Editare celule in preview: mod editare pe rand. #
|
||||
# Swap pe rand (#preview-row-N) + OOB contoare, NU pe #import-section (D-3.1). #
|
||||
# Status rederivat DOAR prin _resolve_row_for_preview (H2 — fara clasificator). #
|
||||
# Editare celule in preview: mod editare pe rand. #
|
||||
# Swap pe rand (#preview-row-N) + OOB contoare, NU pe #import-section. #
|
||||
# Status rederivat DOAR prin _resolve_row_for_preview (fara clasificator). #
|
||||
# =========================================================================== #
|
||||
|
||||
def _preview_one_row(conn, import_id: int, account_id: int, row_index: int):
|
||||
"""Recalculeaza preview-ul si extrage un singur rand.
|
||||
|
||||
Statusul e rederivat prin `_resolve_row_for_preview` (H2, fara clasificator duplicat),
|
||||
Statusul e rederivat prin `_resolve_row_for_preview` (fara clasificator duplicat),
|
||||
iar `_web_compute_preview` persista `resolved_status` pentru toate randurile — astfel
|
||||
confirmarea (commit) vede starea editata. Intoarce (result, row) sau (mesaj, None)."""
|
||||
result = _web_compute_preview(conn, import_id, account_id)
|
||||
@@ -2366,7 +2361,7 @@ def _render_preview_rand(
|
||||
|
||||
@router.get("/_import/{import_id}/rand/{row_index}/editare", response_class=HTMLResponse)
|
||||
def web_rand_editare(request: Request, import_id: int, row_index: int) -> HTMLResponse:
|
||||
"""Intra in mod editare pe un rand de preview (randul devine FORM propriu, D-3.3)."""
|
||||
"""Intra in mod editare pe un rand de preview (randul devine FORM propriu)."""
|
||||
account_id = require_login(request)
|
||||
conn = get_connection()
|
||||
try:
|
||||
@@ -2400,11 +2395,11 @@ def web_rand_display(request: Request, import_id: int, row_index: int) -> HTMLRe
|
||||
|
||||
@router.post("/_import/{import_id}/rand/{row_index}/editeaza", response_class=HTMLResponse)
|
||||
async def web_editeaza_rand(request: Request, import_id: int, row_index: int) -> HTMLResponse:
|
||||
"""Alias web (US-001/US-002): persista override (mutatie pura) + re-randeaza DOAR randul.
|
||||
"""Persista override (mutatie pura) + re-randeaza DOAR randul.
|
||||
|
||||
Statusul e rederivat prin `_resolve_row_for_preview` (H2). Swap pe rand + OOB
|
||||
contoare (D-3.1). Daca raman erori de continut pe camp, randul ramane in editare
|
||||
cu valorile pastrate si mesajul pe campul vinovat (D-2.1/D-2.2)."""
|
||||
Statusul e rederivat prin `_resolve_row_for_preview`. Swap pe rand + OOB contoare.
|
||||
Daca raman erori de continut pe camp, randul ramane in editare cu valorile pastrate
|
||||
si mesajul pe campul vinovat."""
|
||||
account_id = require_login(request)
|
||||
form = await request.form()
|
||||
verify_csrf(request, str(form.get("csrf_token") or ""))
|
||||
@@ -2502,8 +2497,8 @@ async def web_confirma_import(
|
||||
|
||||
Replica logica din import_router.commit_import dar cu input din form HTML
|
||||
si raspuns HTML (nu JSON). INSERT per-rand ON CONFLICT DO NOTHING (TOCTOU).
|
||||
C8/OV-2: account_id din sesiune propagat consecvent la build_key si toate lookup-urile.
|
||||
C12: require_login — pe scrieri NICIODATA fallback cont 1 in prod.
|
||||
account_id din sesiune propagat consecvent la build_key si toate lookup-urile.
|
||||
require_login — pe scrieri NICIODATA fallback cont 1 in prod.
|
||||
"""
|
||||
account_id = require_login(request)
|
||||
acct = account_or_default(account_id)
|
||||
@@ -2644,11 +2639,11 @@ async def web_confirma_import(
|
||||
# Mapare operatii
|
||||
mapping_meta = load_mapping_meta(conn, acct)
|
||||
mapping_ops = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
|
||||
# T2: validare nomenclator + reguli text incarcate O DATA, inainte de bucla pe randuri.
|
||||
# validare nomenclator + reguli text incarcate O DATA, inainte de bucla pe randuri.
|
||||
valid_codes = load_nomenclator_codes(conn) or None
|
||||
text_rules = load_text_rules(conn, acct)
|
||||
|
||||
# Enqueue in tranzactie explicita (Issue 6) — INSERT ON CONFLICT DO NOTHING (TOCTOU)
|
||||
# Enqueue in tranzactie explicita — INSERT ON CONFLICT DO NOTHING (TOCTOU)
|
||||
enqueued: list[dict] = []
|
||||
toctou: list[int] = []
|
||||
rows_for_hash: list[str] = []
|
||||
@@ -2696,7 +2691,7 @@ async def web_confirma_import(
|
||||
"odometru_final": canon["odometru_final"],
|
||||
})
|
||||
|
||||
# Override editat in preview (3.6) — aplicat ULTIMUL, ca in resolver.
|
||||
# Override editat in preview — aplicat ULTIMUL, ca in resolver.
|
||||
override = item.get("override") or {}
|
||||
if override:
|
||||
mapped.update(override)
|
||||
@@ -2729,7 +2724,7 @@ async def web_confirma_import(
|
||||
if cur.rowcount == 0:
|
||||
toctou.append(row_index)
|
||||
else:
|
||||
# US-010: telemetrie pentru itemii rezolvati prin regula text.
|
||||
# telemetrie pentru itemii rezolvati prin regula text.
|
||||
_emite_text_rule_hits(conn, acct, int(cur.lastrowid), resolved_p)
|
||||
enqueued.append({"submission_id": cur.lastrowid, "row_index": row_index})
|
||||
|
||||
@@ -2740,7 +2735,7 @@ async def web_confirma_import(
|
||||
|
||||
n_enqueued = len(enqueued)
|
||||
|
||||
# Log atestare (Voce#9)
|
||||
# Log atestare
|
||||
rows_hash = hashlib.sha256(
|
||||
json.dumps(sorted(rows_for_hash), ensure_ascii=False).encode("utf-8")
|
||||
).hexdigest() if rows_for_hash else ""
|
||||
@@ -2757,7 +2752,7 @@ async def web_confirma_import(
|
||||
|
||||
# Succes → bara de upload slim cu mesaj de confirmare. are_trimiteri=True:
|
||||
# contul tocmai a pus randuri in coada -> bara ramane slim si dezvaluie
|
||||
# sectiunea "Trimiterile tale" de pe Acasa (US-003/US-004).
|
||||
# sectiunea "Trimiterile tale" de pe Acasa.
|
||||
toctou_msg = f" ({len(toctou)} coliziuni TOCTOU excluse)" if toctou else ""
|
||||
return templates.TemplateResponse("_upload.html", _ctx(
|
||||
request,
|
||||
@@ -2773,8 +2768,8 @@ async def web_confirma_import(
|
||||
|
||||
|
||||
# =========================================================================== #
|
||||
# US-007 — Sectiune "Contul meu": rotire cheie API + creds RAR din UI #
|
||||
# Rute web proprii scoped pe sesiune (C13: nu reutilizeaza /v1/conturi/rar-creds
|
||||
# Sectiune "Contul meu": rotire cheie API + creds RAR din UI. #
|
||||
# Rute web proprii scoped pe sesiune (nu reutilizeaza /v1/conturi/rar-creds #
|
||||
# care cere cheie API; sesiunea web e suficienta ca identitate). #
|
||||
# =========================================================================== #
|
||||
|
||||
@@ -2846,10 +2841,9 @@ def integrare_test_cheie(
|
||||
) -> HTMLResponse:
|
||||
"""Verifica cheia API lipita de utilizator — scoped pe contul sesiunii.
|
||||
|
||||
US-004 (PRD Etapa 5): permite utilizatorului sa confirme ca o cheie copiata
|
||||
din generatorul de exemple corespunde contului sau, fara efecte secundare
|
||||
(fara creare/rotire). Cheie goala, invalida sau a altui cont -> mesaj de
|
||||
eroare neutru (fara eco al cheii in raspuns).
|
||||
Permite utilizatorului sa confirme ca o cheie copiata din generatorul de exemple
|
||||
corespunde contului sau, fara efecte secundare (fara creare/rotire). Cheie goala,
|
||||
invalida sau a altui cont -> mesaj de eroare neutru (fara eco al cheii in raspuns).
|
||||
"""
|
||||
account_id = require_login(request)
|
||||
verify_csrf(request, csrf_token)
|
||||
@@ -2901,7 +2895,7 @@ def cont_rar_creds(
|
||||
rar_parola: str = Form(""),
|
||||
csrf_token: str | None = Form(None),
|
||||
) -> HTMLResponse:
|
||||
"""Seteaza creds RAR per cont din sesiune (ruta web proprie, C13).
|
||||
"""Seteaza creds RAR per cont din sesiune (ruta web proprie).
|
||||
|
||||
Camp parola NICIODATA re-pus in value= la re-randare.
|
||||
Validare minima: email si parola negoale.
|
||||
|
||||
Reference in New Issue
Block a user