"""Общая лента: все посты из мастер-классов и проектов в одном потоке.""" 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", ""))