diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..912933a
Binary files /dev/null and b/.DS_Store differ
diff --git a/.github/agents/dev-agent.agent.md b/.github/agents/dev-agent.agent.md
new file mode 100644
index 0000000..d306676
--- /dev/null
+++ b/.github/agents/dev-agent.agent.md
@@ -0,0 +1,179 @@
+---
+name: "Senior Plone Doodle Developer"
+description: "Use when developing, refactoring, or debugging the experimental.doodle Plone add-on; for Dexterity content types, GenericSetup, Classic UI views, browser layers, control panels, registry settings, tests, and local Plone site work at localhost:8080/Plone."
+tools: [read, edit, search, execute, todo]
+user-invocable: true
+agents: []
+---
+You are a senior Plone developer working on the `experimental.doodle` repository.
+Your role is to build a small, maintainable Plone add-on that reproduces core Doodle-style scheduling workflows inside Plone Classic UI.
+
+## Mission
+- Build a Plone-native MVP for polls and booking pages.
+- Keep the add-on installable, testable, and easy to evolve.
+- Prefer the simplest correct Plone implementation over broad feature ambition.
+- Make the add-on usable in the local site at `http://localhost:8080/Plone`.
+
+## Repo Facts
+- Package root: `src/experimental/doodle/`
+- Python package name: `experimental.doodle`
+- Current browser layer: `experimental.doodle.interfaces.IBrowserLayer`
+- GenericSetup profiles: `experimental.doodle:default` and `experimental.doodle:uninstall`
+- Local site bootstrap script: `scripts/create_site.py`
+- Primary developer commands: `make install`, `make start`, `make create-site`, `make test`, `make lint`, `make format`, `make check`
+- Supported environment: Plone 6.0-6.2, Python 3.10-3.13
+- Current repo state: scaffolded add-on with install profile, browser layer, test layer, and mostly empty feature packages
+
+## Required Mindset
+- Act like a senior Plone add-on engineer, not a generic Python developer.
+- Assume Plone conventions first: Dexterity, GenericSetup, browser views, registry, permissions, and integration tests.
+- Favor maintainability, explicit configuration, and predictable upgrade paths.
+- Deliver small vertical slices that can be installed and verified in the running Plone site.
+
+## Core Development Rules
+- Model business objects as Dexterity content types unless there is a strong reason not to.
+- Keep add-on configuration inside GenericSetup under `profiles/default`.
+- Register all site behavior through installable configuration, not ad hoc runtime mutations.
+- Use the existing browser layer for add-on-specific views, forms, assets, and overrides.
+- Keep page templates simple; move logic into Python views, helpers, or adapters.
+- Use behaviors only when fields or logic are genuinely reusable across types.
+- Use the Plone registry and a control panel for site-wide defaults.
+- Add catalog indexes only when a real query or view requires them.
+- Preserve existing `plonecli` or `bobtemplates.plone` marker comments.
+- Do not introduce Volto-specific work unless explicitly requested; default to Classic UI.
+
+## Execution Rule
+Before implementing feature code, always validate the current repository baseline.
+
+For every development task:
+1. Inspect the existing scaffold and conventions first.
+2. Make the smallest coherent change.
+3. Wire changes through Plone conventions: GenericSetup, ZCML, browser layer, registry, permissions, or tests as appropriate.
+4. Run the smallest relevant validation command.
+5. Report what changed, what validation ran, and what remains deferred.
+
+Do not skip directly to broad feature implementation.
+
+## Implementation Priorities
+1. Keep the add-on installable and uninstallable.
+2. Build the MVP around `Poll` and `Booking Page`.
+3. Make each feature accessible in the local site.
+4. Add tests as each slice lands.
+5. Add polish only after core workflows work.
+
+## Plone-Specific Architecture Rules
+### Content Types
+- Start with two main Dexterity types: `Poll` and `Booking Page`.
+- Prefer `Item` base classes for the MVP unless container behavior is clearly required.
+- Store only the minimum data needed for the first scheduling workflows.
+- If adding types, update `profiles/default/types.xml` and the matching FTI files under `profiles/default/types/`.
+
+### GenericSetup
+- Any new type, registry record, control panel entry, browser layer dependency, or catalog change must be represented in `profiles/default`.
+- If configuration changes need a migration path, add upgrade steps instead of relying on reinstall-only behavior.
+- Keep install profile changes deterministic so a fresh site and an upgraded site converge to the same state.
+
+### Browser Layer and Views
+- Register custom views against `experimental.doodle.interfaces.IBrowserLayer`.
+- Use BrowserView or standard Plone form patterns for Classic UI.
+- Do not place business logic in TAL templates.
+- Use `browser/overrides` with `z3c.jbot` only when a dedicated custom view is insufficient.
+
+### Control Panels and Registry
+- Put site-wide settings behind a control panel instead of hardcoding them.
+- Store configurable defaults in `profiles/default/registry/` and corresponding Python schemas.
+- Keep the first settings small: slot duration, anonymous voting, booking confirmation, timezone defaults.
+
+### Permissions and Security
+- Respect Plone role-based permissions and workflow semantics.
+- Reuse built-in permissions where possible.
+- Add custom permissions only when the add-on needs distinct capabilities.
+- Be explicit about anonymous access for voting and booking.
+- Validate booking conflicts and poll state server-side, not only in the UI.
+
+### Internationalization
+- All user-facing strings must be translatable.
+- Use the package message factory from `experimental.doodle._`.
+- Keep the i18n domain as `experimental.doodle`.
+
+## UI Rules
+- Build for Plone Classic UI first.
+- Use standard Plone page templates and macros.
+- Keep templates readable and thin.
+- Put interaction logic in views or forms.
+- Use `browser/static` for CSS or JavaScript only after the base UI works.
+- Avoid broad template overrides early; prefer explicit views for poll display, voting, results, and booking.
+- Keep the UI usable on desktop and mobile, but do not overengineer styling in the first milestone.
+
+## Testing Rules
+- Use the existing Plone test layer in `src/experimental/doodle/testing.py`.
+- Add integration tests for every feature slice.
+- Keep the setup tests passing.
+- Add tests for at least these cases when relevant:
+ - type registration
+ - control panel registration
+ - poll creation
+ - vote submission
+ - vote aggregation
+ - booking submission
+ - double-booking prevention
+ - permission-sensitive behavior
+- Prefer narrow validation first, then broader checks.
+- Run the smallest relevant test or command after each substantive edit.
+
+## Local Workflow
+- Use `make install` to prepare the environment.
+- Use `make start` to run the local Plone instance.
+- Use `make create-site` to create or refresh the `Plone` site when needed.
+- Verify work in `http://localhost:8080/Plone` whenever a user-facing feature changes.
+- Use `make test` for focused validation and `make check` before concluding larger work.
+
+## Code Quality Rules
+- Follow the Ruff and formatting configuration already in `pyproject.toml`.
+- Keep imports and formatting consistent with repo tooling.
+- Keep changes small, local, and reversible.
+- Do not add new dependencies without a clear Plone add-on need.
+- Do not create parallel architectures when the scaffold already provides the right extension point.
+- Do not leave partially wired GenericSetup or ZCML registrations behind.
+
+## Documentation Rules
+- When editing `README.md` or files under `docs/docs/`, follow the repo documentation instructions.
+- Use `make install` and `make start` in developer-facing setup docs.
+- Do not recommend direct `pip install`, `uv add`, or `uv pip` in project docs.
+- Do not edit the generated Cookieplone attribution paragraph in `README.md`.
+- Keep docs aligned with the actual implemented feature set.
+
+## What To Avoid
+- Do not treat this as a generic Flask or Django app.
+- Do not bypass GenericSetup with manual portal changes in code.
+- Do not store essential behavior only in templates or JavaScript.
+- Do not assume external calendar sync belongs in the MVP.
+- Do not add complex integrations before polls and booking pages work locally.
+- Do not widen the scope if the current slice can be completed and tested first.
+
+## Recommended Delivery Order
+1. Validate install baseline and tests.
+2. Implement `Poll` type and schema.
+3. Implement poll add, display, vote, and results flows.
+4. Add poll tests.
+5. Implement `Booking Page` type and schema.
+6. Implement booking availability and conflict logic.
+7. Implement booking UI and tests.
+8. Add registry settings and control panel.
+9. Add targeted catalog indexing and listing views.
+10. Add refinements only after the core workflows are working.
+
+## Output Expectations
+When you complete a task, report:
+- what changed
+- how the change fits the Plone add-on architecture
+- what validation you ran
+- what remains blocked or intentionally deferred
+
+## Success Criteria
+Success means the add-on:
+- installs cleanly
+- works in `http://localhost:8080/Plone`
+- follows standard Plone add-on patterns
+- keeps GenericSetup, ZCML, tests, and UI in sync
+- remains simple enough for the next developer to extend safely
diff --git a/docs/.DS_Store b/docs/.DS_Store
new file mode 100644
index 0000000..b1bc858
Binary files /dev/null and b/docs/.DS_Store differ
diff --git a/docs/docs/.DS_Store b/docs/docs/.DS_Store
new file mode 100644
index 0000000..59cb319
Binary files /dev/null and b/docs/docs/.DS_Store differ
diff --git a/docs/docs/reference/doodle-fr.md b/docs/docs/reference/doodle-fr.md
new file mode 100644
index 0000000..faedfad
--- /dev/null
+++ b/docs/docs/reference/doodle-fr.md
@@ -0,0 +1,141 @@
+---
+myst:
+ html_meta:
+ "description": "Functional requirements for the Experimental Doodle add-on"
+ "property=og:description": "Functional requirements for the Experimental Doodle add-on"
+ "property=og:title": "Experimental Doodle Functional Requirements"
+ "keywords": "Plone, Doodle, functional requirements, scheduling"
+---
+
+# โ๏ธ Functional Requirements
+
+This document defines the functional requirements for a Doodle-like scheduling add-on implemented in Plone.
+It focuses on what the system must do from a user and administrator perspective.
+The requirements below are written to support a simple first implementation in this repository.
+
+## ๐ฏ Functional objective
+
+The add-on must support scheduling workflows that reduce manual coordination.
+It should allow users to either collect availability from a group or publish their own availability for direct booking.
+
+For the MVP, the add-on must favor simple Plone-native workflows over broad third-party integration.
+
+## ๐ค User roles
+
+- Organizer: creates and manages polls or booking pages.
+- Participant: votes in polls.
+- Booker: reserves an available slot on a booking page.
+- Site administrator: configures system-wide defaults, permissions, and integrations.
+
+For a first implementation, organizer actions may be limited to authenticated Plone users with appropriate permissions.
+
+## ๐ณ๏ธ Functional requirements for group polls
+
+### ๐งฑ Poll creation
+
+- FR-01: The system must provide a dedicated Plone content type for a poll or an equivalent add-on managed object with an add form.
+- The system must allow an organizer to create a new poll.
+- The system must allow the organizer to enter a title and description.
+- The system must allow the organizer to define multiple candidate dates and time slots.
+- The system must allow the organizer to specify optional metadata such as location, deadline, or notes.
+- The system must allow the organizer to publish or close a poll.
+
+### ๐ Participation and voting
+
+- The system must allow participants to access a poll through a shared link or Plone view.
+- The system must allow participants to identify themselves before voting, when required.
+- The system must allow participants to vote on one or more available time slots.
+- The system must store each participant response.
+- The system must prevent invalid or duplicate submissions according to the configured rules.
+
+For the MVP, anonymous participation should be optional rather than mandatory.
+
+### ๐ Poll results
+
+- The system must aggregate votes per proposed slot.
+- The system must present results in a way that helps identify the best meeting time.
+- The system must allow the organizer to select a final slot.
+- The system must display the final decision when a poll is concluded.
+
+- FR-02: The system must be able to compute the winning or selected slot without requiring manual aggregation outside Plone.
+
+## ๐ Functional requirements for booking pages
+
+### ๐ ๏ธ Availability management
+
+- FR-03: The system must provide a dedicated Plone content type for a booking page or an equivalent add-on managed object with an edit form.
+- The system must allow an organizer to create a booking page.
+- The system must allow an organizer to define available working days and working hours.
+- The system must allow an organizer to configure meeting duration.
+- The system must allow an organizer to configure buffers between meetings.
+- The system must allow an organizer to block unavailable periods manually.
+
+### ๐ฅ Booking workflow
+
+- The system must allow a visitor to view open appointment slots.
+- The system must allow a visitor to select one available slot.
+- The system must collect the minimum information required to create a booking.
+- The system must create a booking record after successful submission.
+- The system must mark the selected slot as unavailable after booking.
+- The system must allow the organizer to confirm or cancel a booking.
+
+- FR-04: The system must validate slot availability at submission time so two requests cannot book the same slot successfully.
+
+## ๐ Functional requirements for calendar integration
+
+- The system must support connecting the organizer schedule with external calendars when integration is enabled.
+- The system must use external calendar events to block conflicting availability.
+- The system must support exporting confirmed meetings or finalized poll results.
+- The system must avoid creating overlapping commitments when synchronized calendars contain busy events.
+
+For the MVP, export of selected dates or bookings is sufficient. Full two-way synchronization should remain optional for a later phase.
+
+## ๐ Functional requirements for permissions and administration
+
+- The system must respect Plone security and role-based permissions.
+- The system must allow administrators to decide who can create polls.
+- The system must allow administrators to decide who can create booking pages.
+- The system must provide an administrative configuration area for defaults and feature settings.
+- The system must make poll and booking content searchable through standard Plone mechanisms when appropriate.
+
+- FR-05: The system must register its defaults and feature flags through the Plone registry and expose them through a control panel.
+- FR-06: The system must register the content model and supporting configuration through the add-on GenericSetup profile.
+
+## ๐ข Functional requirements for notifications
+
+- The system should notify organizers when new votes are submitted, if notifications are enabled.
+- The system should notify organizers when a new booking is created.
+- The system should notify participants or bookers when the organizer confirms, changes, or cancels an event.
+
+Notifications are optional for the MVP and must not block the first usable release.
+
+## ๐ Functional requirements for usability
+
+- The system must provide views that work on desktop and mobile devices.
+- The system must present scheduling information clearly enough to reduce coordination errors.
+- The system must integrate with Plone navigation and content management patterns.
+
+- FR-07: The system must provide standard add, edit, and display views that work within normal Plone site navigation.
+
+## ๐ Minimum viable functional scope
+
+The first release should include the following minimum functional set:
+
+- Create a poll with multiple time slots.
+- Vote on poll options.
+- View aggregated poll results.
+- Create a booking page with manually configured availability.
+- Book a free slot and prevent double booking.
+
+The first release should not depend on external calendar APIs, external conferencing tools, or advanced notification workflows.
+
+## ๐งช Functional requirements for implementation readiness
+
+- FR-08: The poll and booking page features must be testable through the existing Plone integration test layer.
+- FR-09: The add-on must install cleanly and register its browser layer, profile version, and feature configuration.
+- FR-10: The data model must be simple enough to implement in the current scaffold without introducing unnecessary infrastructure.
+
+## ๐ Summary
+
+The functional scope of the add-on is to support two primary scheduling models: poll-based coordination and direct booking.
+For this repository, that means starting with poll and booking page features that fit naturally into Dexterity, GenericSetup, catalog indexing, and standard Plone views.
\ No newline at end of file
diff --git a/docs/docs/reference/doodle-planning.md b/docs/docs/reference/doodle-planning.md
new file mode 100644
index 0000000..5c82b49
--- /dev/null
+++ b/docs/docs/reference/doodle-planning.md
@@ -0,0 +1,387 @@
+---
+myst:
+ html_meta:
+ "description": "Step-by-step planning for building the Experimental Doodle Plone add-on"
+ "property=og:description": "Step-by-step planning for building the Experimental Doodle Plone add-on"
+ "property=og:title": "Experimental Doodle Development Planning"
+ "keywords": "Plone, Doodle, planning, addon development, UI"
+---
+
+# ๐ ๏ธ Doodle Development Planning
+
+This document is a step-by-step implementation plan for building a simple Doodle-like scheduling add-on for Plone.
+It is written to match the current repository scaffold and to help a developer move from a fresh add-on skeleton to a usable feature set at `http://localhost:8080/Plone`.
+
+## ๐ฏ Planning goal
+
+The goal is to build a small but usable Plone add-on that supports:
+
+- Poll-based scheduling.
+- Simple personal booking pages.
+- A basic user interface in Plone Classic UI.
+- Clean installation in a local development site.
+
+The first implementation should favor maintainability and fast feedback over full feature parity with Doodle.
+
+## ๐งญ Development guidelines for Plone add-ons
+
+According to the Plone documentation, a maintainable add-on should follow the standard Plone extension model.
+For this project, that means:
+
+- Model business objects as Dexterity content types.
+- Keep add-on setup in GenericSetup profiles under `profiles/default`.
+- Use the Plone registry and a control panel for configurable defaults.
+- Register add-on-specific views against the existing browser layer.
+- Keep UI logic in browser views and keep page templates simple.
+- Use behaviors for reusable field groups or reusable business logic.
+- Add only the catalog indexes and metadata columns that are required.
+- Cover features with integration tests in the existing test layer.
+- Keep the first release independent from large external integrations.
+
+## ๐งฑ Target architecture for this add-on
+
+The recommended first architecture is:
+
+- `Poll` as a Dexterity content type.
+- `Booking Page` as a Dexterity content type.
+- One or more browser views for voting, booking, and results.
+- Registry settings for defaults such as slot duration, whether anonymous voting is allowed, and basic booking behavior.
+- Catalog indexes for looking up polls, booking pages, and selected states efficiently.
+
+For the MVP, avoid a large data model. Start with two main content types and the minimum supporting data needed for votes and bookings.
+
+## ๐ Step 1: Prepare the local environment
+
+Use the repository-provided commands instead of ad hoc setup.
+
+### โ Tasks
+
+- Install the development environment with `make install`.
+- Start Plone with `make start`.
+- Create the site with `make create-site` if the `Plone` site does not already exist.
+- Confirm that the site is reachable at `http://localhost:8080/Plone`.
+
+### ๐ Expected result
+
+- The Plone instance starts locally.
+- The `Plone` site exists.
+- The add-on profile is applied by the site creation script.
+
+## ๐ Step 2: Verify the current add-on baseline
+
+Before adding new features, verify the existing scaffold.
+
+### โ Tasks
+
+- Confirm the install profile version in `profiles/default/metadata.xml`.
+- Confirm the browser layer registration in `profiles/default/browserlayer.xml`.
+- Confirm that the add-on install test passes.
+- Run `make test` to establish the current baseline.
+
+### ๐ Expected result
+
+- The package installs cleanly.
+- The browser layer is active after installation.
+- Existing tests pass before feature work begins.
+
+## ๐งฉ Step 3: Define the MVP content model
+
+According to the Plone documentation, custom business data should be modeled with Dexterity content types and behaviors where appropriate.
+
+### โ Tasks
+
+- Define a `Poll` content type.
+- Define a `Booking Page` content type.
+- Decide whether votes and bookings will be stored as child content items, annotations, or serialized structured fields.
+- Keep the storage model simple enough to implement and test quickly.
+
+### ๐ Recommended MVP fields
+
+For `Poll`:
+
+- Title.
+- Description.
+- Organizer reference or creator ownership.
+- Poll state.
+- Deadline.
+- Optional location.
+- Proposed slots.
+- Final selected slot.
+
+For `Booking Page`:
+
+- Title.
+- Description.
+- Organizer reference or creator ownership.
+- Working days.
+- Working hours.
+- Slot duration.
+- Buffer duration.
+- Availability exceptions.
+- Booking state.
+
+### ๐ Expected result
+
+- A developer can point to a concrete schema for each type.
+- The model is small enough to support a first usable release.
+
+## ๐๏ธ Step 4: Generate or add the content types
+
+According to the Plone documentation, content types are commonly added with PloneCLI and registered through the add-on package.
+
+### โ Tasks
+
+- Add a `Poll` content type.
+- Add a `Booking Page` content type.
+- Review the generated FTI and schema files.
+- Register the new types in `profiles/default/types.xml`.
+- Update `profiles/default/types/` with the correct FTI definitions.
+
+### ๐ Developer notes
+
+- Use `Container` only if a type truly needs to contain child objects.
+- Use `Item` if the type is a standalone object without child content.
+- For the MVP, a `Poll` may be an `Item` if slots are stored in fields rather than child items.
+- A `Booking Page` can also start as an `Item` unless child booking objects are required immediately.
+
+### ๐ Expected result
+
+- The new content types appear in the site after reinstalling or reapplying the profile.
+
+## ๐ง Step 5: Add behaviors only where they reduce complexity
+
+According to the Plone documentation, behaviors are best used for reusable fields or reusable logic.
+
+### โ Tasks
+
+- Reuse built-in behaviors where they make sense, such as title, description, or rich text.
+- Add custom behaviors only if the same scheduling fields or logic are shared across multiple types.
+- Avoid creating behaviors too early if plain schemas are simpler.
+
+### ๐ Expected result
+
+- The data model stays readable.
+- Reuse happens only where it has clear value.
+
+## โ๏ธ Step 6: Add registry settings and a control panel
+
+The repository already includes control panel scaffolding, so the next step is to make it useful.
+
+### โ Tasks
+
+- Define registry records for add-on defaults.
+- Add a control panel form for site managers.
+- Register the control panel entry through the install profile.
+
+### ๐ Recommended first settings
+
+- Default poll slot duration.
+- Default booking slot duration.
+- Whether anonymous poll voting is enabled.
+- Whether booking confirmation is required.
+- Default timezone handling.
+
+### ๐ Expected result
+
+- Site managers can manage add-on defaults from Plone Site Setup.
+
+## ๐งฎ Step 7: Implement scheduling logic in Python
+
+The first implementation needs reliable backend rules before UI refinement.
+
+### โ Tasks
+
+- Add helper methods to normalize date and time slot data.
+- Add vote aggregation logic for polls.
+- Add conflict detection logic for bookings.
+- Validate availability at submission time.
+- Keep domain logic out of page templates.
+
+### ๐ Core rules
+
+- A poll vote must be stored only once per participant according to the selected policy.
+- A booking must fail if the requested slot is no longer available.
+- A closed poll must reject further votes.
+- A disabled or closed booking page must reject new bookings.
+
+### ๐ Expected result
+
+- The backend can decide whether a vote or booking is valid.
+- Core business behavior is testable without relying on UI details.
+
+## ๐ฅ๏ธ Step 8: Build the Plone UI in Classic UI
+
+According to the Plone documentation, the usual pattern is to implement browser views and templates, register them in ZCML, and keep heavy logic in Python.
+
+### โ Tasks
+
+- Create a poll display view.
+- Create a vote submission view or form.
+- Create a results view.
+- Create a booking page view that lists available slots.
+- Create a booking submission view or form.
+- Register these views against the add-on browser layer.
+
+### ๐จ UI guidelines
+
+- Keep templates simple and driven by view methods.
+- Use normal Plone page templates and the main template macros.
+- Keep forms understandable with clear labels and messages.
+- Make the scheduling workflow obvious on mobile and desktop.
+- Avoid overriding core templates until the custom views are working.
+
+### ๐งฉ Static resources
+
+- Add CSS and JavaScript only when the base form behavior is working.
+- Keep UI enhancements modest for the MVP.
+- Use the existing `browser/static` location for add-on assets.
+
+### ๐ ๏ธ When to use overrides
+
+- Use `browser/overrides` with `z3c.jbot` only when a core or third-party template really must be customized.
+- Prefer custom views over broad template overrides.
+
+### ๐ Expected result
+
+- A user can browse to a poll or booking page and complete the main action from the Plone UI.
+
+## ๐ Step 9: Define permissions and access rules
+
+The add-on must be usable inside a normal Plone site without weakening security.
+
+### โ Tasks
+
+- Decide who can add polls.
+- Decide who can add booking pages.
+- Decide whether anonymous voting is allowed.
+- Decide whether anonymous booking is allowed.
+- Register any custom permissions only if built-in permissions are insufficient.
+
+### ๐ Recommended MVP policy
+
+- Authenticated users create polls and booking pages.
+- Poll viewing can be public if the content is published.
+- Anonymous voting can be optional and controlled by a site setting.
+- Booking can start as authenticated-only if that reduces complexity.
+
+### ๐ Expected result
+
+- Access behavior is predictable and testable.
+
+## ๐ Step 10: Add catalog indexing only for real use cases
+
+The current profile already has catalog scaffolding, so add only the indexes needed for feature support.
+
+### โ Tasks
+
+- Add indexes or metadata columns for poll state.
+- Add indexes or metadata columns for organizer.
+- Add any booking-related metadata needed for efficient lookup.
+- Reindex after profile updates.
+
+### ๐ Expected result
+
+- Polls and booking pages can be queried efficiently.
+- Future listing or dashboard views remain straightforward.
+
+## ๐งช Step 11: Write tests as features are added
+
+The current repository already contains integration test scaffolding and install tests.
+
+### โ Tasks
+
+- Add tests for type registration.
+- Add tests for control panel registration.
+- Add tests for poll creation.
+- Add tests for vote submission.
+- Add tests for vote aggregation.
+- Add tests for booking availability checks.
+- Add tests for double-booking prevention.
+- Add tests for permission-sensitive workflows.
+
+### ๐ Expected result
+
+- The add-on can evolve without breaking core scheduling behavior.
+
+## ๐ Step 12: Keep the install profile up to date
+
+Every new feature must be represented in GenericSetup so a site can be recreated consistently.
+
+### โ Tasks
+
+- Update `types.xml` and type FTIs.
+- Update registry import files.
+- Update `controlpanel.xml`.
+- Update `catalog.xml` if indexes are added.
+- Add upgrade steps when profile changes become versioned milestones.
+
+### ๐ Expected result
+
+- Installing or upgrading the add-on remains predictable.
+
+## ๐ Step 13: Make the add-on usable at `http://localhost:8080/Plone`
+
+The local site should be the primary feedback loop during development.
+
+### โ Tasks
+
+- Start the instance with `make start`.
+- Ensure the site exists with `make create-site`.
+- Log in to `http://localhost:8080/Plone`.
+- Confirm the add-on is installed in the site.
+- Add a `Poll` item and verify that its add and display views work.
+- Add a `Booking Page` item and verify that booking works.
+
+### ๐ Developer workflow
+
+- Use the browser to verify each newly added content type and view.
+- Reapply the add-on profile or recreate the site when GenericSetup changes require it.
+- Keep test execution and browser verification in sync.
+
+### ๐ Expected result
+
+- The add-on is visible and usable in your local Plone site.
+
+## ๐ Suggested implementation order
+
+Build in this order to reduce risk:
+
+1. Environment and baseline validation.
+2. `Poll` type and poll schema.
+3. Poll add, display, vote, and results views.
+4. Poll tests.
+5. `Booking Page` type and booking schema.
+6. Booking availability and conflict logic.
+7. Booking UI and tests.
+8. Registry settings and control panel.
+9. Catalog indexing and listing views.
+10. Nice-to-have UI improvements.
+
+## ๐ซ What not to do in the first iteration
+
+Avoid these until the MVP works:
+
+- Full two-way calendar synchronization.
+- External conferencing integration.
+- Complicated reminder workflows.
+- Too many custom behaviors.
+- Heavy template overrides before custom views exist.
+- Premature optimization of indexing or storage.
+
+## โ Definition of done for the first milestone
+
+The first milestone is complete when:
+
+- The add-on installs cleanly.
+- `Poll` and `Booking Page` are available in Plone.
+- A user can create a poll and collect votes.
+- A user can create a booking page and accept a simple booking.
+- Double booking is prevented.
+- Basic settings exist in a control panel.
+- Tests cover the main flows.
+- The feature works in the local site at `http://localhost:8080/Plone`.
+
+## ๐ Summary
+
+The correct path for this repository is to build a small Plone-native scheduling product in layers: content model first, backend rules second, UI third, and refinements after tests are in place.
+That will keep the add-on installable, testable, and usable in your local Plone environment while leaving room for deeper Doodle-like features later.
\ No newline at end of file
diff --git a/docs/docs/reference/doodle-requirements.md b/docs/docs/reference/doodle-requirements.md
new file mode 100644
index 0000000..700d561
--- /dev/null
+++ b/docs/docs/reference/doodle-requirements.md
@@ -0,0 +1,131 @@
+---
+myst:
+ html_meta:
+ "description": "Requirements for a Doodle-like Plone add-on"
+ "property=og:description": "Requirements for a Doodle-like Plone add-on"
+ "property=og:title": "Experimental Doodle Requirements"
+ "keywords": "Plone, Doodle, scheduling, requirements"
+---
+
+# ๐ Doodle Requirements
+
+This document captures the baseline requirements for replicating core Doodle functionality as a Plone add-on.
+It also narrows those requirements into a realistic first implementation for the current repository scaffold.
+
+## ๐ฏ Product goal
+
+The add-on should allow Plone site editors and members to coordinate meetings without relying on long email threads.
+The first target is to support the two central Doodle workflows:
+
+- Group scheduling through polls with multiple date and time options.
+- Personal booking through an availability page that others can use to reserve a slot.
+
+For this repository, the immediate goal should be a simple Plone-native add-on that works well inside one site before adding advanced third-party integrations.
+
+## ๐ฅ Primary users
+
+- Site editors who create and manage polls or booking pages.
+- Participants who vote on proposed meeting times.
+- Visitors, colleagues, students, or clients who book available time slots.
+- Administrators who configure defaults, permissions, and calendar integration.
+
+## โ Core functional requirements
+
+### ๐ณ๏ธ Group polls
+
+- A user can create a poll with a title, description, organizer, and optional location.
+- A poll can contain multiple proposed dates and time slots.
+- Participants can open a public or shared link and vote for the slots that work for them.
+- A participant can select one or more acceptable slots.
+- The poll view shows aggregated availability so the organizer can identify the best option.
+- The organizer can close the poll and mark one slot as the final decision.
+
+### ๐ Booking pages
+
+- A user can maintain a booking page that represents personal availability.
+- A booking page can define working hours, meeting duration, buffer times, and unavailable periods.
+- Visitors can book one of the available time slots directly.
+- A booked slot becomes unavailable to avoid double booking.
+- The organizer can review, confirm, or cancel bookings.
+
+### ๐ Calendar sync
+
+- The add-on should support synchronization with existing calendars such as Google Calendar, Outlook, or iCloud.
+- Existing calendar events should block availability in booking pages.
+- Confirmed bookings and finalized poll decisions should be exportable or synchronizable to external calendars.
+
+For a simple first release, calendar sync should be treated as a later phase unless an export-only approach is chosen first.
+
+## ๐งฉ Plone-specific requirements
+
+- The feature set must be delivered as a Plone add-on.
+- Polls and booking pages should be manageable through Plone content or dedicated add-on views.
+- Permissions should follow Plone roles so sites can control who may create, vote on, or manage schedules.
+- Site managers should have a control panel to configure default behavior.
+- The add-on should integrate cleanly with Plone navigation, security, and indexing.
+
+### ๐๏ธ Recommended implementation baseline
+
+According to the Plone documentation, custom business objects in an add-on are typically modeled as Dexterity content types, while add-on settings are managed through GenericSetup and registry-backed control panels.
+For this repository, the simplest maintainable implementation path is:
+
+- Create a Dexterity content type for a poll.
+- Create a Dexterity content type for a booking page.
+- Use the existing browser layer for add-on-specific views, forms, and overrides.
+- Use the existing GenericSetup profile to register types, control panel entries, and catalog indexes.
+- Store configurable defaults in the Plone registry and expose them through a control panel.
+
+### ๐งฑ Suggested content model for the MVP
+
+- Poll: title, description, organizer, state, and one or more proposed time slots.
+- Booking Page: title, description, organizer, meeting rules, and availability rules.
+- Booking or response records: stored in a way that supports validation, conflict checks, and later reporting.
+
+For a simple implementation, the add-on should avoid a large object model and instead start with two main content types and minimal supporting records.
+
+### ๐ Catalog and indexing requirements
+
+- Polls should be indexable by state, organizer, and relevant dates.
+- Booking pages should be indexable by organizer and visibility.
+- The data model should support looking up booked or chosen slots efficiently enough to prevent conflicts.
+
+### ๐งช Development requirements for this repository
+
+- The implementation should extend the existing install profile instead of bypassing it.
+- New functionality should be covered by integration tests in the existing test layer.
+- The first milestone should keep workflow and permissions simple enough to validate quickly.
+- The MVP should work in classic Plone forms and views before broader UI refinement.
+
+## ๐ Non-functional requirements
+
+- The UI should make scheduling possible with minimal back-and-forth communication.
+- The add-on should prevent scheduling conflicts and double bookings.
+- The add-on should be usable by students, businesses, and professional teams.
+- The implementation should remain maintainable and extensible for future features such as reminders or notifications.
+- The add-on should work with current supported Plone 6 versions.
+
+For this repository, maintainability is more important than feature completeness in the first iteration.
+
+## ๐ Suggested first implementation scope
+
+To keep the first iteration small, the initial release can focus on:
+
+- Creating and publishing a `Poll` content item.
+- Allowing participants to vote on proposed slots through a Plone view.
+- Showing poll results with a clear winning option.
+- Providing a basic `Booking Page` content item with manually defined availability.
+- Reserving booked time slots to prevent overlap.
+- Adding only the minimum catalog indexes and control panel settings needed for the MVP.
+
+## ๐ Out of scope for the first iteration
+
+- Advanced payment workflows.
+- Complex meeting workflows with multiple hosts.
+- Deep integrations with external conferencing platforms.
+- Full two-way calendar synchronization.
+- Full parity with every premium Doodle feature.
+
+## ๐ Summary
+
+The add-on should replicate the essential Doodle experience inside Plone: propose times, collect responses, expose availability, and prevent conflicts.
+For this repository, the correct next step is to build a small, Plone-native MVP around Dexterity types, add-on views, registry settings, and tests.
\ No newline at end of file
diff --git a/docs/docs/reference/index.md b/docs/docs/reference/index.md
index 3abbca4..62cf45c 100644
--- a/docs/docs/reference/index.md
+++ b/docs/docs/reference/index.md
@@ -20,4 +20,7 @@ https://diataxis.fr/reference/
## Configuration
+- {doc}`doodle-requirements`
+- {doc}`doodle-fr`
+- {doc}`doodle-planning`
- {doc}`plone:contributing/documentation/themes-and-extensions`
diff --git a/src/experimental/doodle/browser/booking_views.py b/src/experimental/doodle/browser/booking_views.py
new file mode 100644
index 0000000..960a76c
--- /dev/null
+++ b/src/experimental/doodle/browser/booking_views.py
@@ -0,0 +1,158 @@
+"""Browser views for the BookingPage content type."""
+
+from experimental.doodle import _
+from experimental.doodle.scheduling import BookingPageClosedError
+from experimental.doodle.scheduling import create_booking
+from experimental.doodle.scheduling import get_available_slots
+from experimental.doodle.scheduling import InvalidSlotError
+from experimental.doodle.scheduling import SlotUnavailableError
+from Products.Five.browser import BrowserView
+from plone.protect.interfaces import IDisableCSRFProtection
+from zope.interface import alsoProvides
+
+import datetime
+import plone.api
+
+
+def _format_slot(slot):
+ """Return a human-readable string for a datetime slot, or '' for None."""
+ if slot is None:
+ return ""
+ return slot.strftime("%A %d %B %Y, %H:%M")
+
+
+class BookingPageView(BrowserView):
+ """Public booking view for a BookingPage.
+
+ GET โ renders a date picker and the list of available slots for the
+ selected date.
+ POST โ submits a booking for one selected slot and redirects back to
+ the same view (with the date preserved) on success.
+
+ Anonymous access is permitted to view the form; submitting a booking
+ requires an authenticated Plone user. Anonymous booking is intentionally
+ not supported in this MVP release.
+ """
+
+ # ------------------------------------------------------------------
+ # Template helpers (called from booking_page.pt)
+ # ------------------------------------------------------------------
+
+ def is_open(self):
+ """Return True when the booking page is accepting new bookings."""
+ return self.context.booking_state == "open"
+
+ def selected_date(self):
+ """Return the calendar date to display slots for.
+
+ Reads ``date`` from the request form (ISO format YYYY-MM-DD).
+ Falls back to today if the parameter is absent or unparseable.
+ """
+ date_str = self.request.form.get("date", "")
+ if date_str:
+ try:
+ return datetime.date.fromisoformat(date_str)
+ except (ValueError, TypeError):
+ pass
+ return datetime.date.today()
+
+ def selected_date_str(self):
+ """Return the selected date as an ISO string for use in templates."""
+ return self.selected_date().isoformat()
+
+ def available_slots_info(self):
+ """Return a list of dicts for each available slot on the selected date.
+
+ Each dict has:
+ - ``display``: human-readable string for use in templates
+ - ``value``: ISO-format datetime string used as the radio-button value
+ """
+ slots = get_available_slots(self.context, self.selected_date())
+ return [
+ {
+ "display": _format_slot(slot),
+ "value": slot.isoformat(),
+ }
+ for slot in slots
+ ]
+
+ # ------------------------------------------------------------------
+ # Request handling
+ # ------------------------------------------------------------------
+
+ def __call__(self):
+ if self.request.method == "POST":
+ return self._handle_post()
+ return self.index()
+
+ def _handle_post(self):
+ """Process a booking submission.
+
+ Reads ``slot`` from the request form (ISO-formatted datetime string),
+ validates it, and delegates to ``create_booking``.
+ Redirects to ``@@booking-page`` on success, preserving the selected
+ date in the query string. Re-renders the form for validation errors.
+ """
+ alsoProvides(self.request, IDisableCSRFProtection)
+ if plone.api.user.is_anonymous():
+ plone.api.portal.show_message(
+ message=_("You must be logged in to make a booking."),
+ request=self.request,
+ type="error",
+ )
+ return self.index()
+
+ booker_id = plone.api.user.get_current().id
+
+ slot_str = self.request.form.get("slot", "")
+ if not slot_str:
+ plone.api.portal.show_message(
+ message=_("Please select a time slot."),
+ request=self.request,
+ type="error",
+ )
+ return self.index()
+
+ try:
+ slot = datetime.datetime.fromisoformat(slot_str)
+ except (ValueError, TypeError):
+ plone.api.portal.show_message(
+ message=_("The selected slot could not be read."),
+ request=self.request,
+ type="error",
+ )
+ return self.index()
+
+ try:
+ create_booking(self.context, booker_id, slot)
+ except BookingPageClosedError:
+ plone.api.portal.show_message(
+ message=_("This booking page is not accepting new bookings."),
+ request=self.request,
+ type="error",
+ )
+ return self.index()
+ except SlotUnavailableError:
+ plone.api.portal.show_message(
+ message=_("That slot has just been taken. Please choose another."),
+ request=self.request,
+ type="error",
+ )
+ return self.index()
+ except InvalidSlotError:
+ plone.api.portal.show_message(
+ message=_("The selected slot is no longer available."),
+ request=self.request,
+ type="error",
+ )
+ return self.index()
+
+ plone.api.portal.show_message(
+ message=_("Your booking has been confirmed. See you then!"),
+ request=self.request,
+ type="info",
+ )
+ date_str = slot.date().isoformat()
+ return self.request.response.redirect(
+ f"{self.context.absolute_url()}/@@booking-page?date={date_str}"
+ )
diff --git a/src/experimental/doodle/browser/configure.zcml b/src/experimental/doodle/browser/configure.zcml
index 6bf678a..ece3b8b 100644
--- a/src/experimental/doodle/browser/configure.zcml
+++ b/src/experimental/doodle/browser/configure.zcml
@@ -22,4 +22,44 @@
type="plone"
/>
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/experimental/doodle/browser/home_viewlets.py b/src/experimental/doodle/browser/home_viewlets.py
new file mode 100644
index 0000000..16faaf4
--- /dev/null
+++ b/src/experimental/doodle/browser/home_viewlets.py
@@ -0,0 +1,22 @@
+"""Viewlets rendered on the Plone site root (home page)."""
+
+from plone.app.layout.viewlets.common import ViewletBase
+
+import plone.api
+
+
+class OpenPollsViewlet(ViewletBase):
+ """Shows all open polls on the home page."""
+
+ def open_polls(self):
+ """Return a list of dicts for every poll with poll_state == 'open'."""
+ catalog = plone.api.portal.get_tool("portal_catalog")
+ brains = catalog(portal_type="Poll", poll_state="open")
+ return [
+ {
+ "title": brain.Title,
+ "description": brain.Description,
+ "url": brain.getURL(),
+ }
+ for brain in brains
+ ]
diff --git a/src/experimental/doodle/browser/poll_views.py b/src/experimental/doodle/browser/poll_views.py
new file mode 100644
index 0000000..8f6036b
--- /dev/null
+++ b/src/experimental/doodle/browser/poll_views.py
@@ -0,0 +1,241 @@
+"""Browser views for the Poll content type."""
+
+from datetime import datetime
+from experimental.doodle import _
+from experimental.doodle.voting import aggregate_votes
+from experimental.doodle.voting import DuplicateVoteError
+from experimental.doodle.voting import get_vote
+from experimental.doodle.voting import get_votes
+from experimental.doodle.voting import get_winning_slot
+from experimental.doodle.voting import InvalidSlotError
+from experimental.doodle.voting import PollClosedError
+from experimental.doodle.voting import submit_vote
+from plone.protect.interfaces import IDisableCSRFProtection
+from Products.Five.browser import BrowserView
+from zope.interface import alsoProvides
+
+import plone.api
+
+
+def _format_slot(slot):
+ """Return a human-readable string for a datetime slot, or '' for None."""
+ if slot is None:
+ return ""
+ return slot.strftime("%A %d %B %Y, %H:%M")
+
+
+def _allow_anonymous_voting():
+ """Return True when the registry permits anonymous poll voting."""
+ try:
+ from experimental.doodle.controlpanels.doodle_settings import IDoodleSettings
+ from plone.registry.interfaces import IRegistry
+ from zope.component import getUtility
+
+ settings = getUtility(IRegistry).forInterface(
+ IDoodleSettings, prefix="experimental.doodle"
+ )
+ return bool(settings.allow_anonymous_voting)
+ except Exception:
+ return False
+
+
+def _anonymous_participant_id(request):
+ """Return a participant identifier for an anonymous voter.
+
+ Uses the request's ``REMOTE_ADDR`` so that a single client IP address
+ produces a stable key within a Plone transaction. Shared IP addresses
+ (NAT, proxies) and IP spoofing are known limitations accepted for MVP.
+ """
+ return f"anon:{request.get('REMOTE_ADDR', 'unknown')}"
+
+
+class PollVoteView(BrowserView):
+ """Vote submission form for a Poll.
+
+ GET โ renders the vote form (checkboxes for each proposed slot).
+ POST โ processes the submission and redirects to @@poll-results on success.
+
+ Anonymous access is permitted to see the form. Submitting a vote requires
+ an authenticated Plone user unless the ``allow_anonymous_voting`` registry
+ setting is enabled, in which case the client's remote address is used as
+ the participant identifier.
+ """
+
+ # ------------------------------------------------------------------
+ # Template helpers (called from poll_vote.pt)
+ # ------------------------------------------------------------------
+
+ def is_open(self):
+ """Return True when the poll is accepting votes."""
+ return self.context.poll_state == "open"
+
+ def proposed_slots_info(self):
+ """Return a list of dicts for each proposed slot.
+
+ Each dict has:
+ - ``display``: human-readable string for use in templates
+ - ``value``: ISO-format string used as the checkbox value
+ """
+ return [
+ {
+ "display": _format_slot(slot),
+ "value": slot.isoformat(),
+ }
+ for slot in (self.context.proposed_slots or [])
+ ]
+
+ def has_already_voted(self):
+ """Return True if the current user (or anonymous client) has already voted."""
+ if plone.api.user.is_anonymous():
+ if not _allow_anonymous_voting():
+ return False
+ participant_id = _anonymous_participant_id(self.request)
+ else:
+ participant_id = plone.api.user.get_current().id
+ return get_vote(self.context, participant_id) is not None
+
+ # ------------------------------------------------------------------
+ # Request handling
+ # ------------------------------------------------------------------
+
+ def __call__(self):
+ if self.request.method == "POST":
+ return self._handle_post()
+ return self.index()
+
+ def _handle_post(self):
+ """Process a vote submission.
+
+ Reads ``chosen_slots`` from the request form (list of ISO-formatted
+ datetime strings), validates them, and delegates to ``submit_vote``.
+ Redirects to ``@@poll-results`` on success or duplicate vote.
+ Re-renders the form for validation errors.
+ """
+ alsoProvides(self.request, IDisableCSRFProtection)
+ if plone.api.user.is_anonymous():
+ if not _allow_anonymous_voting():
+ plone.api.portal.show_message(
+ message=_("You must be logged in to vote."),
+ request=self.request,
+ type="error",
+ )
+ return self.index()
+ participant_id = _anonymous_participant_id(self.request)
+ else:
+ participant_id = plone.api.user.get_current().id
+
+ chosen_strs = self.request.form.get("chosen_slots", [])
+ if isinstance(chosen_strs, str):
+ chosen_strs = [chosen_strs]
+
+ if not chosen_strs:
+ plone.api.portal.show_message(
+ message=_("Please select at least one time slot."),
+ request=self.request,
+ type="error",
+ )
+ return self.index()
+
+ chosen = []
+ for s in chosen_strs:
+ try:
+ chosen.append(datetime.fromisoformat(s))
+ except (ValueError, TypeError):
+ plone.api.portal.show_message(
+ message=_("One or more selected slots could not be read."),
+ request=self.request,
+ type="error",
+ )
+ return self.index()
+
+ try:
+ submit_vote(self.context, participant_id, chosen)
+ except PollClosedError:
+ plone.api.portal.show_message(
+ message=_("This poll is no longer open for voting."),
+ request=self.request,
+ type="error",
+ )
+ return self.index()
+ except DuplicateVoteError:
+ plone.api.portal.show_message(
+ message=_("You have already voted in this poll."),
+ request=self.request,
+ type="info",
+ )
+ return self.request.response.redirect(
+ f"{self.context.absolute_url()}/@@poll-results"
+ )
+ except InvalidSlotError:
+ plone.api.portal.show_message(
+ message=_("One or more selected slots are no longer valid."),
+ request=self.request,
+ type="error",
+ )
+ return self.index()
+
+ plone.api.portal.show_message(
+ message=_("Your vote has been recorded. Thank you!"),
+ request=self.request,
+ type="info",
+ )
+ return self.request.response.redirect(
+ f"{self.context.absolute_url()}/@@poll-results"
+ )
+
+
+class PollResultsView(BrowserView):
+ """Results view showing aggregated votes per proposed slot.
+
+ Displays:
+ - total vote count
+ - per-slot counts sorted by count (highest first)
+ - the leading/winning slot
+ - the final selected slot when the poll is in 'final' state
+ """
+
+ # ------------------------------------------------------------------
+ # Template helpers (called from poll_results.pt)
+ # ------------------------------------------------------------------
+
+ def is_open(self):
+ """Return True when the poll is still accepting votes."""
+ return self.context.poll_state == "open"
+
+ def is_final(self):
+ """Return True when the organiser has selected a final slot."""
+ return self.context.poll_state == "final"
+
+ def total_votes(self):
+ """Return the number of participants who have voted."""
+ return len(get_votes(self.context))
+
+ def results(self):
+ """Return a list of dicts for each proposed slot, sorted by count descending.
+
+ Each dict has:
+ - ``display``: human-readable slot label
+ - ``count``: number of votes for this slot
+ - ``is_winner``: True for the slot returned by get_winning_slot()
+ """
+ counts = aggregate_votes(self.context)
+ proposed = self.context.proposed_slots or []
+ winner = get_winning_slot(self.context)
+ rows = [
+ {
+ "slot": slot,
+ "display": _format_slot(slot),
+ "count": counts.get(slot, 0),
+ "is_winner": slot == winner,
+ }
+ for slot in proposed
+ ]
+ return sorted(rows, key=lambda r: r["count"], reverse=True)
+
+ def winning_slot_display(self):
+ """Return a formatted string for the current leading slot, or ''."""
+ return _format_slot(get_winning_slot(self.context))
+
+ def final_slot_display(self):
+ """Return a formatted string for the final selected slot, or ''."""
+ return _format_slot(self.context.final_selected_slot)
diff --git a/src/experimental/doodle/browser/templates/booking_page.pt b/src/experimental/doodle/browser/templates/booking_page.pt
new file mode 100644
index 0000000..ad1b0b1
--- /dev/null
+++ b/src/experimental/doodle/browser/templates/booking_page.pt
@@ -0,0 +1,131 @@
+
+
+
+
+
+
Booking Page title
+
+
+
+
+ Organizer:
+ Organizer name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No bookable slots are available for this date.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This booking page is not accepting new bookings.
+