diff --git a/discord_rss_bot/main.py b/discord_rss_bot/main.py
index d639d27..f0f9a29 100644
--- a/discord_rss_bot/main.py
+++ b/discord_rss_bot/main.py
@@ -715,9 +715,11 @@ def build_filter_preview_context(
helper_text: str = "Saved whitelist rules still apply while previewing blacklist changes."
if filter_name == "blacklist":
- preview_blacklist_values = coerce_filter_values("blacklist", form_values)
+ if form_values is not None:
+ preview_blacklist_values = coerce_filter_values("blacklist", form_values)
else:
- preview_whitelist_values = coerce_filter_values("whitelist", form_values)
+ if form_values is not None:
+ preview_whitelist_values = coerce_filter_values("whitelist", form_values)
helper_text = "Saved blacklist rules still apply while previewing whitelist changes."
preview_entries: list[Entry] = list(reader.get_entries(feed=feed, limit=FILTER_PREVIEW_LIMIT))
@@ -761,17 +763,22 @@ def build_filter_preview_context(
},
)
- preview_html: str = create_html_for_feed(
- reader=reader,
- entries=preview_entries,
- current_feed_url=feed.url,
- entry_decisions=preview_decisions,
- )
+ preview_html = ""
+ if sent_count:
+ preview_html = create_html_for_feed(
+ reader=reader,
+ entries=[
+ entry for entry in preview_entries if preview_decisions[get_entry_decision_key(entry)].should_send
+ ],
+ current_feed_url=feed.url,
+ entry_decisions=preview_decisions,
+ )
return {
"filter_name": filter_name,
"filter_label": filter_name.title(),
"preview_entries": preview_entries,
+ "preview_rendered_count": sent_count,
"preview_rows": preview_rows,
"preview_html": preview_html,
"preview_limit": FILTER_PREVIEW_LIMIT,
diff --git a/discord_rss_bot/templates/_filter_preview.html b/discord_rss_bot/templates/_filter_preview.html
index dccebd7..3deb6ff 100644
--- a/discord_rss_bot/templates/_filter_preview.html
+++ b/discord_rss_bot/templates/_filter_preview.html
@@ -79,7 +79,11 @@
{{ preview_html|safe }}
{% else %}
-
Rendered preview will appear here when entries are available.
+ {% if preview_entries %}
+
No entries would be sent with the current rules.
+ {% else %}
+
Rendered preview will appear here when entries are available.
+ {% endif %}
{% endif %}
diff --git a/tests/test_main.py b/tests/test_main.py
index 117422c..1dc7db9 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -252,6 +252,172 @@ def test_whitelist_page_uses_live_preview_layout() -> None:
assert "Whitelist Rules" in response.text
+def test_blacklist_page_initial_preview_uses_saved_blacklist_rules() -> None:
+ @dataclass(slots=True)
+ class DummyFeed:
+ url: str
+ title: str
+
+ @dataclass(slots=True)
+ class DummyEntry:
+ id: str
+ feed: DummyFeed
+ title: str
+ summary: str
+ author: str
+ link: str
+ published: datetime | None
+ content: list[object] = field(default_factory=list)
+
+ class StubReader:
+ def __init__(self) -> None:
+ self.feed = DummyFeed(url="https://example.com/preview.xml", title="Preview Feed")
+ self.entries: list[Entry] = [
+ cast(
+ "Entry",
+ DummyEntry(
+ id="blocked-only",
+ feed=self.feed,
+ title="Blocked update",
+ summary="Summary",
+ author="Author",
+ link="https://example.com/blocked-only",
+ published=datetime(2024, 1, 1, tzinfo=UTC),
+ ),
+ ),
+ cast(
+ "Entry",
+ DummyEntry(
+ id="allowed-only",
+ feed=self.feed,
+ title="Allowed note",
+ summary="Summary",
+ author="Author",
+ link="https://example.com/allowed-only",
+ published=datetime(2024, 1, 2, tzinfo=UTC),
+ ),
+ ),
+ ]
+
+ def get_feed(self, _feed_url: str) -> DummyFeed:
+ return self.feed
+
+ def get_entries(self, **_kwargs: object) -> list[Entry]:
+ return self.entries
+
+ def get_tag(self, _resource: object, key: str, default: object = None) -> object:
+ if key == "blacklist_title":
+ return "blocked"
+ return default
+
+ stub_reader = StubReader()
+ app.dependency_overrides[get_reader_dependency] = lambda: stub_reader
+ rendered_titles: list[str] = []
+
+ def fake_create_html_for_feed(*, entries: list[Entry], **_kwargs: object) -> str:
+ entry_titles: list[str] = [entry.title or entry.id for entry in entries]
+ rendered_titles.extend(entry_titles)
+ return " | ".join(entry_titles)
+
+ try:
+ with patch("discord_rss_bot.main.create_html_for_feed", side_effect=fake_create_html_for_feed):
+ response: Response = client.get(url="/blacklist", params={"feed_url": stub_reader.feed.url})
+
+ assert response.status_code == 200, f"/blacklist failed: {response.text}"
+ assert rendered_titles == ["Allowed note"]
+ finally:
+ app.dependency_overrides = {}
+
+
+def test_whitelist_page_initial_preview_uses_saved_whitelist_rules() -> None:
+ @dataclass(slots=True)
+ class DummyFeed:
+ url: str
+ title: str
+
+ @dataclass(slots=True)
+ class DummyEntry:
+ id: str
+ feed: DummyFeed
+ title: str
+ summary: str
+ author: str
+ link: str
+ published: datetime | None
+ content: list[object] = field(default_factory=list)
+
+ class StubReader:
+ def __init__(self) -> None:
+ self.feed = DummyFeed(url="https://example.com/preview.xml", title="Preview Feed")
+ self.entries: list[Entry] = [
+ cast(
+ "Entry",
+ DummyEntry(
+ id="blocked-only",
+ feed=self.feed,
+ title="Blocked update",
+ summary="Summary",
+ author="Author",
+ link="https://example.com/blocked-only",
+ published=datetime(2024, 1, 1, tzinfo=UTC),
+ ),
+ ),
+ cast(
+ "Entry",
+ DummyEntry(
+ id="allowed-only",
+ feed=self.feed,
+ title="Allowed note",
+ summary="Summary",
+ author="Author",
+ link="https://example.com/allowed-only",
+ published=datetime(2024, 1, 2, tzinfo=UTC),
+ ),
+ ),
+ cast(
+ "Entry",
+ DummyEntry(
+ id="blocked-and-allowed",
+ feed=self.feed,
+ title="Blocked allowed",
+ summary="Summary",
+ author="Author",
+ link="https://example.com/blocked-and-allowed",
+ published=datetime(2024, 1, 3, tzinfo=UTC),
+ ),
+ ),
+ ]
+
+ def get_feed(self, _feed_url: str) -> DummyFeed:
+ return self.feed
+
+ def get_entries(self, **_kwargs: object) -> list[Entry]:
+ return self.entries
+
+ def get_tag(self, _resource: object, key: str, default: object = None) -> object:
+ if key == "whitelist_title":
+ return "allowed"
+ return default
+
+ stub_reader = StubReader()
+ app.dependency_overrides[get_reader_dependency] = lambda: stub_reader
+ rendered_titles: list[str] = []
+
+ def fake_create_html_for_feed(*, entries: list[Entry], **_kwargs: object) -> str:
+ entry_titles: list[str] = [entry.title or entry.id for entry in entries]
+ rendered_titles.extend(entry_titles)
+ return " | ".join(entry_titles)
+
+ try:
+ with patch("discord_rss_bot.main.create_html_for_feed", side_effect=fake_create_html_for_feed):
+ response: Response = client.get(url="/whitelist", params={"feed_url": stub_reader.feed.url})
+
+ assert response.status_code == 200, f"/whitelist failed: {response.text}"
+ assert rendered_titles == ["Allowed note", "Blocked allowed"]
+ finally:
+ app.dependency_overrides = {}
+
+
def test_blacklist_preview_does_not_persist_unsaved_rules() -> None:
reader: Reader = ensure_preview_feed_exists()
reader.set_tag(feed_url, "blacklist_title", "saved-blacklist") # pyright: ignore[reportArgumentType]
@@ -294,6 +460,275 @@ def test_whitelist_preview_shows_precedence_over_blacklist() -> None:
reader.delete_tag(feed_url, "blacklist_title")
+def test_whitelist_preview_rendered_entries_respect_saved_blacklist_rules() -> None:
+ @dataclass(slots=True)
+ class DummyFeed:
+ url: str
+ title: str
+
+ @dataclass(slots=True)
+ class DummyEntry:
+ id: str
+ feed: DummyFeed
+ title: str
+ summary: str
+ author: str
+ link: str
+ published: datetime | None
+ content: list[object] = field(default_factory=list)
+
+ class StubReader:
+ def __init__(self) -> None:
+ self.feed = DummyFeed(url="https://example.com/preview.xml", title="Preview Feed")
+ self.entries: list[Entry] = [
+ cast(
+ "Entry",
+ DummyEntry(
+ id="blocked-only",
+ feed=self.feed,
+ title="Blocked update",
+ summary="Summary",
+ author="Author",
+ link="https://example.com/blocked-only",
+ published=datetime(2024, 1, 1, tzinfo=UTC),
+ ),
+ ),
+ cast(
+ "Entry",
+ DummyEntry(
+ id="allowed-only",
+ feed=self.feed,
+ title="Allowed note",
+ summary="Summary",
+ author="Author",
+ link="https://example.com/allowed-only",
+ published=datetime(2024, 1, 2, tzinfo=UTC),
+ ),
+ ),
+ cast(
+ "Entry",
+ DummyEntry(
+ id="blocked-and-allowed",
+ feed=self.feed,
+ title="Blocked allowed",
+ summary="Summary",
+ author="Author",
+ link="https://example.com/blocked-and-allowed",
+ published=datetime(2024, 1, 3, tzinfo=UTC),
+ ),
+ ),
+ ]
+
+ def get_feed(self, _feed_url: str) -> DummyFeed:
+ return self.feed
+
+ def get_entries(self, **_kwargs: object) -> list[Entry]:
+ return self.entries
+
+ def get_tag(self, _resource: object, key: str, default: object = None) -> object:
+ if key == "blacklist_title":
+ return "blocked"
+ return default
+
+ stub_reader = StubReader()
+ app.dependency_overrides[get_reader_dependency] = lambda: stub_reader
+ rendered_titles: list[str] = []
+
+ def fake_create_html_for_feed(*, entries: list[Entry], **_kwargs: object) -> str:
+ entry_titles: list[str] = [entry.title or entry.id for entry in entries]
+ rendered_titles.extend(entry_titles)
+ return " | ".join(entry_titles)
+
+ try:
+ with patch("discord_rss_bot.main.create_html_for_feed", side_effect=fake_create_html_for_feed):
+ response: Response = client.get(
+ url="/whitelist_preview",
+ params={"feed_url": stub_reader.feed.url, "whitelist_title": "allowed"},
+ )
+
+ assert response.status_code == 200, f"/whitelist_preview failed: {response.text}"
+ assert rendered_titles == ["Allowed note", "Blocked allowed"]
+ finally:
+ app.dependency_overrides = {}
+
+
+def test_blacklist_preview_rendered_entries_respect_saved_whitelist_rules() -> None:
+ @dataclass(slots=True)
+ class DummyFeed:
+ url: str
+ title: str
+
+ @dataclass(slots=True)
+ class DummyEntry:
+ id: str
+ feed: DummyFeed
+ title: str
+ summary: str
+ author: str
+ link: str
+ published: datetime | None
+ content: list[object] = field(default_factory=list)
+
+ class StubReader:
+ def __init__(self) -> None:
+ self.feed = DummyFeed(url="https://example.com/preview.xml", title="Preview Feed")
+ self.entries: list[Entry] = [
+ cast(
+ "Entry",
+ DummyEntry(
+ id="blocked-only",
+ feed=self.feed,
+ title="Blocked update",
+ summary="Summary",
+ author="Author",
+ link="https://example.com/blocked-only",
+ published=datetime(2024, 1, 1, tzinfo=UTC),
+ ),
+ ),
+ cast(
+ "Entry",
+ DummyEntry(
+ id="allowed-only",
+ feed=self.feed,
+ title="Allowed note",
+ summary="Summary",
+ author="Author",
+ link="https://example.com/allowed-only",
+ published=datetime(2024, 1, 2, tzinfo=UTC),
+ ),
+ ),
+ cast(
+ "Entry",
+ DummyEntry(
+ id="blocked-and-allowed",
+ feed=self.feed,
+ title="Blocked allowed",
+ summary="Summary",
+ author="Author",
+ link="https://example.com/blocked-and-allowed",
+ published=datetime(2024, 1, 3, tzinfo=UTC),
+ ),
+ ),
+ ]
+
+ def get_feed(self, _feed_url: str) -> DummyFeed:
+ return self.feed
+
+ def get_entries(self, **_kwargs: object) -> list[Entry]:
+ return self.entries
+
+ def get_tag(self, _resource: object, key: str, default: object = None) -> object:
+ if key == "whitelist_title":
+ return "allowed"
+ return default
+
+ stub_reader = StubReader()
+ app.dependency_overrides[get_reader_dependency] = lambda: stub_reader
+ rendered_titles: list[str] = []
+
+ def fake_create_html_for_feed(*, entries: list[Entry], **_kwargs: object) -> str:
+ entry_titles: list[str] = [entry.title or entry.id for entry in entries]
+ rendered_titles.extend(entry_titles)
+ return " | ".join(entry_titles)
+
+ try:
+ with patch("discord_rss_bot.main.create_html_for_feed", side_effect=fake_create_html_for_feed):
+ response: Response = client.get(
+ url="/blacklist_preview",
+ params={"feed_url": stub_reader.feed.url, "blacklist_title": "blocked"},
+ )
+
+ assert response.status_code == 200, f"/blacklist_preview failed: {response.text}"
+ assert rendered_titles == ["Allowed note", "Blocked allowed"]
+ finally:
+ app.dependency_overrides = {}
+
+
+def test_blacklist_preview_shows_no_rendered_entries_message_when_all_entries_are_skipped() -> None:
+ @dataclass(slots=True)
+ class DummyFeed:
+ url: str
+ title: str
+
+ @dataclass(slots=True)
+ class DummyEntry:
+ id: str
+ feed: DummyFeed
+ title: str
+ summary: str
+ author: str
+ link: str
+ published: datetime | None
+ content: list[object] = field(default_factory=list)
+
+ class StubReader:
+ def __init__(self) -> None:
+ self.feed = DummyFeed(url="https://example.com/preview.xml", title="Preview Feed")
+ self.entries: list[Entry] = [
+ cast(
+ "Entry",
+ DummyEntry(
+ id="blocked-only",
+ feed=self.feed,
+ title="Blocked update",
+ summary="Summary",
+ author="Author",
+ link="https://example.com/blocked-only",
+ published=datetime(2024, 1, 1, tzinfo=UTC),
+ ),
+ ),
+ cast(
+ "Entry",
+ DummyEntry(
+ id="allowed-only",
+ feed=self.feed,
+ title="Allowed note",
+ summary="Summary",
+ author="Author",
+ link="https://example.com/allowed-only",
+ published=datetime(2024, 1, 2, tzinfo=UTC),
+ ),
+ ),
+ cast(
+ "Entry",
+ DummyEntry(
+ id="blocked-and-allowed",
+ feed=self.feed,
+ title="Blocked allowed",
+ summary="Summary",
+ author="Author",
+ link="https://example.com/blocked-and-allowed",
+ published=datetime(2024, 1, 3, tzinfo=UTC),
+ ),
+ ),
+ ]
+
+ def get_feed(self, _feed_url: str) -> DummyFeed:
+ return self.feed
+
+ def get_entries(self, **_kwargs: object) -> list[Entry]:
+ return self.entries
+
+ def get_tag(self, _resource: object, _key: str, default: object = None) -> object:
+ return default
+
+ stub_reader = StubReader()
+ app.dependency_overrides[get_reader_dependency] = lambda: stub_reader
+
+ try:
+ with patch("discord_rss_bot.main.create_html_for_feed") as create_html_mock:
+ response: Response = client.get(
+ url="/blacklist_preview",
+ params={"feed_url": stub_reader.feed.url, "blacklist_title": "blocked,allowed,note"},
+ )
+
+ assert response.status_code == 200, f"/blacklist_preview failed: {response.text}"
+ create_html_mock.assert_not_called()
+ assert "No entries would be sent with the current rules." in response.text
+ finally:
+ app.dependency_overrides = {}
+
+
def test_blacklist_preview_uses_50_entry_limit() -> None:
@dataclass(slots=True)
class DummyContent: