From a2cfa8f1e945252e300f96e3f432d3ad06d3ec99 Mon Sep 17 00:00:00 2001 From: "Aliaksei Yaletski (Tiendil)" Date: Mon, 31 Aug 2026 17:27:39 +0200 Subject: [PATCH 1/8] base refactoring --- ffun/ffun/api/spa/entities.py | 13 ++- ffun/ffun/api/spa/tests/test_entities.py | 21 ++++ ffun/ffun/library/entities.py | 6 +- ffun/ffun/library/tests/test_entities.py | 12 +-- site/src/components/EntriesList.vue | 18 +++- site/src/components/EntryForList.vue | 95 ++++++++----------- .../body_list/ReverseTimeColumn.vue | 5 +- .../notifications/LoadedOldNews.vue | 2 +- site/src/logic/enums.ts | 9 +- site/src/logic/tests/types.test.ts | 12 ++- site/src/logic/types.ts | 24 +++-- site/src/values/Icon.vue | 4 - site/src/views/NewsView.vue | 1 - site/src/views/PublicCollectionView.vue | 1 - 14 files changed, 128 insertions(+), 95 deletions(-) diff --git a/ffun/ffun/api/spa/entities.py b/ffun/ffun/api/spa/entities.py index d4f1946f1..04aeec8a3 100644 --- a/ffun/ffun/api/spa/entities.py +++ b/ffun/ffun/api/spa/entities.py @@ -155,7 +155,9 @@ class Entry(BaseEntity): markers: list[Marker] = [] score: int scoreContributions: dict[TagId, int] - publishedAt: datetime.datetime + effectivePublishedAt: datetime.datetime + firstSeenAt: datetime.datetime + sourcePublishedAt: datetime.datetime body: str | None = None references: list[Reference] | None = None @@ -177,12 +179,9 @@ def from_internal( # noqa: CFQ002 markers=list(markers), score=score, scoreContributions=score_contributions, - # THIS IS AN INTENDED BEHAVIOR - # we set publishedAt for the frontend as global entry creation time - # because it is the only reliable time with "published" meaning - # actual published_at is absolutely unreliable because comes from the third-party sources - # and can be broken in numerous ways. - publishedAt=entry.created_at, + effectivePublishedAt=entry.effective_published_at, + firstSeenAt=entry.created_at, + sourcePublishedAt=entry.published_at, # Some APIs return full entry info, some return shorter info to save traffic and speed up the response. body=entry.body if with_body else None, references=[Reference.from_internal(reference) for reference in entry.references] if with_body else None, diff --git a/ffun/ffun/api/spa/tests/test_entities.py b/ffun/ffun/api/spa/tests/test_entities.py index 0f019c688..2ea0d18d9 100644 --- a/ffun/ffun/api/spa/tests/test_entities.py +++ b/ffun/ffun/api/spa/tests/test_entities.py @@ -7,6 +7,7 @@ from ffun.api.spa.entities import ( EntitlementKind, + Entry, Feed, FeedInfo, Marker, @@ -31,6 +32,7 @@ from ffun.entitlements.tests.make import make_effective_entitlement_interval, make_source_entitlement from ffun.feeds.entities import Feed as InternalFeed from ffun.feeds.entities import FeedError +from ffun.library.entities import CollectedEntry from ffun.parsers import entities as p_entities from ffun.product.entities import Credit, Resource from ffun.resources import entities as r_entities @@ -87,6 +89,25 @@ def test_from_internal__with_last_error(self, loaded_feed: InternalFeed) -> None assert external_feed.lastError == error.name +class TestEntry: + def test_from_internal__maps_entry_dates(self, new_entry: CollectedEntry) -> None: + first_seen_at = utils.now() + source_published_at = first_seen_at + datetime.timedelta(days=1) + internal_entry = new_entry.replace(published_at=source_published_at).fake_entry(first_seen_at) + + external_entry = Entry.from_internal( + internal_entry, + tags=[], + markers=[], + score=0, + score_contributions={}, + ) + + assert external_entry.effectivePublishedAt == first_seen_at + assert external_entry.firstSeenAt == first_seen_at + assert external_entry.sourcePublishedAt == source_published_at + + class TestFeedInfo: def test_from_internal__keeps_site_url(self) -> None: feed_url = str_to_feed_url("https://example.com/feed") diff --git a/ffun/ffun/library/entities.py b/ffun/ffun/library/entities.py index d437753ef..9b367ec08 100644 --- a/ffun/ffun/library/entities.py +++ b/ffun/ffun/library/entities.py @@ -102,8 +102,8 @@ class Entry(BaseEntry): created_at: datetime.datetime @property - def published_at_for_processing(self) -> datetime.datetime: - """Entry published_at that is used to determine whether the entry is too old for processing. + def effective_published_at(self) -> datetime.datetime: + """Best available published_at value for product behavior. We use the smart algorithm to determine the "real" published_at value because an actual third-party published_at is absolutely unreliable. @@ -141,7 +141,7 @@ def published_at_for_processing(self) -> datetime.datetime: @property def age_for_processing(self) -> datetime.timedelta: - return utils.now() - self.published_at_for_processing + return utils.now() - self.effective_published_at def collected_entry(self) -> "CollectedEntry": return CollectedEntry( diff --git a/ffun/ffun/library/tests/test_entities.py b/ffun/ffun/library/tests/test_entities.py index 1846db968..b0512a8ea 100644 --- a/ffun/ffun/library/tests/test_entities.py +++ b/ffun/ffun/library/tests/test_entities.py @@ -40,7 +40,7 @@ class TestEntry: ), ], ) - def test_published_at_for_processing( + def test_effective_published_at( self, new_entry: CollectedEntry, published_at: datetime.datetime, @@ -49,20 +49,20 @@ def test_published_at_for_processing( ) -> None: entry = new_entry.replace(published_at=published_at).fake_entry(created_at) - assert entry.published_at_for_processing == expected_published_at + assert entry.effective_published_at == expected_published_at - def test_published_at_for_processing__asserts_timezone_in_published_at(self, new_entry: CollectedEntry) -> None: + def test_effective_published_at__asserts_timezone_in_published_at(self, new_entry: CollectedEntry) -> None: created_at = datetime.datetime(2026, 1, 2, 3, 4, 5, tzinfo=datetime.UTC) entry = new_entry.replace(published_at=datetime.datetime(2026, 1, 2, 3, 4, 4)).fake_entry(created_at) with pytest.raises(AssertionError): - _ = entry.published_at_for_processing + _ = entry.effective_published_at - def test_published_at_for_processing__asserts_timezone_in_created_at(self, new_entry: CollectedEntry) -> None: + def test_effective_published_at__asserts_timezone_in_created_at(self, new_entry: CollectedEntry) -> None: entry = new_entry.fake_entry(datetime.datetime(2026, 1, 2, 3, 4, 5)) with pytest.raises(AssertionError): - _ = entry.published_at_for_processing + _ = entry.effective_published_at def test_age_for_processing(self, new_entry: CollectedEntry) -> None: created_at = datetime.datetime(2026, 1, 2, 3, 4, 5, tzinfo=datetime.UTC) diff --git a/site/src/components/EntriesList.vue b/site/src/components/EntriesList.vue index 6478630c3..094aabc8b 100644 --- a/site/src/components/EntriesList.vue +++ b/site/src/components/EntriesList.vue @@ -1,6 +1,22 @@