From ff2923fce8bb5f6f4ee9e4abd7031f73b913aac9 Mon Sep 17 00:00:00 2001 From: Richard Freman Date: Fri, 3 Jul 2026 17:52:03 -0400 Subject: [PATCH] feat: parse and persist plural default calendar alarms Expose defaultAlarmsPartDay/defaultAlarmsFullDay on the Calendar model with JSON array parsing and PROPPATCH serialization for the new CalDAV properties default-alarms-part-day and default-alarms-full-day. Assisted-by: Grok:grok-4 Signed-off-by: Richard Freeman --- src/models/calendar.js | 6 +++++ src/parser.js | 28 ++++++++++++++++++++++++ src/propset/calendarPropSet.js | 14 ++++++++++++ test/unit/models/calendarTest.js | 17 ++++++++++++++ test/unit/parserTest.js | 25 +++++++++++++++++++++ test/unit/propset/calendarPropSetTest.js | 15 +++++++++++++ 6 files changed, 105 insertions(+) diff --git a/src/models/calendar.js b/src/models/calendar.js index 66048006..1baa1c48 100644 --- a/src/models/calendar.js +++ b/src/models/calendar.js @@ -32,6 +32,8 @@ const debug = debugFactory('Calendar') * - timezone * - defaultAlarmPartDay * - defaultAlarmFullDay + * - defaultAlarmsPartDay + * - defaultAlarmsFullDay * - transparency * - components * @@ -58,6 +60,8 @@ export class Calendar extends davCollectionPublishable(davCollectionShareable(Da super._exposeProperty('transparency', NS.IETF_CALDAV, 'schedule-calendar-transp', true) super._exposeProperty('defaultAlarmPartDay', NS.NEXTCLOUD, 'default-alarm-part-day', true) super._exposeProperty('defaultAlarmFullDay', NS.NEXTCLOUD, 'default-alarm-full-day', true) + super._exposeProperty('defaultAlarmsPartDay', NS.NEXTCLOUD, 'default-alarms-part-day', true) + super._exposeProperty('defaultAlarmsFullDay', NS.NEXTCLOUD, 'default-alarms-full-day', true) } /** @@ -288,6 +292,8 @@ export class Calendar extends davCollectionPublishable(davCollectionShareable(Da [NS.OWNCLOUD, 'calendar-enabled'], [NS.NEXTCLOUD, 'default-alarm-part-day'], [NS.NEXTCLOUD, 'default-alarm-full-day'], + [NS.NEXTCLOUD, 'default-alarms-part-day'], + [NS.NEXTCLOUD, 'default-alarms-full-day'], [NS.NEXTCLOUD, 'owner-displayname'], [NS.NEXTCLOUD, 'trash-bin-retention-duration'], [NS.NEXTCLOUD, 'deleted-at'], diff --git a/src/parser.js b/src/parser.js index 149d174a..5d3f83fd 100644 --- a/src/parser.js +++ b/src/parser.js @@ -180,6 +180,8 @@ export default class Parser { this.registerParser('{http://owncloud.org/ns}read-only', Parser.bool) this.registerParser('{http://nextcloud.com/ns}default-alarm-part-day', Parser.decInt) this.registerParser('{http://nextcloud.com/ns}default-alarm-full-day', Parser.decInt) + this.registerParser('{http://nextcloud.com/ns}default-alarms-part-day', Parser.defaultAlarms) + this.registerParser('{http://nextcloud.com/ns}default-alarms-full-day', Parser.defaultAlarms) this.registerParser('{http://nextcloud.com/ns}owner-displayname', Parser.text) this.registerParser('{http://nextcloud.com/ns}deleted-at', Parser.iso8601DateTime) this.registerParser('{http://nextcloud.com/ns}calendar-uri', Parser.text) @@ -595,6 +597,32 @@ export default class Parser { * @param {XPathNSResolver} resolver * @return {string[]} */ + /** + * Parses a {http://nextcloud.com/ns}default-alarms-* Node (JSON array of alarm templates) + * + * @param {Document} document + * @param {Node} node + * @param {XPathNSResolver} resolver + * @return {Array|null} + */ + static defaultAlarms(document, node, resolver) { + const text = Parser.text(document, node, resolver) + if (text === null || text === '') { + return null + } + + try { + const parsed = JSON.parse(text) + if (!Array.isArray(parsed) || parsed.length === 0) { + return null + } + + return parsed + } catch (error) { + return null + } + } + static ocAccess(document, node, resolver) { const result = [] const privileges = document.evaluate('oc:access/*', node, resolver, XPathResult.ANY_TYPE, null) diff --git a/src/propset/calendarPropSet.js b/src/propset/calendarPropSet.js index 62610804..3b877945 100644 --- a/src/propset/calendarPropSet.js +++ b/src/propset/calendarPropSet.js @@ -20,6 +20,8 @@ import * as NS from '../utility/namespaceUtility.js' * - {http://owncloud.org/ns}calendar-enabled * - {http://nextcloud.com/ns}default-alarm-part-day * - {http://nextcloud.com/ns}default-alarm-full-day + * - {http://nextcloud.com/ns}default-alarms-part-day + * - {http://nextcloud.com/ns}default-alarms-full-day * * @param {object} props * @return {object} @@ -85,6 +87,18 @@ export default function calendarPropSet(props) { value, }) break + case '{http://nextcloud.com/ns}default-alarms-part-day': + xmlified.push({ + name: [NS.NEXTCLOUD, 'default-alarms-part-day'], + value: Array.isArray(value) ? JSON.stringify(value) : value, + }) + break + case '{http://nextcloud.com/ns}default-alarms-full-day': + xmlified.push({ + name: [NS.NEXTCLOUD, 'default-alarms-full-day'], + value: Array.isArray(value) ? JSON.stringify(value) : value, + }) + break case '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': xmlified.push({ name: [NS.IETF_CALDAV, 'schedule-calendar-transp'], diff --git a/test/unit/models/calendarTest.js b/test/unit/models/calendarTest.js index 6b68cc00..20047487 100644 --- a/test/unit/models/calendarTest.js +++ b/test/unit/models/calendarTest.js @@ -129,6 +129,23 @@ END:VCALENDAR expect(calendar.defaultAlarmFullDay).toEqual(39600); }); + it('should inherit expose the property defaultAlarmsPartDay', () => { + const parent = new DavCollectionMock(); + const request = new RequestMock(); + const url = '/foo/bar/folder'; + const props = returnDefaultProps(); + props['{http://nextcloud.com/ns}default-alarms-part-day'] = [ + { trigger: -86400, action: 'EMAIL' }, + { trigger: -900, action: 'DISPLAY' }, + ]; + + const calendar = new Calendar(parent, request, url, props); + expect(calendar.defaultAlarmsPartDay).toEqual([ + { trigger: -86400, action: 'EMAIL' }, + { trigger: -900, action: 'DISPLAY' }, + ]); + }); + it('should find all VObjects', () => { const parent = new DavCollectionMock(); const request = new RequestMock(); diff --git a/test/unit/parserTest.js b/test/unit/parserTest.js index 48d05a56..cf7e2908 100644 --- a/test/unit/parserTest.js +++ b/test/unit/parserTest.js @@ -1907,6 +1907,31 @@ END:VALARM`); expect(parser.parse(document, node, resolver)).toEqual(32400); }); + it('should properly handle {http://nextcloud.com/ns}default-alarms-part-day', () => { + const parser = new Parser(); + + const xml = ` + + + /foo + + + [{"trigger":-86400,"action":"EMAIL"},{"trigger":-900,"action":"DISPLAY"}] + + HTTP/1.1 200 OK + + +`; + + const [document, node, resolver] = getDocumentNodeResolverFromXML(xml); + + expect(parser.canParse('{http://nextcloud.com/ns}default-alarms-part-day')).toEqual(true); + expect(parser.parse(document, node, resolver)).toEqual([ + { trigger: -86400, action: 'EMAIL' }, + { trigger: -900, action: 'DISPLAY' }, + ]); + }); + it('should properly handle {http://nextcloud.com/ns}default-alarm-full-day', () => { const parser = new Parser(); diff --git a/test/unit/propset/calendarPropSetTest.js b/test/unit/propset/calendarPropSetTest.js index ae60dd17..a21019f7 100644 --- a/test/unit/propset/calendarPropSetTest.js +++ b/test/unit/propset/calendarPropSetTest.js @@ -131,6 +131,21 @@ describe('Calendar prop-set', () => { ]); }); + it('should serialize {http://nextcloud.com/ns}default-alarms-part-day correctly', () => { + const alarms = [ + { trigger: -86400, action: 'EMAIL' }, + { trigger: -900, action: 'DISPLAY' }, + ]; + expect(calendarPropSet({ + '{http://nextcloud.com/ns}default-alarms-part-day': alarms, + })).toEqual([ + { + name: ['http://nextcloud.com/ns', 'default-alarms-part-day'], + value: JSON.stringify(alarms), + }, + ]); + }); + it('should serialize {urn:ietf:params:xml:ns:caldav}schedule-calendar-transp correctly - transparent', () => { expect(calendarPropSet({ '{Foo:}bar': 123,