Events as iCalendar: export every dated thing, import an .ics file, and replace the hand-written reader and writer with a library #661
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#661
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
From the maintainer's list: “Preferable export/import format of events details is iCalendar. A step closer to official DAV plugin.” Postulo writes iCalendar for interviews and for three kinds of whole day, by hand, and reads none. The DAV plugin reads and writes it with a second hand-written parser, and the two have already disagreed about escaping, time zones and alarms. This issue completes the export, adds the import, and settles the library question under the rule of #653; it is three commits, in the order given.
What exists today
applications/ical.py(270 lines):escape(:63),parameter(:74),fold(:92),event_linesfor an interview (:137),DayEntryandday_linesfor a whole day (:202,:220),calendar(:248,METHOD:PUBLISH). Its docstring says a dependency “would bring the rest of the standard along for nothing” (:1-6). Content-line injection was closed in #218; the interviewUIDis still written unescaped and an unknown outcome raises (#450, open).InterviewCalendarView(applications/views.py:1023) servesinterviews.ics: upcoming interviews, and, for the next 183 days (FEED_DAYS,:1020), the application deadlines, the listing closing dates and the offer answer-by dates as whole days (dated_days,:1058, read throughagenda.events_between,applications/agenda.py:197); and one interview. The API serves the interviews only (api/routers/interviews.py:72-77, #451); the page's feed needs a browser session (#451). Reminders are on the calendar page (agenda.py:75) and in no file.uid, unique per owner (applications/models.py:692-694,:734,:762). A whole day's UID is a UUID over its absolute address (ical.py:202-218), and the view builds that address from the request (views.py:1076,build_absolute_uri): an instance reached under two allowed host names would give one deadline two UIDs. Not reproduced;tests/test_deadlines.py:270fetches twice from one host. A reminder has no UID (applications/models.py:600)..ics. An interview cannot exist without an application (Interview.applicationis required), andschedule_interview(applications/services.py:378) is how one is made, with its timeline entry and reminder.postulo-dav/src/postulo_dav/text.py:234-303:text_to_eventsdoes not track nested components, so aVALARM's text replaces an interview's notes (postulo/postulo-dav#11);parse_momentreads an unknown zone, a floating time or a date as UTC and moves the interview (postulo/postulo-dav#24). Its open issues design the rest: pairing an event by collection andUID(#7, #11), reminders and milestones asVTODO(#12). Core's surface offersevent_lines,calendar_status,reschedule_interviewandsettle_interview(plugins/api.py:199-226);escapeis deliberately not on it, so the plugin keeps its own copy.Checked against
mainatf09b6010b.Proposal
icalendarwith a floor of 7.1.3 (see Prior art); a module that reads text into plain values (uid, start, end or day, summary, location, description with an alarm's text kept apart, status, whether it recurs) and refuses what it cannot place. Its bounds are checked before the parser runs: size, number of components, nesting depth, line length..icsthrough the review page #659 factors out: eachVEVENTorVTODOlisted with what it would become, nothing made until the person chooses. A timed event becomes an interview on an application they pick, throughschedule_interview(the kind chosen; the timeline records the file); an event or a task becomes a reminder; or it is skipped. AUIDequal to an interview's is shown as already there, so the same file twice adds nothing. Not read:ATTACH,URLandCONFERENCEare never fetched,ATTENDEEis not turned into contacts (a stranger's names),VALARMis ignored,RRULEandRDATEare refused with a note, as postulo-dav#11 leaves recurrence out.TZIDis resolved throughzoneinfo, then the library's Windows-name table, then the file's ownVTIMEZONE; one that does not resolve, a floating time and a date alone are shown on the review page as needing a choice, and never silently become UTC (postulo-dav#24).VTODO(SUMMARY,DUE,STATUS,COMPLETED,RELATED-TOthe application), in an address of their own: the interviews feed is what existing subscribers read, and its meaning is not changed under them. A Download as iCalendar on the calendar page for the kinds its legend shows, fromagenda.events_between, the one place that decides what a deadline is.Decisions
icalendar, for reading and writing. The new reader is new code, which is what the rule asks to check; the writer is touched to addVTODOand to fix #450, which is when #653 says to look again. Two defects of the plugin's parser, a zone read as UTC and an alarm's text taken for the notes, are what a component tree and a zone table are for; that argues the same way.POSTULO_PUBLIC_URL-derived (config/settings/base.py:538, optional); a stored column. Recommended:POSTULO_PUBLIC_URLwhere set, else the address as today; and a storeduidonReminder, minted as an interview's is, because a pairing by UID (postulo-dav#7) needs one that survives an export and a move. The column is a migration and an archive bump.METHOD:REQUESTinvitations. Recommended: read as events; no reply is ever sent.Prior art
Looked for under #653, 2026-10-02:
collective/icalendar, pushed 2026-10-02, 169 open issues and pull requests); needspython-dateutilandtzdata, andtyping-extensionsbelow Python 3.13, of whichtzdataandtyping-extensionsare inuv.lockalready andpython-dateutilandsixwould be new; it ships a Windows-to-IANA table and a fuzzing corpus in the repository. OSV: one advisory (listed twice, as GHSA and PYSEC), GHSA-cv84-9p8j-fj68 / CVE-2026-55099, High (CVSS 7.5):Component.__eq__takes exponential time in the nesting depth, and the parser has no depth limit; introduced in 7.1.0, fixed in 7.1.3 (2026-06-15). Patched, so not the rule's exception; the floor is>=7.1.3and the importer bounds depth and never compares parsed components. Adopt.allenporter/ical, used by Home Assistant; needspydantic(locked, 2.13.5),python-dateutilandtzdata; Python 3.11 or later; no advisory. Strict validation and built-in recurrence. Not preferred: fourteen major versions and a coupling to pydantic's upgrades. A fair second choice ificalendardisappoints.arrow,tatsubelow 5.16,attrs,six,python-dateutil; a 0.8 development line since the same day;tatsu's latest release lists BSD-4-Clause (advertising clause), not verified for the pinned range. Reject.django-scheduler(0.12.0, BSD-3-Clause, no advisory) is event models with recurrence rules, not an iCalendar reader; it would not replace the calendar page, which draws other records (agenda.py:15-21). Reject.ical.py,text.py): the precedent, and the cost recorded above.caldav3.3.1, GPL-3.0-or-later or Apache-2.0) is a question for that repository.Builds on / Needed by
Builds on #14, #204, #238, #218, #450, #451, #330, #331, postulo/postulo-dav#7, #11, #12, #24 and #653, #659. Needed by postulo-dav#11 and #12.
Done when
tests/test_ical_import.py(new): a timed event, a task and an all-day event each read as the table says; aTZIDin a Windows name resolves; a floating time and a date alone are never UTC unseen; an alarm's text never reaches the notes; a recurring event is refused with a note; aUIDalready held is shown as present; a file nested thousands of components deep, one with an enormous number of components and one past the size bound are refused before parsing.tests/test_interviews.py,tests/test_deadlines.pyandtests/security/test_exports.pyhold the new writer to what they hold today, including that no control character, lone carriage return or unescapedUIDcan add a property;tests/security/test_feeds_and_pictures.pycovers the new download and the reminders address.tests/test_security_audit.pygains a test that the lock holdsicalendar7.1.3 or later.tests/e2e/test_ical_import.py(new): upload, choose an application, confirm; the interview exists with its timeline entry. Pages jointests/test_page_coverage.pyandtests/security/test_isolation_sweep.py; new surface names get their line in Writing a plugin (tests/test_wiki_surface.py) and a test intests/test_plugin_surface.py.Remindergains auid: a migration, the archive format (29 today) moved to one more than whatever it is when the commit lands, the importer and the API's reminder shape carrying it, the candidate format (4) untouched. Strings in en-GB, fr-FR and pt-PT (drafts) and Weblate (#349); the wiki's calendar page.From the list of improvements of 2026-10-02.
Landed on main with a green Forgejo CI run. Its entry in the CHANGELOG's Unreleased section ends in (#661).
ATTENDEE, against the form's promise #963