|
| 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. |
0 commit comments