Opening Hours

The opening hours feature lets editors describe when a place is open on a Contact. It supports general weekly opening hours, special opening hours that override the general ones for a date range, and a reference to a separate contact that supplies holidays. The serialized output is computed at request time and reports whether the place is currently open or closed, plus a human-readable state.

Overview

The opening hours behavior adds these fields to a Contact:

  • show_opening_hours – a flag indicating whether the frontend should render opening hours at all.

  • opening_hours_text – free rich-text shown alongside the structured hours.

  • structured_opening_hours – the general weekly opening hours, stored following the schema.org OpeningHoursSpecification structure.

  • special_opening_hours – entries that override the general hours for a validity date range (for example public holidays or seasonal hours).

  • holidays – a reference to another Contact whose opening/special hours are merged in (lets you maintain a shared holiday calendar once and reuse it).

The structured fields are edited with a dedicated widget but always serialize as plain JSON for consumers.

Specification format

Each entry in structured_opening_hours.openingHoursSpecification carries:

  • dayOfWeek – a list of weekday names (MondaySunday).

  • opens / closes – times in HH:MM (or HH:MM:SS). An entry with opens and closes both 00:00 means closed.

  • validFrom / validThrough – optional ISO date-times bounding the validity of the entry. If one is set, both must be set.

Special opening hours use the same per-entry shape under specialOpeningHoursSpecification.

REST API

Computed opening hours in content

The structured_opening_hours field is computed on the fly when a Contact is serialized. The computed value merges the general hours, special hours and referenced holidays, evaluates the current open/closed state, and adds the following keys:

  • @type – always Place.

  • name – the contact title.

  • closed – boolean, whether the place is currently closed.

  • current_isodate – the date-time the state was evaluated for.

  • opens_at / closes_at – the next opening / current closing time, or null.

  • human_readable_state – a translated message such as “Opens at 08:00” or “Closed”.

  • html – a pre-rendered HTML representation of the opening hours table.

The raw special_opening_hours field is dropped from the Contact serialization; its entries are already merged into the computed structured_opening_hours.

const response = await fetch('/Plone/contacts/town-hall', {
    headers: { 'Accept': 'application/json' }
});
const contact = await response.json();
const hours = contact.structured_opening_hours;
console.log(hours.closed);                 // false
console.log(hours.human_readable_state);   // "Closes at 17:00"

GET @opening-hours

The @opening-hours endpoint returns just the computed opening hours for a contact, without serializing the whole object. This is the lightweight endpoint to poll for the current open/closed state.

GET /Plone/contacts/town-hall/@opening-hours HTTP/1.1
Host: localhost:8080
Accept: application/json
{
    "@id": "http://localhost:8080/Plone/contacts/town-hall/@opening-hours",
    "structured_opening_hours": {
        "@type": "Place",
        "name": "Town Hall",
        "openingHoursSpecification": [
            {
                "@type": "OpeningHoursSpecification",
                "dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
                "opens": "08:00",
                "closes": "12:00"
            }
        ],
        "closed": false,
        "current_isodate": "2026-06-03T10:00:00",
        "opens_at": null,
        "closes_at": "2026-06-03T12:00:00",
        "human_readable_state": "Closes at 12:00",
        "html": "<table>...</table>"
    }
}
const response = await fetch('/Plone/contacts/town-hall/@opening-hours', {
    headers: { 'Accept': 'application/json' }
});
const data = await response.json();
console.log(data.structured_opening_hours.closed);

Evaluating for a specific time

By default the state is computed for the current server time. Pass an isodate query parameter to evaluate the open/closed state for a different moment (for example to preview a holiday). Invalid values fall back to the current time.

const when = '2026-12-25T10:00:00';
const response = await fetch(
    `/Plone/contacts/town-hall/@opening-hours?isodate=${encodeURIComponent(when)}`,
    { headers: { 'Accept': 'application/json' } }
);
const data = await response.json();
console.log(data.structured_opening_hours.closed);  // true on the holiday

Member blocks

When a member block references a contact and inherits its data, the contact’s opening hours fields are merged into the block serialization. The computed structured_opening_hours from the referenced contact fills in the block only when the block has no opening hours of its own.

Configuration

A single registry record controls expired-entry handling:

wcs.backend.opening_hours.filter_expired When set to True, special opening hours entries whose validThrough date lies in the past are removed from the serialized output. Defaults to False.