mirror of
https://github.com/nextcloud/server.git
synced 2026-04-22 23:03:00 -04:00
feat: create example event when a user logs in for the first time
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
This commit is contained in:
parent
10852e01be
commit
4a6909ffef
23 changed files with 971 additions and 41 deletions
|
|
@ -278,6 +278,7 @@ return array(
|
|||
'OCA\\DAV\\Events\\SubscriptionCreatedEvent' => $baseDir . '/../lib/Events/SubscriptionCreatedEvent.php',
|
||||
'OCA\\DAV\\Events\\SubscriptionDeletedEvent' => $baseDir . '/../lib/Events/SubscriptionDeletedEvent.php',
|
||||
'OCA\\DAV\\Events\\SubscriptionUpdatedEvent' => $baseDir . '/../lib/Events/SubscriptionUpdatedEvent.php',
|
||||
'OCA\\DAV\\Exception\\ExampleEventException' => $baseDir . '/../lib/Exception/ExampleEventException.php',
|
||||
'OCA\\DAV\\Exception\\ServerMaintenanceMode' => $baseDir . '/../lib/Exception/ServerMaintenanceMode.php',
|
||||
'OCA\\DAV\\Exception\\UnsupportedLimitOnInitialSyncException' => $baseDir . '/../lib/Exception/UnsupportedLimitOnInitialSyncException.php',
|
||||
'OCA\\DAV\\Files\\BrowserErrorPagePlugin' => $baseDir . '/../lib/Files/BrowserErrorPagePlugin.php',
|
||||
|
|
@ -349,6 +350,7 @@ return array(
|
|||
'OCA\\DAV\\Migration\\Version1029Date20231004091403' => $baseDir . '/../lib/Migration/Version1029Date20231004091403.php',
|
||||
'OCA\\DAV\\Migration\\Version1030Date20240205103243' => $baseDir . '/../lib/Migration/Version1030Date20240205103243.php',
|
||||
'OCA\\DAV\\Migration\\Version1031Date20240610134258' => $baseDir . '/../lib/Migration/Version1031Date20240610134258.php',
|
||||
'OCA\\DAV\\Model\\ExampleEvent' => $baseDir . '/../lib/Model/ExampleEvent.php',
|
||||
'OCA\\DAV\\Paginate\\LimitedCopyIterator' => $baseDir . '/../lib/Paginate/LimitedCopyIterator.php',
|
||||
'OCA\\DAV\\Paginate\\PaginateCache' => $baseDir . '/../lib/Paginate/PaginateCache.php',
|
||||
'OCA\\DAV\\Paginate\\PaginatePlugin' => $baseDir . '/../lib/Paginate/PaginatePlugin.php',
|
||||
|
|
@ -365,6 +367,7 @@ return array(
|
|||
'OCA\\DAV\\ServerFactory' => $baseDir . '/../lib/ServerFactory.php',
|
||||
'OCA\\DAV\\Service\\AbsenceService' => $baseDir . '/../lib/Service/AbsenceService.php',
|
||||
'OCA\\DAV\\Service\\DefaultContactService' => $baseDir . '/../lib/Service/DefaultContactService.php',
|
||||
'OCA\\DAV\\Service\\ExampleEventService' => $baseDir . '/../lib/Service/ExampleEventService.php',
|
||||
'OCA\\DAV\\Settings\\Admin\\SystemAddressBookSettings' => $baseDir . '/../lib/Settings/Admin/SystemAddressBookSettings.php',
|
||||
'OCA\\DAV\\Settings\\AvailabilitySettings' => $baseDir . '/../lib/Settings/AvailabilitySettings.php',
|
||||
'OCA\\DAV\\Settings\\CalDAVSettings' => $baseDir . '/../lib/Settings/CalDAVSettings.php',
|
||||
|
|
|
|||
|
|
@ -293,6 +293,7 @@ class ComposerStaticInitDAV
|
|||
'OCA\\DAV\\Events\\SubscriptionCreatedEvent' => __DIR__ . '/..' . '/../lib/Events/SubscriptionCreatedEvent.php',
|
||||
'OCA\\DAV\\Events\\SubscriptionDeletedEvent' => __DIR__ . '/..' . '/../lib/Events/SubscriptionDeletedEvent.php',
|
||||
'OCA\\DAV\\Events\\SubscriptionUpdatedEvent' => __DIR__ . '/..' . '/../lib/Events/SubscriptionUpdatedEvent.php',
|
||||
'OCA\\DAV\\Exception\\ExampleEventException' => __DIR__ . '/..' . '/../lib/Exception/ExampleEventException.php',
|
||||
'OCA\\DAV\\Exception\\ServerMaintenanceMode' => __DIR__ . '/..' . '/../lib/Exception/ServerMaintenanceMode.php',
|
||||
'OCA\\DAV\\Exception\\UnsupportedLimitOnInitialSyncException' => __DIR__ . '/..' . '/../lib/Exception/UnsupportedLimitOnInitialSyncException.php',
|
||||
'OCA\\DAV\\Files\\BrowserErrorPagePlugin' => __DIR__ . '/..' . '/../lib/Files/BrowserErrorPagePlugin.php',
|
||||
|
|
@ -364,6 +365,7 @@ class ComposerStaticInitDAV
|
|||
'OCA\\DAV\\Migration\\Version1029Date20231004091403' => __DIR__ . '/..' . '/../lib/Migration/Version1029Date20231004091403.php',
|
||||
'OCA\\DAV\\Migration\\Version1030Date20240205103243' => __DIR__ . '/..' . '/../lib/Migration/Version1030Date20240205103243.php',
|
||||
'OCA\\DAV\\Migration\\Version1031Date20240610134258' => __DIR__ . '/..' . '/../lib/Migration/Version1031Date20240610134258.php',
|
||||
'OCA\\DAV\\Model\\ExampleEvent' => __DIR__ . '/..' . '/../lib/Model/ExampleEvent.php',
|
||||
'OCA\\DAV\\Paginate\\LimitedCopyIterator' => __DIR__ . '/..' . '/../lib/Paginate/LimitedCopyIterator.php',
|
||||
'OCA\\DAV\\Paginate\\PaginateCache' => __DIR__ . '/..' . '/../lib/Paginate/PaginateCache.php',
|
||||
'OCA\\DAV\\Paginate\\PaginatePlugin' => __DIR__ . '/..' . '/../lib/Paginate/PaginatePlugin.php',
|
||||
|
|
@ -380,6 +382,7 @@ class ComposerStaticInitDAV
|
|||
'OCA\\DAV\\ServerFactory' => __DIR__ . '/..' . '/../lib/ServerFactory.php',
|
||||
'OCA\\DAV\\Service\\AbsenceService' => __DIR__ . '/..' . '/../lib/Service/AbsenceService.php',
|
||||
'OCA\\DAV\\Service\\DefaultContactService' => __DIR__ . '/..' . '/../lib/Service/DefaultContactService.php',
|
||||
'OCA\\DAV\\Service\\ExampleEventService' => __DIR__ . '/..' . '/../lib/Service/ExampleEventService.php',
|
||||
'OCA\\DAV\\Settings\\Admin\\SystemAddressBookSettings' => __DIR__ . '/..' . '/../lib/Settings/Admin/SystemAddressBookSettings.php',
|
||||
'OCA\\DAV\\Settings\\AvailabilitySettings' => __DIR__ . '/..' . '/../lib/Settings/AvailabilitySettings.php',
|
||||
'OCA\\DAV\\Settings\\CalDAVSettings' => __DIR__ . '/..' . '/../lib/Settings/CalDAVSettings.php',
|
||||
|
|
|
|||
|
|
@ -10,9 +10,13 @@ declare(strict_types=1);
|
|||
namespace OCA\DAV\Controller;
|
||||
|
||||
use OCA\DAV\AppInfo\Application;
|
||||
use OCA\DAV\Service\ExampleEventService;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\ApiController;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
|
||||
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
|
||||
use OCP\AppFramework\Http\DataDownloadResponse;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\Files\AppData\IAppDataFactory;
|
||||
use OCP\Files\IAppData;
|
||||
|
|
@ -23,12 +27,14 @@ use Psr\Log\LoggerInterface;
|
|||
|
||||
class ExampleContentController extends ApiController {
|
||||
private IAppData $appData;
|
||||
|
||||
public function __construct(
|
||||
IRequest $request,
|
||||
IAppManager $appManager,
|
||||
private IConfig $config,
|
||||
private IAppDataFactory $appDataFactory,
|
||||
private IAppManager $appManager,
|
||||
private LoggerInterface $logger,
|
||||
private ExampleEventService $exampleEventService,
|
||||
) {
|
||||
parent::__construct(Application::APP_ID, $request);
|
||||
$this->appData = $this->appDataFactory->get('dav');
|
||||
|
|
@ -83,4 +89,37 @@ class ExampleContentController extends ApiController {
|
|||
return $folder->fileExists('defaultContact.vcf');
|
||||
}
|
||||
|
||||
#[FrontpageRoute(verb: 'POST', url: '/api/exampleEvent/enable')]
|
||||
public function setCreateExampleEvent(bool $enable): JSONResponse {
|
||||
$this->exampleEventService->setCreateExampleEvent($enable);
|
||||
return new JsonResponse([]);
|
||||
}
|
||||
|
||||
#[FrontpageRoute(verb: 'GET', url: '/api/exampleEvent/event')]
|
||||
#[NoCSRFRequired]
|
||||
public function downloadExampleEvent(): DataDownloadResponse {
|
||||
$exampleEvent = $this->exampleEventService->getExampleEvent();
|
||||
return new DataDownloadResponse(
|
||||
$exampleEvent->getIcs(),
|
||||
'example_event.ics',
|
||||
'text/calendar',
|
||||
);
|
||||
}
|
||||
|
||||
#[FrontpageRoute(verb: 'POST', url: '/api/exampleEvent/event')]
|
||||
public function uploadExampleEvent(string $ics): JSONResponse {
|
||||
if (!$this->exampleEventService->shouldCreateExampleEvent()) {
|
||||
return new JSONResponse([], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
$this->exampleEventService->saveCustomExampleEvent($ics);
|
||||
return new JsonResponse([]);
|
||||
}
|
||||
|
||||
#[FrontpageRoute(verb: 'DELETE', url: '/api/exampleEvent/event')]
|
||||
public function deleteExampleEvent(): JSONResponse {
|
||||
$this->exampleEventService->deleteCustomExampleEvent();
|
||||
return new JsonResponse([]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
13
apps/dav/lib/Exception/ExampleEventException.php
Normal file
13
apps/dav/lib/Exception/ExampleEventException.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Exception;
|
||||
|
||||
class ExampleEventException extends \Exception {
|
||||
}
|
||||
|
|
@ -13,13 +13,13 @@ use OCA\DAV\CalDAV\CalDavBackend;
|
|||
use OCA\DAV\CardDAV\CardDavBackend;
|
||||
use OCA\DAV\CardDAV\SyncService;
|
||||
use OCA\DAV\Service\DefaultContactService;
|
||||
use OCA\DAV\Service\ExampleEventService;
|
||||
use OCP\Accounts\UserUpdatedEvent;
|
||||
use OCP\Defaults;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use OCP\Server;
|
||||
use OCP\User\Events\BeforeUserDeletedEvent;
|
||||
use OCP\User\Events\BeforeUserIdUnassignedEvent;
|
||||
use OCP\User\Events\UserChangedEvent;
|
||||
|
|
@ -47,6 +47,8 @@ class UserEventsListener implements IEventListener {
|
|||
private CardDavBackend $cardDav,
|
||||
private Defaults $themingDefaults,
|
||||
private DefaultContactService $defaultContactService,
|
||||
private ExampleEventService $exampleEventService,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -137,17 +139,31 @@ class UserEventsListener implements IEventListener {
|
|||
|
||||
public function firstLogin(IUser $user): void {
|
||||
$principal = 'principals/users/' . $user->getUID();
|
||||
|
||||
$calendarId = null;
|
||||
if ($this->calDav->getCalendarsForUserCount($principal) === 0) {
|
||||
try {
|
||||
$this->calDav->createCalendar($principal, CalDavBackend::PERSONAL_CALENDAR_URI, [
|
||||
$calendarId = $this->calDav->createCalendar($principal, CalDavBackend::PERSONAL_CALENDAR_URI, [
|
||||
'{DAV:}displayname' => CalDavBackend::PERSONAL_CALENDAR_NAME,
|
||||
'{http://apple.com/ns/ical/}calendar-color' => $this->themingDefaults->getColorPrimary(),
|
||||
'components' => 'VEVENT'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
|
||||
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
if ($calendarId !== null) {
|
||||
try {
|
||||
$this->exampleEventService->createExampleEvent($calendarId);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Failed to create example event: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'userId' => $user->getUID(),
|
||||
'calendarId' => $calendarId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$addressBookId = null;
|
||||
if ($this->cardDav->getAddressBooksForUserCount($principal) === 0) {
|
||||
try {
|
||||
|
|
@ -155,7 +171,7 @@ class UserEventsListener implements IEventListener {
|
|||
'{DAV:}displayname' => CardDavBackend::PERSONAL_ADDRESSBOOK_NAME,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
|
||||
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
if ($addressBookId) {
|
||||
|
|
|
|||
31
apps/dav/lib/Model/ExampleEvent.php
Normal file
31
apps/dav/lib/Model/ExampleEvent.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Model;
|
||||
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
|
||||
/**
|
||||
* Simple DTO to store a parsed example event and its UID.
|
||||
*/
|
||||
final class ExampleEvent {
|
||||
public function __construct(
|
||||
private readonly VCalendar $vCalendar,
|
||||
private readonly string $uid,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getUid(): string {
|
||||
return $this->uid;
|
||||
}
|
||||
|
||||
public function getIcs(): string {
|
||||
return $this->vCalendar->serialize();
|
||||
}
|
||||
}
|
||||
205
apps/dav/lib/Service/ExampleEventService.php
Normal file
205
apps/dav/lib/Service/ExampleEventService.php
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Service;
|
||||
|
||||
use OCA\DAV\AppInfo\Application;
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\Exception\ExampleEventException;
|
||||
use OCA\DAV\Model\ExampleEvent;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\Files\IAppData;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\Files\NotPermittedException;
|
||||
use OCP\IAppConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\Security\ISecureRandom;
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
use Sabre\VObject\Component\VEvent;
|
||||
|
||||
class ExampleEventService {
|
||||
private const FOLDER_NAME = 'example_event';
|
||||
private const FILE_NAME = 'example_event.ics';
|
||||
private const ENABLE_CONFIG_KEY = 'create_example_event';
|
||||
|
||||
public function __construct(
|
||||
private readonly CalDavBackend $calDavBackend,
|
||||
private readonly ISecureRandom $random,
|
||||
private readonly ITimeFactory $time,
|
||||
private readonly IAppData $appData,
|
||||
private readonly IAppConfig $appConfig,
|
||||
private readonly IL10N $l10n,
|
||||
) {
|
||||
}
|
||||
|
||||
public function createExampleEvent(int $calendarId): void {
|
||||
if (!$this->shouldCreateExampleEvent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$exampleEvent = $this->getExampleEvent();
|
||||
$uid = $exampleEvent->getUid();
|
||||
$this->calDavBackend->createCalendarObject(
|
||||
$calendarId,
|
||||
"$uid.ics",
|
||||
$exampleEvent->getIcs(),
|
||||
);
|
||||
}
|
||||
|
||||
private function getStartDate(): \DateTimeInterface {
|
||||
return $this->time->now()
|
||||
->add(new \DateInterval('P7D'))
|
||||
->setTime(10, 00);
|
||||
}
|
||||
|
||||
private function getEndDate(): \DateTimeInterface {
|
||||
return $this->time->now()
|
||||
->add(new \DateInterval('P7D'))
|
||||
->setTime(11, 00);
|
||||
}
|
||||
|
||||
private function getDefaultEvent(string $uid): VCalendar {
|
||||
$defaultDescription = $this->l10n->t(<<<EOF
|
||||
Welcome to Nextcloud Calendar!
|
||||
|
||||
This is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!
|
||||
|
||||
With Nextcloud Calendar, you can:
|
||||
- Create, edit, and manage events effortlessly.
|
||||
- Create multiple calendars and share them with teammates, friends, or family.
|
||||
- Check availability and display your busy times to others.
|
||||
- Seamlessly integrate with apps and devices via CalDAV.
|
||||
- Customize your experience: schedule recurring events, adjust notifications and other settings.
|
||||
EOF);
|
||||
|
||||
$vCalendar = new VCalendar();
|
||||
$props = [
|
||||
'UID' => $uid,
|
||||
'DTSTAMP' => $this->time->now(),
|
||||
'SUMMARY' => $this->l10n->t('Example event - open me!'),
|
||||
'DTSTART' => $this->getStartDate(),
|
||||
'DTEND' => $this->getEndDate(),
|
||||
'DESCRIPTION' => $defaultDescription,
|
||||
];
|
||||
$vCalendar->add('VEVENT', $props);
|
||||
return $vCalendar;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null The ics of the custom example event or null if no custom event was uploaded.
|
||||
* @throws ExampleEventException If reading the custom ics file fails.
|
||||
*/
|
||||
private function getCustomExampleEvent(): ?string {
|
||||
try {
|
||||
$folder = $this->appData->getFolder(self::FOLDER_NAME);
|
||||
$icsFile = $folder->getFile(self::FILE_NAME);
|
||||
} catch (NotFoundException $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return $icsFile->getContent();
|
||||
} catch (NotFoundException|NotPermittedException $e) {
|
||||
throw new ExampleEventException(
|
||||
'Failed to read custom example event',
|
||||
0,
|
||||
$e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured example event or the default one.
|
||||
*
|
||||
* @throws ExampleEventException If loading the custom example event fails.
|
||||
*/
|
||||
public function getExampleEvent(): ExampleEvent {
|
||||
$uid = $this->random->generate(32, ISecureRandom::CHAR_ALPHANUMERIC);
|
||||
$customIcs = $this->getCustomExampleEvent();
|
||||
if ($customIcs === null) {
|
||||
return new ExampleEvent($this->getDefaultEvent($uid), $uid);
|
||||
}
|
||||
|
||||
[$vCalendar, $vEvent] = $this->parseEvent($customIcs);
|
||||
$vEvent->UID = $uid;
|
||||
$vEvent->DTSTART = $this->getStartDate();
|
||||
$vEvent->DTEND = $this->getEndDate();
|
||||
$vEvent->remove('ORGANIZER');
|
||||
$vEvent->remove('ATTENDEE');
|
||||
return new ExampleEvent($vCalendar, $uid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return list{VCalendar, VEvent} The VCALENDAR document and its VEVENT child component
|
||||
* @throws ExampleEventException If parsing the event fails or if it is invalid.
|
||||
*/
|
||||
private function parseEvent(string $ics): array {
|
||||
try {
|
||||
$vCalendar = \Sabre\VObject\Reader::read($ics);
|
||||
if (!($vCalendar instanceof VCalendar)) {
|
||||
throw new ExampleEventException('Custom event does not contain a VCALENDAR component');
|
||||
}
|
||||
|
||||
/** @var VEvent|null $vEvent */
|
||||
$vEvent = $vCalendar->getBaseComponent('VEVENT');
|
||||
if ($vEvent === null) {
|
||||
throw new ExampleEventException('Custom event does not contain a VEVENT component');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
throw new ExampleEventException('Failed to parse custom event: ' . $e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
return [$vCalendar, $vEvent];
|
||||
}
|
||||
|
||||
public function saveCustomExampleEvent(string $ics): void {
|
||||
// Parse and validate the event before attempting to save it to prevent run time errors
|
||||
$this->parseEvent($ics);
|
||||
|
||||
try {
|
||||
$folder = $this->appData->getFolder(self::FOLDER_NAME);
|
||||
} catch (NotFoundException $e) {
|
||||
$folder = $this->appData->newFolder(self::FOLDER_NAME);
|
||||
}
|
||||
|
||||
try {
|
||||
$existingFile = $folder->getFile(self::FILE_NAME);
|
||||
$existingFile->putContent($ics);
|
||||
} catch (NotFoundException $e) {
|
||||
$folder->newFile(self::FILE_NAME, $ics);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteCustomExampleEvent(): void {
|
||||
try {
|
||||
$folder = $this->appData->getFolder(self::FOLDER_NAME);
|
||||
$file = $folder->getFile(self::FILE_NAME);
|
||||
} catch (NotFoundException $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
$file->delete();
|
||||
}
|
||||
|
||||
public function hasCustomExampleEvent(): bool {
|
||||
try {
|
||||
return $this->getCustomExampleEvent() !== null;
|
||||
} catch (ExampleEventException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function setCreateExampleEvent(bool $enable): void {
|
||||
$this->appConfig->setValueBool(Application::APP_ID, self::ENABLE_CONFIG_KEY, $enable);
|
||||
}
|
||||
|
||||
public function shouldCreateExampleEvent(): bool {
|
||||
return $this->appConfig->getValueBool(Application::APP_ID, self::ENABLE_CONFIG_KEY, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ declare(strict_types=1);
|
|||
namespace OCA\DAV\Settings;
|
||||
|
||||
use OCA\DAV\AppInfo\Application;
|
||||
use OCA\DAV\Service\ExampleEventService;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
|
|
@ -16,21 +17,40 @@ use OCP\IConfig;
|
|||
use OCP\Settings\ISettings;
|
||||
|
||||
class ExampleContentSettings implements ISettings {
|
||||
|
||||
public function __construct(
|
||||
private IConfig $config,
|
||||
private IInitialState $initialState,
|
||||
private IAppManager $appManager,
|
||||
private readonly IConfig $config,
|
||||
private readonly IInitialState $initialState,
|
||||
private readonly IAppManager $appManager,
|
||||
private readonly ExampleEventService $exampleEventService,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getForm(): TemplateResponse {
|
||||
$enableDefaultContact = $this->config->getAppValue(Application::APP_ID, 'enableDefaultContact', 'no');
|
||||
$this->initialState->provideInitialState('enableDefaultContact', $enableDefaultContact);
|
||||
$calendarEnabled = $this->appManager->isEnabledForUser('calendar');
|
||||
$contactsEnabled = $this->appManager->isEnabledForUser('contacts');
|
||||
$this->initialState->provideInitialState('calendarEnabled', $calendarEnabled);
|
||||
$this->initialState->provideInitialState('contactsEnabled', $contactsEnabled);
|
||||
|
||||
if ($calendarEnabled) {
|
||||
$enableDefaultEvent = $this->exampleEventService->shouldCreateExampleEvent();
|
||||
$this->initialState->provideInitialState('create_example_event', $enableDefaultEvent);
|
||||
$this->initialState->provideInitialState(
|
||||
'has_custom_example_event',
|
||||
$this->exampleEventService->hasCustomExampleEvent(),
|
||||
);
|
||||
}
|
||||
|
||||
if ($contactsEnabled) {
|
||||
$enableDefaultContact = $this->config->getAppValue(Application::APP_ID, 'enableDefaultContact', 'no');
|
||||
$this->initialState->provideInitialState('enableDefaultContact', $enableDefaultContact);
|
||||
}
|
||||
|
||||
return new TemplateResponse(Application::APP_ID, 'settings-example-content');
|
||||
}
|
||||
|
||||
public function getSection(): ?string {
|
||||
if (!$this->appManager->isEnabledForUser('contacts')) {
|
||||
if (!$this->appManager->isEnabledForUser('contacts')
|
||||
&& !$this->appManager->isEnabledForUser('calendar')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -40,5 +60,4 @@ class ExampleContentSettings implements ISettings {
|
|||
public function getPriority(): int {
|
||||
return 10;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,20 +4,16 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<NcSettingsSection id="exmaple-content"
|
||||
:name="$t('dav', 'Example Content')"
|
||||
class="example-content-setting"
|
||||
:description="$t('dav', 'Set example content to be created on new user first login.')">
|
||||
<div class="example-content-setting__contacts">
|
||||
<input id="enable-default-contact"
|
||||
v-model="enableDefaultContact"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
@change="updateEnableDefaultContact">
|
||||
<label for="enable-default-contact"> {{ $t('dav',"Default contact is added to the user's own address book on user's first login.") }} </label>
|
||||
<div v-if="enableDefaultContact" class="example-content-setting__contacts__buttons">
|
||||
<div class="example-contact-settings">
|
||||
<div class="example-content-setting__form">
|
||||
<NcCheckboxRadioSwitch :checked="enableDefaultContact"
|
||||
type="switch"
|
||||
@update:model-value="updateEnableDefaultContact">
|
||||
{{ $t('dav',"Default contact is added to the user's own address book on user's first login.") }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<div v-if="enableDefaultContact" class="example-contact-settings__form__buttons">
|
||||
<NcButton type="primary"
|
||||
class="example-content-setting__contacts__buttons__button"
|
||||
class="example-contact-settings__form__buttons__button"
|
||||
@click="toggleModal">
|
||||
<template #icon>
|
||||
<IconUpload :size="20" />
|
||||
|
|
@ -25,7 +21,7 @@
|
|||
{{ $t('dav', 'Import contact') }}
|
||||
</NcButton>
|
||||
<NcButton type="secondary"
|
||||
class="example-content-setting__contacts__buttons__button"
|
||||
class="example-contact-settings__form__buttons__button"
|
||||
@click="resetContact">
|
||||
<template #icon>
|
||||
<IconRestore :size="20" />
|
||||
|
|
@ -48,18 +44,19 @@
|
|||
accept=".vcf"
|
||||
class="hidden-visually"
|
||||
@change="processFile">
|
||||
</NcSettingsSection>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { NcDialog, NcButton, NcSettingsSection } from '@nextcloud/vue'
|
||||
import { NcDialog, NcButton, NcCheckboxRadioSwitch } from '@nextcloud/vue'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import IconUpload from 'vue-material-design-icons/Upload.vue'
|
||||
import IconRestore from 'vue-material-design-icons/Restore.vue'
|
||||
import IconCancel from '@mdi/svg/svg/cancel.svg?raw'
|
||||
import IconCheck from '@mdi/svg/svg/check.svg?raw'
|
||||
import logger from '../service/logger.js'
|
||||
|
||||
const enableDefaultContact = loadState('dav', 'enableDefaultContact') === 'yes'
|
||||
|
||||
|
|
@ -68,7 +65,7 @@ export default {
|
|||
components: {
|
||||
NcDialog,
|
||||
NcButton,
|
||||
NcSettingsSection,
|
||||
NcCheckboxRadioSwitch,
|
||||
IconUpload,
|
||||
IconRestore,
|
||||
},
|
||||
|
|
@ -95,9 +92,10 @@ export default {
|
|||
methods: {
|
||||
updateEnableDefaultContact() {
|
||||
axios.put(generateUrl('apps/dav/api/defaultcontact/config'), {
|
||||
allow: this.enableDefaultContact ? 'yes' : 'no',
|
||||
}).catch(() => {
|
||||
allow: this.enableDefaultContact ? 'no' : 'yes',
|
||||
}).then(() => {
|
||||
this.enableDefaultContact = !this.enableDefaultContact
|
||||
}).catch(() => {
|
||||
showError(this.$t('dav', 'Error while saving settings'))
|
||||
})
|
||||
},
|
||||
|
|
@ -114,7 +112,7 @@ export default {
|
|||
showSuccess(this.$t('dav', 'Contact reset successfully'))
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error importing contact:', error)
|
||||
logger.error('Error importing contact:', { error })
|
||||
showError(this.$t('dav', 'Error while resetting contact'))
|
||||
})
|
||||
.finally(() => {
|
||||
|
|
@ -133,7 +131,7 @@ export default {
|
|||
await axios.put(generateUrl('/apps/dav/api/defaultcontact/contact'), { contactData: reader.result })
|
||||
showSuccess(this.$t('dav', 'Contact imported successfully'))
|
||||
} catch (error) {
|
||||
console.error('Error importing contact:', error)
|
||||
logger.error('Error importing contact:', { error })
|
||||
showError(this.$t('dav', 'Error while importing contact'))
|
||||
} finally {
|
||||
this.loading = false
|
||||
|
|
@ -146,11 +144,14 @@ export default {
|
|||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.example-content-setting{
|
||||
&__contacts{
|
||||
.example-contact-settings {
|
||||
margin-block-start: 2rem;
|
||||
|
||||
&__form{
|
||||
&__buttons{
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
|
||||
&__button{
|
||||
margin-inline-end: 5px;
|
||||
}
|
||||
214
apps/dav/src/components/ExampleEventSettings.vue
Normal file
214
apps/dav/src/components/ExampleEventSettings.vue
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="example-event-settings">
|
||||
<NcCheckboxRadioSwitch :checked="createExampleEvent"
|
||||
:disabled="savingConfig"
|
||||
type="switch"
|
||||
@update:model-value="updateCreateExampleEvent">
|
||||
{{ t('dav', "Add example event to user's calendar when they first log in") }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<div v-if="createExampleEvent"
|
||||
class="example-event-settings__buttons">
|
||||
<NcButton type="tertiary"
|
||||
:href="exampleEventDownloadUrl">
|
||||
<template #icon>
|
||||
<IconCalendarBlank :size="20" />
|
||||
</template>
|
||||
<span class="example-event-settings__buttons__download-link">
|
||||
example_event.ics
|
||||
<IconDownload :size="20" />
|
||||
</span>
|
||||
</NcButton>
|
||||
<NcButton type="secondary"
|
||||
@click="showImportModal = true">
|
||||
<template #icon>
|
||||
<IconUpload :size="20" />
|
||||
</template>
|
||||
{{ t('dav', 'Import calendar event') }}
|
||||
</NcButton>
|
||||
<NcButton v-if="hasCustomEvent"
|
||||
type="tertiary"
|
||||
:disabled="deleting"
|
||||
@click="deleteCustomEvent">
|
||||
<template #icon>
|
||||
<IconRestore :size="20" />
|
||||
</template>
|
||||
{{ t('dav', 'Restore default event') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
<NcDialog :open.sync="showImportModal"
|
||||
:name="t('dav', 'Import calendar event')">
|
||||
<div class="import-event-modal">
|
||||
<p>
|
||||
{{ t('dav', 'Uploading a new event will overwrite the existing one.') }}
|
||||
</p>
|
||||
<input ref="event-file"
|
||||
:disabled="uploading"
|
||||
type="file"
|
||||
accept=".ics,text/calendar"
|
||||
class="import-event-modal__file-picker"
|
||||
@change="selectFile" />
|
||||
<div class="import-event-modal__buttons">
|
||||
<NcButton :disabled="uploading || !selectedFile"
|
||||
type="primary"
|
||||
@click="uploadCustomEvent()">
|
||||
<template #icon>
|
||||
<IconUpload :size="20" />
|
||||
</template>
|
||||
{{ t('dav', 'Upload event') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</NcDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NcButton, NcCheckboxRadioSwitch, NcDialog } from '@nextcloud/vue'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import IconDownload from 'vue-material-design-icons/Download.vue'
|
||||
import IconCalendarBlank from 'vue-material-design-icons/CalendarBlank.vue'
|
||||
import IconUpload from 'vue-material-design-icons/Upload.vue'
|
||||
import IconRestore from 'vue-material-design-icons/Restore.vue'
|
||||
import * as ExampleEventService from '../service/ExampleEventService.js'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import logger from '../service/logger.js'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
export default {
|
||||
name: 'ExampleEventSettings',
|
||||
components: {
|
||||
NcButton,
|
||||
NcCheckboxRadioSwitch,
|
||||
NcDialog,
|
||||
IconDownload,
|
||||
IconCalendarBlank,
|
||||
IconUpload,
|
||||
IconRestore,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
createExampleEvent: loadState('dav', 'create_example_event', false),
|
||||
hasCustomEvent: loadState('dav', 'has_custom_example_event', false),
|
||||
showImportModal: false,
|
||||
uploading: false,
|
||||
deleting: false,
|
||||
savingConfig: false,
|
||||
selectedFile: undefined,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
exampleEventDownloadUrl() {
|
||||
return generateUrl('/apps/dav/api/exampleEvent/event')
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectFile() {
|
||||
this.selectedFile = this.$refs['event-file']?.files[0]
|
||||
},
|
||||
async updateCreateExampleEvent() {
|
||||
this.savingConfig = true
|
||||
|
||||
const enable = !this.createExampleEvent
|
||||
try {
|
||||
await ExampleEventService.setCreateExampleEvent(enable)
|
||||
} catch (error) {
|
||||
showError(t('dav', 'Failed to save example event creation setting'))
|
||||
logger.error('Failed to save example event creation setting', {
|
||||
error,
|
||||
enable,
|
||||
})
|
||||
} finally {
|
||||
this.savingConfig = false
|
||||
}
|
||||
|
||||
this.createExampleEvent = enable
|
||||
},
|
||||
uploadCustomEvent() {
|
||||
if (!this.selectedFile) {
|
||||
return
|
||||
}
|
||||
|
||||
this.uploading = true
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.addEventListener('load', async () => {
|
||||
const ics = reader.result
|
||||
|
||||
try {
|
||||
await ExampleEventService.uploadExampleEvent(ics)
|
||||
} catch (error) {
|
||||
showError(t('dav', 'Failed to upload the example event'))
|
||||
logger.error('Failed to upload example ICS', {
|
||||
error,
|
||||
ics,
|
||||
})
|
||||
return
|
||||
} finally {
|
||||
this.uploading = false
|
||||
}
|
||||
|
||||
showSuccess(t('dav', 'Custom example event was saved successfully'))
|
||||
this.showImportModal = false
|
||||
this.hasCustomEvent = true
|
||||
})
|
||||
reader.readAsText(this.selectedFile)
|
||||
},
|
||||
async deleteCustomEvent() {
|
||||
this.deleting = true
|
||||
|
||||
try {
|
||||
await ExampleEventService.deleteExampleEvent()
|
||||
} catch (error) {
|
||||
showError(t('dav', 'Failed to delete the custom example event'))
|
||||
logger.error('Failed to delete the custom example event', {
|
||||
error,
|
||||
})
|
||||
return
|
||||
} finally {
|
||||
this.deleting = false
|
||||
}
|
||||
|
||||
showSuccess(t('dav', 'Custom example event was deleted successfully'))
|
||||
this.hasCustomEvent = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.example-event-settings {
|
||||
margin-block: 2rem;
|
||||
|
||||
&__buttons {
|
||||
display: flex;
|
||||
gap: calc(var(--default-grid-baseline) * 2);
|
||||
margin-top: calc(var(--default-grid-baseline) * 2);
|
||||
|
||||
&__download-link {
|
||||
display: flex;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.import-event-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--default-grid-baseline) * 2);
|
||||
padding: calc(var(--default-grid-baseline) * 2);
|
||||
|
||||
&__file-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
43
apps/dav/src/service/ExampleEventService.js
Normal file
43
apps/dav/src/service/ExampleEventService.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import axios from '@nextcloud/axios'
|
||||
|
||||
/**
|
||||
* Configure the creation of example events on a user's first login.
|
||||
*
|
||||
* @param {boolean} enable Whether to enable or disable the feature.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export async function setCreateExampleEvent(enable) {
|
||||
const url = generateUrl('/apps/dav/api/exampleEvent/enable')
|
||||
await axios.post(url, {
|
||||
enable,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a custom example event.
|
||||
*
|
||||
* @param {string} ics The ICS data of the event.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export async function uploadExampleEvent(ics) {
|
||||
const url = generateUrl('/apps/dav/api/exampleEvent/event')
|
||||
await axios.post(url, {
|
||||
ics,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a previously uploaded custom example event.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export async function deleteExampleEvent() {
|
||||
const url = generateUrl('/apps/dav/api/exampleEvent/event')
|
||||
await axios.delete(url)
|
||||
}
|
||||
|
|
@ -4,10 +4,15 @@
|
|||
*/
|
||||
import Vue from 'vue'
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
import ExampleContactSettings from './views/ExampleContactSettings.vue'
|
||||
import ExampleContentSettingsSection from './views/ExampleContentSettingsSection.vue'
|
||||
|
||||
Vue.prototype.$t = translate
|
||||
Vue.mixin({
|
||||
methods: {
|
||||
t: translate,
|
||||
$t: translate,
|
||||
}
|
||||
})
|
||||
|
||||
const View = Vue.extend(ExampleContactSettings);
|
||||
const View = Vue.extend(ExampleContentSettingsSection);
|
||||
|
||||
(new View({})).$mount('#settings-example-content')
|
||||
|
|
|
|||
38
apps/dav/src/views/ExampleContentSettingsSection.vue
Normal file
38
apps/dav/src/views/ExampleContentSettingsSection.vue
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<template>
|
||||
<NcSettingsSection id="example-content"
|
||||
:name="$t('dav', 'Example content')"
|
||||
class="example-content-setting"
|
||||
:description="$t('dav', 'Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content.')">
|
||||
<ExampleContactSettings v-if="hasContactsApp" />
|
||||
<ExampleEventSettings v-if="hasCalendarApp" />
|
||||
</NcSettingsSection>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { NcSettingsSection } from '@nextcloud/vue'
|
||||
import ExampleEventSettings from '../components/ExampleEventSettings.vue'
|
||||
import ExampleContactSettings from '../components/ExampleContactSettings.vue'
|
||||
|
||||
export default {
|
||||
name: 'ExampleContentSettingsSection',
|
||||
components: {
|
||||
NcSettingsSection,
|
||||
ExampleContactSettings,
|
||||
ExampleEventSettings,
|
||||
},
|
||||
computed: {
|
||||
hasContactsApp() {
|
||||
return loadState('dav', 'contactsEnabled')
|
||||
},
|
||||
hasCalendarApp() {
|
||||
return loadState('dav', 'calendarEnabled')
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -15,10 +15,12 @@ use OCA\DAV\CardDAV\CardDavBackend;
|
|||
use OCA\DAV\CardDAV\SyncService;
|
||||
use OCA\DAV\Listener\UserEventsListener;
|
||||
use OCA\DAV\Service\DefaultContactService;
|
||||
use OCA\DAV\Service\ExampleEventService;
|
||||
use OCP\Defaults;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Test\TestCase;
|
||||
|
||||
class UserEventsListenerTest extends TestCase {
|
||||
|
|
@ -27,19 +29,24 @@ class UserEventsListenerTest extends TestCase {
|
|||
private CalDavBackend&MockObject $calDavBackend;
|
||||
private CardDavBackend&MockObject $cardDavBackend;
|
||||
private Defaults&MockObject $defaults;
|
||||
|
||||
private DefaultContactService&MockObject $defaultContactService;
|
||||
private ExampleEventService&MockObject $exampleEventService;
|
||||
private LoggerInterface&MockObject $logger;
|
||||
|
||||
private UserEventsListener $userEventsListener;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->userManager = $this->createMock(IUserManager::class);
|
||||
$this->syncService = $this->createMock(SyncService::class);
|
||||
$this->calDavBackend = $this->createMock(CalDavBackend::class);
|
||||
$this->cardDavBackend = $this->createMock(CardDavBackend::class);
|
||||
$this->defaults = $this->createMock(Defaults::class);
|
||||
$this->defaultContactService = $this->createMock(DefaultContactService::class);
|
||||
$this->exampleEventService = $this->createMock(ExampleEventService::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
|
||||
$this->userEventsListener = new UserEventsListener(
|
||||
$this->userManager,
|
||||
$this->syncService,
|
||||
|
|
@ -47,6 +54,8 @@ class UserEventsListenerTest extends TestCase {
|
|||
$this->cardDavBackend,
|
||||
$this->defaults,
|
||||
$this->defaultContactService,
|
||||
$this->exampleEventService,
|
||||
$this->logger,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +72,13 @@ class UserEventsListenerTest extends TestCase {
|
|||
'{DAV:}displayname' => 'Personal',
|
||||
'{http://apple.com/ns/ical/}calendar-color' => '#745bca',
|
||||
'components' => 'VEVENT'
|
||||
]);
|
||||
])
|
||||
->willReturn(1000);
|
||||
$this->calDavBackend->expects(self::never())
|
||||
->method('getCalendarsForUser');
|
||||
$this->exampleEventService->expects(self::once())
|
||||
->method('createExampleEvent')
|
||||
->with(1000);
|
||||
|
||||
$this->cardDavBackend->expects($this->once())->method('getAddressBooksForUserCount')->willReturn(0);
|
||||
$this->cardDavBackend->expects($this->once())->method('createAddressBook')->with(
|
||||
|
|
@ -79,6 +94,10 @@ class UserEventsListenerTest extends TestCase {
|
|||
|
||||
$this->calDavBackend->expects($this->once())->method('getCalendarsForUserCount')->willReturn(1);
|
||||
$this->calDavBackend->expects($this->never())->method('createCalendar');
|
||||
$this->calDavBackend->expects(self::never())
|
||||
->method('createCalendar');
|
||||
$this->exampleEventService->expects(self::never())
|
||||
->method('createExampleEvent');
|
||||
|
||||
$this->cardDavBackend->expects($this->once())->method('getAddressBooksForUserCount')->willReturn(1);
|
||||
$this->cardDavBackend->expects($this->never())->method('createAddressBook');
|
||||
|
|
|
|||
196
apps/dav/tests/unit/Service/ExampleEventServiceTest.php
Normal file
196
apps/dav/tests/unit/Service/ExampleEventServiceTest.php
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Tests\unit\Service;
|
||||
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\Service\ExampleEventService;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\Files\IAppData;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\Files\SimpleFS\ISimpleFile;
|
||||
use OCP\Files\SimpleFS\ISimpleFolder;
|
||||
use OCP\IAppConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\Security\ISecureRandom;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
|
||||
class ExampleEventServiceTest extends TestCase {
|
||||
private ExampleEventService $service;
|
||||
|
||||
private CalDavBackend&MockObject $calDavBackend;
|
||||
private ISecureRandom&MockObject $random;
|
||||
private ITimeFactory&MockObject $time;
|
||||
private IAppData&MockObject $appData;
|
||||
private IAppConfig&MockObject $appConfig;
|
||||
private IL10N&MockObject $l10n;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->calDavBackend = $this->createMock(CalDavBackend::class);
|
||||
$this->random = $this->createMock(ISecureRandom::class);
|
||||
$this->time = $this->createMock(ITimeFactory::class);
|
||||
$this->appData = $this->createMock(IAppData::class);
|
||||
$this->appConfig = $this->createMock(IAppConfig::class);
|
||||
$this->l10n = $this->createMock(IL10N::class);
|
||||
|
||||
$this->l10n->method('t')
|
||||
->willReturnArgument(0);
|
||||
|
||||
$this->service = new ExampleEventService(
|
||||
$this->calDavBackend,
|
||||
$this->random,
|
||||
$this->time,
|
||||
$this->appData,
|
||||
$this->appConfig,
|
||||
$this->l10n,
|
||||
);
|
||||
}
|
||||
|
||||
public static function provideCustomEventData(): array {
|
||||
return [
|
||||
[file_get_contents(__DIR__ . '/../test_fixtures/example-event.ics')],
|
||||
[file_get_contents(__DIR__ . '/../test_fixtures/example-event-with-attendees.ics')],
|
||||
];
|
||||
}
|
||||
|
||||
/** @dataProvider provideCustomEventData */
|
||||
public function testCreateExampleEventWithCustomEvent($customEventIcs): void {
|
||||
$this->appConfig->expects(self::once())
|
||||
->method('getValueBool')
|
||||
->with('dav', 'create_example_event', true)
|
||||
->willReturn(true);
|
||||
|
||||
$exampleEventFolder = $this->createMock(ISimpleFolder::class);
|
||||
$this->appData->expects(self::once())
|
||||
->method('getFolder')
|
||||
->with('example_event')
|
||||
->willReturn($exampleEventFolder);
|
||||
$exampleEventFile = $this->createMock(ISimpleFile::class);
|
||||
$exampleEventFolder->expects(self::once())
|
||||
->method('getFile')
|
||||
->with('example_event.ics')
|
||||
->willReturn($exampleEventFile);
|
||||
$exampleEventFile->expects(self::once())
|
||||
->method('getContent')
|
||||
->willReturn($customEventIcs);
|
||||
|
||||
$this->random->expects(self::once())
|
||||
->method('generate')
|
||||
->with(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
|
||||
->willReturn('RANDOM-UID');
|
||||
|
||||
$now = new \DateTimeImmutable('2025-01-21T00:00:00Z');
|
||||
$this->time->expects(self::exactly(2))
|
||||
->method('now')
|
||||
->willReturn($now);
|
||||
|
||||
$expectedIcs = file_get_contents(__DIR__ . '/../test_fixtures/example-event-expected.ics');
|
||||
$this->calDavBackend->expects(self::once())
|
||||
->method('createCalendarObject')
|
||||
->with(1000, 'RANDOM-UID.ics', $expectedIcs);
|
||||
|
||||
$this->service->createExampleEvent(1000);
|
||||
}
|
||||
|
||||
public function testCreateExampleEventWithDefaultEvent(): void {
|
||||
$this->appConfig->expects(self::once())
|
||||
->method('getValueBool')
|
||||
->with('dav', 'create_example_event', true)
|
||||
->willReturn(true);
|
||||
|
||||
$this->appData->expects(self::once())
|
||||
->method('getFolder')
|
||||
->with('example_event')
|
||||
->willThrowException(new NotFoundException());
|
||||
|
||||
$this->random->expects(self::once())
|
||||
->method('generate')
|
||||
->with(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
|
||||
->willReturn('RANDOM-UID');
|
||||
|
||||
$now = new \DateTimeImmutable('2025-01-21T00:00:00Z');
|
||||
$this->time->expects(self::exactly(3))
|
||||
->method('now')
|
||||
->willReturn($now);
|
||||
|
||||
$expectedIcs = file_get_contents(__DIR__ . '/../test_fixtures/example-event-default-expected.ics');
|
||||
$this->calDavBackend->expects(self::once())
|
||||
->method('createCalendarObject')
|
||||
->with(1000, 'RANDOM-UID.ics', $expectedIcs);
|
||||
|
||||
$this->service->createExampleEvent(1000);
|
||||
}
|
||||
|
||||
public function testCreateExampleWhenDisabled(): void {
|
||||
$this->appConfig->expects(self::once())
|
||||
->method('getValueBool')
|
||||
->with('dav', 'create_example_event', true)
|
||||
->willReturn(false);
|
||||
|
||||
$this->calDavBackend->expects(self::never())
|
||||
->method('createCalendarObject');
|
||||
|
||||
$this->service->createExampleEvent(1000);
|
||||
}
|
||||
|
||||
/** @dataProvider provideCustomEventData */
|
||||
public function testGetExampleEventWithCustomEvent($customEventIcs): void {
|
||||
$exampleEventFolder = $this->createMock(ISimpleFolder::class);
|
||||
$this->appData->expects(self::once())
|
||||
->method('getFolder')
|
||||
->with('example_event')
|
||||
->willReturn($exampleEventFolder);
|
||||
$exampleEventFile = $this->createMock(ISimpleFile::class);
|
||||
$exampleEventFolder->expects(self::once())
|
||||
->method('getFile')
|
||||
->with('example_event.ics')
|
||||
->willReturn($exampleEventFile);
|
||||
$exampleEventFile->expects(self::once())
|
||||
->method('getContent')
|
||||
->willReturn($customEventIcs);
|
||||
|
||||
$this->random->expects(self::once())
|
||||
->method('generate')
|
||||
->with(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
|
||||
->willReturn('RANDOM-UID');
|
||||
|
||||
$now = new \DateTimeImmutable('2025-01-21T00:00:00Z');
|
||||
$this->time->expects(self::exactly(2))
|
||||
->method('now')
|
||||
->willReturn($now);
|
||||
|
||||
$expectedIcs = file_get_contents(__DIR__ . '/../test_fixtures/example-event-expected.ics');
|
||||
$actualIcs = $this->service->getExampleEvent()->getIcs();
|
||||
$this->assertEquals($expectedIcs, $actualIcs);
|
||||
}
|
||||
|
||||
public function testGetExampleEventWithDefault(): void {
|
||||
$this->appData->expects(self::once())
|
||||
->method('getFolder')
|
||||
->with('example_event')
|
||||
->willThrowException(new NotFoundException());
|
||||
|
||||
$this->random->expects(self::once())
|
||||
->method('generate')
|
||||
->with(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
|
||||
->willReturn('RANDOM-UID');
|
||||
|
||||
$now = new \DateTimeImmutable('2025-01-21T00:00:00Z');
|
||||
$this->time->expects(self::exactly(3))
|
||||
->method('now')
|
||||
->willReturn($now);
|
||||
|
||||
$expectedIcs = file_get_contents(__DIR__ . '/../test_fixtures/example-event-default-expected.ics');
|
||||
$actualIcs = $this->service->getExampleEvent()->getIcs();
|
||||
$this->assertEquals($expectedIcs, $actualIcs);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Sabre//Sabre VObject 4.5.6//EN
|
||||
CALSCALE:GREGORIAN
|
||||
BEGIN:VEVENT
|
||||
UID:RANDOM-UID
|
||||
DTSTAMP:20250121T000000Z
|
||||
SUMMARY:Example event - open me!
|
||||
DTSTART:20250128T100000Z
|
||||
DTEND:20250128T110000Z
|
||||
DESCRIPTION:Welcome to Nextcloud Calendar!\n\nThis is a sample event - expl
|
||||
ore the flexibility of planning with Nextcloud Calendar by making any edit
|
||||
s you want!\n\nWith Nextcloud Calendar\, you can:\n- Create\, edit\, and m
|
||||
anage events effortlessly.\n- Create multiple calendars and share them wit
|
||||
h teammates\, friends\, or family.\n- Check availability and display your
|
||||
busy times to others.\n- Seamlessly integrate with apps and devices via Ca
|
||||
lDAV.\n- Customize your experience: schedule recurring events\, adjust not
|
||||
ifications and other settings.
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
18
apps/dav/tests/unit/test_fixtures/example-event-expected.ics
Normal file
18
apps/dav/tests/unit/test_fixtures/example-event-expected.ics
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
PRODID:-//IDN nextcloud.com//Calendar app 5.2.0-dev.1//EN
|
||||
BEGIN:VEVENT
|
||||
CREATED:20250128T091147Z
|
||||
DTSTAMP:20250128T091507Z
|
||||
LAST-MODIFIED:20250128T091507Z
|
||||
SEQUENCE:2
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:Welcome!
|
||||
DESCRIPTION:Welcome!!!
|
||||
LOCATION:Test
|
||||
UID:RANDOM-UID
|
||||
DTSTART:20250128T100000Z
|
||||
DTEND:20250128T110000Z
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
PRODID:-//IDN nextcloud.com//Calendar app 5.2.0-dev.1//EN
|
||||
BEGIN:VEVENT
|
||||
CREATED:20250128T091147Z
|
||||
DTSTAMP:20250128T091507Z
|
||||
LAST-MODIFIED:20250128T091507Z
|
||||
SEQUENCE:2
|
||||
UID:3b4df6a8-84df-43d5-baf9-377b43390b70
|
||||
DTSTART;VALUE=DATE:20250130
|
||||
DTEND;VALUE=DATE:20250131
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:Welcome!
|
||||
DESCRIPTION:Welcome!!!
|
||||
LOCATION:Test
|
||||
ATTENDEE;CN=user a;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICI
|
||||
PANT;RSVP=TRUE;LANGUAGE=en;SCHEDULE-STATUS=1.1:mailto:usera@imap.localhost
|
||||
ORGANIZER;CN=Admin Account:mailto:admin@imap.localhost
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
18
apps/dav/tests/unit/test_fixtures/example-event.ics
Normal file
18
apps/dav/tests/unit/test_fixtures/example-event.ics
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
PRODID:-//IDN nextcloud.com//Calendar app 5.2.0-dev.1//EN
|
||||
BEGIN:VEVENT
|
||||
CREATED:20250128T091147Z
|
||||
DTSTAMP:20250128T091507Z
|
||||
LAST-MODIFIED:20250128T091507Z
|
||||
SEQUENCE:2
|
||||
UID:3b4df6a8-84df-43d5-baf9-377b43390b70
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:Welcome!
|
||||
DESCRIPTION:Welcome!!!
|
||||
LOCATION:Test
|
||||
DTSTART:20250204T100000Z
|
||||
DTEND:20250204T110000Z
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
Loading…
Reference in a new issue