Refactored polls module into layered architecture with typed domain objects, isolated SSE handling, and comprehensive tests.
CI / Formatting (push) Successful in 5s
CI / Linting (push) Successful in 4s
CI / Tests (Python 3.12) (push) Successful in 2m54s
CI / Tests (Python 3.13) (push) Successful in 2m54s
CI / Tests (Python 3.14) (push) Successful in 2m52s
CI / Type Checking (push) Successful in 9s
CI / Spelling (push) Successful in 5s

This commit is contained in:
2026-05-11 11:58:47 -04:00
parent 4918547c7d
commit 06b4a0bb9c
19 changed files with 4172 additions and 1002 deletions
+20 -15
View File
@@ -43,17 +43,16 @@ from .commands import (
vote_command,
)
from .manager import PollManager, get_manager
from .repository import PollRepository
from .routes import (
create_page,
create_submit,
current_page,
events_stream,
mod_cancel,
mod_end,
results_page,
static_create_js,
static_polls_css,
static_polls_js,
vote_page,
static_asset,
vote_submit,
)
@@ -61,6 +60,7 @@ __all__ = [
"cancel_poll_command",
"create_page",
"create_submit",
"current_page",
"end_poll_command",
"events_stream",
"handle_bare_vote",
@@ -72,12 +72,9 @@ __all__ = [
"poll_command",
"results_page",
"setup",
"static_create_js",
"static_polls_css",
"static_polls_js",
"static_asset",
"teardown",
"vote_command",
"vote_page",
"vote_submit",
]
@@ -88,13 +85,17 @@ async def setup(ctx: ModuleContext) -> None:
:param ctx: Module context with config, storage, and other services.
"""
ctx.state["manager"] = PollManager(ctx)
repo = PollRepository()
ctx.state["manager"] = PollManager(ctx, repo)
@on_event(EventType.STREAM_STOPPED)
async def on_stream_stopped(ctx: EventContext[StreamStoppedEvent]) -> None:
"""Begin a grace period when the stream goes offline.
If the stream does not restart before the grace period expires, any active
poll is cancelled.
:param ctx: The event context.
"""
get_manager(ctx.module).begin_stream_grace()
@@ -113,8 +114,8 @@ async def on_stream_started(ctx: EventContext[StreamStartedEvent]) -> None:
async def on_user_joined(ctx: EventContext[UserJoinedEvent]) -> None:
"""Notify a user joining chat that a poll is active.
Sends a private message with the poll question, options, and voting
instructions.
Sends a private message with the poll question, options, and a protected
current-poll link.
:param ctx: The event context.
"""
@@ -128,11 +129,14 @@ async def on_user_joined(ctx: EventContext[UserJoinedEvent]) -> None:
f"<strong>{i}.</strong> {escape(opt)}<br>"
for i, opt in enumerate(poll.options, 1)
)
if poll.allow_multiple:
instructions = "Use <strong>!vote</strong> to get a voting link."
selections = manager.get_selections(ctx.event.user)
vote_url = ctx.session_url_for("/current")
if selections is not None:
instructions = (
f'<a href="{vote_url}"><u>Click here to view live results</u></a>.'
)
else:
instructions = "Type a number in chat or use <strong>!vote</strong> to vote."
instructions = f'<a href="{vote_url}"><u>Click here to vote</u></a>.'
await ctx.owncast_client.send_system_message_to_client(
ctx.event.client_id,
@@ -151,3 +155,4 @@ async def teardown(ctx: ModuleContext) -> None:
"""
manager = get_manager(ctx)
await manager.teardown()
ctx.state["manager"] = None