diff --git a/src/routes/issues.py b/src/routes/issues.py index d90e16b..82a091c 100644 --- a/src/routes/issues.py +++ b/src/routes/issues.py @@ -3,7 +3,7 @@ import json from flask import Blueprint, render_template, send_file, redirect, url_for, flash from app import db -from src.models import Issue, Article +from src.models import Issue, Article, Image from src.cover import generate_cover from src.epub_builder import build_epub import config @@ -112,14 +112,22 @@ def regenerate(issue_id): a.title for a in articles_for_issue if "Obituaries" not in json.loads(a.categories) ] - categories_list = [] + + image_paths = [] for a in articles_for_issue: - categories_list.extend(json.loads(a.categories)) + first_image = Image.query.filter_by(article_id=a.id).first() + if first_image: + image_paths.append(first_image.local_path) + + cover_method = issue.cover_method + if cover_method == "ai": + issue.cover_method = "mosaic" + cover_method = "mosaic" try: cover_path = generate_cover( - issue.cover_method, config.ISSUES_DIR, - issue.week_start, issue.week_end, headlines, categories_list + cover_method, config.ISSUES_DIR, + issue.week_start, issue.week_end, headlines, image_paths ) epub_path = build_epub( issue.week_start, issue.week_end, article_ids, diff --git a/tests/test_issues.py b/tests/test_issues.py new file mode 100644 index 0000000..df2008a --- /dev/null +++ b/tests/test_issues.py @@ -0,0 +1,142 @@ +import json +import os +from datetime import date, datetime +from unittest.mock import MagicMock, patch + +from PIL import Image as PILImage + +from app import db +from src.models import Article, Image, Issue + + +def test_regenerate_passes_ordered_image_paths_to_generate_cover(app, client, db, tmp_path): + """First image per article, in pub_date order, passed to generate_cover.""" + os.makedirs(tmp_path, exist_ok=True) + img1 = str(tmp_path / "a1.jpg") + img2 = str(tmp_path / "a2.jpg") + for p in (img1, img2): + PILImage.new("RGB", (100, 100), color="blue").save(p, format="JPEG") + + with app.app_context(): + a1 = Article( + guid="g1", + title="Earlier Article", + author="A", + pub_date=datetime(2026, 4, 6, 10, 0), + categories=json.dumps(["Government"]), + link="http://example.com/1", + content_html="
x
", + ) + a2 = Article( + guid="g2", + title="Later Article", + author="B", + pub_date=datetime(2026, 4, 7, 10, 0), + categories=json.dumps(["Culture"]), + link="http://example.com/2", + content_html="y
", + ) + db.session.add_all([a1, a2]) + db.session.flush() + db.session.add_all( + [ + Image( + article_id=a1.id, + original_url="https://example.com/1.jpg", + local_path=img1, + width=100, + height=100, + ), + Image( + article_id=a2.id, + original_url="https://example.com/2.jpg", + local_path=img2, + width=100, + height=100, + ), + ] + ) + id1, id2 = a1.id, a2.id + issue = Issue( + week_start=date(2026, 4, 6), + week_end=date(2026, 4, 12), + cover_method="mosaic", + cover_path=str(tmp_path / "old-cover.jpg"), + epub_path=str(tmp_path / "old.epub"), + article_ids=json.dumps([id2, id1]), + status="published", + ) + db.session.add(issue) + db.session.commit() + issue_id = issue.id + + mock_cover = MagicMock(return_value=str(tmp_path / "new-cover.jpg")) + mock_epub = MagicMock(return_value=str(tmp_path / "new.epub")) + + with patch("src.routes.issues.generate_cover", mock_cover), patch( + "src.routes.issues.build_epub", mock_epub + ): + resp = client.post(f"/issues/{issue_id}/regenerate") + + assert resp.status_code in (302, 303) + mock_cover.assert_called_once() + args = mock_cover.call_args[0] + assert args[0] == "mosaic" + assert args[4] == ["Earlier Article", "Later Article"] + assert args[5] == [img1, img2] + + +def test_regenerate_maps_ai_cover_method_to_mosaic(app, client, db, tmp_path): + """Legacy ai issues use mosaic generation and persist cover_method=mosaic.""" + os.makedirs(tmp_path, exist_ok=True) + img1 = str(tmp_path / "a1.jpg") + PILImage.new("RGB", (100, 100), color="blue").save(img1, format="JPEG") + + with app.app_context(): + a1 = Article( + guid="g-ai", + title="Some Article", + author="A", + pub_date=datetime(2026, 4, 6, 10, 0), + categories=json.dumps(["Government"]), + link="http://example.com/1", + content_html="x
", + ) + db.session.add(a1) + db.session.flush() + db.session.add( + Image( + article_id=a1.id, + original_url="https://example.com/1.jpg", + local_path=img1, + width=100, + height=100, + ) + ) + issue = Issue( + week_start=date(2026, 4, 6), + week_end=date(2026, 4, 12), + cover_method="ai", + cover_path=str(tmp_path / "old-cover.jpg"), + epub_path=str(tmp_path / "old.epub"), + article_ids=json.dumps([a1.id]), + status="published", + ) + db.session.add(issue) + db.session.commit() + issue_id = issue.id + + mock_cover = MagicMock(return_value=str(tmp_path / "new-cover.jpg")) + mock_epub = MagicMock(return_value=str(tmp_path / "new.epub")) + + with patch("src.routes.issues.generate_cover", mock_cover), patch( + "src.routes.issues.build_epub", mock_epub + ): + client.post(f"/issues/{issue_id}/regenerate") + + mock_cover.assert_called_once() + assert mock_cover.call_args[0][0] == "mosaic" + + with app.app_context(): + updated = db.session.get(Issue, issue_id) + assert updated.cover_method == "mosaic"