Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions docs/docs/concepts/domain-model.md
Original file line number Diff line number Diff line change
@@ -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
</reference/vote-storage>`, 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.
8 changes: 8 additions & 0 deletions docs/docs/concepts/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
2 changes: 1 addition & 1 deletion docs/docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions docs/docs/how-to-guides/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
57 changes: 57 additions & 0 deletions docs/docs/how-to-guides/vote-on-a-poll.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 4 additions & 4 deletions docs/docs/index.md
Original file line number Diff line number Diff line change
@@ -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`.

Expand Down
24 changes: 24 additions & 0 deletions docs/docs/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
144 changes: 144 additions & 0 deletions docs/docs/reference/poll-content-type.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading