158 lines
7.0 KiB
Python
158 lines
7.0 KiB
Python
"""Общая лента: все посты из мастер-классов и проектов в одном потоке."""
|
||
from datetime import datetime
|
||
from flask import render_template, request, session
|
||
from sqlmodel import Session, select
|
||
from ..db import engine
|
||
from ..models import (
|
||
Workshop, WorkshopPost, WorkshopPostAttachment,
|
||
Project, ProjectPost, ProjectPostAttachment, ProjectMember,
|
||
PortalFile, User,
|
||
)
|
||
from .. import gitea_api
|
||
|
||
|
||
def register(app):
|
||
|
||
@app.route("/feed")
|
||
def feed():
|
||
user_id = session.get("user_id")
|
||
user_role = session.get("role")
|
||
source = (request.args.get("source") or "all").strip() # all | workshops | projects
|
||
q = (request.args.get("q") or "").strip().lower()
|
||
|
||
items = []
|
||
all_file_ids = set()
|
||
|
||
with Session(engine) as db_session:
|
||
# ─── Посты МК ───────────────────────────────────────────────
|
||
if source in ("all", "workshops"):
|
||
rows = db_session.exec(
|
||
select(WorkshopPost, Workshop, User)
|
||
.join(Workshop, WorkshopPost.workshop_id == Workshop.id)
|
||
.join(User, WorkshopPost.author_id == User.id)
|
||
.order_by(WorkshopPost.created_at.desc())
|
||
.limit(200)
|
||
).all()
|
||
|
||
# Фильтр видимости
|
||
visible_ws_rows = []
|
||
for post, ws, au in rows:
|
||
if ws.status == "draft" and not (user_role == "admin" or ws.lead_id == user_id):
|
||
continue
|
||
if ws.visibility == "internal" and not user_id:
|
||
continue
|
||
visible_ws_rows.append((post, ws, au))
|
||
|
||
# Аттачи
|
||
ws_post_ids = [p.id for p, _, _ in visible_ws_rows]
|
||
atts_by_post = {}
|
||
if ws_post_ids:
|
||
for a in db_session.exec(
|
||
select(WorkshopPostAttachment)
|
||
.where(WorkshopPostAttachment.post_id.in_(ws_post_ids))
|
||
).all():
|
||
atts_by_post.setdefault(a.post_id, []).append(a.file_id)
|
||
all_file_ids.add(a.file_id)
|
||
|
||
for post, ws, au in visible_ws_rows:
|
||
items.append({
|
||
"kind": "workshop",
|
||
"kind_label": "Мастер-класс",
|
||
"kind_icon": "🎓",
|
||
"post_id": post.id,
|
||
"title": post.title,
|
||
"body": post.body,
|
||
"author_username": au.username,
|
||
"author_name": au.full_name or au.username,
|
||
"created_at": post.created_at,
|
||
"is_pinned": post.is_pinned,
|
||
"source_title": ws.title,
|
||
"source_url": f"/workshops/{ws.id}",
|
||
"attachments": atts_by_post.get(post.id, []),
|
||
})
|
||
|
||
# ─── Посты проектов ─────────────────────────────────────────
|
||
if source in ("all", "projects"):
|
||
rows = db_session.exec(
|
||
select(ProjectPost, Project, User)
|
||
.join(Project, ProjectPost.project_id == Project.id)
|
||
.join(User, ProjectPost.author_id == User.id)
|
||
.order_by(ProjectPost.created_at.desc())
|
||
.limit(200)
|
||
).all()
|
||
|
||
# Предзагрузка membership (чтобы не делать запрос в цикле)
|
||
membership_map = {}
|
||
if user_id:
|
||
for m in db_session.exec(
|
||
select(ProjectMember).where(ProjectMember.user_id == user_id)
|
||
).all():
|
||
membership_map[m.project_id] = m
|
||
|
||
visible_proj_rows = []
|
||
for post, proj, au in rows:
|
||
if proj.visibility == "internal" and not user_id:
|
||
continue
|
||
if proj.visibility == "private":
|
||
if not user_id: continue
|
||
if proj.owner_id != user_id and user_role != "admin" and proj.id not in membership_map:
|
||
continue
|
||
visible_proj_rows.append((post, proj, au))
|
||
|
||
proj_post_ids = [p.id for p, _, _ in visible_proj_rows]
|
||
atts_by_post = {}
|
||
if proj_post_ids:
|
||
for a in db_session.exec(
|
||
select(ProjectPostAttachment)
|
||
.where(ProjectPostAttachment.post_id.in_(proj_post_ids))
|
||
).all():
|
||
atts_by_post.setdefault(a.post_id, []).append(a.file_id)
|
||
all_file_ids.add(a.file_id)
|
||
|
||
for post, proj, au in visible_proj_rows:
|
||
items.append({
|
||
"kind": "project",
|
||
"kind_label": "Проект",
|
||
"kind_icon": "🗂",
|
||
"post_id": post.id,
|
||
"title": post.title,
|
||
"body": post.body,
|
||
"author_username": au.username,
|
||
"author_name": au.full_name or au.username,
|
||
"created_at": post.created_at,
|
||
"is_pinned": post.is_pinned,
|
||
"source_title": proj.title,
|
||
"source_url": f"/projects/{proj.id}",
|
||
"attachments": atts_by_post.get(post.id, []),
|
||
})
|
||
|
||
# Файлы для превью
|
||
files_map = {}
|
||
if all_file_ids:
|
||
for f in db_session.exec(
|
||
select(PortalFile).where(PortalFile.id.in_(list(all_file_ids)))
|
||
).all():
|
||
files_map[f.id] = f
|
||
|
||
# Сортировка: закреплённые сверху, потом по дате
|
||
items.sort(key=lambda x: (not x["is_pinned"], -(x["created_at"].timestamp() if x["created_at"] else 0)))
|
||
|
||
# Поиск
|
||
if q:
|
||
items = [i for i in items
|
||
if q in i["title"].lower()
|
||
or (i["body"] and q in i["body"].lower())
|
||
or q in i["source_title"].lower()
|
||
or q in i["author_username"].lower()]
|
||
|
||
# Ограничение для рендера
|
||
total_count = len(items)
|
||
items = items[:100]
|
||
|
||
return render_template("feed.html",
|
||
items=items,
|
||
files_map=files_map,
|
||
total_count=total_count,
|
||
current_source=source,
|
||
q=request.args.get("q", ""))
|