Skip to content

Commit 701b61f

Browse files
committed
Basic poll content type to add time slots
1 parent afb81af commit 701b61f

20 files changed

Lines changed: 596 additions & 20 deletions

File tree

docs/docs/concepts/domain-model.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
myst:
3+
html_meta:
4+
"description": "The Doodle-style domain model used by experimental.doodle"
5+
"property=og:description": "The Doodle-style domain model used by experimental.doodle"
6+
"property=og:title": "Domain model"
7+
"keywords": "Plone, Experimental Doodle, domain, model, poll, vote"
8+
---
9+
10+
# Domain model
11+
12+
`experimental.doodle` provides a Doodle-style service to find the best time
13+
for an appointment among several people. This page explains the building
14+
blocks of that model.
15+
16+
## Poll
17+
18+
A **Poll** is the central object. It represents one scheduling question
19+
("Lunch this week, when?") and carries:
20+
21+
- a **title** and an optional **description**, both provided by the
22+
standard Plone Dublin Core behavior;
23+
- a list of **proposed time slots**: at least two date/time options that
24+
participants can vote on.
25+
26+
A Poll is a leaf content type and holds no sub-content. The Poll stores
27+
its own voting data directly, rather than relying on separate content
28+
objects.
29+
30+
## Options
31+
32+
The **options** form an ordered list of timezone-aware date/time values.
33+
The order matters: each vote refers to options by their position in this
34+
list. Editing or reordering options after participants have voted will
35+
therefore break existing votes. Treat options as fixed once you share the
36+
poll with participants.
37+
38+
The current version enforces a minimum of two options at the schema level.
39+
A single-option poll isn't a poll.
40+
41+
## Participants and votes
42+
43+
A **participant** identifies themselves with a free-text name. Voting
44+
requires no Plone account, which matches how Doodle itself works: you
45+
share a poll by link, and anyone with the link can respond.
46+
47+
A **vote** is one participant's response to a poll. It contains a `yes` or
48+
`no` choice for each option. The current version intentionally omits a
49+
`maybe` value.
50+
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+
```
55+
56+
## Tally
57+
58+
The **tally** is the aggregated result of all votes: for each option, the
59+
number of `yes` responses. The package computes it on demand from the
60+
stored votes and doesn't persist it.
61+
62+
## Why these choices
63+
64+
- A single Dexterity content type keeps the data flat and easy to reason
65+
about. There are no per-option or per-vote objects polluting the catalog.
66+
- Votes live as annotations on the Poll because they belong to that poll
67+
and have no independent lifecycle.
68+
- Free-text participant names match Doodle's actual product behavior and
69+
keep the current version usable without authentication infrastructure.

docs/docs/concepts/index.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,11 @@ The Diátaxis framework also calls this class of documentation _explanation_.
1818
```{seealso}
1919
https://diataxis.fr/explanation/
2020
```
21+
22+
## Available concepts
23+
24+
```{toctree}
25+
:maxdepth: 1
26+
27+
domain-model
28+
```

docs/docs/glossary.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ myst:
88
---
99

1010
This glossary provides example terms and definitions relevant to **Experimental Doodle**.
11-
A new addon for Plone
11+
A new add-on for Plone
1212

1313
```{note}
1414
This is an example glossary demonstrating MyST Markdown’s `{glossary}` directive. You can adapt it for your project’s appendix by editing or replacing these entries with your own terms and definitions.

docs/docs/index.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
---
22
myst:
33
html_meta:
4-
"description": "A new addon for Plone"
5-
"property=og:description": "A new addon for Plone"
4+
"description": "A new add-on for Plone"
5+
"property=og:description": "A new add-on for Plone"
66
"property=og:title": "Experimental Doodle"
7-
"keywords": "Experimental Doodle, documentation, A new addon for Plone"
7+
"keywords": "Experimental Doodle, documentation, A new add-on for Plone"
88
---
99

1010
# Experimental Doodle
1111

1212
Welcome to the documentation for Experimental Doodle!
13-
A new addon for Plone
13+
A new add-on for Plone
1414

1515
This scaffold provides a ready-to-use environment for creating comprehensive documentation for {term}`Plone` projects, based on {term}`Plone Sphinx Theme`.
1616

docs/docs/reference/index.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ This part of the documentation contains reference material, including APIs, conf
1818
https://diataxis.fr/reference/
1919
```
2020

21+
## Content types
22+
23+
```{toctree}
24+
:maxdepth: 1
25+
26+
poll-content-type
27+
```
28+
2129
## Configuration
2230

2331
- {doc}`plone:contributing/documentation/themes-and-extensions`
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
---
2+
myst:
3+
html_meta:
4+
"description": "Reference for the Poll Dexterity content type"
5+
"property=og:description": "Reference for the Poll Dexterity content type"
6+
"property=og:title": "Poll content type"
7+
"keywords": "Plone, Dexterity, Poll, content type, experimental.doodle"
8+
---
9+
10+
# `Poll` content type
11+
12+
The `Poll` Dexterity content type represents a single Doodle-style poll.
13+
The `experimental.doodle:default` GenericSetup profile registers it, and
14+
the type is globally addable to any folderish container.
15+
16+
## Schema
17+
18+
The module `experimental.doodle.content.poll` defines the `IPoll` schema.
19+
20+
| Field | Type | Required | Notes |
21+
|---|---|---|---|
22+
| `title` | `TextLine` (via `plone.dublincore`) | yes | Inherited from the standard Dublin Core behavior. |
23+
| `description` | `Text` (via `plone.dublincore`) | no | Inherited from the standard Dublin Core behavior. |
24+
| `options` | `List(value_type=Datetime)` | yes | Proposed time slots. A schema invariant enforces a minimum of two entries. |
25+
26+
The `options` field uses `defaultFactory=list` so each new Poll instance
27+
starts with a fresh empty list rather than a shared mutable default. The
28+
"at least two options" rule lives in a schema invariant rather than the
29+
field's `min_length`, so the add form can render with an empty list and
30+
only complains on submit.
31+
32+
## Factory type information
33+
34+
The package ships the Factory Type Information (FTI) at
35+
`src/experimental/doodle/profiles/default/types/Poll.xml`.
36+
37+
Key properties:
38+
39+
- **`klass`**: `experimental.doodle.content.poll.Poll`
40+
- **`schema`**: `experimental.doodle.content.poll.IPoll`
41+
- **`factory`**: `Poll`
42+
- **`add_permission`**: `cmf.AddPortalContent`
43+
- **`global_allow`**: `True`
44+
- **`filter_content_types`**: `True` with an empty `allowed_content_types`
45+
list. A Poll is a leaf and can't contain other content.
46+
- **`default_view`** and **`immediate_view`**: `view` (the standard
47+
Dexterity default view; a dedicated view ships in a later step).
48+
49+
### Enabled behaviors
50+
51+
- `plone.dublincore`: title, description, dates, language, and related metadata.
52+
- `plone.namefromtitle`: derives the id from the title.
53+
- `plone.shortname`: supports manual override of the id when needed.
54+
- `plone.ownership`: tracks creator and contributors.
55+
56+
## Creating a poll programmatically
57+
58+
```python
59+
from datetime import datetime, timedelta, timezone
60+
from plone import api
61+
62+
63+
def create_lunch_poll(container):
64+
now = datetime.now(tz=timezone.utc)
65+
return api.content.create(
66+
container=container,
67+
type="Poll",
68+
title="Team lunch",
69+
description="When can everyone make it?",
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+
78+
The `options` list must contain at least two date/time values. Otherwise,
79+
schema validation raises `zope.interface.Invalid`.
80+
81+
## Creating a poll through `plone.restapi`
82+
83+
```http
84+
POST /plone/@@plone.restapi.services HTTP/1.1
85+
Content-Type: application/json
86+
Accept: application/json
87+
88+
{
89+
"@type": "Poll",
90+
"title": "Team lunch",
91+
"description": "When can everyone make it?",
92+
"options": [
93+
"2026-06-01T12:00:00+00:00",
94+
"2026-06-01T13:00:00+00:00",
95+
"2026-06-02T12:00:00+00:00"
96+
]
97+
}
98+
```
99+
100+
`plone.restapi` handles the standard create endpoint via `POST` on the
101+
container address with `@type: "Poll"`.
102+
103+
## Validation
104+
105+
Schema-level constraints enforced today:
106+
107+
- The `options` field is **mandatory**.
108+
- Every element of `options` must be a `datetime`.
109+
- The schema invariant `at_least_two_options` rejects a submission whose
110+
`options` list has fewer than two entries. The invariant runs on form
111+
submit, not on widget render, so the add form opens cleanly.
112+
113+
Other rules, for example forbidding past date/time values, removing
114+
duplicate slots, or restricting how options can change after votes exist,
115+
are intentionally out of scope for the current version. Follow-up steps
116+
will address them.
117+
118+
## Upgrading existing sites
119+
120+
The `experimental.doodle:default` profile registers the `Poll` FTI at
121+
version `1001`. Sites installed with profile version `1000` can upgrade in
122+
place:
123+
124+
1. Open the Plone control panel.
125+
2. Go to {guilabel}`Add-ons` and run the available upgrade step for
126+
`experimental.doodle` ("Install Poll content type").
127+
128+
You can run the same step programmatically:
129+
130+
```python
131+
from plone import api
132+
133+
setup_tool = api.portal.get_tool("portal_setup")
134+
setup_tool.upgradeProfile("experimental.doodle:default")
135+
```
136+
137+
The upgrade handler lives at
138+
`experimental.doodle.upgrades.v1001.install_poll_type` and re-imports the
139+
GenericSetup `typeinfo` step. The step is idempotent: it's safe to run on
140+
sites that already have the `Poll` FTI.
141+
142+
## Related concepts
143+
144+
- {doc}`/concepts/domain-model`: the overall Doodle-style domain.

news/poll-type.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `Poll` content type with title, description, and a list of proposed time slots.

src/experimental/doodle/configure.zcml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<include file="profiles.zcml" />
1717
<include file="permissions.zcml" />
1818

19+
<include package=".content" />
1920
<include package=".controlpanels" />
2021
<include package=".indexers" />
2122
<include package=".vocabularies" />
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<configure
2+
xmlns="http://namespaces.zope.org/zope"
3+
i18n_domain="experimental.doodle"
4+
>
5+
6+
<!-- Dexterity classes are wired through the FTI XML in
7+
profiles/default/types/Poll.xml. This file exists so the content
8+
subpackage can be included from the main configure.zcml. -->
9+
10+
</configure>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""Poll content type: a Doodle-style scheduling poll."""
2+
3+
from experimental.doodle import _
4+
from plone.dexterity.content import Item
5+
from plone.supermodel import model
6+
from zope import schema
7+
from zope.interface import implementer
8+
from zope.interface import Invalid
9+
from zope.interface import invariant
10+
11+
12+
MIN_OPTIONS = 2
13+
14+
15+
class IPoll(model.Schema):
16+
"""Schema for a Doodle-style poll.
17+
18+
A poll proposes a list of date/time options and collects yes/no votes
19+
from participants who identify themselves by a free-text name.
20+
"""
21+
22+
options = schema.List(
23+
title=_("Proposed time slots"),
24+
description=_("Date and time options that participants can vote on."),
25+
value_type=schema.Datetime(
26+
title=_("Time slot"),
27+
),
28+
required=True,
29+
defaultFactory=list,
30+
)
31+
32+
@invariant
33+
def at_least_two_options(data):
34+
"""A poll needs at least two distinct time slots to be meaningful."""
35+
options = getattr(data, "options", None) or []
36+
if len(options) < MIN_OPTIONS:
37+
raise Invalid(
38+
_("A poll needs at least two proposed time slots."),
39+
)
40+
41+
42+
@implementer(IPoll)
43+
class Poll(Item):
44+
"""A Doodle-style poll."""

0 commit comments

Comments
 (0)