Dates for the sites built on omnisense/omnibase:
a harpist's concerts, a violinist's, a school's open days, a shop's late openings - where they
happen, with whom, what is played, where the tickets are; the calendar visitors subscribe to,
the "add to my calendar" links, the same dates as JSON for other sites and apps, and the dates
read from where they are kept: a Google Calendar the artist fills in from her phone, an agency's
feed, a former Squarespace site, another omnibase site. It is a Symfony bundle
(Base\Agenda\AgendaBundle) on the core 3 or 4, with its back office on omnibase/admin.
An Event is an omnibase Thread (a JOINED subclass): a title (the concert's name), a
headline, an excerpt, the text, tags, owners, a slug, publish states and scheduling, revisions,
soft delete, translations; the bundle adds the moment (startsAt, endsAt, the hall's
timezone, allDay), the venue, the ensemble and the conductor, the artist's role, the
programme and the performers, the tickets' and the organiser's links, a cover, and
cancelled - a cancelled date stays on the page, struck through: who had a ticket must find it.
A Venue is a plain row: typed once, chosen on every date.
The artist never needs to log into the site: agenda:sync reads her calendars, and the back
office shows what a sync would change before it is applied.
How this manual is organised
- Installation: requirements, the bundle, the routes, the schema, the cron.
- Dates and venues: the entities, their fields and methods, the roles, the queries.
- The pages and the JSON feed:
/agenda, a date's page,/agenda.json, the pictures, the sitemap. - ICS feeds and Google Calendar links: the calendars to subscribe to and to add.
- Where the dates come from: the sources, Google Calendar, the words of a description, how a sync matches and changes, Squarespace, another site, the dates gone.
- Structured data (JSON-LD): the schema.org event of each date.
- In a host page: the Twig functions, the partials, the stylesheet, the digest.
- The back office: the screens, the calendar page, merging venues, the tile.
- Configuration reference:
agenda:, the settings, the services. - Testing.
Propose a change docs/index.md
Installation
Requirements
What composer.json requires: PHP 8.2 or later, doctrine/orm 2.15 or 3,
omnisense/omnibase 3.0 or 4.0, omnibridge/symfony 1.x, omnibase/admin,
twig/intl-extra 3 (the templates' date filters), and symfony/config, console,
dependency-injection, form, http-client (reading the calendars), http-kernel,
security-bundle and string 6, 7 or 8. It suggests omnibase/newsletter: the coming dates in
its monthly digest.
The bundle
// config/bundles.php
return [
// ...
Base\BaseBundle::class => ['all' => true],
Base\Admin\AdminBundle::class => ['all' => true],
Base\Agenda\AgendaBundle::class => ['all' => true],
];
AgendaBundle aliases Base\Agenda\Entity\* to App\Entity\Agenda\* and the repositories to
App\Repository\Agenda\*, unless the application declares its own; templates/ is @Agenda,
public/ is bundles/agenda/.
The routes
# config/routes.yaml
agenda_controller:
resource: "@AgendaBundle/src/Controller/Client"
type: attribute
prefix: /
# the back office's Google Calendar page (omnibase/admin)
agenda_admin_controller:
resource: "@AgendaBundle/src/Controller/Admin/CalendarController.php"
type: attribute
prefix: /
and the page in the dashboard's menu:
MenuItem::linkToRoute('agenda_admin_calendar', [], 'Google Calendar', 'fa-brands fa-google'),
Configuration
# config/packages/agenda.yaml (every key optional)
agenda:
sources: []
ics: ~ # '%env(AGENDA_ICS)%' for a secret address
past_per_page: 24
timezone: Europe/Berlin
calendar_name: ~
jsonld: true
jsonld_type: MusicEvent
show_past: true
The schema and the public files
bin/console doctrine:migrations:diff
bin/console doctrine:migrations:migrate
bin/console assets:install
Tables agenda_event (joined to the core's thread table) and agenda_venue; the stylesheet
bundles/agenda/css/agenda.css.
The sync
*/30 * * * * bin/console agenda:sync
What the layout provides
The pages extend layout1.html.twig and fill title, description, content and
stylesheets (where /agenda also announces its JSON feed with a
<link rel="alternate" type="application/json">). They use twig/intl-extra's date filters and
the core's |wysiwyg.
Propose a change docs/installation.md
Dates and venues
Event
Base\Agenda\Entity\Event extends the core's Thread (agenda_event, discriminator
agenda_event, indexed on startsAt):
| Property | Column | Meaning |
|---|---|---|
startsAt |
datetime_immutable, required |
stored as given; read on the hall's clock with getStartsAtLocal() |
endsAt |
nullable | for whole days, the last day |
timezone |
string(64) |
the hall's (agenda.timezone for a new date) |
allDay |
boolean |
|
venue |
many-to-one Venue, SET NULL |
|
ensemble, conductor |
string(180) |
|
role |
string(24) |
a Role value: soloist, leader (concertmaster), chamber, orchestra, recital, masterclass, other (default); Role::of() reads anything, other when unknown |
programme |
text |
one work per line: "Glière — Harp Concerto op. 74" |
performers |
text |
one per line: "Anna Meyer, harp" |
ticketsUrl, eventUrl |
string(500), URLs |
where the tickets are; the organiser's page |
cancelled |
boolean |
struck through, never removed |
cover |
upload, image, 8 MB | |
sourceUid (unique), source, sourceUpdatedAt |
the calendar entry it was read from | |
syncLocked |
boolean |
"Keep the site's texts": a sync brings only its hours and its cancellation |
Methods:
getStartsAtLocal(),getEndsAtLocal(),getDateTimeZone()(UTC when the timezone is not one);isMultiDay();isUpcoming(?$now)- its (last) day not over, on the hall's clock;isPast();isToday(?$now)- not cancelled, today in its own timezone or within its run;isTonight()- today and starting at 17:00 or later, not all day;isOnStage()- started and not over (two hours without an end);getProgrammeLines(),getPerformerLines(),getProgrammeWorks()({composer, work}split on a dash between spaces or a colon),getPerformerNames()({name, part});getPictureUrl()- its cover, else its venue's photo;getPictureCredit()- the venue's photo credit when the picture is the venue's;getDescriptionText()- what a calendar entry says under the title, in the words the sync reads back (Orchestra:,Conductor:,Role:,With:,Tickets:,Programme:);toCalendarEntry(string $uid, ?string $url)- the core'sCalendarEntry(nullwithout a start), two hours (Event::DEFAULT_DURATION,PT2H) without an end;duplicate(string $titleSuffix)- a copy to start the next date of a tour from: same times, venue, programme, people and links, its owners, no calendar behind it;isImported()- read from a calendar.
Venue
agenda_venue: name (not blank), hall, address, postcode, city, country (ISO),
mapUrl, website, latitude, longitude, photo (8 MB), photoCredit, aliases (the
spellings merged into it), slug (unique, given at the first save from the name and town, -2
when taken).
getLabel() ("Elbphilharmonie, Hamburg"), getFullAddress() (name, hall, address, postcode and
town, country), getMapLink() (the map typed, else an OpenStreetMap point from the coordinates,
else a search of the address), isKnownAs(string $name, ?string $city) (its own name and town or
an alias, case and accents aside), Venue::key(), getPhotoUrl().
Repositories
| Repository | Methods |
|---|---|
EventRepository |
findUpcoming(?int $limit) (published dates whose day is not over, nearest first), countUpcoming(), findToday(?$now), findPast(?int $limit, int $page), countPast(), findOnePublished(string $slug), findBetween($from, $to), findSince($from), findForFeed(bool $past, ?$from, ?$to, int $limit, int $page) (one more than the limit, to know there is a next page), findAllPublished(), findByMonth(), findBySourceUid(), findUpcomingOfSource() |
VenueRepository |
findOneByNameAndCity(), findOneByFullAddress(), findOneByAlias(), merge(Venue $from, Venue $into): int, findAllByName() |
merge() moves every date of $from (drafts and trashed ones included) to $into, fills what
$into lacks from $from (hall, address, postcode, country, map, site, coordinates, the photo
copied with its credit), keeps $from's spellings as aliases of $into, deletes $from, and
returns the dates moved; a house is never merged into itself.
Service\Months::group($events) groups dates by month for the pages (agenda_months()).
Propose a change docs/concepts.md
The pages and the JSON feed
| Route | Path | Methods | |
|---|---|---|---|
agenda_index |
/agenda |
any | the dates to come by month, the past ones folded; sitemap 0.8, weekly |
agenda_event |
/agenda/{slug} |
any | one date, its programme, its JSON-LD; sitemap 0.6, weekly |
agenda_feed |
/agenda.ics |
any | ICS feeds |
agenda_event_ics |
/agenda/{slug}.ics |
any | |
agenda_json |
/agenda.json |
GET, HEAD | the dates as JSON |
agenda_event_json |
/agenda/{slug}.json |
GET, HEAD | one date as JSON |
agenda_event_cover |
/agenda/{slug}/cover |
GET, HEAD | a date's cover with its type |
agenda_venue_photo |
/agenda/venue/{slug}/photo |
GET, HEAD | a house's photo with its type |
A slug that names no published date is a 404. Every date's page is in /sitemap.xml
(EventListener\SitemapListener, on the core's SitemapEvent::BUILD); the calendars and the
feeds are not.
/agenda
upcoming and months (the dates to come, by month); with agenda.show_past, the past dates
(past, past_months) folded under them, agenda.past_per_page a page - ?past=1&page=2
opens them.
/agenda.json
Anyone may read it, from anywhere: Content-Type: application/json,
Access-Control-Allow-Origin: *, Cache-Control: public, max-age=300, an ETag of its content
(the moment it was written aside) - a reader sending it back in If-None-Match gets a 304.
| Query | |
|---|---|
| (none) | the dates to come, the nearest first |
?past=1 |
the dates gone by, the latest first |
?from=2026-11-01&to=2026-12-31 |
a span of days, both included; a malformed day is a 400 with {"error": ...} |
?limit= |
100 by default, 1 to 500 |
?page= |
the page; next is the next one's address, null on the last |
The document (Service\Feed::document()) is {calendar, events, next}: calendar holds the
name, url, timezone, ics and generatedAt; each event (Feed::event()) id, slug,
url, ics, title, start, end, timezone, allDay, cancelled, role, roleLabel (in
the request's language), ensemble, conductor, performers and programme (lists),
tickets, eventUrl, image and imageCredit (the picture the date shows: its cover, else its
house's photo and the photographer), venue (name, hall, address, postcode, city,
country, latitude, longitude, map, photo), updatedAt.
id is the calendar entry's UID when the date was read from one, else <id>@<host> - the same
as in the ICS files. start and end carry the hall's offset; a date of whole days starts at
midnight, allDay true, end its last day. Service\FeedParser reads the document back: one
omnibase site can take another's dates (Where the dates come from).
/agenda/{slug}.json is Feed::event() alone, with the same headers.
The pictures
/agenda/{slug}/cover and /agenda/venue/{slug}/photo serve the upload with its own type, CORS
open, cached a day (public), 304 when not modified; a 404 without an image. They are the
addresses the JSON feed and the JSON-LD give; the pages' own <img> keep the files' addresses.
Propose a change docs/usage-pages.md
ICS feeds and Google Calendar links
The iCalendar writing (RFC 5545) and the Google Calendar link are
omnisense/omnibase's Base\Service\Calendar\{Ics, GoogleCalendarLink}, on a
CalendarEntry. The agenda maps its dates there:
Event::toCalendarEntry(string $uid, ?string $url): the date as a calendar sees it (the hall's timezone, whole days, the venue's address and GPS point, cancelled or not,Event::DEFAULT_DURATIONwithout an end).Base\Agenda\Service\Ics:calendar($events, $name)andevent($event)(the/agenda.icsand/agenda/{slug}.icsfeeds),google($event)(the Twig functionagenda_google_url()),uid($event)(the source's UID, else<id>@<host>),entry($event).Ics::escape()andIcs::fold()are the core's.
Nothing to configure.
What the files hold
The files are written by the core's Base\Service\Calendar\Ics (RFC 5545): CRLF, lines folded
at 75 octets, texts escaped, the hour in the hall's timezone with its VTIMEZONE, whole days as
dates, STATUS:CANCELLED, and the UID of the entry a date was imported from - so a calendar
that holds both never shows it twice. PRODID is -//omnibase//agenda//EN.
| Address | Holds | Headers |
|---|---|---|
/agenda.ics (agenda_feed) |
every published date from a year back, named agenda.calendar_name (else the site's title, else "Agenda") |
text/calendar; charset=utf-8, inline agenda.ics, Cache-Control: public, max-age=900 |
/agenda/{slug}.ics (agenda_event_ics) |
one date, named after its title | the same, <slug>.ics |
The page offers webcal://…/agenda.ics (agenda_webcal_url()): a phone, Apple Calendar or
Outlook subscribes to it and follows the changes; Google Calendar takes the same address in
Other calendars → From URL. Each date has two links more: Google Calendar's template page
(agenda_google_url(event)) and its .ics (agenda_ics_url(event));
@Agenda/client/_calendar_links.html.twig renders both.
Propose a change docs/calendar.md
Where the dates come from
The artist (or her agency) keeps the dates where she already keeps them. agenda:sync reads
every source and applies what changed; the back office reads them on demand and shows what they
would change before anything is applied.
bin/console agenda:sync # every source
bin/console agenda:sync --dry-run # what would change, each field as it was and as it becomes; nothing applied
bin/console agenda:sync --source=<address|file> # one instead (repeatable; not remembered as the last sync)
bin/console agenda:sync -v # the changes too
With no source it says where to set one and exits 0; it fails (exit 1) only when every source
failed. The summary lists created, updated, cancelled, missing and unchanged.
The sources
Base\Agenda\Service\IcsImporter::getSources(): agenda.sources, agenda.ics (comma-separated:
an environment variable) and the address pasted in the back office (the setting agenda.ics).
What a source holds is told by its content: an iCalendar file (IcsParser), a Squarespace
events collection (SquarespaceParser, six pages back), an agenda feed (FeedParser, its next
pages followed, twenty at most). An address is http(s):// or webcal(s)://; anything else is a
file.
Google Calendar
- In Google Calendar, a calendar of its own for the concerts, shared with the agency if it keeps the dates too.
- When the site has dates already: the site's own
/agenda.icsimported into it (Settings → Import & export → Import). Each date carries its UID, which the sync matches on, and its fields in the words the sync reads (Event::getDescriptionText()): read back, the site's dates change nothing. - Its Settings and sharing → Integrate calendar → Secret address in iCal format. It needs no OAuth, but whoever holds it reads the whole calendar: keep it out of git.
- Paste it on the back office's calendar page (
/admin/agenda/calendar): kept in theagenda.icssetting, shown half hidden (orsecrets:set AGENDA_ICSandagenda: { ics: '%env(AGENDA_ICS)%' }). - A concert is an event of that calendar: the title is the concert's name, the location the hall and its address, the description one piece of information per line.
Orchestre : NDR Elbphilharmonie Orchester
Chef : Vasily Petrenko
Rôle : soliste
Billets : https://www.elbphilharmonie.de/...
Programme :
Glière — Concerto pour harpe op. 74
Debussy — Danses sacrée et profane
The words of a description
Base\Agenda\Service\DescriptionParser::parse(?string $description) reads them whatever their
case and accents, in English, French, German and Italian:
| Field | Words |
|---|---|
| ensemble | Orchestra, Ensemble, Orchestre, Orchester |
| conductor | Conductor, Chef, Direction, Dirigent, Leitung, Direttore |
| role | Role, Rôle, Rolle, Ruolo - soloist, concertmaster, chamber, orchestra, recital, masterclass in any of the four languages |
| performers | With, Avec, Mit, Con - on the line, comma-separated ("Anna Meyer, harpe" stays one), or one per line under it |
| tickets | Tickets, Billets, Karten, Biglietti - the first address |
| programme | Programme, Program, Programm, Programma - the lines under it, until a blank line or another word |
A line no word opens is the programme once a word was read; a description with no word at all is the programme. Google's HTML (bold, line breaks, wrapped links) is read as text. Run in omnisense/omnibase's harness image:
use Base\Agenda\Service\DescriptionParser;
print_r(DescriptionParser::parse("Orchestre : NDR Elbphilharmonie Orchester\nChef : Vasily Petrenko\nRôle : soliste\nBillets : https://www.elbphilharmonie.de/de/programm/123\nProgramme :\nGlière — Concerto pour harpe op. 74\nDebussy — Danses sacrée et profane"));
Array
(
[ensemble] => NDR Elbphilharmonie Orchester
[conductor] => Vasily Petrenko
[role] => soloist
[performers] => Array
(
)
[tickets] => https://www.elbphilharmonie.de/de/programm/123
[programme] => Array
(
[0] => Glière — Concerto pour harpe op. 74
[1] => Debussy — Danses sacrée et profane
)
[rest] =>
)
The calendar is the source of truth
A date is matched by its entry's UID. At each sync it takes from its calendar the title, the
hours, the place - a Venue found by its full address as the site writes it, else by name and
town or an alias merged into it, else opened from LOCATION (the name before the first comma,
the town in the last part) -, the URL as the organiser's page, STATUS:CANCELLED, and what the
description says: a field the description does not mention keeps what the site has; a word with
nothing after it ("Chef :") clears it. A house's picture the source gives becomes the photo of a
house that has none (once); a date's own picture the cover of a date that has none. A repeating
entry gives its first occurrence. A new date goes online at once.
A date whose texts were rewritten on the site is protected by Keep the site's texts
(syncLocked): the sync brings its hours and its cancellation, nothing else.
Each change is a Base\Agenda\Model\Change - created, updated (each field as it was and as it
becomes, the hours on the hall's clock), cancelled by its calendar, or missing from it.
IcsImporter::import(..., apply: false) and sync(apply: false) only tell them: nothing written,
no picture fetched. Model\ImportSummary adds them up, with the sources that could not be read
(errors).
The dates gone from their calendar
A date that leaves its calendar is neither cancelled nor deleted behind anyone's back: it waits
on the calendar page (the setting agenda.sync.missing) until someone says Mark cancelled or
Keep on the site (the site's own from then on: no calendar asks about it again;
IcsImporter::resolveMissing(array $ids, bool $cancel)). Only the dates to come are checked, per
source.
What a sync leaves
IcsImporter::remember(ImportSummary) keeps the last sync (agenda.sync.last: when, the counts,
the errors) and, when it changed something, appends it to agenda.sync.history (the last ten,
a hundred changes each), for the calendar page. A calendar that could not be read leaves the dates
already waiting where they are. A --source or --dry-run run is not kept.
A Squarespace site, during a move
The JSON of a Squarespace events page (https://…/performances?format=json, with or without
?format=json) is read by Service\SquarespaceParser: uid squarespace:<id>, the bold first line
of the body as the ensemble, "Name Conductor" / "Name Piano" as the conductor and the
performers, "Composer Work" as the programme, the sourceUrl as the organiser's page, the
assetUrl as the hall's photo (the cover of a date that names no hall), an end typed a day late as
the same evening's, the past dates followed six pages back. Squarespace keeps every hour on the
site's clock (Berlin's): the hour typed is set back in the hall's timezone, the country's
(addressCountry, else the end of the address).
Another omnibase site
Another site's /agenda.json is a source too: its dates keep their id, so a date read from two
places is one.
Reading an ICS file
Base\Agenda\Service\IcsParser::parse(string $ics) gives Model\ParsedEvents (UID, summary,
start, end - for whole days the last day -, timezone - the entry's TZID, else agenda.timezone
-, description, location, URL, last modified, cancelled, sequence, recurring); the description is
read by DescriptionParser when the importer applies it:
use Base\Agenda\Service\IcsParser;
$ics = implode("\r\n", [
'BEGIN:VCALENDAR', 'VERSION:2.0', 'BEGIN:VEVENT',
'UID:abc123@google.com',
'DTSTART;TZID=Europe/Berlin:20261115T200000',
'DTEND;TZID=Europe/Berlin:20261115T220000',
'SUMMARY:Perspectives concertantes',
'LOCATION:Elbphilharmonie\, Platz der Deutschen Einheit 4\, Hamburg',
'END:VEVENT', 'END:VCALENDAR', '',
]);
foreach ((new IcsParser('Europe/Berlin'))->parse($ics) as $event) {
echo $event->uid, ' | ', $event->summary, ' | ', $event->start->format(DATE_ATOM), ' | ', $event->timezone, "\n";
echo 'location: ', $event->location, "\n";
}
abc123@google.com | Perspectives concertantes | 2026-11-15T20:00:00+01:00 | Europe/Berlin
location: Elbphilharmonie, Platz der Deutschen Einheit 4, Hamburg
Propose a change docs/usage-sync.md
Structured data (JSON-LD)
Each date's page (agenda_event) carries a schema.org event in a
<script type="application/ld+json">, written by Service\JsonLd from the
Event: its name, its start and end in ISO 8601 with the hall's offset (a
date of whole days as days), eventStatus (EventCancelled when cancelled),
the Place and its PostalAddress (and GeoCoordinates when the venue has
them), the cover, the tickets as an Offer, the performers - the site's own
name first, then each performer, then the ensemble -, and the programme as
workPerformed.
The type
# config/packages/agenda.yaml
agenda:
jsonld: true # false: no JSON-LD at all
jsonld_type: EducationEvent # MusicEvent (default), EducationEvent, Event
jsonld_type |
For | @type |
Ensemble | Programme |
|---|---|---|---|---|
MusicEvent (default) |
a musician | MusicEvent; a date whose role is masterclass: EducationEvent |
MusicGroup |
each line "Composer — Work": a CreativeWork with its creator |
EducationEvent |
a lecturer, a teacher trainer, a school | EducationEvent |
PerformingGroup |
each line a CreativeWork named as written (the talks' titles) |
Event |
anything else | Event |
PerformingGroup |
idem |
An unknown value is refused by the configuration; new JsonLd('…') outside
the container falls back to MusicEvent.
Outside the page
use Base\Agenda\Service\JsonLd;
public function __construct(private readonly JsonLd $jsonLd) {}
$data = $this->jsonLd->for($event, $absoluteUrl, $absoluteImageUrl, 'Monica Neagoy');
$this->jsonLd->getType(); // 'EducationEvent'
The fourth argument is the person or group the site is about: the first performer of every date.
Propose a change docs/structured-data.md
In a host page
Base\Agenda\Twig\AgendaExtension:
| Returns | |
|---|---|
agenda_upcoming(limit = 3) |
the next dates, for a home page |
agenda_next() |
the very next one, for a banner |
agenda_today() |
today's dates, each in its own timezone, cancelled ones left out |
agenda_google_url(event) |
Google Calendar's template page for the date |
agenda_ics_url(event, absolute = false) |
the date's .ics |
agenda_webcal_url() |
webcal://…/agenda.ics, the subscription address |
agenda_months(events) |
dates grouped by month |
role|agenda_role_label |
the role's name in the agenda domain |
Partials: @Agenda/client/_event.html.twig (a date as the list renders it),
_calendar_links.html.twig (the two calendar links), _today.html.twig (today's concert before
anything else: "Tonight", the hour, the hall, the part, the orchestra, the tickets; "On stage now"
while it plays) and _today_chip.html.twig (the same in one line, for a home page's first screen);
both render nothing on a day without a date. Every template is overridden in
templates/bundles/AgendaBundle/....
The stylesheet bundles/agenda/css/agenda.css follows the host's custom properties when it
defines them - --agenda-accent, --agenda-on-accent, --agenda-ink, --agenda-soft,
--agenda-line, --agenda-surface, --agenda-font-display, --agenda-measure - and is neutral
otherwise.
Translations: the domains agenda and entities, ICU, in English, French and German.
In the newsletter's digest
With omnibase/newsletter installed, Base\Agenda\Digest\AgendaDigestSource (tagged
newsletter.digest_source) gives its digest a part named agenda: each date between the two
days, its title, a line - "cancelled" when it is, the day and hour in the letter's language, the
venue, the ensemble and conductor -, its page and its start.
Propose a change docs/usage-twig.md
The back office
Loaded when omnibase/admin is installed. The two CRUDs carry omnibase/admin's
#[OpenToAdmins]: ROLE_ADMIN creates, edits and deletes, and uses their own actions.
Dates
EventCrudController (slug events, newest first): title, state, slug (following the title),
start and end, timezone, all day, cancelled, venue, role, ensemble, conductor, programme,
performers, tickets, organiser's page, cover, headline, excerpt, text, the source UID (detail),
and - for an imported date only - "Keep the site's texts". Filters: state, venue, role, start,
cancelled. A new date is in agenda.timezone, owned by the signed-in user.
- Duplicate (
/admin/events/{entityId}/duplicate) - a copy as a draft, " (copy)" in its language after the title, opened for editing: the next date of a tour. - Read the calendars now (
/admin/events/sync, when there are sources) - the calendar page's preview.
Venues
VenueCrudController (venues, by name): name, hall, address, postcode, town, country, site, map,
latitude, longitude, photo and credit, aliases and slug (detail). Filters: town, country.
Merge into… (/admin/venues/{entityId}/merge) lists the other houses, the likely same first
(names a letter or two apart, the same town counting: 80 or more out of 100), with their dates;
posted, the dates move and this house goes (VenueRepository::merge()).
The calendar page
Base\Agenda\Controller\Admin\CalendarController, ROLE_ADMIN, in the admin's layout:
| Route | Path | Method | |
|---|---|---|---|
agenda_admin_calendar |
/admin/agenda/calendar |
GET | the address (half hidden), the configured calendars, the last sync, the dates gone waiting for a word, the syncs that changed something, the how-to; ?preview=1 reads the calendars now and shows what they would change |
agenda_admin_calendar_save |
the same | POST | the address saved (webcal://, webcals://, http(s):// only) or forgotten (forget); token agenda-calendar |
agenda_admin_calendar_sync |
/admin/agenda/calendar/sync |
POST | every calendar read and applied, as the cron does; token agenda-sync |
agenda_admin_calendar_missing |
/admin/agenda/calendar/missing |
POST | ids[] and decision=cancel or keep; token agenda-missing |
The tile
agenda_upcoming (UpcomingEventsWidgetType): how many dates are to come and the next five, each
opening its record.
Propose a change docs/back-office.md
Configuration reference
agenda:
Base\Agenda\DependencyInjection\AgendaConfiguration, dumped with Symfony's
YamlReferenceDumper in omnisense/omnibase's harness image:
agenda:
# ICS addresses agenda:sync reads (a Google Calendar's private address, an orchestra's feed).
sources: []
# More ICS addresses, comma-separated, read when the command runs: the place for "%env(AGENDA_ICS)%", which a list cannot take.
ics: null
# Past dates listed per page.
past_per_page: 24
# The timezone of a new date, and of an imported one that names none.
timezone: Europe/Berlin
# The name of the ICS feed; null: the site's title.
calendar_name: null
# A schema.org event on each date's page.
jsonld: true
# Its schema.org type: a musician's MusicEvent (a masterclass stays an EducationEvent), a lecturer's EducationEvent, or Event.
jsonld_type: MusicEvent # One of "MusicEvent"; "EducationEvent"; "Event"
# The past dates under the ones to come.
show_past: true
| Option | Type | Default | Read by |
|---|---|---|---|
sources |
list of addresses | [] |
IcsImporter |
ics |
comma-separated string or null | null |
IcsImporter |
past_per_page |
int, 1 or more | 24 |
AgendaController |
timezone |
timezone | Europe/Berlin |
IcsParser, SquarespaceParser, AgendaController, EventCrudController |
calendar_name |
string or null | null |
AgendaController (ICS and JSON); null: base.settings.title, else "Agenda" |
jsonld |
bool | true |
AgendaController |
jsonld_type |
MusicEvent, EducationEvent, Event |
MusicEvent |
JsonLd |
show_past |
bool | true |
AgendaController |
Settings
Kept in the core's setting bag by IcsImporter: agenda.ics (the address pasted),
agenda.sync.last, agenda.sync.history, agenda.sync.missing.
Services
config/services.php autowires and autoconfigures src/ but DependencyInjection/, Entity/,
Enum/, Model/, Controller/Admin/, Admin/, Digest/ and the bundle class;
Controller/Client/ is tagged controller.service_arguments; with omnibase/admin,
Controller/Admin/ and Admin/; with omnibase/newsletter's DigestSourceInterface, Digest/.
SyncCommand is #[AsCommand], SitemapListener #[AsEventListener].
Propose a change docs/configuration.md
Testing
vendor/bin/phpunit
Unit tests of the ICS writer and reader, the Google link, the JSON-LD, the JSON feed (written and
read back), the description reader, the Squarespace reader (on a real excerpt,
tests/Fixtures/squarespace.json) and the Event's own rules; none needs a kernel, but
tests/Controller/Admin/OpenToAdminsTest (an administrator writes in the back office, a plain
user does not), which runs inside a host application and is skipped elsewhere.
In omnisense/omnibase's harness, this checkout mounted over the package:
docker run --rm --network none -v "$PWD":/srv/demo/vendor/omnibase/agenda:ro omnisense/omnibase-harness test agenda
.............................................................. 62 / 62 (100%)
OK (62 tests, 569 assertions)
tests/Service/IcsTest::testEveryPieceComesBackWhereItWas proves the round trip of
Google Calendar's step 2: a date written to the site's .ics and
read back changes nothing.