feat(candidates): флаг прочитанности (is_read), жирные непрочитанные, голубая заливка выбранного, дефолтная группировка по дате

This commit is contained in:
kpa39l
2026-09-15 16:42:55 +00:00
parent 562d787f1b
commit c09dff08ac
8 changed files with 211 additions and 5 deletions
+23 -3
View File
@@ -75,6 +75,21 @@ def _db() -> sqlite3.Connection:
return conn
def _migrate():
"""Идемпотентные миграции при старте веб-сервиса."""
conn = sqlite3.connect(DB_PATH, timeout=10)
try:
cols = [r[1] for r in conn.execute("PRAGMA table_info(posts)").fetchall()]
if "is_read" not in cols:
conn.execute("ALTER TABLE posts ADD COLUMN is_read INTEGER DEFAULT 0")
conn.commit()
finally:
conn.close()
_migrate()
def _session_ok(request: Request) -> bool:
return request.cookies.get(SESSION_COOKIE) == os.getenv("VESTI_WEB_SESSION", "")
@@ -277,7 +292,7 @@ def _fetch_candidates(conn, direction: str, status: str, own: str, q: str, group
@app.get("/candidates", response_class=HTMLResponse)
def candidates(request: Request, direction: str = "", status: str = "", own: str = "",
q: str = "", group_by: str = "source", selected: int = 0, error: str = ""):
q: str = "", group_by: str = "date", selected: int = 0, error: str = ""):
_require_auth(request)
conn = _db()
grouped, counts, status = _fetch_candidates(conn, direction, status, own, q, group_by)
@@ -294,6 +309,11 @@ def candidates(request: Request, direction: str = "", status: str = "", own: str
else:
continue
break
# открытие поста = прочтение (как в почте): сразу помечаем, чтобы карточка перестала быть жирной
if sel is not None and sel["id"]:
conn.execute("UPDATE posts SET is_read=1 WHERE id=? AND is_read=0", (sel["id"],))
sel["is_read"] = 1
conn.commit()
conn.close()
# строка параметров фильтров для ссылок (выбор поста, смена группировки)
qs = []
@@ -313,7 +333,7 @@ def candidates(request: Request, direction: str = "", status: str = "", own: str
@app.post("/candidates/select")
def candidates_select(request: Request, id: int = Form(...), direction: str = Form(""),
status: str = Form(""), own: str = Form(""), q: str = Form(""), group_by: str = Form("source")):
status: str = Form(""), own: str = Form(""), q: str = Form(""), group_by: str = Form("date")):
"""Выбор кандидата левой панели (обычная форма без JS)."""
_require_auth(request)
url = f"/candidates?selected={id}&group_by={group_by}"
@@ -326,7 +346,7 @@ def candidates_select(request: Request, id: int = Form(...), direction: str = Fo
@app.post("/candidates/bulk")
def candidates_bulk(request: Request, action: str = Form(...), ids: list[int] = Form(default_factory=list),
direction: str = Form(""), status: str = Form(""), own: str = Form(""),
q: str = Form(""), group_by: str = Form("source")):
q: str = Form(""), group_by: str = Form("date")):
"""Bulk-операции: approve/reject/reject-old/process по отмеченным ids."""
_require_auth(request)
back = f"/candidates?group_by={group_by}"
+5
View File
@@ -9,6 +9,9 @@
body { padding-top: 4.5rem; background: #f5f6fa; }
.card { box-shadow: 0 1px 4px rgba(0,0,0,.08); }
.post-text { white-space: pre-wrap; max-height: 140px; overflow-y: auto; font-size: .9rem; }
/* список кандидатов: выбранный пост — светло-голубая заливка карточки */
.list-group-item.active-row { background-color: #dcecfc; }
.list-group-item.active-row:hover { background-color: #d0e4f8; }
</style>
</head>
<body>
@@ -19,6 +22,8 @@
<a class="nav-link" href="/candidates">Кандидаты</a>
<a class="nav-link" href="/published">Опубликовано</a>
<a class="nav-link" href="/metrics">Метрики</a>
<a class="nav-link" href="/crawlers">Краулеры</a>
<a class="nav-link" href="/sources">Источники</a>
<a class="nav-link text-danger" href="/logout">Выйти</a>
</div>
</div>
+2 -2
View File
@@ -75,8 +75,8 @@
</summary>
<div class="list-group list-group-flush small">
{% for p in g.posts %}
<div class="list-group-item d-flex align-items-start">
<a class="flex-grow-1 text-decoration-none{% if selected and selected.id == p.id %} fw-bold text-primary{% endif %}"
<div class="list-group-item d-flex align-items-start{% if selected and selected.id == p.id %} active-row{% endif %}{% if not p.is_read %} fw-bold{% endif %}">
<a class="flex-grow-1 text-decoration-none"
href="/candidates?selected={{ p.id }}&group_by={{ group_by }}{% if filters_qs %}&{{ filters_qs }}{% endif %}">
<span class="d-block text-truncate">{{ (p.text or '')[:90] }}</span>
<span class="d-block text-muted">