Skip to content

Commit 32b4673

Browse files
committed
Docs for the vote storage adapter
1 parent 8057e2d commit 32b4673

3 files changed

Lines changed: 107 additions & 4 deletions

File tree

docs/docs/concepts/domain-model.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,9 @@ A **vote** is one participant's response to a poll. It contains a `yes` or
4848
`no` choice for each option. The current version intentionally omits a
4949
`maybe` value.
5050

51-
```{note}
52-
Votes and the vote-casting API arrive in a later step. This page describes
53-
the target model; the `Poll` content type is the first piece in place.
54-
```
51+
Votes are stored through the {doc}`vote-storage adapter
52+
</reference/vote-storage>`, an annotation-backed Python API on each Poll.
53+
HTTP endpoints and a vote form arrive in later steps.
5554

5655
## Tally
5756

docs/docs/reference/index.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ https://diataxis.fr/reference/
2626
poll-content-type
2727
```
2828

29+
## Adapters
30+
31+
```{toctree}
32+
:maxdepth: 1
33+
34+
vote-storage
35+
```
36+
2937
## Configuration
3038

3139
- {doc}`plone:contributing/documentation/themes-and-extensions`
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
myst:
3+
html_meta:
4+
"description": "Reference for the IVoteStorage adapter on Poll"
5+
"property=og:description": "Reference for the IVoteStorage adapter on Poll"
6+
"property=og:title": "Vote storage"
7+
"keywords": "Plone, experimental.doodle, vote, adapter, annotation"
8+
---
9+
10+
# Vote storage
11+
12+
The `IVoteStorage` adapter records and aggregates votes for a `Poll`. It
13+
provides the Python API that higher-level layers (REST services, browser
14+
views) will build on in later steps.
15+
16+
## Interface
17+
18+
The interface lives at `experimental.doodle.interfaces.IVoteStorage` and
19+
adapts an `IPoll`. The implementation is
20+
`experimental.doodle.adapters.vote_storage.VoteStorage`, registered through
21+
ZCML.
22+
23+
| Method | Returns | Notes |
24+
|---|---|---|
25+
| `cast_vote(name, votes)` | `None` | Records a participant's vote. Idempotent: casting again with the same name overwrites. Raises `ValueError` on invalid input. |
26+
| `get_vote(name)` | `list[bool]` or `None` | The participant's stored votes, or `None` if they haven't voted. |
27+
| `get_votes()` | `dict[str, list[bool]]` | Plain snapshot of all stored votes; safe to mutate. |
28+
| `remove_vote(name)` | `bool` | Removes the entry. Returns `True` if a vote was removed, `False` otherwise. |
29+
| `participants()` | `list[str]` | Participant names sorted alphabetically. |
30+
| `tally()` | `list[int]` | Yes-counts per option, parallel to `poll.options`. |
31+
32+
## Validation rules
33+
34+
`cast_vote` enforces three rules:
35+
36+
- The `name` must be a non-empty string after `.strip()`. Leading and
37+
trailing whitespace is removed; case is preserved.
38+
- The `votes` sequence must have exactly `len(poll.options)` entries.
39+
- Each entry of `votes` is coerced through `bool(...)` before storage.
40+
41+
Violations raise `ValueError` with a descriptive message. Reading methods
42+
(`get_vote`, `get_votes`, `remove_vote`, `participants`, `tally`) never
43+
raise on missing data; they return sensible empty values instead.
44+
45+
## Where the data lives
46+
47+
Votes are stored as a `zope.annotation` entry on the Poll under the key
48+
`experimental.doodle.votes`. The value is a `PersistentMapping` of
49+
participant names to `PersistentList` choices. Both types come from
50+
`persistent` so that ZODB picks up mutations correctly.
51+
52+
The annotation is created on first adapter instantiation. A poll that
53+
has never been voted on costs nothing extra in storage until someone
54+
casts a vote.
55+
56+
## Example
57+
58+
```python
59+
from datetime import datetime, timedelta, timezone
60+
from experimental.doodle.interfaces import IVoteStorage
61+
from plone import api
62+
63+
64+
def create_and_vote(container):
65+
now = datetime.now(tz=timezone.utc)
66+
poll = api.content.create(
67+
container=container,
68+
type="Poll",
69+
title="Team lunch",
70+
options=[
71+
now + timedelta(days=1, hours=12),
72+
now + timedelta(days=1, hours=13),
73+
now + timedelta(days=2, hours=12),
74+
],
75+
)
76+
77+
storage = IVoteStorage(poll)
78+
storage.cast_vote("Alice", [True, False, True])
79+
storage.cast_vote("Bob", [True, True, False])
80+
81+
return storage.tally() # -> [2, 1, 1]
82+
```
83+
84+
## What's not in this layer
85+
86+
The adapter intentionally stays minimal. The following arrive in later
87+
steps:
88+
89+
- HTTP endpoints (`plone.restapi` services).
90+
- Browser views and a vote form.
91+
- Anonymous voter tokens, email notifications, vote history.
92+
93+
## Related references
94+
95+
- {doc}`poll-content-type`: the content type the adapter works on.
96+
- {doc}`/concepts/domain-model`: the overall Doodle-style domain.

0 commit comments

Comments
 (0)