import json from datetime import date, timedelta from calendar import monthrange from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify from app import db from src.models import Article, Issue from src.cover import generate_cover from src.epub_builder import build_epub import config publish_bp = Blueprint("publish", __name__) def _get_week_bounds(d: date) -> tuple[date, date]: week_start = d - timedelta(days=d.weekday()) week_end = week_start + timedelta(days=6) return week_start, week_end def _calendar_data(year: int, month: int) -> list[dict]: """Build calendar grid data for a given month.""" first_day = date(year, month, 1) _, days_in_month = monthrange(year, month) weeks = [] current = first_day - timedelta(days=first_day.weekday()) while current.month <= month or (current + timedelta(days=6)).month <= month or current < first_day: week_start = current week_end = current + timedelta(days=6) iso_week = week_start.isocalendar()[1] article_count = Article.query.filter( Article.pub_date >= str(week_start), Article.pub_date < str(week_end + timedelta(days=1)), ).count() days = [] for i in range(7): d = current + timedelta(days=i) days.append({ "day": d.day, "date": d.isoformat(), "in_month": d.month == month, }) weeks.append({ "iso_week": iso_week, "week_start": week_start.isoformat(), "week_end": week_end.isoformat(), "days": days, "article_count": article_count, }) current += timedelta(days=7) if current.month > month and current.year >= year: if not any(d["in_month"] for d in days): weeks.pop() break return weeks @publish_bp.route("/publish", methods=["GET"]) def index(): today = date.today() cal_year = request.args.get("cal_year", today.year, type=int) cal_month = request.args.get("cal_month", today.month, type=int) week_start, week_end = _get_week_bounds(today) calendar_weeks = _calendar_data(cal_year, cal_month) articles = ( Article.query .filter( Article.pub_date >= str(week_start), Article.pub_date < str(week_end + timedelta(days=1)), ) .order_by(Article.pub_date.asc()) .all() ) all_articles = ( Article.query .order_by(Article.pub_date.desc()) .all() ) return render_template( "publish.html", articles=articles, all_articles=all_articles, week_start=week_start, week_end=week_end, calendar_weeks=calendar_weeks, cal_year=cal_year, cal_month=cal_month, cal_month_name=date(cal_year, cal_month, 1).strftime("%B %Y"), ) @publish_bp.route("/publish/articles", methods=["GET"]) def articles_api(): start = request.args.get("start") end = request.args.get("end") if not start or not end: return jsonify([]) try: start_date = date.fromisoformat(start) end_date = date.fromisoformat(end) except ValueError: return jsonify([]) articles = ( Article.query .filter( Article.pub_date >= str(start_date), Article.pub_date < str(end_date + timedelta(days=1)), ) .order_by(Article.pub_date.asc()) .all() ) return jsonify([ { "id": a.id, "title": a.title, "author": a.author, "pub_date": a.pub_date.strftime("%b %d, %Y"), "categories": json.loads(a.categories), } for a in articles ]) @publish_bp.route("/publish/calendar", methods=["GET"]) def calendar_api(): year = request.args.get("year", type=int) month = request.args.get("month", type=int) if not year or not month: return jsonify([]) return jsonify(_calendar_data(year, month)) @publish_bp.route("/publish", methods=["POST"]) def create_issue(): week_start_str = request.form.get("week_start") week_end_str = request.form.get("week_end") cover_method = request.form.get("cover_method", "text") issue_type = request.form.get("issue_type", "weekly") included_ids = request.form.getlist("article_ids", type=int) if not included_ids: flash("No articles selected.", "error") return redirect(url_for("publish.index")) week_start = date.fromisoformat(week_start_str) week_end = date.fromisoformat(week_end_str) headlines = [ a.title for a in Article.query.filter(Article.id.in_(included_ids)) .order_by(Article.pub_date.asc()).all() ] categories_list = [] for a in Article.query.filter(Article.id.in_(included_ids)).all(): categories_list.extend(json.loads(a.categories)) try: cover_path = generate_cover( cover_method, config.ISSUES_DIR, week_start, week_end, headlines, categories_list ) epub_path = build_epub( week_start, week_end, included_ids, cover_path, config.ISSUES_DIR, issue_type=issue_type ) except Exception as e: flash(f"Error generating issue: {e}", "error") return redirect(url_for("publish.index")) all_week_articles = ( Article.query .filter( Article.pub_date >= str(week_start), Article.pub_date < str(week_end + timedelta(days=1)), ) .all() ) all_ids = {a.id for a in all_week_articles} excluded_ids = list(all_ids - set(included_ids)) issue = Issue( week_start=week_start, week_end=week_end, cover_method=cover_method, cover_path=cover_path, epub_path=epub_path, article_ids=json.dumps(included_ids), excluded_article_ids=json.dumps(excluded_ids), status="published", issue_type=issue_type, ) db.session.add(issue) db.session.commit() flash(f"Issue published! {len(included_ids)} articles included.") return redirect(url_for("issues.index"))