v0.4.0: live feed updates (auto-insert new posts)
New artifacts appear at the top of the feed without a manual refresh. - GET /feed/updates (session-authed): new artifacts since a timestamp marker, owner-scoped and filter-aware (source/label/kind/q), returned as row HTML. - Feed JS polls every 5s, paused while the tab is hidden (immediate poll on refocus). Prepends new rows into today's group without touching the selected row or the preview iframe — what you're reading stays put and selected. - Fresh rows show "now", aging into their real time after ~3 min; brief highlight on arrival. - Extracted _artifact_rows.html partial, shared by the day-group render and the updates endpoint, so live rows match exactly. - Polling chosen over SSE/WebSockets: zero infra on sync-WSGI + SQLite (see planning/next_steps for the SSE upgrade path). 142 tests pass; feed page + endpoint verified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f6f004c689
commit
cae414c307
9 changed files with 289 additions and 40 deletions
75
tests/views/test_feed_updates.py
Normal file
75
tests/views/test_feed_updates.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from artifacts.models import Artifact
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class FeedUpdatesViewTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(username="alice", password="pw")
|
||||
self.other = User.objects.create_user(username="bob", password="pw")
|
||||
self.client.force_login(self.user)
|
||||
self._n = 0
|
||||
|
||||
def make(self, owner=None, title="Note", source="agent"):
|
||||
self._n += 1
|
||||
return Artifact.objects.create(
|
||||
id=f"upd{self._n:05d}",
|
||||
owner=owner or self.user,
|
||||
source=source,
|
||||
title=title,
|
||||
kind=Artifact.MARKDOWN,
|
||||
day=timezone.localdate(),
|
||||
path=f"{source}/x.html",
|
||||
original_kind=Artifact.MARKDOWN,
|
||||
size_bytes=1,
|
||||
)
|
||||
|
||||
def marker(self, seconds_ago=60):
|
||||
return (timezone.now() - timedelta(seconds=seconds_ago)).isoformat()
|
||||
|
||||
def get(self, **params):
|
||||
return self.client.get(reverse("feed-updates"), params)
|
||||
|
||||
def test_requires_login(self):
|
||||
self.client.logout()
|
||||
response = self.get(after=self.marker())
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_returns_new_since_marker(self):
|
||||
self.make(title="Fresh Post")
|
||||
response = self.get(after=self.marker())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self.assertEqual(data["count"], 1)
|
||||
self.assertIn("Fresh Post", data["html"])
|
||||
self.assertIn("newest", data)
|
||||
self.assertIn("today_label", data)
|
||||
|
||||
def test_nothing_newer_than_now(self):
|
||||
self.make()
|
||||
response = self.get(after=self.marker(seconds_ago=-60)) # 60s in the future
|
||||
self.assertEqual(response.json()["count"], 0)
|
||||
|
||||
def test_missing_marker_returns_zero(self):
|
||||
self.make()
|
||||
self.assertEqual(self.get().json()["count"], 0)
|
||||
|
||||
def test_owner_scoped(self):
|
||||
self.make(owner=self.other, title="Not mine")
|
||||
data = self.get(after=self.marker()).json()
|
||||
self.assertEqual(data["count"], 0)
|
||||
|
||||
def test_respects_source_filter(self):
|
||||
self.make(title="Hermes post", source="hermes")
|
||||
self.make(title="Janus post", source="janus")
|
||||
data = self.get(after=self.marker(), source="hermes").json()
|
||||
self.assertEqual(data["count"], 1)
|
||||
self.assertIn("Hermes post", data["html"])
|
||||
self.assertNotIn("Janus post", data["html"])
|
||||
Loading…
Add table
Add a link
Reference in a new issue