billscroll should now remember user's position across devices

This commit is contained in:
Disco DeDisco
2026-03-24 17:44:34 -04:00
parent a0f8aeb791
commit cde231d43c
9 changed files with 206 additions and 4 deletions

View File

@@ -2,7 +2,7 @@ from django.test import TestCase
from django.urls import reverse
from apps.applets.models import Applet
from apps.drama.models import GameEvent, record
from apps.drama.models import GameEvent, ScrollPosition, record
from apps.epic.models import Room
from apps.lyric.models import User
@@ -133,3 +133,52 @@ class BillscrollViewTest(TestCase):
def test_passes_page_class_billscroll(self):
response = self.client.get(f"/billboard/room/{self.room.id}/scroll/")
self.assertEqual(response.context["page_class"], "page-billscroll")
def test_passes_scroll_position_zero_when_none_saved(self):
response = self.client.get(f"/billboard/room/{self.room.id}/scroll/")
self.assertEqual(response.context["scroll_position"], 0)
def test_passes_saved_scroll_position_in_context(self):
ScrollPosition.objects.create(user=self.user, room=self.room, position=250)
response = self.client.get(f"/billboard/room/{self.room.id}/scroll/")
self.assertEqual(response.context["scroll_position"], 250)
class SaveScrollPositionTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="test@savescroll.io")
self.client.force_login(self.user)
self.room = Room.objects.create(name="Test Room", owner=self.user)
def test_post_saves_scroll_position(self):
self.client.post(
f"/billboard/room/{self.room.id}/scroll-position/",
{"position": 300},
)
sp = ScrollPosition.objects.get(user=self.user, room=self.room)
self.assertEqual(sp.position, 300)
def test_post_updates_existing_position(self):
ScrollPosition.objects.create(user=self.user, room=self.room, position=100)
self.client.post(
f"/billboard/room/{self.room.id}/scroll-position/",
{"position": 450},
)
self.assertEqual(
ScrollPosition.objects.get(user=self.user, room=self.room).position, 450
)
def test_post_returns_204(self):
response = self.client.post(
f"/billboard/room/{self.room.id}/scroll-position/",
{"position": 100},
)
self.assertEqual(response.status_code, 204)
def test_post_requires_login(self):
self.client.logout()
response = self.client.post(
f"/billboard/room/{self.room.id}/scroll-position/",
{"position": 100},
)
self.assertEqual(response.status_code, 302)

View File

@@ -8,4 +8,5 @@ urlpatterns = [
path("", views.billboard, name="billboard"),
path("toggle-applets", views.toggle_billboard_applets, name="toggle_applets"),
path("room/<uuid:room_id>/scroll/", views.room_scroll, name="scroll"),
path("room/<uuid:room_id>/scroll-position/", views.save_scroll_position, name="save_scroll_position"),
]

View File

@@ -4,7 +4,7 @@ from django.shortcuts import redirect, render
from apps.applets.models import Applet, UserApplet
from apps.applets.utils import applet_context
from apps.drama.models import GameEvent
from apps.drama.models import GameEvent, ScrollPosition
from apps.epic.models import GateSlot, Room, RoomInvite
@@ -61,9 +61,26 @@ def toggle_billboard_applets(request):
def room_scroll(request, room_id):
room = Room.objects.get(id=room_id)
events = room.events.select_related("actor").all()
sp = ScrollPosition.objects.filter(user=request.user, room=room).first()
return render(request, "apps/billboard/room_scroll.html", {
"room": room,
"events": events,
"viewer": request.user,
"scroll_position": sp.position if sp else 0,
"page_class": "page-billscroll",
})
@login_required(login_url="/")
def save_scroll_position(request, room_id):
if request.method != "POST":
from django.http import HttpResponseNotAllowed
return HttpResponseNotAllowed(["POST"])
room = Room.objects.get(id=room_id)
position = int(request.POST.get("position", 0))
ScrollPosition.objects.update_or_create(
user=request.user, room=room,
defaults={"position": position},
)
from django.http import HttpResponse
return HttpResponse(status=204)

View File

@@ -83,6 +83,25 @@ class GameEvent(models.Model):
return f"[{self.timestamp:%Y-%m-%d %H:%M}] {actor}{self.verb}"
class ScrollPosition(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
related_name="scroll_positions",
)
room = models.ForeignKey(
"epic.Room", on_delete=models.CASCADE,
related_name="scroll_positions",
)
position = models.PositiveIntegerField(default=0)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = [("user", "room")]
def __str__(self):
return f"{self.user.email} @ {self.room.name}: {self.position}px"
def record(room, verb, actor=None, **data):
"""Record a game event in the drama log."""
return GameEvent.objects.create(room=room, actor=actor, verb=verb, data=data)

View File

@@ -1,6 +1,7 @@
from django.test import TestCase
from django.db import IntegrityError
from apps.drama.models import GameEvent, record
from apps.drama.models import GameEvent, ScrollPosition, record
from apps.epic.models import Room
from apps.lyric.models import User
@@ -42,3 +43,31 @@ class GameEventModelTest(TestCase):
def test_str_without_actor_shows_system(self):
event = record(self.room, GameEvent.ROLES_REVEALED)
self.assertIn("system", str(event))
class ScrollPositionModelTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="reader@test.io")
self.room = Room.objects.create(name="Test Room", owner=self.user)
def test_can_save_scroll_position(self):
sp = ScrollPosition.objects.create(user=self.user, room=self.room, position=150)
self.assertEqual(ScrollPosition.objects.count(), 1)
self.assertEqual(sp.position, 150)
def test_default_position_is_zero(self):
sp = ScrollPosition.objects.create(user=self.user, room=self.room)
self.assertEqual(sp.position, 0)
def test_unique_per_user_and_room(self):
ScrollPosition.objects.create(user=self.user, room=self.room, position=50)
with self.assertRaises(IntegrityError):
ScrollPosition.objects.create(user=self.user, room=self.room, position=100)
def test_upsert_updates_existing_position(self):
ScrollPosition.objects.create(user=self.user, room=self.room, position=50)
ScrollPosition.objects.update_or_create(
user=self.user, room=self.room,
defaults={"position": 200},
)
self.assertEqual(ScrollPosition.objects.get(user=self.user, room=self.room).position, 200)