Calm Filters MVP: device-local personalization across feed/brief/counts
- API endpoints (feed, brief, category-counts) accept a 'prefs' JSON query param, parsed tolerantly into FilterPrefs (bad blobs never break the feed). - Feed over-fetches then applies word-boundary filters in Python and slices to the page; brief is filtered down (no refill); counts are computed over the same filtered set so browse numbers match the feed exactly. - Pause.active() coerces naive datetimes to UTC; FilterPrefs.from_dict skips malformed pauses and non-string list entries. - Static site adds the humane ladder (Not today / Less like this / Always hide) plus a Calm filters panel managing pauses, mutes, and avoid-terms in localStorage. Nothing leaves the device. - Tests now 38 (added forgiving-parse and naive-now cases). README documents it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ from goodnews.filters import (
|
||||
FilterPrefs,
|
||||
Pause,
|
||||
filter_articles,
|
||||
prefs_from_json,
|
||||
text_matches_avoid_terms,
|
||||
)
|
||||
|
||||
@@ -86,3 +87,37 @@ def test_pause_active_helper():
|
||||
assert Pause("topic", "health", "2026-06-02T00:00:00Z").active(NOW)
|
||||
assert not Pause("topic", "health", "2026-05-01T00:00:00Z").active(NOW)
|
||||
assert not Pause("topic", "health", "garbage").active(NOW)
|
||||
|
||||
|
||||
def test_pause_active_tolerates_naive_now():
|
||||
# A naive `now` must not raise an aware-vs-naive comparison error.
|
||||
naive = datetime(2026, 6, 1)
|
||||
assert Pause("topic", "health", "2026-06-02T00:00:00Z").active(naive)
|
||||
|
||||
|
||||
# --- forgiving parsing (bad blobs must never break the feed) ---
|
||||
|
||||
def test_prefs_from_json_tolerates_garbage():
|
||||
assert prefs_from_json("not json").is_empty()
|
||||
assert prefs_from_json(None).is_empty()
|
||||
assert prefs_from_json("[1,2,3]").is_empty() # wrong shape
|
||||
|
||||
|
||||
def test_from_dict_skips_malformed_pauses():
|
||||
prefs = FilterPrefs.from_dict(
|
||||
{
|
||||
"mute_topics": ["health"],
|
||||
"pauses": [
|
||||
{"kind": "topic", "value": "science", "until": "2026-06-02T00:00:00Z"},
|
||||
{"kind": "topic"}, # malformed — missing value/until
|
||||
"garbage", # not even a dict
|
||||
],
|
||||
}
|
||||
)
|
||||
assert prefs.mute_topics == ["health"]
|
||||
assert len(prefs.pauses) == 1 # only the well-formed pause survives
|
||||
|
||||
|
||||
def test_from_dict_ignores_non_string_list_entries():
|
||||
prefs = FilterPrefs.from_dict({"avoid_terms": ["ok", 5, None, "fine"]})
|
||||
assert prefs.avoid_terms == ["ok", "fine"]
|
||||
|
||||
Reference in New Issue
Block a user