Skip to content

Commit 2920c37

Browse files
rich0SebastianKrupinski
authored andcommitted
feat(dav): store multiple default calendar alarms as JSON
Signed-off-by: Richard Freeman <rich@rich0.org> Assisted-by: Grok:grok-4
1 parent cbbb991 commit 2920c37

7 files changed

Lines changed: 175 additions & 21 deletions

File tree

apps/dav/appinfo/info.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<name>WebDAV</name>
1111
<summary>WebDAV endpoint</summary>
1212
<description>WebDAV endpoint</description>
13-
<version>2.0.0-dev.0</version>
13+
<version>2.0.0-dev.1</version>
1414
<licence>agpl</licence>
1515
<author>owncloud.org</author>
1616
<namespace>DAV</namespace>

apps/dav/composer/composer/autoload_classmap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,8 @@
401401
'OCA\\DAV\\Migration\\Version1038Date20260302000000' => $baseDir . '/../lib/Migration/Version1038Date20260302000000.php',
402402
'OCA\\DAV\\Migration\\Version1039Date20260408000000' => $baseDir . '/../lib/Migration/Version1039Date20260408000000.php',
403403
'OCA\\DAV\\Migration\\Version1040Date20260805000000' => $baseDir . '/../lib/Migration/Version1040Date20260805000000.php',
404+
'OCA\\DAV\\Migration\\Version2000Date20260703100001' => $baseDir . '/../lib/Migration/Version2000Date20260703100001.php',
405+
'OCA\\DAV\\Migration\\Version2000Date20260703100002' => $baseDir . '/../lib/Migration/Version2000Date20260703100002.php',
404406
'OCA\\DAV\\Model\\ExampleEvent' => $baseDir . '/../lib/Model/ExampleEvent.php',
405407
'OCA\\DAV\\Paginate\\LimitedCopyIterator' => $baseDir . '/../lib/Paginate/LimitedCopyIterator.php',
406408
'OCA\\DAV\\Paginate\\PaginateCache' => $baseDir . '/../lib/Paginate/PaginateCache.php',

apps/dav/composer/composer/autoload_static.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,8 @@ class ComposerStaticInitDAV
416416
'OCA\\DAV\\Migration\\Version1038Date20260302000000' => __DIR__ . '/..' . '/../lib/Migration/Version1038Date20260302000000.php',
417417
'OCA\\DAV\\Migration\\Version1039Date20260408000000' => __DIR__ . '/..' . '/../lib/Migration/Version1039Date20260408000000.php',
418418
'OCA\\DAV\\Migration\\Version1040Date20260805000000' => __DIR__ . '/..' . '/../lib/Migration/Version1040Date20260805000000.php',
419+
'OCA\\DAV\\Migration\\Version2000Date20260703100001' => __DIR__ . '/..' . '/../lib/Migration/Version2000Date20260703100001.php',
420+
'OCA\\DAV\\Migration\\Version2000Date20260703100002' => __DIR__ . '/..' . '/../lib/Migration/Version2000Date20260703100002.php',
419421
'OCA\\DAV\\Model\\ExampleEvent' => __DIR__ . '/..' . '/../lib/Model/ExampleEvent.php',
420422
'OCA\\DAV\\Paginate\\LimitedCopyIterator' => __DIR__ . '/..' . '/../lib/Paginate/LimitedCopyIterator.php',
421423
'OCA\\DAV\\Paginate\\PaginateCache' => __DIR__ . '/..' . '/../lib/Paginate/PaginateCache.php',

apps/dav/lib/CalDAV/CalDavBackend.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
151151
'{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'],
152152
'{http://apple.com/ns/ical/}calendar-color' => ['calendarcolor', 'string'],
153153
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => ['deleted_at', 'int'],
154-
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}default-alarm-part-day' => ['default_alarm_pday', 'int'],
155-
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}default-alarm-full-day' => ['default_alarm_fday', 'int'],
154+
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}default-alarms-part-day' => ['default_alarms_pday', 'string'],
155+
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}default-alarms-full-day' => ['default_alarms_fday', 'string'],
156156
];
157157

158158
/**
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\DAV\Migration;
11+
12+
use Closure;
13+
use OCP\DB\ISchemaWrapper;
14+
use OCP\DB\Types;
15+
use OCP\IDBConnection;
16+
use OCP\Migration\Attributes\AddColumn;
17+
use OCP\Migration\Attributes\ColumnType;
18+
use OCP\Migration\Attributes\DataCleansing;
19+
use OCP\Migration\IOutput;
20+
use OCP\Migration\SimpleMigrationStep;
21+
use Override;
22+
23+
/**
24+
* Add JSON default-alarm columns and copy legacy single-int defaults via set-based UPDATEs.
25+
*/
26+
#[DataCleansing(table: 'calendars', description: 'Migrate legacy default_alarm_* integers to default_alarms_* JSON')]
27+
#[AddColumn(table: 'calendars', name: 'default_alarms_pday', type: ColumnType::TEXT)]
28+
#[AddColumn(table: 'calendars', name: 'default_alarms_fday', type: ColumnType::TEXT)]
29+
class Version2000Date20260703100001 extends SimpleMigrationStep {
30+
public function __construct(
31+
private IDBConnection $db,
32+
) {
33+
}
34+
35+
#[Override]
36+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
37+
/** @var ISchemaWrapper $schema */
38+
$schema = $schemaClosure();
39+
40+
$calendarsTable = $schema->getTable('calendars');
41+
42+
if (!$calendarsTable->hasColumn('default_alarms_pday')) {
43+
$calendarsTable->addColumn('default_alarms_pday', Types::TEXT, [
44+
'notnull' => false,
45+
'default' => null,
46+
]);
47+
}
48+
49+
if (!$calendarsTable->hasColumn('default_alarms_fday')) {
50+
$calendarsTable->addColumn('default_alarms_fday', Types::TEXT, [
51+
'notnull' => false,
52+
'default' => null,
53+
]);
54+
}
55+
56+
return $schema;
57+
}
58+
59+
#[Override]
60+
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
61+
/** @var ISchemaWrapper $schema */
62+
$schema = $schemaClosure();
63+
$calendarsTable = $schema->getTable('calendars');
64+
65+
if ($calendarsTable->hasColumn('default_alarm_pday') && $calendarsTable->hasColumn('default_alarms_pday')) {
66+
$this->migrateLegacyIntColumn('default_alarm_pday', 'default_alarms_pday');
67+
}
68+
69+
if ($calendarsTable->hasColumn('default_alarm_fday') && $calendarsTable->hasColumn('default_alarms_fday')) {
70+
$this->migrateLegacyIntColumn('default_alarm_fday', 'default_alarms_fday');
71+
}
72+
}
73+
74+
/**
75+
* Encode a single legacy trigger int as [{"trigger":N,"action":"DISPLAY"}]
76+
* for all rows still missing the JSON column value.
77+
*/
78+
private function migrateLegacyIntColumn(string $legacyColumn, string $jsonColumn): void {
79+
$qb = $this->db->getQueryBuilder();
80+
$qb->update('calendars')
81+
->set($jsonColumn, $qb->func()->concat(
82+
$qb->expr()->literal('[{"trigger":'),
83+
$legacyColumn,
84+
$qb->expr()->literal(',"action":"DISPLAY"}]'),
85+
))
86+
->where($qb->expr()->isNotNull($legacyColumn))
87+
->andWhere($qb->expr()->isNull($jsonColumn));
88+
$qb->executeStatement();
89+
}
90+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\DAV\Migration;
11+
12+
use Closure;
13+
use OCP\DB\ISchemaWrapper;
14+
use OCP\Migration\Attributes\DropColumn;
15+
use OCP\Migration\IOutput;
16+
use OCP\Migration\SimpleMigrationStep;
17+
use Override;
18+
19+
/**
20+
* Drop legacy single-int default alarm columns after JSON migration.
21+
*/
22+
#[DropColumn(table: 'calendars', name: 'default_alarm_pday', description: 'Replaced by default_alarms_pday JSON')]
23+
#[DropColumn(table: 'calendars', name: 'default_alarm_fday', description: 'Replaced by default_alarms_fday JSON')]
24+
class Version2000Date20260703100002 extends SimpleMigrationStep {
25+
#[Override]
26+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
27+
/** @var ISchemaWrapper $schema */
28+
$schema = $schemaClosure();
29+
30+
$calendarsTable = $schema->getTable('calendars');
31+
32+
if ($calendarsTable->hasColumn('default_alarm_pday')) {
33+
$calendarsTable->dropColumn('default_alarm_pday');
34+
}
35+
36+
if ($calendarsTable->hasColumn('default_alarm_fday')) {
37+
$calendarsTable->dropColumn('default_alarm_fday');
38+
}
39+
40+
return $schema;
41+
}
42+
}

apps/dav/tests/unit/CalDAV/CalDavBackendTest.php

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2081,46 +2081,64 @@ public function testUnshare(): void {
20812081

20822082
}
20832083

2084-
public function testDefaultAlarmProperties(): void {
2084+
public function testDefaultAlarmsProperties(): void {
20852085
$calendarId = $this->createTestCalendar();
20862086

2087-
// Test setting both default alarm properties
2087+
$partDayProperty = '{http://nextcloud.com/ns}default-alarms-part-day';
2088+
$fullDayProperty = '{http://nextcloud.com/ns}default-alarms-full-day';
2089+
2090+
$partDayJson = json_encode([
2091+
['trigger' => -86400, 'action' => 'EMAIL'],
2092+
['trigger' => -900, 'action' => 'DISPLAY'],
2093+
], JSON_THROW_ON_ERROR);
2094+
$fullDayJson = json_encode([
2095+
['trigger' => -3600, 'action' => 'EMAIL'],
2096+
], JSON_THROW_ON_ERROR);
2097+
2098+
// Set plural default-alarms properties
20882099
$patch = new PropPatch([
2089-
'{http://nextcloud.com/ns}default-alarm-part-day' => -900,
2090-
'{http://nextcloud.com/ns}default-alarm-full-day' => -3600,
2100+
$partDayProperty => $partDayJson,
2101+
$fullDayProperty => $fullDayJson,
20912102
]);
20922103
$this->backend->updateCalendar($calendarId, $patch);
20932104
$patch->commit();
20942105

2095-
// Verify the properties were set
20962106
$calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
20972107
$this->assertCount(1, $calendars);
2098-
$this->assertEquals(-900, $calendars[0]['{http://nextcloud.com/ns}default-alarm-part-day']);
2099-
$this->assertEquals(-3600, $calendars[0]['{http://nextcloud.com/ns}default-alarm-full-day']);
2100-
2101-
// Test updating to different values
2108+
$this->assertEquals($partDayJson, $calendars[0][$partDayProperty]);
2109+
$this->assertEquals($fullDayJson, $calendars[0][$fullDayProperty]);
2110+
$this->assertArrayNotHasKey('{http://nextcloud.com/ns}default-alarm-part-day', $calendars[0]);
2111+
$this->assertArrayNotHasKey('{http://nextcloud.com/ns}default-alarm-full-day', $calendars[0]);
2112+
2113+
// Update to different values
2114+
$updatedPartDayJson = json_encode([
2115+
['trigger' => -1800, 'action' => 'DISPLAY'],
2116+
], JSON_THROW_ON_ERROR);
2117+
$updatedFullDayJson = json_encode([
2118+
['trigger' => -43200, 'action' => 'DISPLAY'],
2119+
], JSON_THROW_ON_ERROR);
21022120
$patch = new PropPatch([
2103-
'{http://nextcloud.com/ns}default-alarm-part-day' => -86400,
2104-
'{http://nextcloud.com/ns}default-alarm-full-day' => -43200,
2121+
$partDayProperty => $updatedPartDayJson,
2122+
$fullDayProperty => $updatedFullDayJson,
21052123
]);
21062124
$this->backend->updateCalendar($calendarId, $patch);
21072125
$patch->commit();
21082126

21092127
$calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
2110-
$this->assertEquals(-86400, $calendars[0]['{http://nextcloud.com/ns}default-alarm-part-day']);
2111-
$this->assertEquals(-43200, $calendars[0]['{http://nextcloud.com/ns}default-alarm-full-day']);
2128+
$this->assertEquals($updatedPartDayJson, $calendars[0][$partDayProperty]);
2129+
$this->assertEquals($updatedFullDayJson, $calendars[0][$fullDayProperty]);
21122130

2113-
// Test setting to null
2131+
// Clear properties
21142132
$patch = new PropPatch([
2115-
'{http://nextcloud.com/ns}default-alarm-part-day' => null,
2116-
'{http://nextcloud.com/ns}default-alarm-full-day' => null,
2133+
$partDayProperty => null,
2134+
$fullDayProperty => null,
21172135
]);
21182136
$this->backend->updateCalendar($calendarId, $patch);
21192137
$patch->commit();
21202138

21212139
$calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
2122-
$this->assertNull($calendars[0]['{http://nextcloud.com/ns}default-alarm-part-day']);
2123-
$this->assertNull($calendars[0]['{http://nextcloud.com/ns}default-alarm-full-day']);
2140+
$this->assertNull($calendars[0][$partDayProperty]);
2141+
$this->assertNull($calendars[0][$fullDayProperty]);
21242142

21252143
// Clean up
21262144
$this->backend->deleteCalendar($calendars[0]['id'], true);

0 commit comments

Comments
 (0)