mirror of
https://gitverse.ru/kpa39l/vesti.git
synced 2026-09-29 18:05:03 +00:00
6.2 KiB
6.2 KiB
Design — archive-navigation
Контекст
- Роут:
GET /published(web/app.py:735), шаблонweb/templates/published.html(42 строки). - Дата:
posts.published_at TEXT— ISO 8601 с таймзоной (2026-09-08T08:02:45+00:00). - Сейчас:
LIMIT 100без пагинации, без фильтров. - Фронтенд: локальный Bootstrap (
web/static/bootstrap.min.css), без JS-фреймворков, htmx запрещён (AGENT.MD). Паттерн фильтра по направлениям — query-параметры (_cand_back).
1. Хелпер дерева навигации
В web/app.py, рядом с _db():
MONTH_NAMES_RU = ("", "Янв", "Фев", "Мар", "Апр", "Май", "Июн",
"Июл", "Авг", "Сен", "Окт", "Ноя", "Дек")
def _archive_tree(conn):
"""Дерево навигации архива: [{year, months:[{m, label, n}], n}] + {no_date:n}.
Строится только из существующих периодов; пустые месяцы отсутствуют."""
rows = conn.execute(
"""SELECT strftime('%Y', published_at) y,
strftime('%m', published_at) m, COUNT(*) n
FROM posts WHERE status='published' AND published_at IS NOT NULL
AND published_at != ''
GROUP BY y, m ORDER BY y DESC, m DESC"""
).fetchall()
years, cur = [], None
for r in rows:
if cur is None or cur["year"] != r["y"]:
cur = {"year": r["y"], "months": [], "n": 0}
years.append(cur)
cur["months"].append({"m": r["m"], "label": f"{MONTH_NAMES_RU[int(r['m'])]} {r['y']}",
"n": r["n"]})
cur["n"] += r["n"]
no_date = conn.execute(
"""SELECT COUNT(*) FROM posts
WHERE status='published' AND (published_at IS NULL OR published_at='')"""
).fetchone()[0]
return {"years": years, "no_date": no_date}
- Сортировка: годы и месяцы — по убыванию (свежие сверху, Q9).
- Пустой период исключается самой группировкой
GROUP BY— ровно как Hugo-архивы (site.RegularPages | groupBy "Date.Year"), где календарь не итерируется (Q8).
2. Роут GET /published
ARCHIVE_PAGE_SIZE = 10
@app.get("/published", response_class=HTMLResponse)
def published(request: Request, year: str = "", month: str = "", page: int = 1):
_require_auth(request)
page = max(1, page)
conn = _db()
tree = _archive_tree(conn)
where, params = ["p.status='published'"], []
if year:
where.append("strftime('%Y', p.published_at) = ?"); params.append(year)
if month:
where.append("strftime('%m', p.published_at) = ?"); params.append(month)
# год без месяца = все посты за год; "Без даты" не попадает в year/month
if not year and not month:
where.append("(p.published_at IS NOT NULL AND p.published_at != '')")
total = conn.execute(f"SELECT COUNT(*) FROM posts p WHERE {' AND '.join(where)}",
params).fetchone()[0]
pages = max(1, (total + ARCHIVE_PAGE_SIZE - 1) // ARCHIVE_PAGE_SIZE)
page = min(page, pages)
rows = conn.execute(
f"""SELECT p.*, s.name source_name,
pub.tg_message_id, pub.views pub_views, pub.bundle_path, pub.distributed_dirs
FROM posts p
LEFT JOIN sources s ON s.id=p.source_id
LEFT JOIN published pub ON pub.post_id=p.id
WHERE {' AND '.join(where)}
ORDER BY p.published_at DESC LIMIT ? OFFSET ?""",
params + [ARCHIVE_PAGE_SIZE, (page - 1) * ARCHIVE_PAGE_SIZE]
).fetchall()
conn.close()
html = tpl.get_template("published.html").render(
posts=rows, tree=tree, year=year, month=month,
page=page, pages=pages, total=total,
)
return HTMLResponse(html)
- Блок «Без даты» показывается как ссылка
/?year=&month=? Нет — отдельный фильтр: кнопка «Без даты» (querynodate=1), тоже с деревом и пагинацией. (Добавлено в spec ниже — сценарий «Без даты»).
3. Шаблон published.html
- Обёртка:
row→col-lg-9(список) +col-lg-3(дерево,position-sticky top-2). - Дерево:
- ссылка «📄 Все посты» →
/published(активна, если нет year/month/nodate). - по годам:
<h6>2026 ▾</h6>+<ul>месяцев; месяц — ссылка/published?year=2026&month=09, счётчик(4). Активный месяц подсвечен (active-row-стиль, как в кандидатах). - на мобильных: дерево в
collapse(кнопка «🗂 Архив» +data-bs-toggle).
- ссылка «📄 Все посты» →
- Список: карточки как сейчас, но посты только за выбранный период.
- Пагинация снизу: Bootstrap
pagination,page-item activeдля текущей, prev/next, номера 1..pages (с сохранением year/month/nodate). - Пустой период:
alert-info«Нет постов за этот период» (дерево всё равно видно).
4. Пагинация
- Bootstrap HTML, без JS:
?year=2026&month=09&page=N. - Текущая страница —
active, prev/next — disabled на границах. - total = число постов в периоде; pages = ceil(total/10).
5. Проверка
openspec validate archive-navigation— чисто.curl -s 'http://127.0.0.1:8400/published?year=2026&month=09'— видно 4 поста.curl -s 'http://127.0.0.1:8400/published?month=07'— 2 поста.- В HTML дерева нет месяца без постов (только 07, 08, 09) — «февраль отсутствует».
?page=1..N — пагинация с номерами; вне диапазона — клампится к последней.- Мобильная вёрстка: сужение окна → колонка сворачивается.