Skip to content

Commit 425e770

Browse files
committed
feat(dav): implement personal absence settings
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
1 parent 3e6642a commit 425e770

File tree

12 files changed

+567
-197
lines changed

12 files changed

+567
-197
lines changed

apps/dav/appinfo/routes.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*
55
* @author Georg Ehrke <oc.list@georgehrke.com>
66
* @author Roeland Jago Douma <roeland@famdouma.nl>
7+
* @author Richard Steinmetz <richard@steinmetz.cloud>
78
*
89
* @license GNU AGPL version 3 or any later version
910
*
@@ -28,7 +29,9 @@
2829
['name' => 'invitation_response#accept', 'url' => '/invitation/accept/{token}', 'verb' => 'GET'],
2930
['name' => 'invitation_response#decline', 'url' => '/invitation/decline/{token}', 'verb' => 'GET'],
3031
['name' => 'invitation_response#options', 'url' => '/invitation/moreOptions/{token}', 'verb' => 'GET'],
31-
['name' => 'invitation_response#processMoreOptionsResult', 'url' => '/invitation/moreOptions/{token}', 'verb' => 'POST']
32+
['name' => 'invitation_response#processMoreOptionsResult', 'url' => '/invitation/moreOptions/{token}', 'verb' => 'POST'],
33+
['name' => 'availability_settings#updateAbsence', 'url' => '/settings/absence', 'verb' => 'POST'],
34+
['name' => 'availability_settings#clearAbsence', 'url' => '/settings/absence', 'verb' => 'DELETE'],
3235
],
3336
'ocs' => [
3437
['name' => 'direct#getUrl', 'url' => '/api/v1/direct', 'verb' => 'POST'],

apps/dav/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@
188188
'OCA\\DAV\\Connector\\Sabre\\SharesPlugin' => $baseDir . '/../lib/Connector/Sabre/SharesPlugin.php',
189189
'OCA\\DAV\\Connector\\Sabre\\TagList' => $baseDir . '/../lib/Connector/Sabre/TagList.php',
190190
'OCA\\DAV\\Connector\\Sabre\\TagsPlugin' => $baseDir . '/../lib/Connector/Sabre/TagsPlugin.php',
191+
'OCA\\DAV\\Controller\\AvailabilitySettingsController' => $baseDir . '/../lib/Controller/AvailabilitySettingsController.php',
191192
'OCA\\DAV\\Controller\\BirthdayCalendarController' => $baseDir . '/../lib/Controller/BirthdayCalendarController.php',
192193
'OCA\\DAV\\Controller\\DirectController' => $baseDir . '/../lib/Controller/DirectController.php',
193194
'OCA\\DAV\\Controller\\InvitationResponseController' => $baseDir . '/../lib/Controller/InvitationResponseController.php',

apps/dav/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ class ComposerStaticInitDAV
203203
'OCA\\DAV\\Connector\\Sabre\\SharesPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/SharesPlugin.php',
204204
'OCA\\DAV\\Connector\\Sabre\\TagList' => __DIR__ . '/..' . '/../lib/Connector/Sabre/TagList.php',
205205
'OCA\\DAV\\Connector\\Sabre\\TagsPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/TagsPlugin.php',
206+
'OCA\\DAV\\Controller\\AvailabilitySettingsController' => __DIR__ . '/..' . '/../lib/Controller/AvailabilitySettingsController.php',
206207
'OCA\\DAV\\Controller\\BirthdayCalendarController' => __DIR__ . '/..' . '/../lib/Controller/BirthdayCalendarController.php',
207208
'OCA\\DAV\\Controller\\DirectController' => __DIR__ . '/..' . '/../lib/Controller/DirectController.php',
208209
'OCA\\DAV\\Controller\\InvitationResponseController' => __DIR__ . '/..' . '/../lib/Controller/InvitationResponseController.php',
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
7+
*
8+
* @author Richard Steinmetz <richard@steinmetz.cloud>
9+
*
10+
* @license AGPL-3.0-or-later
11+
*
12+
* This program is free software: you can redistribute it and/or modify
13+
* it under the terms of the GNU General Public License as published by
14+
* the Free Software Foundation, either version 3 of the License, or
15+
* (at your option) any later version.
16+
*
17+
* This program is distributed in the hope that it will be useful,
18+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
* GNU General Public License for more details.
21+
*
22+
* You should have received a copy of the GNU General Public License
23+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
24+
*
25+
*/
26+
27+
namespace OCA\DAV\Controller;
28+
29+
use DateTimeImmutable;
30+
use OCA\DAV\AppInfo\Application;
31+
use OCA\DAV\Service\AbsenceService;
32+
use OCP\AppFramework\Controller;
33+
use OCP\AppFramework\Http;
34+
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
35+
use OCP\AppFramework\Http\JSONResponse;
36+
use OCP\AppFramework\Http\Response;
37+
use OCP\IRequest;
38+
39+
class AvailabilitySettingsController extends Controller {
40+
public function __construct(
41+
IRequest $request,
42+
private ?string $userId,
43+
private AbsenceService $absenceService,
44+
) {
45+
parent::__construct(Application::APP_ID, $request);
46+
}
47+
48+
/**
49+
* @throws \OCP\DB\Exception
50+
* @throws \Exception
51+
*/
52+
#[NoAdminRequired]
53+
public function updateAbsence(
54+
string $firstDay,
55+
string $lastDay,
56+
string $status,
57+
string $message,
58+
): Response {
59+
$userId = $this->userId;
60+
if ($userId === null) {
61+
return new JSONResponse([], Http::STATUS_FORBIDDEN);
62+
}
63+
64+
$parsedFirstDay = new DateTimeImmutable($firstDay);
65+
$parsedLastDay = new DateTimeImmutable($lastDay);
66+
if ($parsedFirstDay->getTimestamp() >= $parsedLastDay->getTimestamp()) {
67+
throw new \Exception('First day is on or after last day');
68+
}
69+
70+
$absence = $this->absenceService->createOrUpdateAbsence(
71+
$userId,
72+
$firstDay,
73+
$lastDay,
74+
$status,
75+
$message,
76+
);
77+
return new JSONResponse($absence);
78+
}
79+
80+
/**
81+
* @throws \OCP\DB\Exception
82+
*/
83+
#[NoAdminRequired]
84+
public function clearAbsence(): Response {
85+
$userId = $this->userId;
86+
if ($userId === null) {
87+
return new JSONResponse([], Http::STATUS_FORBIDDEN);
88+
}
89+
90+
$this->absenceService->clearAbsence($userId);
91+
return new JSONResponse([]);
92+
}
93+
94+
}

apps/dav/lib/Settings/AvailabilitySettings.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
77
*
88
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
9+
* @author Richard Steinmetz <richard@steinmetz.cloud>
910
*
1011
* @license GNU AGPL version 3 or any later version
1112
*
@@ -26,10 +27,13 @@
2627
namespace OCA\DAV\Settings;
2728

2829
use OCA\DAV\AppInfo\Application;
30+
use OCA\DAV\Db\AbsenceMapper;
31+
use OCP\AppFramework\Db\DoesNotExistException;
2932
use OCP\AppFramework\Http\TemplateResponse;
3033
use OCP\AppFramework\Services\IInitialState;
3134
use OCP\IConfig;
3235
use OCP\Settings\ISettings;
36+
use Psr\Log\LoggerInterface;
3337

3438
class AvailabilitySettings implements ISettings {
3539
protected IConfig $config;
@@ -38,7 +42,9 @@ class AvailabilitySettings implements ISettings {
3842

3943
public function __construct(IConfig $config,
4044
IInitialState $initialState,
41-
?string $userId) {
45+
?string $userId,
46+
private LoggerInterface $logger,
47+
private AbsenceMapper $absenceMapper) {
4248
$this->config = $config;
4349
$this->initialState = $initialState;
4450
$this->userId = $userId;
@@ -54,6 +60,25 @@ public function getForm(): TemplateResponse {
5460
'no'
5561
)
5662
);
63+
$hideAbsenceSettings = $this->config->getAppValue(
64+
Application::APP_ID,
65+
'hide_absence_settings',
66+
'yes',
67+
) === 'yes';
68+
$this->initialState->provideInitialState('hide_absence_settings', $hideAbsenceSettings);
69+
if (!$hideAbsenceSettings) {
70+
try {
71+
$absence = $this->absenceMapper->findByUserId($this->userId);
72+
$this->initialState->provideInitialState('absence', $absence);
73+
} catch (DoesNotExistException) {
74+
// The user has not yet set up an absence period.
75+
// Logging this error is not necessary.
76+
} catch (\OCP\DB\Exception $e) {
77+
$this->logger->error("Could not find absence data for user $this->userId: " . $e->getMessage(), [
78+
'exception' => $e,
79+
]);
80+
}
81+
}
5782

5883
return new TemplateResponse(Application::APP_ID, 'settings-personal-availability');
5984
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<!--
2+
- @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
3+
-
4+
- @author Richard Steinmetz <richard@steinmetz.cloud>
5+
-
6+
- @license AGPL-3.0-or-later
7+
-
8+
- This program is free software: you can redistribute it and/or modify
9+
- it under the terms of the GNU General Public License as published by
10+
- the Free Software Foundation, either version 3 of the License, or
11+
- (at your option) any later version.
12+
-
13+
- This program is distributed in the hope that it will be useful,
14+
- but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
- GNU General Public License for more details.
17+
-
18+
- You should have received a copy of the GNU General Public License
19+
- along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
-
21+
-->
22+
23+
<template>
24+
<div class="absence">
25+
<div class="absence__dates">
26+
<NcDateTimePickerNative id="absence-first-day"
27+
v-model="firstDay"
28+
:label="$t('dav', 'First day')"
29+
class="absence__dates__picker" />
30+
<NcDateTimePickerNative id="absence-last-day"
31+
v-model="lastDay"
32+
:label="$t('dav', 'Last day (inclusive)')"
33+
class="absence__dates__picker" />
34+
</div>
35+
<NcTextField :value.sync="status" :label="$t('dav', 'Short absence status')" />
36+
<NcTextArea :value.sync="message" :label="$t('dav', 'Long absence Message')" />
37+
38+
<div class="absence__buttons">
39+
<NcButton :disabled="loading || !valid"
40+
type="primary"
41+
@click="saveForm">
42+
{{ $t('dav', 'Save') }}
43+
</NcButton>
44+
<NcButton :disabled="loading || !valid"
45+
type="error"
46+
@click="clearAbsence">
47+
{{ $t('dav', 'Disable absence') }}
48+
</NcButton>
49+
</div>
50+
</div>
51+
</template>
52+
53+
<script>
54+
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
55+
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
56+
import NcTextArea from '@nextcloud/vue/dist/Components/NcTextArea.js'
57+
import NcDateTimePickerNative from '@nextcloud/vue/dist/Components/NcDateTimePickerNative.js'
58+
import { generateUrl } from '@nextcloud/router'
59+
import axios from '@nextcloud/axios'
60+
import { formatDateAsYMD } from '../utils/date.js'
61+
import { loadState } from '@nextcloud/initial-state'
62+
import { showError } from '@nextcloud/dialogs'
63+
64+
export default {
65+
name: 'AbsenceForm',
66+
components: {
67+
NcButton,
68+
NcTextField,
69+
NcTextArea,
70+
NcDateTimePickerNative,
71+
},
72+
data() {
73+
const { firstDay, lastDay, status, message } = loadState('dav', 'absence', {})
74+
75+
return {
76+
loading: false,
77+
status: status ?? '',
78+
message: message ?? '',
79+
firstDay: firstDay ? new Date(firstDay) : new Date(),
80+
lastDay: lastDay ? new Date(lastDay) : null,
81+
}
82+
},
83+
computed: {
84+
/**
85+
* @return {boolean}
86+
*/
87+
valid() {
88+
return !!this.firstDay
89+
&& !!this.lastDay
90+
&& !!this.status
91+
&& this.lastDay > this.firstDay
92+
},
93+
},
94+
methods: {
95+
resetForm() {
96+
this.status = ''
97+
this.message = ''
98+
this.firstDay = new Date()
99+
this.lastDay = null
100+
},
101+
async saveForm() {
102+
if (!this.valid) {
103+
return
104+
}
105+
106+
this.loading = true
107+
try {
108+
await axios.post(generateUrl('/apps/dav/settings/absence'), {
109+
firstDay: formatDateAsYMD(this.firstDay),
110+
lastDay: formatDateAsYMD(this.lastDay),
111+
status: this.status,
112+
message: this.message,
113+
})
114+
} catch (error) {
115+
showError(this.$t('dav', 'Failed to save your absence settings'))
116+
} finally {
117+
this.loading = false
118+
}
119+
},
120+
async clearAbsence() {
121+
this.loading = true
122+
try {
123+
await axios.delete(generateUrl('/apps/dav/settings/absence'))
124+
this.resetForm()
125+
} catch (error) {
126+
showError(this.$t('dav', 'Failed to clear your absence settings'))
127+
} finally {
128+
this.loading = false
129+
}
130+
},
131+
},
132+
}
133+
</script>
134+
135+
<style lang="scss" scoped>
136+
.absence {
137+
display: flex;
138+
flex-direction: column;
139+
gap: 5px;
140+
141+
&__dates {
142+
display: flex;
143+
gap: 10px;
144+
width: 100%;
145+
146+
&__picker {
147+
flex: 1 auto;
148+
149+
::v-deep .native-datetime-picker--input {
150+
margin-bottom: 0;
151+
}
152+
}
153+
}
154+
155+
&__buttons {
156+
display: flex;
157+
gap: 5px;
158+
}
159+
}
160+
</style>

0 commit comments

Comments
 (0)