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
OpeningHoursSpecificationstructure.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
Contactwhose 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 (Monday…Sunday).opens/closes– times inHH:MM(orHH:MM:SS). An entry withopensandclosesboth00:00means 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– alwaysPlace.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, ornull.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.