diff options
| -rw-r--r-- | src/calendarstore.cpp | 51 | ||||
| -rw-r--r-- | tests/test_calendarstore.cpp | 74 |
2 files changed, 122 insertions, 3 deletions
diff --git a/src/calendarstore.cpp b/src/calendarstore.cpp index c388cd6..ef67eba 100644 --- a/src/calendarstore.cpp +++ b/src/calendarstore.cpp @@ -385,9 +385,26 @@ QByteArray serialise(icalcomponent *root) return text ? QByteArray(text.get()) : QByteArray(); } -icalcomponent *overrideFor(icalcomponent *, icalcomponent *master, const QDateTime &) +/// The override component for `recurrenceId`, created when absent: a copy of +/// the master without RRULE, RDATE or EXDATE, carrying a RECURRENCE-ID in the +/// master's own time form. +icalcomponent *overrideFor(icalcomponent *root, icalcomponent *master, + const QDateTime &recurrenceId) { - return master; // replaced in Task 7 + for (icalcomponent *c = icalcomponent_get_first_component(root, ICAL_VEVENT_COMPONENT); + c; c = icalcomponent_get_next_component(root, ICAL_VEVENT_COMPONENT)) { + icalproperty *rid = icalcomponent_get_first_property(c, ICAL_RECURRENCEID_PROPERTY); + if (c != master && rid + && toDateTime(icalcomponent_get_recurrenceid(c), tzidOf(rid), nullptr) == recurrenceId) + return c; + } + icalcomponent *copy = icalcomponent_new_clone(master); + removeAll(copy, ICAL_RRULE_PROPERTY); + removeAll(copy, ICAL_RDATE_PROPERTY); + removeAll(copy, ICAL_EXDATE_PROPERTY); + setTime(copy, ICAL_RECURRENCEID_PROPERTY, recurrenceId, formOf(root, master)); + icalcomponent_add_component(root, copy); + return copy; } } // namespace @@ -595,7 +612,35 @@ QByteArray newEvent(const EventEdit &edit, const QByteArray &zoneId) return serialise(root.get()); } -QByteArray deleteOccurrence(const QByteArray &, const QDateTime &) { return {}; } +QByteArray deleteOccurrence(const QByteArray &text, const QDateTime &recurrenceId) +{ + IcalComponent root = parseRoot(text); + icalcomponent *master = root ? masterOf(root.get()) : nullptr; + if (!master) + return {}; + // Collect first: removing a component mid-walk invalidates the iterator. + QList<icalcomponent *> doomed; + for (icalcomponent *c = icalcomponent_get_first_component(root.get(), ICAL_VEVENT_COMPONENT); + c; c = icalcomponent_get_next_component(root.get(), ICAL_VEVENT_COMPONENT)) { + icalproperty *rid = icalcomponent_get_first_property(c, ICAL_RECURRENCEID_PROPERTY); + if (c != master && rid + && toDateTime(icalcomponent_get_recurrenceid(c), tzidOf(rid), nullptr) == recurrenceId) + doomed.append(c); + } + for (icalcomponent *c : doomed) { + icalcomponent_remove_component(root.get(), c); + icalcomponent_free(c); + } + // setTime replaces every property of its kind, which for EXDATE would + // drop the existing ones: add this one alongside instead. + const TimeForm form = formOf(root.get(), master); + icalproperty *ex = icalproperty_new_exdate(wallTime(recurrenceId, form)); + if (form.kind == TimeForm::Zoned) + icalproperty_add_parameter(ex, icalparameter_new_tzid(form.tzid.constData())); + icalcomponent_add_property(master, ex); + stamp(master, true); + return serialise(root.get()); +} bool sameMeaning(const QByteArray &a, const QByteArray &b) { diff --git a/tests/test_calendarstore.cpp b/tests/test_calendarstore.cpp index ba2d5c7..921a194 100644 --- a/tests/test_calendarstore.cpp +++ b/tests/test_calendarstore.cpp @@ -23,6 +23,8 @@ #include <QTemporaryDir> #include <QTimeZone> +#include <algorithm> + #include "calendarstore.h" namespace { @@ -98,6 +100,16 @@ CalEvent reparse(const QByteArray &text) return CalendarStore::parseEvent(text, QStringLiteral("/x/e.ics"), QStringLiteral("x"), &unknown); } +const QString kDailySeries = QStringLiteral( + "UID:d@example.org\r\nDTSTAMP:20260901T000000Z\r\n" + "DTSTART;TZID=Europe/Rome:20260921T100000\r\nDTEND;TZID=Europe/Rome:20260921T110000\r\n" + "RRULE:FREQ=DAILY;COUNT=5\r\nSUMMARY:Daily\r\n"); + +QDateTime rome(int d, int h) +{ + return QDateTime(QDate(2026, 9, d), QTime(h, 0), QTimeZone("Europe/Rome")); +} + const QString kRichEvent = QStringLiteral( "UID:rich@example.org\r\nDTSTAMP:20260901T000000Z\r\nSEQUENCE:2\r\n" "DTSTART;TZID=Europe/Rome:20260922T100000\r\nDTEND;TZID=Europe/Rome:20260922T110000\r\n" @@ -141,6 +153,10 @@ private slots: void sameMeaningIgnoresFormatting(); void sameMeaningMatchesOverridesRegardlessOfOrder(); void sameMeaningRejectsDifferentUids(); + + void editingOneOccurrenceWritesAnOverride(); + void editingTheSameOccurrenceAgainReplacesItsOverride(); + void deletingOneOccurrenceAddsAnExdateAndDropsItsOverride(); }; void TestCalendarStore::parsesAUtcEvent() @@ -587,5 +603,63 @@ void TestCalendarStore::sameMeaningRejectsDifferentUids() QVERIFY(CalendarStore::sameMeaning(a, a)); } +void TestCalendarStore::editingOneOccurrenceWritesAnOverride() +{ + const CalEvent series = parse(vevent(kDailySeries)); + EventEdit edit = editOf(series); + edit.summary = QStringLiteral("Just this one"); + edit.start = rome(22, 15); + edit.end = rome(22, 16); + const QByteArray after = CalendarStore::applyEdit( + series.rawText, edit, CalendarStore::Scope::ThisOccurrence, rome(22, 10)); + + const CalEvent e = reparse(after); + QCOMPARE(e.summary, QStringLiteral("Daily")); // master untouched + QCOMPARE(e.repeat.count, 5); + QCOMPARE(e.overrides.size(), 1); + QCOMPARE(e.overrides[0].recurrenceId, rome(22, 10)); + QCOMPARE(e.overrides[0].summary, QStringLiteral("Just this one")); + QCOMPARE(after.count("RRULE"), 1); // the override carries none + + const QList<Occurrence> occ = CalendarStore::occurrences( + { e }, rome(21, 0), rome(26, 0)); + QCOMPARE(occ.size(), 5); // still five, one moved + QVERIFY(std::any_of(occ.cbegin(), occ.cend(), + [](const Occurrence &o) { return o.isOverride && o.start == rome(22, 15); })); +} + +void TestCalendarStore::editingTheSameOccurrenceAgainReplacesItsOverride() +{ + const CalEvent series = parse(vevent(kDailySeries)); + EventEdit edit = editOf(series); + edit.start = rome(22, 15); + edit.end = rome(22, 16); + QByteArray text = CalendarStore::applyEdit( + series.rawText, edit, CalendarStore::Scope::ThisOccurrence, rome(22, 10)); + edit.start = rome(22, 17); + edit.end = rome(22, 18); + text = CalendarStore::applyEdit(text, edit, CalendarStore::Scope::ThisOccurrence, rome(22, 10)); + const CalEvent e = reparse(text); + QCOMPARE(e.overrides.size(), 1); + QCOMPARE(e.overrides[0].start, rome(22, 17)); +} + +void TestCalendarStore::deletingOneOccurrenceAddsAnExdateAndDropsItsOverride() +{ + const CalEvent series = parse(vevent(kDailySeries)); + EventEdit edit = editOf(series); + edit.start = rome(23, 15); + edit.end = rome(23, 16); + const QByteArray withOverride = CalendarStore::applyEdit( + series.rawText, edit, CalendarStore::Scope::ThisOccurrence, rome(23, 10)); + + const QByteArray after = CalendarStore::deleteOccurrence(withOverride, rome(23, 10)); + const CalEvent e = reparse(after); + QCOMPARE(e.overrides.size(), 0); + QCOMPARE(e.exdates, QList<QDateTime>{ rome(23, 10) }); + QVERIFY2(after.contains("EXDATE;TZID=Europe/Rome:20260923T100000"), after.constData()); + QCOMPARE(CalendarStore::occurrences({ e }, rome(21, 0), rome(26, 0)).size(), 4); +} + QTEST_MAIN(TestCalendarStore) #include "test_calendarstore.moc" |
