diff --git a/docs/docs/concepts/domain-model.md b/docs/docs/concepts/domain-model.md new file mode 100644 index 0000000..e247c94 --- /dev/null +++ b/docs/docs/concepts/domain-model.md @@ -0,0 +1,68 @@ +--- +myst: + html_meta: + "description": "The Doodle-style domain model used by experimental.doodle" + "property=og:description": "The Doodle-style domain model used by experimental.doodle" + "property=og:title": "Domain model" + "keywords": "Plone, Experimental Doodle, domain, model, poll, vote" +--- + +# Domain model + +`experimental.doodle` provides a Doodle-style service to find the best time +for an appointment among several people. This page explains the building +blocks of that model. + +## Poll + +A **Poll** is the central object. It represents one scheduling question +("Lunch this week, when?") and carries: + +- a **title** and an optional **description**, both provided by the + standard Plone Dublin Core behavior; +- a list of **proposed time slots**: at least two date/time options that + participants can vote on. + +A Poll is a leaf content type and holds no sub-content. The Poll stores +its own voting data directly, rather than relying on separate content +objects. + +## Options + +The **options** form an ordered list of timezone-aware date/time values. +The order matters: each vote refers to options by their position in this +list. Editing or reordering options after participants have voted will +therefore break existing votes. Treat options as fixed once you share the +poll with participants. + +The current version enforces a minimum of two options at the schema level. +A single-option poll isn't a poll. + +## Participants and votes + +A **participant** identifies themselves with a free-text name. Voting +requires no Plone account, which matches how Doodle itself works: you +share a poll by link, and anyone with the link can respond. + +A **vote** is one participant's response to a poll. It contains a `yes` or +`no` choice for each option. The current version intentionally omits a +`maybe` value. + +Votes are stored through the {doc}`vote-storage adapter +`, an annotation-backed Python API on each Poll. +HTTP endpoints and a vote form arrive in later steps. + +## Tally + +The **tally** is the aggregated result of all votes: for each option, the +number of `yes` responses. The package computes it on demand from the +stored votes and doesn't persist it. + +## Why these choices + +- A single Dexterity content type keeps the data flat and easy to reason + about. There are no per-option or per-vote objects polluting the catalog. +- Votes live as annotations on the Poll because they belong to that poll + and have no independent lifecycle. +- Free-text participant names match Doodle's actual product behavior and + keep the current version usable without authentication infrastructure. diff --git a/docs/docs/concepts/index.md b/docs/docs/concepts/index.md index 6328240..61d37af 100644 --- a/docs/docs/concepts/index.md +++ b/docs/docs/concepts/index.md @@ -18,3 +18,11 @@ The Diátaxis framework also calls this class of documentation _explanation_. ```{seealso} https://diataxis.fr/explanation/ ``` + +## Available concepts + +```{toctree} +:maxdepth: 1 + +domain-model +``` diff --git a/docs/docs/glossary.md b/docs/docs/glossary.md index 3997cbd..325921e 100644 --- a/docs/docs/glossary.md +++ b/docs/docs/glossary.md @@ -8,7 +8,7 @@ myst: --- This glossary provides example terms and definitions relevant to **Experimental Doodle**. -A new addon for Plone +A new add-on for Plone ```{note} 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. diff --git a/docs/docs/how-to-guides/index.md b/docs/docs/how-to-guides/index.md index 517fc2e..5610857 100644 --- a/docs/docs/how-to-guides/index.md +++ b/docs/docs/how-to-guides/index.md @@ -18,6 +18,13 @@ This part of the documentation contains how-to guides, including installation an https://diataxis.fr/how-to-guides/ ``` +## Polls + +```{toctree} +:maxdepth: 1 + +vote-on-a-poll +``` ## Authors diff --git a/docs/docs/how-to-guides/vote-on-a-poll.md b/docs/docs/how-to-guides/vote-on-a-poll.md new file mode 100644 index 0000000..277bfa7 --- /dev/null +++ b/docs/docs/how-to-guides/vote-on-a-poll.md @@ -0,0 +1,57 @@ +--- +myst: + html_meta: + "description": "How to vote on a poll and read the results in experimental.doodle" + "property=og:description": "How to vote on a poll and read the results" + "property=og:title": "Vote on a poll" + "keywords": "Plone, experimental.doodle, poll, vote, results" +--- + +# Vote on a poll + +This guide explains how a visitor casts a vote on a published Poll and +reads the aggregated results. + +## Prerequisites + +- A Plone site with `experimental.doodle` installed. +- At least one published Poll with two or more time slots. + +## Cast a vote + +1. Open the Poll in your browser. The default view shows the vote form. +2. Enter your name in the **Your name** field. Any non-empty string is + accepted; no account is needed. +3. For each proposed time slot, select **Yes** or **No**. Every slot + defaults to **No** when the page loads. +4. Click **Cast vote**. The page reloads and confirms your vote was + recorded. You can vote again at any time. Casting a second vote with + the same name replaces your earlier response. + +## Read the results + +Click **View results** below the vote form, or navigate to the Poll's +`@@results` view directly (append `/@@results` to the Poll address). The +results page shows: + +- A table with each time slot and the number of **Yes** votes it received. +- A proportional bar for each slot so you can see relative support at a + glance. +- A list of participants who have voted. + +Click **Back to poll** to return to the vote form. + +## Create a poll (for editors) + +1. Navigate to the folder where you want to create the poll. +2. Add a new **Poll** content item. +3. Enter a **title** and, optionally, a **description**. +4. Add at least two **time slots** using the date/time picker for each + entry in the **Proposed time slots** field. +5. Save the item and publish it so visitors can reach it without logging + in. + +## Related references + +- {doc}`/reference/poll-content-type`: the Poll schema and FTI details. +- {doc}`/reference/vote-storage`: the Python API behind the vote form. diff --git a/docs/docs/index.md b/docs/docs/index.md index 278ec0f..ea9b79b 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -1,16 +1,16 @@ --- myst: html_meta: - "description": "A new addon for Plone" - "property=og:description": "A new addon for Plone" + "description": "A new add-on for Plone" + "property=og:description": "A new add-on for Plone" "property=og:title": "Experimental Doodle" - "keywords": "Experimental Doodle, documentation, A new addon for Plone" + "keywords": "Experimental Doodle, documentation, A new add-on for Plone" --- # Experimental Doodle Welcome to the documentation for Experimental Doodle! -A new addon for Plone +A new add-on for Plone This scaffold provides a ready-to-use environment for creating comprehensive documentation for {term}`Plone` projects, based on {term}`Plone Sphinx Theme`. diff --git a/docs/docs/reference/index.md b/docs/docs/reference/index.md index 3abbca4..116764e 100644 --- a/docs/docs/reference/index.md +++ b/docs/docs/reference/index.md @@ -18,6 +18,30 @@ This part of the documentation contains reference material, including APIs, conf https://diataxis.fr/reference/ ``` +## Content types + +```{toctree} +:maxdepth: 1 + +poll-content-type +``` + +## Adapters + +```{toctree} +:maxdepth: 1 + +vote-storage +``` + +## Views + +```{toctree} +:maxdepth: 1 + +poll-views +``` + ## Configuration - {doc}`plone:contributing/documentation/themes-and-extensions` diff --git a/docs/docs/reference/poll-content-type.md b/docs/docs/reference/poll-content-type.md new file mode 100644 index 0000000..371eff6 --- /dev/null +++ b/docs/docs/reference/poll-content-type.md @@ -0,0 +1,144 @@ +--- +myst: + html_meta: + "description": "Reference for the Poll Dexterity content type" + "property=og:description": "Reference for the Poll Dexterity content type" + "property=og:title": "Poll content type" + "keywords": "Plone, Dexterity, Poll, content type, experimental.doodle" +--- + +# `Poll` content type + +The `Poll` Dexterity content type represents a single Doodle-style poll. +The `experimental.doodle:default` GenericSetup profile registers it, and +the type is globally addable to any folderish container. + +## Schema + +The module `experimental.doodle.content.poll` defines the `IPoll` schema. + +| Field | Type | Required | Notes | +|---|---|---|---| +| `title` | `TextLine` (via `plone.dublincore`) | yes | Inherited from the standard Dublin Core behavior. | +| `description` | `Text` (via `plone.dublincore`) | no | Inherited from the standard Dublin Core behavior. | +| `options` | `List(value_type=Datetime)` | yes | Proposed time slots. A schema invariant enforces a minimum of two entries. | + +The `options` field uses `defaultFactory=list` so each new Poll instance +starts with a fresh empty list rather than a shared mutable default. The +"at least two options" rule lives in a schema invariant rather than the +field's `min_length`, so the add form can render with an empty list and +only complains on submit. + +## Factory type information + +The package ships the Factory Type Information (FTI) at +`src/experimental/doodle/profiles/default/types/Poll.xml`. + +Key properties: + +- **`klass`**: `experimental.doodle.content.poll.Poll` +- **`schema`**: `experimental.doodle.content.poll.IPoll` +- **`factory`**: `Poll` +- **`add_permission`**: `cmf.AddPortalContent` +- **`global_allow`**: `True` +- **`filter_content_types`**: `True` with an empty `allowed_content_types` + list. A Poll is a leaf and can't contain other content. +- **`default_view`** and **`immediate_view`**: `view` (the standard + Dexterity default view; a dedicated view ships in a later step). + +### Enabled behaviors + +- `plone.dublincore`: title, description, dates, language, and related metadata. +- `plone.namefromtitle`: derives the id from the title. +- `plone.shortname`: supports manual override of the id when needed. +- `plone.ownership`: tracks creator and contributors. + +## Creating a poll programmatically + +```python +from datetime import datetime, timedelta, timezone +from plone import api + + +def create_lunch_poll(container): + now = datetime.now(tz=timezone.utc) + return api.content.create( + container=container, + type="Poll", + title="Team lunch", + description="When can everyone make it?", + options=[ + now + timedelta(days=1, hours=12), + now + timedelta(days=1, hours=13), + now + timedelta(days=2, hours=12), + ], + ) +``` + +The `options` list must contain at least two date/time values. Otherwise, +schema validation raises `zope.interface.Invalid`. + +## Creating a poll through `plone.restapi` + +```http +POST /plone/@@plone.restapi.services HTTP/1.1 +Content-Type: application/json +Accept: application/json + +{ + "@type": "Poll", + "title": "Team lunch", + "description": "When can everyone make it?", + "options": [ + "2026-06-01T12:00:00+00:00", + "2026-06-01T13:00:00+00:00", + "2026-06-02T12:00:00+00:00" + ] +} +``` + +`plone.restapi` handles the standard create endpoint via `POST` on the +container address with `@type: "Poll"`. + +## Validation + +Schema-level constraints enforced today: + +- The `options` field is **mandatory**. +- Every element of `options` must be a `datetime`. +- The schema invariant `at_least_two_options` rejects a submission whose + `options` list has fewer than two entries. The invariant runs on form + submit, not on widget render, so the add form opens cleanly. + +Other rules, for example forbidding past date/time values, removing +duplicate slots, or restricting how options can change after votes exist, +are intentionally out of scope for the current version. Follow-up steps +will address them. + +## Upgrading existing sites + +The `experimental.doodle:default` profile registers the `Poll` FTI at +version `1001`. Sites installed with profile version `1000` can upgrade in +place: + +1. Open the Plone control panel. +2. Go to {guilabel}`Add-ons` and run the available upgrade step for + `experimental.doodle` ("Install Poll content type"). + +You can run the same step programmatically: + +```python +from plone import api + +setup_tool = api.portal.get_tool("portal_setup") +setup_tool.upgradeProfile("experimental.doodle:default") +``` + +The upgrade handler lives at +`experimental.doodle.upgrades.v1001.install_poll_type` and re-imports the +GenericSetup `typeinfo` step. The step is idempotent: it's safe to run on +sites that already have the `Poll` FTI. + +## Related concepts + +- {doc}`/concepts/domain-model`: the overall Doodle-style domain. diff --git a/docs/docs/reference/poll-views.md b/docs/docs/reference/poll-views.md new file mode 100644 index 0000000..bbfec43 --- /dev/null +++ b/docs/docs/reference/poll-views.md @@ -0,0 +1,91 @@ +--- +myst: + html_meta: + "description": "Reference for the Poll browser views in experimental.doodle" + "property=og:description": "Reference for the Poll browser views" + "property=og:title": "Poll views" + "keywords": "Plone, experimental.doodle, poll, view, results, browser" +--- + +# Poll views + +`experimental.doodle` ships two browser views for `Poll` objects, both +registered against `IPoll` on `IBrowserLayer`. + +## Vote form (`@@poll_view`) + +**Class:** `experimental.doodle.browser.poll.PollView` +**Permission:** `zope2.View` (no login required) +**Default view:** yes (`default_view` on the FTI since profile version `1002`) + +The vote form renders the poll title, description, and a `