date-website

Polls Development Notes

Models

Views & Flow

Validation Logic (polls/vote.py)

Room-Only Polls (attendance)

A poll becomes room-only when an attendance.AttendancePoll row attaches it to an attendance.AttendanceEvent. voter_is_present() then allows a vote only when the newest AttendanceChange for that member at that event is an arrival, read through AttendanceEvent.is_attendee_present() with no timestamp so it means “right now”. An anonymous voter is refused rather than matched, because there is no member to look up in the meeting’s changes. A question with no AttendancePoll row is an ordinary poll and keeps every rule above, which is the per-poll opt-in and also the behaviour on the associations that do not install attendance.

The model lives in attendance, not in polls, because every association installs polls while date alone installs attendance (core/settings/date.py). A field on Question pointing at attendance.AttendanceEvent would fail manage.py check on the other six associations, and neither core/settings/test.py (which inherits the date app set) nor makemigrations --check would notice. So polls reaches the link in the other direction: attendance/models.py declares question with the string target 'polls.Question', which creates the Question.attendance_poll accessor, and polls/vote.py reads it with getattr(question, 'attendance_poll', None) behind apps.is_installed('attendance'). polls imports nothing from attendance at module level, and on a site without the app the accessor does not exist and the getattr returns None.

The gate runs after the authorization and already-voted checks on purpose: a member who is merely not in the room must not displace the message that says the poll is not for them.

Presence is read from the newest change, so a member who checks in and never checks out is present for as long as that row says so: the meeting’s own end time is not consulted, and AttendanceEvent.has_ended plays no part here. An attached poll therefore keeps accepting that member’s vote after the meeting has ended, until an editor stops it with end_vote or the member checks out. Closing the poll is the control for that, and it is the one an editor already has.

The poll page’s presence prompt

The page and the vote check share one definition of the attachment. attendance_requirement(question, user) in polls/vote.py returns the meeting, whether the visitor is signed in and whether they are in the room, or None when the poll is ordinary, and voter_is_present() is a thin wrapper over it. polls/views.py puts that object in the polls/detail.html context, and handle_vote() puts it there too when it re-renders the page with a refusal, so the page and the POST cannot disagree about the room.

templates/common/polls/detail.html draws the prompt above the vote form, in the place the error message occupies, and it has three cases:

The vote form is hidden when the meeting’s attendance requirement is unmet: {% if not attendance_requirement or attendance_requirement.is_present %}. An ordinary poll, and a member the meeting counts as present, get the form; a guest and a signed-in member outside the room get the explanation and the single link and no form. The condition is about presence alone: a member who is in the room still meets the poll’s own rules on the vote view, which can refuse a vote for Valmöjligheter, for a poll that has been ended or for a vote already cast. That is what keeps a refused voter from picking an answer, submitting it and losing the selection to the re-rendered refusal: the choice inputs and the “Rösta” submit are not on the page to begin with.

Inside the form, the choices sit in a fieldset whose legend is visually-hidden, because the <h3> above already names the question. Each choice is a full-width label row, d-flex align-items-start gap-3 border rounded p-3 mb-2, that wraps its own input, so the whole row toggles the radio or checkbox and stays comfortable to hit on a 390px phone; no <br> is used as layout. The submit sits in its own div with mt-3. The links and the submit all use the site’s own theme-button class, which every association defines in static/<association>/core/css/base.css, instead of the Bootstrap btn btn-light the old markup applied to an <a> and an <input> that then rendered differently from each other.

A poll with no attachment, and every poll on an association that does not install the app, renders no prompt at all: the object is None, so neither the message nor the check-in link is reached, and the form is rendered as the ordinary case. The prompt sits inside {% if attendance_requirement %}, so the block and the {% url %} tags in it are never reached when the helper answers None.

The return path is built from the URL name rather than from request.path, because this template draws the prompt twice: on the GET that shows the poll, where the path is /polls/<id>/, and again when a refused vote re-renders it, where the path is the vote endpoint. A value taken from request.path would send a member who pressed “Rösta” first, and only then went to check in, back to /polls/<id>/vote/, which renders the poll again with a spurious “Du valde inget alternativ.” message. Both links therefore name polls:detail, and a test pins it.

Admin

Extending