Appointments Slot Support

Signed-off-by: Anna Larch <anna@nextcloud.com>
This commit is contained in:
Anna Larch 2021-09-13 11:01:05 +02:00 committed by Christoph Wurst
parent 84d70110c5
commit 9087968333
No known key found for this signature in database
GPG Key ID: CC42AC2A7F0E56D8
19 changed files with 2804 additions and 300 deletions

View File

@ -15,7 +15,7 @@
* ☑️ Tasks! See tasks with a due date directly in the calendar
* 🙈 **Were not reinventing the wheel!** Based on the great [c-dav library](https://github.com/nextcloud/cdav-library), [ical.js](https://github.com/mozilla-comm/ical.js) and [fullcalendar](https://github.com/fullcalendar/fullcalendar) libraries.
]]></description>
<version>2.4.0-RC.2</version>
<version>2.4.0-RC.1</version>
<licence>agpl</licence>
<author homepage="https://georg.coffee">Georg Ehrke</author>
<author homepage="https://tcit.fr">Thomas Citharel</author>

654
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,230 @@
<?php
declare(strict_types=1);
/**
* Calendar App
*
* @copyright 2021 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Calendar\Controller;
use OCA\Calendar\Db\AppointmentConfig;
use OCA\Calendar\Exception\ServiceException;
use OCA\Calendar\Http\JsonResponse;
use OCA\Calendar\Service\Appointments\AppointmentConfigService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IInitialStateService;
use OCP\IRequest;
use OCP\IUser;
/**
* Class PublicViewController
*
* @package OCA\Calendar\Controller
*/
class AppointmentConfigController extends Controller {
/** @var IInitialStateService */
private $initialStateService;
/** @var IUser */
private $user;
/** @var AppointmentConfigService */
private $appointmentConfigService;
/**
* @param string $appName
* @param IRequest $request an instance of the request
* @param IInitialStateService $initialStateService
* @param IUser $user
* @param AppointmentConfigService $appointmentService
*/
public function __construct(string $appName,
IRequest $request,
IInitialStateService $initialStateService,
IUser $user,
AppointmentConfigService $appointmentService) {
parent::__construct($appName, $request);
$this->initialStateService = $initialStateService;
$this->user = $user;
$this->appointmentConfigService = $appointmentService;
}
/**
* @param string $renderAs
* @return TemplateResponse
*/
public function index(string $renderAs
):TemplateResponse {
$appointmentConfigs = [];
try {
$appointmentConfigs = $this->appointmentConfigService->getAllAppointmentConfigurations($this->user->getUID());
} catch (ServiceException $e) {
// do nothing and don't show any appointments
}
$this->initialStateService->provideInitialState($this->appName, 'appointmentConfigurations', $appointmentConfigs);
return new TemplateResponse($this->appName, 'main', [
], $renderAs);
}
/**
* @param string $name
* @param string $description
* @param string $location
* @param string $visibility
* @param string $targetCalendarUri
* @param string $availability
* @param int $length
* @param int $increment
* @param int $preparationDuration
* @param int $followupDuration
* @param int $buffer
* @param int|null $dailyMax
* @param string|null $freebusyUris
* @return JsonResponse
*/
public function create(
string $name,
string $description,
string $location,
string $visibility,
string $targetCalendarUri,
string $availability,
int $length,
int $increment,
int $preparationDuration = 0,
int $followupDuration = 0,
int $buffer = 0,
?int $dailyMax = null,
?string $freebusyUris = null): JsonResponse {
$appointmentConfig = new AppointmentConfig();
$appointmentConfig->setName($name);
$appointmentConfig->setDescription($description);
$appointmentConfig->setLocation($location);
$appointmentConfig->setVisibility($visibility);
$appointmentConfig->setUserId($this->user->getUID());
$appointmentConfig->setTargetCalendarUri($targetCalendarUri);
$appointmentConfig->setAvailability($availability);
$appointmentConfig->setLength($length);
$appointmentConfig->setIncrement($increment);
$appointmentConfig->setPreparationDuration($preparationDuration);
$appointmentConfig->setFollowupDuration($followupDuration);
$appointmentConfig->setBuffer($buffer);
$appointmentConfig->setDailyMax($dailyMax);
$appointmentConfig->setCalendarFreebusyUris($freebusyUris);
try {
$appointmentConfig = $this->appointmentConfigService->create($appointmentConfig);
return JsonResponse::success($appointmentConfig);
} catch (ServiceException $e) {
return JsonResponse::errorFromThrowable($e);
}
}
/**
* @param int $id
* @return JsonResponse
*/
public function show(int $id): JsonResponse {
try {
$appointmentConfig = $this->appointmentConfigService->findByIdAndUser($id, $this->user->getUID());
return JsonResponse::success($appointmentConfig);
} catch (ServiceException $e) {
return JsonResponse::errorFromThrowable($e);
}
}
/**
* @param int $id
* @param string $name
* @param string $description
* @param string $location
* @param string $visibility
* @param string $targetCalendarUri
* @param string $availability
* @param int $length
* @param int $increment
* @param int $preparationDuration
* @param int $followupDuration
* @param int $buffer
* @param int|null $dailyMax
* @param string|null $freebusyUris
* @return JsonResponse
*/
public function update(
int $id,
string $name,
string $description,
string $location,
string $visibility,
string $targetCalendarUri,
string $availability,
int $length,
int $increment,
int $preparationDuration = 0,
int $followupDuration = 0,
int $buffer = 0,
?int $dailyMax = null,
?string $freebusyUris = null): JsonResponse {
try {
$appointmentConfig = $this->appointmentConfigService->findByIdAndUser($id, $this->user->getUID());
} catch (ServiceException $e) {
return JsonResponse::errorFromThrowable($e);
}
$appointmentConfig->setName($name);
$appointmentConfig->setDescription($description);
$appointmentConfig->setLocation($location);
$appointmentConfig->setVisibility($visibility);
$appointmentConfig->setUserId($this->user->getUID());
$appointmentConfig->setTargetCalendarUri($targetCalendarUri);
$appointmentConfig->setAvailability($availability);
$appointmentConfig->setLength($length);
$appointmentConfig->setIncrement($increment);
$appointmentConfig->setPreparationDuration($preparationDuration);
$appointmentConfig->setFollowupDuration($followupDuration);
$appointmentConfig->setBuffer($buffer);
$appointmentConfig->setDailyMax($dailyMax);
$appointmentConfig->setCalendarFreebusyUris($freebusyUris);
try {
$appointmentConfig = $this->appointmentConfigService->update($appointmentConfig);
return JsonResponse::success($appointmentConfig);
} catch (ServiceException $e) {
return JsonResponse::errorFromThrowable($e, 403);
}
}
/**
* @param int $id
* @return JsonResponse
*/
public function delete(int $id): JsonResponse {
try {
$this->appointmentConfigService->delete($id, $this->user->getUID());
return JsonResponse::success();
} catch (ServiceException $e) {
return JsonResponse::errorFromThrowable($e, 403);
}
}
}

View File

@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
/**
* Calendar App
*
* @copyright 2021 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Calendar\Controller;
use OCA\Calendar\Exception\ServiceException;
use OCA\Calendar\Http\JsonResponse;
use OCA\Calendar\Service\Appointments\AppointmentConfigService;
use OCA\Calendar\Service\Appointments\Booking;
use OCA\Calendar\Service\Appointments\BookingService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Calendar\IManager;
use OCP\IConfig;
use OCP\IInitialStateService;
use OCP\IRequest;
use OCP\IURLGenerator;
/**
* Class PublicViewController
*
* @package OCA\Calendar\Controller
*/
class BookingController extends Controller {
/** @var BookingService */
private $bookingService;
/** @var ITimeFactory */
private $timeFactory;
/** @var AppointmentConfigService */
private $appointmentConfigService;
/** @var IManager */
private $manager;
/**
* @param string $appName
* @param IRequest $request an instance of the request
* @param IConfig $config
* @param IInitialStateService $initialStateService
* @param IURLGenerator $urlGenerator
*/
public function __construct(string $appName,
IRequest $request,
ITimeFactory $timeFactory,
BookingService $bookingService,
AppointmentConfigService $appointmentConfigService,
IManager $manager) {
parent::__construct($appName, $request);
$this->bookingService = $bookingService;
$this->timeFactory = $timeFactory;
$this->appointmentConfigService = $appointmentConfigService;
}
/**
* @throws ServiceException
* @throws \JsonException
*/
public function getBookableSlots(int $appointmentConfigId, int $unixStartTime, int $unixEndTime) {
// rate limit this to only allow ranges between 0 to 7 days
if (ceil(($unixEndTime - $unixStartTime) / 86400) > 7) {
return JsonResponse::error('Date Range too large.', 403);
}
if ($this->timeFactory->getTime() > $unixStartTime || $this->timeFactory->getTime() > $unixEndTime) {
throw new ServiceException('Booking time must be in the future', 403);
}
$appointmentConfig = $this->appointmentConfigService->findById($appointmentConfigId);
$booking = new Booking($appointmentConfig, $unixStartTime, $unixEndTime);
$data = $this->bookingService->getSlots($booking);
return JsonResponse::success($data);
}
/**
* @param string $calendarData
*/
public function bookSlot(string $calendarData) {
$this->bookingService->book($calendarData);
return JsonResponse::success();
}
}

View File

@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
// @TODO rename and adjust types from migration
namespace OCA\Calendar\Db;
use JsonSerializable;
use OCP\AppFramework\Db\Entity;
/**
* @method int getId()
* @method void setId(int $id)
* @method string getToken()
* @method void setToken(string $token)
* @method string getName()
* @method void setName(string $name)
* @method string getDescription()
* @method void setDescription(string $name)
* @method string getLocation()
* @method void setLocation(?string $name)
* @method string getVisibility()
* @method void setVisibility(string $visibility)
* @method string getUserId()
* @method void setUserId(string $userId)
* @method string getTargetCalendarUri()
* @method void setTargetCalendarUri(string $calendarUri)
* @method string|null getCalendarFreebusyUris()
* @method void setCalendarFreebusyUris(?string $freebusyUris)
* @method string getAvailability()
* @method void setAvailability(string $availability)
* @method int getLength()
* @method void setLength(int $length)
* @method int getIncrement()
* @method void setIncrement(int $increment)
* @method int getPreparationDuration()
* @method void setPreparationDuration(int $prepDuration)
* @method int getFollowupDuration()
* @method void setFollowupDuration(int $followup)
* @method int getBuffer()
* @method void setBuffer(int $buffer)
* @method int|null getDailyMax()
* @method void setDailyMax(?int $max)
*/
class AppointmentConfig extends Entity implements JsonSerializable {
/** @var string */
protected $token;
/** @var string */
protected $name = '';
/** @var string|null */
protected $description;
/** @var string|null */
protected $location;
/** @var string */
protected $visibility;
/** @var string */
protected $userId;
/** @var string */
protected $targetCalendarUri;
/** @var string|null */
protected $calendarFreebusyUris;
/** @var string */
protected $availability;
/** @var int */
protected $length;
/** @var int */
protected $increment;
/** @var int */
protected $preparationDuration;
/** @var int */
protected $followupDuration;
/** @var int */
protected $buffer;
/** @var int|null */
protected $dailyMax;
/** @var string */
public const VISIBILITY_PUBLIC = 'PUBLIC';
/** @var string */
public const VISIBILITY_PRIVATE = 'PRIVATE';
/**
* Total length of one slot of the appointment config
* in minutes
*
* @return int Minutes of Appointment slot length
*/
public function getTotalLength(): int {
return $this->getLength() + $this->getPreparationDuration() + $this->getFollowupDuration();
}
/**
* Principals always have the same format
*
* @return string
*/
public function getPrincipalUri() : string {
return 'principals/users/' . $this->userId;
}
public function jsonSerialize() {
return [
'id' => $this->id,
'token' => $this->getToken(),
'name' => $this->getName(),
'description' => $this->getDescription(),
'location' => $this->getLocation(),
'visibility' => $this->getVisibility(),
'userId' => $this->getUserId(),
'calendarUri' => $this->getTargetCalendarUri(),
'calendarFreeBusyUris' => $this->getCalendarFreebusyUris(),
'availability' => $this->getAvailability(),
'length' => $this->getLength(),
'increment' => $this->getIncrement(),
'preparationDuration' => $this->getPreparationDuration(),
'followUpDuration' => $this->getFollowupDuration(),
'totalLength' => $this->getTotalLength(),
'buffer' => $this->getBuffer(),
'dailyMax' => $this->getDailyMax()
];
}
}

View File

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\Calendar\Db;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\Exception as DbException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
/**
* @template-extends QBMapper<AppointmentConfig>
*/
class AppointmentConfigMapper extends QBMapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'calendar_appt_configs');
}
/**
* @param int $id
* @param string $userId
* @return AppointmentConfig
* @throws DbException
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
*/
public function findByIdForUser(int $id, string $userId) : AppointmentConfig {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR));
return $this->findEntity($qb);
}
/**
* @param int $id
* @return AppointmentConfig
* @throws DbException
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
*/
public function findById(int $id) : AppointmentConfig {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
return $this->findEntity($qb);
}
/**
* @param string $token
* @return AppointmentConfig
* @throws DbException
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
*/
public function findByToken(string $token) : AppointmentConfig {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where($qb->expr()->eq('token', $qb->createNamedParameter($token, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR));
return $this->findEntity($qb);
}
/**
* @param string $userId
* @return AppointmentConfig[]
* @throws DbException
*/
public function findAllForUser(string $userId): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR));
return $this->findEntities($qb);
}
/**
* @param int $id
* @param string $userId
* @return int
* @throws DbException
*/
public function deleteById(int $id, string $userId): int {
$qb = $this->db->getQueryBuilder();
$qb->delete($this->tableName)
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR));
return $qb->executeStatement();
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/**
* @author Anna Larch <anna.larch@gmx.net>
*
* Calendar
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Calendar\Exception;
use Exception;
class ServiceException extends Exception {
}

134
lib/Http/JsonResponse.php Normal file
View File

@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Anna Larch <anna.larch@gmx.net>
*
* @author 2021 Anna Larch
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\Calendar\Http;
use JsonSerializable;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse as Base;
use Throwable;
use function array_flip;
use function array_intersect_key;
use function array_map;
use function array_merge;
use function get_class;
/**
* @see https://github.com/omniti-labs/jsend
*/
class JsonResponse extends Base {
public function __construct($data = [],
int $statusCode = Http::STATUS_OK) {
parent::__construct($data, $statusCode);
$this->addHeader('x-calendar-response', 'true');
}
/**
* @param array|JsonSerializable|bool|string $data
* @param int $status
*
* @return static
*/
public static function success($data = null,
int $status = Http::STATUS_OK): self {
return new self(
[
'status' => 'success',
'data' => $data,
],
$status
);
}
/**
* @param array|JsonSerializable|bool|string $data
* @param int $status
*
* @return static
*/
public static function fail($data = null,
int $status = Http::STATUS_BAD_REQUEST): self {
return new self(
[
'status' => 'fail',
'data' => $data,
],
$status
);
}
public static function error(string $message,
int $status = Http::STATUS_INTERNAL_SERVER_ERROR,
array $data = [],
int $code = 0): self {
return new self(
[
'status' => 'error',
'message' => $message,
'data' => $data,
'code' => $code,
],
$status
);
}
/**
* @param mixed[] $data
*/
public static function errorFromThrowable(Throwable $error,
int $status = Http::STATUS_INTERNAL_SERVER_ERROR,
array $data = []): self {
return self::error(
$error->getMessage(),
$status,
array_merge(
$data,
self::serializeException($error)
),
$error->getCode()
);
}
private static function serializeException(?Throwable $throwable): ?array {
if ($throwable === null) {
return null;
}
return [
'type' => get_class($throwable),
'message' => $throwable->getMessage(),
'code' => $throwable->getCode(),
'trace' => self::filterTrace($throwable->getTrace()),
'previous' => self::serializeException($throwable->getPrevious()),
];
}
private static function filterTrace(array $original): array {
return array_map(function (array $row) {
return array_intersect_key($row,
array_flip(['file', 'line', 'function', 'class']));
}, $original);
}
}

View File

@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace OCA\Calendar\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version2040Date20210908101001 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
$schema = $schemaClosure();
$table = $schema->createTable('calendar_appt_configs');
$table->addColumn('id', Types::BIGINT, [
'autoincrement' => true,
'notnull' => true,
'length' => 11,
'unsigned' => true
]);
// Appointment
$table->addColumn('token', Types::STRING, [
'notnull' => true,
'length' => 128
]);
// Appointment
$table->addColumn('name', Types::STRING, [
'notnull' => true,
'length' => 128
]);
$table->addColumn('description', Types::TEXT, [
'notnull' => false,
'length' => null
]);
$table->addColumn('location', Types::TEXT, [
'notnull' => false,
'length' => null
]);
//Visibility [enum] - PUBLIC (shown somewhere on the user's profile), PRIVATE (only shareable by link) - possibly other variations?
$table->addColumn('visibility', Types::STRING, [
'notnull' => true,
'length' => 7
]);
$table->addColumn('user_id', 'string', [
'notnull' => true,
'length' => 64
]);
$table->addColumn('target_calendar_uri', Types::STRING, [
'notnull' => true,
'length' => 255
]);
//Calendar(s) for conflict handling [string array]
$table->addColumn('calendar_freebusy_uris', Types::TEXT, [
'notnull' => false,
'length' => null
]);
//Slot availabilities [RRULE] - false for notnull bc db doesn't allow default values for blob types
$table->addColumn('availability', Types::TEXT, [
'notnull' => false,
'length' => null,
]);
$table->addColumn('length', Types::INTEGER, [
'notnull' => true
]);
$table->addColumn('increment', Types::INTEGER, [
'notnull' => true
]);
$table->addColumn('preparation_duration', Types::INTEGER, [
'notnull' => true,
'default' => 0
]);
$table->addColumn('followup_duration', Types::INTEGER, [
'notnull' => true,
'default' => 0
]);
$table->addColumn('buffer', Types::INTEGER, [
'notnull' => true,
'default' => 0
]);
//Maximum slots per day - if 0, fit as many as possible
$table->addColumn('daily_max', Types::INTEGER, [
'notnull' => false,
'default' => null
]);
$table->setPrimaryKey(['id']);
return $schema;
}
}

View File

@ -0,0 +1,226 @@
<?php
declare(strict_types=1);
/**
* Calendar App
*
* @copyright 2021 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Calendar\Service\Appointments;
use OCA\Calendar\Db\AppointmentConfig;
use OCA\Calendar\Db\AppointmentConfigMapper;
use OCA\Calendar\Exception\ServiceException;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\DB\Exception as DbException;
class AppointmentConfigService {
/** @var AppointmentConfigMapper */
private $mapper;
/**
* @param AppointmentConfigMapper $mapper
*/
public function __construct(AppointmentConfigMapper $mapper) {
$this->mapper = $mapper;
}
/**
* @param string $user
* @return AppointmentConfig[]
* @throws ServiceException
*/
public function getAllAppointmentConfigurations(string $user): array {
try {
return $this->mapper->findAllForUser($user);
} catch (DbException $e) {
throw new ServiceException('Error fetching configs', $e->getCode(), $e);
}
}
/**
* @param int $id
* @param string $userId
* @throws ServiceException
*/
public function delete(int $id, string $userId): void {
try {
$this->mapper->deleteById($id, $userId);
} catch (DbException $e) {
throw new ServiceException('Could not delete appointment', $e->getCode(), $e);
}
}
/**
* @param AppointmentConfig $appointmentConfig
* @return AppointmentConfig
* @throws ServiceException
*/
public function update(AppointmentConfig $appointmentConfig): AppointmentConfig {
try {
return $this->mapper->update($appointmentConfig);
} catch (DbException $e) {
throw new ServiceException('Could not update Appointment', $e->getCode(), $e);
}
}
/**
* @param int $id
* @return AppointmentConfig
* @throws ServiceException
*/
public function findById(int $id): AppointmentConfig {
try {
return $this->mapper->findById($id);
} catch (DbException |DoesNotExistException|MultipleObjectsReturnedException $e) {
throw new ServiceException('Could not find a record for id', $e->getCode(), $e);
}
}
/**
* @param int $id
* @return AppointmentConfig
* @throws ServiceException
*/
public function findByIdAndUser(int $id, string $userId): AppointmentConfig {
try {
return $this->mapper->findByIdForUser($id, $userId);
} catch (DbException |DoesNotExistException|MultipleObjectsReturnedException $e) {
throw new ServiceException('Could not find a record for id', $e->getCode(), $e);
}
}
/**
* @param AppointmentConfig $appointmentConfig
* @return AppointmentConfig
* @throws ServiceException
*/
public function create(AppointmentConfig $appointmentConfig): AppointmentConfig {
try {
return $this->mapper->insert($appointmentConfig);
} catch (DbException $e) {
throw new ServiceException('Could not create new appointment', $e->getCode(), $e);
}
}
/**
* @param int $id ?? maybe pass the appointment
* @param int $unixStartTime
* @param int $unixEndTime
* @return array
* @throws ServiceException
*/
public function getSlots(AppointmentConfig $appointmentConfig, int $unixStartTime, int $unixEndTime, string $outboxUri): array {
// rate limit this to only allow ranges between 0 to 7 days?
// move this to controller
//ITimeFactory
if (time() > $unixStartTime || time() > $unixEndTime) {
throw new ServiceException('Booking time must be in the future', 403);
}
// move this to controller
try {
$appointment = $this->mapper->findByIdForUser($id);
} catch (DbException |DoesNotExistException|MultipleObjectsReturnedException $e) {
throw new ServiceException('Appointment not found', 404, $e);
}
//do i need to check the recurrence rule here?
$availability = $this->parseRRule($appointment->getAvailability());
// move this to create
$totalLength = $appointment->getTotalLength() * 60;
// if($totalLength === 0){
// throw new ServiceException('Appointment not bookable');
// }
$bookedSlots = $this->findBookedSlotsAmount($id, $unixStartTime, $unixEndTime);
// negotiate avaliable slots
$bookableSlots = ($appointment->getDailyMax() !== null) ? $appointment->getDailyMax() - $bookedSlots : 99999;
if ($bookableSlots <= 0) {
return [];
}
$slots = [];
$timeblocks = $this->getCalendarFreeTimeblocks($unixStartTime, $unixEndTime, $outboxUri, $appointment->getCalendarFreebusyUris());
// get slots irrespective of conflicts
// Remove unavailable slots via comparing with RRule
// remove conflicting slots via calendar free busy
// check daily max via bookable slots via TDO Slot object
// @TODO - refactor this to functions
// foreach($timeblocks as $calendarTimeblock){
// $time = $calendarTimeblock['start'];
// // we only render slots that fit into the time frame given
// while(($time + $totalLength) <= $calendarTimeblock['end'] ) {
// $slots[] = ['start' => $time];
// // add the increment
// $time += $appointment->getIncrement()*60;
// // reduce the amount of available slots
// $bookableSlots--;
// if($bookableSlots <= 0) {
// // no more slots, let's break the outer loop, too
// break 2;
// }
// }
// }
return $slots; // make a DTO out of this
}
/**
* @param int $time
* @param string $outboxUri
* @param array $freeBusyUris
* @return [][]
*
* Check if slot is conflicting with existing appointments
* returns the start and end times of free blocks
* [
* ['start' => 12345678, 'end' => 1234567]
* ['start' => 12345678, 'end' => 1234567]
* ]
*/
public function getCalendarFreeTimeblocks(int $startTime, int $endTime, string $outboxUri, array $freeBusyUris): array {
// get all blocks of time that are still free
// so if there is an appointment from 10 to 11am, we would return the
// slot from 9am to 10am and the slot from 11am to 5pm
// IManager::search auf calendar in general - empty pattern
return [
['start' => $startTime, 'end' => $endTime]
];
}
public function findBookedSlotsAmount(int $id, int $unixStartDateTime, int $unixEndDateTime): int {
// return amount of booked slot
// IManager::search auf X-NC-Appts-Id
return 0;
}
private function parseRRule($rrule) {
return ['start' => '', 'end', ''];
}
}

View File

@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
/**
* Calendar App
*
* @copyright 2021 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Calendar\Service\Appointments;
use DateTimeImmutable;
use OCA\Calendar\Db\AppointmentConfig;
use Recurr\Exception\InvalidRRule;
use Recurr\Exception\InvalidWeekday;
use Recurr\Recurrence;
use Recurr\Rule;
use Recurr\Transformer\ArrayTransformer;
use Recurr\Transformer\ArrayTransformerConfig;
class Booking {
/** @var AppointmentConfig */
private $appointmentConfig;
/** @var int */
private $startTime;
/** @var int */
private $endTime;
/** @var Slot[] */
private $slots;
public function __construct(AppointmentConfig $appointmentConfig, int $startTime, int $endTime, array $slots = []) {
$this->appointmentConfig = $appointmentConfig;
$this->startTime = $startTime;
$this->endTime = $endTime;
$this->slots = $slots;
}
/**
* @return int
*/
public function getStartTime(): int {
return $this->startTime;
}
// Trait maybe?
public function getStartTimeDTObj() : DateTimeImmutable {
return (new DateTimeImmutable())->setTimestamp($this->startTime);
}
public function getEndTimeDTObj() : DateTimeImmutable {
return (new DateTimeImmutable())->setTimestamp($this->endTime);
}
/**
* @param int $startTime
*/
public function setStartTime(int $startTime): void {
$this->startTime = $startTime;
}
/**
* @return int
*/
public function getEndTime(): int {
return $this->endTime;
}
/**
* @param int $endTime
*/
public function setEndTime(int $endTime): void {
$this->endTime = $endTime;
}
/**
* @return Slot[]
*/
public function getSlots(): array {
return $this->slots;
}
/**
* @param Slot[] $slots
*/
public function setSlots(array $slots): void {
$this->slots = $slots;
}
/**
* @return AppointmentConfig
*/
public function getAppointmentConfig(): AppointmentConfig {
return $this->appointmentConfig;
}
/**
* @param AppointmentConfig $appointmentConfig
*/
public function setAppointmentConfig(AppointmentConfig $appointmentConfig): void {
$this->appointmentConfig = $appointmentConfig;
}
public function generateSlots(): array {
$slots = [];
$unixStartTime = $this->getStartTime();
$length = $this->getAppointmentConfig()->getTotalLength()*60;
while(($unixStartTime + $length) <= $this->getEndTime() ) {
$slots[] = new Slot($unixStartTime, $unixStartTime+$this->getAppointmentConfig()->getTotalLength()*60);
$unixStartTime += $this->getAppointmentConfig()->getIncrement()*60;
}
$this->slots = $slots;
return $slots;
}
/**
* @param int $booked
* @return int
*/
public function getAvailableSlotsAmount(int $booked): int {
return ($this->appointmentConfig->getDailyMax() !== null) ? $this->appointmentConfig->getDailyMax() - $booked : 99999;
}
/**
* @return self;
*/
public function parseRRule(): self {
try {
// RRule Array Transformer does not work with constraints atm
// so this is (kind of) superfluous
$startDT = $this->getStartTimeDTObj();
$endDT = $this->getEndTimeDTObj();
// force UTC
$startDT->setTimezone(new \DateTimeZone('UTC'));
$endDT->setTimezone(new \DateTimeZone('UTC'));
$rule = new Rule($this->appointmentConfig->getAvailability());
} catch (InvalidRRule $e) {
$this->slots = [];
return $this;
}
$config = new ArrayTransformerConfig();
$config->enableLastDayOfMonthFix();
$transformer = new ArrayTransformer();
$transformer->setConfig($config);
try {
$collection = $transformer->transform($rule);
} catch (InvalidWeekday $e) {
// throw an error here?
$this->slots = [];
return $this;
}
$this->slots = $collection->map(function (Recurrence $slot) {
$start = $slot->getStart()->getTimestamp();
$end = $start + ($this->appointmentConfig->getTotalLength() * 60);
return new Slot($start, $end);
})->toArray();
return $this;
}
}

View File

@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
/**
* Calendar App
*
* @copyright 2021 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Calendar\Service\Appointments;
use OC\Calendar\CalendarQuery;
use OCA\Calendar\Db\AppointmentConfigMapper;
use OCP\Calendar\IManager;
class BookingService {
/** @var IManager */
private $manager;
/** @var AppointmentConfigMapper */
private $mapper;
public function __construct(IManager $manager,
AppointmentConfigMapper $mapper) {
$this->manager = $manager;
$this->mapper = $mapper;
}
// CREATE
public function book(string $calendarData) {
// use new ICreateFromString::create method
}
public function getBookingInformation(string $token) {
// unmarshal token for ID would be an option too
// this also returns all bookings for this token
// needs a unique identifier - X-NC-USERID or something?
$config = $this->mapper->findByToken($token);
$query = $this->manager->newQuery($config->getPrincipalUri());
$query->addSearchCalendar($config->getTargetCalendarUri());
return $this->manager->searchForPrincipal($query);
}
public function getSlots(Booking $booking): array {
$bookedSlots = $this->findBookedSlotsAmount($booking);
// negotiate available slots
if ($booking->getAvailableSlotsAmount($bookedSlots) <= 0) {
return [];
}
// decide if we want to use the complete 24 hour period to intersect via:
// $booking->generateSlots();
// Remove unavailable slots via comparing with RRule
$booking->parseRRule();
// remove conflicting slots via calendar free busy
$booking = $this->getCalendarFreeTimeblocks($booking);
return $booking->getSlots();
}
/**
* @param Booking $booking
* @return Booking
*
* Check if slot is conflicting with existing appointments
*/
public function getCalendarFreeTimeblocks(Booking $booking): Booking {
$query = $this->manager->newQuery($booking->getAppointmentConfig()->getPrincipalUri());
$query->addSearchCalendar($booking->getAppointmentConfig()->getTargetCalendarUri());
if (!empty($booking->getAppointmentConfig()->getCalendarFreebusyUris())) {
foreach ($booking->getAppointmentConfig()->getCalendarFreebusyUris() as $uri) {
$query->addSearchCalendar($uri);
}
}
$slots = $booking->getSlots();
foreach ($slots as $k => $slot) {
$query->setTimerangeStart($slot->getStartTimeDTObj());
$query->setTimerangeEnd($slot->getEndTimeDTObj());
// cache the query maybe? or maybe run everything at once?
$events = $this->manager->searchForPrincipal($query);
if (!empty($events)) {
unset($slots[$k]);
}
}
$booking->setSlots($slots);
return $booking;
}
public function findBookedSlotsAmount(Booking $booking): int {
/** @var CalendarQuery $query */
$query = $this->manager->newQuery($booking->getAppointmentConfig()->getPrincipalUri());
$query->addSearchCalendar($booking->getAppointmentConfig()->getTargetCalendarUri());
$query->addSearchProperty('X-NC-APPOINTMENT');
$query->setSearchPattern($booking->getAppointmentConfig()->getToken());
$query->setTimerangeStart($booking->getStartTimeDTObj());
$query->setTimerangeEnd($booking->getEndTimeDTObj());
$events = $this->manager->searchForPrincipal($query);
return count($events);
}
// Update
public function updateBooking() {
// noop for now? we don't support a public update method at the moment
}
// Delete
public function delete() {
// this would be a cancel request to ICreateFromString::create()
}
}

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
/**
* Calendar App
*
* @copyright 2021 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Calendar\Service\Appointments;
use DateTimeImmutable;
class Slot {
private $start;
private $end;
public function __construct(int $start, int $end) {
$this->start = $start;
$this->end = $end;
}
public function getStartTime(): int {
return $this->start;
}
public function setStartTime(int $start): void {
$this->start = $start;
}
public function getEndTime(): int {
return $this->end;
}
public function setEndTime(int $end): void {
$this->end = $end;
}
public function getStartTimeDTObj() : DateTimeImmutable {
return (new DateTimeImmutable())->setTimestamp($this->start);
}
public function getEndTimeDTObj() : DateTimeImmutable {
return (new DateTimeImmutable())->setTimestamp($this->end);
}
public function isViable(int $start, int $end): bool {
return !($this->start > $start || $this->end < $end);
}
}

View File

@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
/**
* Calendar App
*
* @copyright 2021 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Calendar\Service\Appointments;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OC\AppFramework\Bootstrap\Coordinator;
use OC\AppFramework\Bootstrap\RegistrationContext;
use OC\AppFramework\Bootstrap\ServiceRegistration;
use OC\Calendar\Manager;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\CalendarProvider;
use OCA\Calendar\Db\AppointmentConfig;
use OCA\Calendar\Db\AppointmentConfigMapper;
use OCP\IConfig;
use OCP\IL10N;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
class BookingServiceTest extends TestCase {
/** @var AppointmentConfigMapper|MockObject */
private $mapper;
/** @var BookingService */
private $service;
/** @var mixed|Manager|MockObject */
private $manager;
protected function setUp(): void {
parent::setUp();
$backend = \OC::$server->get(CalDavBackend::class);
$container = \OC::$server->get(\Psr\Container\ContainerInterface::class);
$l10n = $this->createMock(IL10N::class);
$conf = $this->createMock(IConfig::class);
$logger = $this->createMock(LoggerInterface::class);
$coordinator = $this->createConfiguredMock(Coordinator::class, [
'getRegistrationContext' => $this->createConfiguredMock(RegistrationContext::class, [
'getCalendarProviders' => [ new ServiceRegistration('calendar', CalendarProvider::class) ]
])
]);
$this->manager = new Manager($coordinator, $container, $logger);
$this->mapper = $this->createMock(AppointmentConfigMapper::class);
$this->service = new BookingService(
$this->manager,
$this->mapper
);
}
public function testGetCalendarFreeTimeblocks() {
$appointmentConfig = new AppointmentConfig();
$appointmentConfig->setPrincipalUri('principals/users/test');
$appointmentConfig->setTargetCalendarUri('personal');
$appointmentConfig ->setLength(30);
$appointmentConfig ->setIncrement(15);
$appointmentConfig ->setPreparationDuration(15);
$appointmentConfig ->setFollowupDuration(15);
$appointmentConfig ->setAvailability("RRULE:FREQ=MINUTELY;INTERVAL=15;WKST=MO;BYDAY=MO;BYHOUR=8,9,10,11");
$booking = new Booking($appointmentConfig, time(), (time() + 84600));
$booking->parseRRule();
$this->service->getCalendarFreeTimeblocks($booking);
}
public function testGetCalendarCount() {
$appointmentConfig = new AppointmentConfig();
$appointmentConfig->setPrincipalUri('principals/users/admin');
$appointmentConfig->setTargetCalendarUri('personal');
$appointmentConfig->setToken('1');
$appointmentConfig ->setLength(30);
$appointmentConfig ->setIncrement(15);
$appointmentConfig ->setPreparationDuration(15);
$appointmentConfig ->setFollowupDuration(15);
$appointmentConfig ->setAvailability("RRULE:FREQ=MINUTELY;INTERVAL=15;WKST=MO;BYDAY=MO;BYHOUR=8,9,10,11");
$booking = new Booking($appointmentConfig, time(), (time() + 84600));
$count = $this->service->findBookedSlotsAmount($booking);
}
}

View File

@ -0,0 +1,248 @@
<?php
declare(strict_types=1);
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\Calendar\Tests\Integration\Db;
use BadFunctionCallException;
use ChristophWurst\Nextcloud\Testing\DatabaseTransaction;
use ChristophWurst\Nextcloud\Testing\TestCase;
use InvalidArgumentException;
use OCA\Calendar\Db\AppointmentConfig;
use OCA\Calendar\Db\AppointmentConfigMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\IDBConnection;
class AppointmentMapperTest extends TestCase {
use DatabaseTransaction;
/** @var IDBConnection */
private $db;
/** @var AppointmentConfigMapper */
private $mapper;
protected function setUp(): void {
parent::setUp();
$this->db = \OC::$server->getDatabaseConnection();
$this->mapper = new AppointmentConfigMapper(
$this->db
);
$qb = $this->db->getQueryBuilder();
$delete = $qb->delete($this->mapper->getTableName());
$delete->execute();
}
public function testFindByIdNoData() {
$this->expectException(DoesNotExistException::class);
$this->mapper->findByIdForUser(1);
}
/**
* @depends testFindByIdNoData
*/
public function testFindById() {
$appointment = new AppointmentConfig();
$appointment->setName('Test 2');
$appointment->setDescription('Test Description');
$appointment->setIncrement(15);
$appointment->setLength(60);
$appointment->setTargetCalendarUri('testuri');
$appointment->setVisibility(AppointmentConfig::VISIBILITY_PUBLIC);
$appointment->setUserId('testuser');
$appointment = $this->mapper->insert($appointment);
$id = $appointment->getId();
$appointment = $this->mapper->findById($id);
$this->assertObjectHasAttribute('name', $appointment);
$this->assertEquals('Test 2', $appointment->getName());
$this->assertObjectHasAttribute('description', $appointment);
$this->assertEquals('Test Description', $appointment->getDescription());
$this->assertObjectHasAttribute('increment', $appointment);
$this->assertEquals(15, $appointment->getIncrement());
$this->assertObjectHasAttribute('length', $appointment);
$this->assertEquals(60, $appointment->getLength());
$this->assertObjectHasAttribute('calendarUri', $appointment);
$this->assertEquals('testuri', $appointment->getCalendarUri());
$this->assertObjectHasAttribute('visibility', $appointment);
$this->assertEquals(AppointmentConfig::VISIBILITY_PUBLIC, $appointment->getVisibility());
$this->assertObjectHasAttribute('userId', $appointment);
$this->assertEquals('testuser', $appointment->getUserId());
}
/**
* @depends testFindByIdNoData
*/
public function testInsertFromData() {
$data = [
'name' => 'Test 1',
'description' => 'Test Description',
'increment' => 15,
'length' => 60,
'calendarUri' => 'testuri',
'visibility' => AppointmentConfig::VISIBILITY_PUBLIC,
'userId' => 'testuser'
];
$appointment = $this->mapper->insertFromData($data);
$this->assertObjectHasAttribute('name', $appointment);
$this->assertEquals('Test 1', $appointment->getName());
$this->assertObjectHasAttribute('description', $appointment);
$this->assertEquals('Test Description', $appointment->getDescription());
$this->assertObjectHasAttribute('increment', $appointment);
$this->assertEquals(15, $appointment->getIncrement());
$this->assertObjectHasAttribute('length', $appointment);
$this->assertEquals(60, $appointment->getLength());
$this->assertObjectHasAttribute('calendarUri', $appointment);
$this->assertEquals('testuri', $appointment->getCalendarUri());
$this->assertObjectHasAttribute('visibility', $appointment);
$this->assertEquals(AppointmentConfig::VISIBILITY_PUBLIC, $appointment->getVisibility());
$this->assertObjectHasAttribute('userId', $appointment);
$this->assertEquals('testuser', $appointment->getUserId());
}
/**
* @depends testFindByIdNoData
*/
public function testInsertFromDataBadFunctionCallException() {
// $data = [
// 'fhskjhfkjsdhj' => 'Failing'
// ];
// $this->expectException(BadFunctionCallException::class);
// $this->mapper->insert($data);
}
/**
* @depends testFindByIdNoData
*/
public function testUpdateFromData() {
$appointment = new AppointmentConfig();
$appointment->setName('Test 3');
$appointment->setDescription('Test Description');
$appointment->setIncrement(15);
$appointment->setLength(60);
$appointment->setTargetCalendarUri('testuri');
$appointment->setVisibility(AppointmentConfig::VISIBILITY_PUBLIC);
$appointment->setUserId('testuser');
$appointment = $this->mapper->insert($appointment);
$id = $appointment->getId();
// $data = [
// 'id' => $id,
// 'name' => 'Test 9001',
// 'description' => 'Test Description updated',
// 'increment' => 15,
// 'length' => 60,
// 'calendarUri' => 'testuri',
// 'visibility' => AppointmentConfig::VISIBILITY_PUBLIC,
// 'userId' => 'testuser',
// 'followupDuration' => 100
// ];
// $appointment = $this->mapper->update($data);
$this->assertObjectHasAttribute('id', $appointment);
$this->assertEquals($id, $appointment->getId());
$this->assertObjectHasAttribute('name', $appointment);
$this->assertEquals('Test 9001', $appointment->getName());
$this->assertObjectHasAttribute('description', $appointment);
$this->assertEquals('Test Description updated', $appointment->getDescription());
$this->assertObjectHasAttribute('increment', $appointment);
$this->assertEquals(15, $appointment->getIncrement());
$this->assertObjectHasAttribute('length', $appointment);
$this->assertEquals(60, $appointment->getLength());
$this->assertObjectHasAttribute('calendarUri', $appointment);
$this->assertEquals('testuri', $appointment->getCalendarUri());
$this->assertObjectHasAttribute('visibility', $appointment);
$this->assertEquals(AppointmentConfig::VISIBILITY_PUBLIC, $appointment->getVisibility());
$this->assertObjectHasAttribute('userId', $appointment);
$this->assertEquals('testuser', $appointment->getUserId());
$this->assertObjectHasAttribute('followupDuration', $appointment);
$this->assertEquals(100, $appointment->getFollowupDuration());
}
/**
* @depends testFindByIdNoData
*/
public function testUpdateFromDataInvalidArgumentException() {
// $data = [
// 'name' => 'Test 9001',
// 'description' => 'Test Description updated',
// 'increment' => 15,
// 'length' => 60,
// 'calendarUri' => 'testuri',
// 'visibility' => AppointmentConfig::VISIBILITY_PUBLIC,
// 'userId' => 'testuser',
// 'followupDuration' => 100
// ];
//
// $this->expectException(InvalidArgumentException::class);
// $this->mapper->update($data);
}
public function testFindAllForUser():void {
$appointment = new AppointmentConfig();
$appointment->setName('Test 2');
$appointment->setDescription('Test Description');
$appointment->setIncrement(15);
$appointment->setLength(60);
$appointment->setTargetCalendarUri('testuri');
$appointment->setVisibility(AppointmentConfig::VISIBILITY_PUBLIC);
$appointment->setUserId('testuser');
$this->mapper->insert($appointment);
$appointments = $this->mapper->findAllForUser('testuser');
$this->assertNotEmpty($appointments);
foreach ($appointments as $appointment) {
$this->assertObjectHasAttribute('userId', $appointment);
$this->assertEquals('testuser', $appointment->getUserId());
}
}
public function testDeleteById():void {
$appointment = new AppointmentConfig();
$appointment->setName('Test 2');
$appointment->setDescription('Test Description');
$appointment->setIncrement(15);
$appointment->setLength(60);
$appointment->setTargetCalendarUri('testuri');
$appointment->setVisibility(AppointmentConfig::VISIBILITY_PUBLIC);
$appointment->setUserId('testuser');
$appointment = $this->mapper->insert($appointment);
$row = $this->mapper->deleteById($appointment->getId(), $appointment->getUserId());
$this->assertEquals(1, $row);
$this->expectException(DoesNotExistException::class);
$this->mapper->findById($appointment->getId());
}
}

View File

@ -0,0 +1,233 @@
<?php
declare(strict_types=1);
/**
* Calendar App
*
* @copyright 2021 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Calendar\Controller;
use OCA\Calendar\Db\AppointmentConfig;
use OCA\Calendar\Exception\ServiceException;
use OCA\Calendar\Service\AppointmentConfigService;
use OCP\Contacts\IManager;
use OCP\IInitialStateService;
use OCP\IRequest;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCP\IUser;
use PHPUnit\Framework\MockObject\MockObject;
class AppointmentsControllerTest extends TestCase {
/** @var string */
protected $appName;
/** @var IRequest|MockObject */
protected $request;
/** @var IManager|MockObject */
protected $manager;
/** @var IInitialStateService|MockObject */
protected $initialState;
/** @var IUser|MockObject */
protected $user;
/** @var AppointmentConfigService|MockObject */
protected $service;
/** @var AppointmentConfigController */
protected $controller;
protected function setUp():void {
parent::setUp();
$this->appName = 'calendar';
$this->request = $this->createMock(IRequest::class);
$this->manager = $this->createMock(IManager::class);
$this->user = $this->createConfiguredMock(IUser::class, [
'getUID' => 'testuser'
]);
$this->initialState = $this->createMock(IInitialStateService::class);
$this->service = $this->createMock(AppointmentConfigService::class);
$this->controller = new AppointmentConfigController(
$this->appName,
$this->request,
$this->initialState,
$this->user,
$this->service
);
}
// public function testIndex(): void {
// $appointments = [new AppointmentConfig()];
// $this->service->expects($this->once())
// ->method('getAllAppointmentConfigurations')
// ->with($this->user->getUID())
// ->willReturn($appointments);
//
// $this->initialState->expects($this->once())
// ->method('provideInitialState')
// ->with(
// $this->appName,
// 'appointments',
// $appointments
// );
//
// $this->controller->index('user');
// }
//
// public function testIndexException(): void {
// $this->service->expects($this->once())
// ->method('getAllAppointmentConfigurations')
// ->with($this->user->getUID())
// ->willThrowException(new ServiceException());
//
// $this->initialState->expects($this->once())
// ->method('provideInitialState')
// ->with(
// $this->appName,
// 'appointments',
// []
// );
//
// $this->controller->index('user');
// }
//
// public function testCreate():void {
// $data = [];
// $appointment = new AppointmentConfig([]);
// $this->service->expects($this->once())
// ->method('create')
// ->with($data)
// ->willReturn($appointment);
//
// $response = $this->controller->create($data);
//
// $this->assertEquals(
// [
// 'status' => 'success',
// 'data' => $appointment
// ], $response->getData());
// $this->assertEquals(200, $response->getStatus());
// }
//
// public function testCreateException():void {
// $data = [];
// $this->service->expects($this->once())
// ->method('create')
// ->with($data)
// ->willThrowException(new ServiceException());
//
// $response = $this->controller->create($data);
//
// $this->assertEquals(500, $response->getStatus());
// }
//
// public function testShow():void {
// $id = 1;
// $appointment = new AppointmentConfig();
// $this->service->expects($this->once())
// ->method('findById')
// ->with(1)
// ->willReturn($appointment);
//
// $response = $this->controller->show($id);
//
// $this->assertEquals(
// [
// 'status' => 'success',
// 'data' => $appointment
// ], $response->getData());
// $this->assertEquals(200, $response->getStatus());
// }
//
// public function testShowException():void {
// $id = 1;
// $this->service->expects($this->once())
// ->method('findById')
// ->with(1)
// ->willThrowException(new ServiceException());
//
// $response = $this->controller->show($id);
//
// $this->assertEquals(500, $response->getStatus());
// }
//
// public function testUpdate():void {
// $data = [];
// $appointment = new AppointmentConfig();
// $this->service->expects($this->once())
// ->method('update')
// ->with($data)
// ->willReturn($appointment);
//
// $response = $this->controller->update([]);
//
// $this->assertEquals(
// [
// 'status' => 'success',
// 'data' => $appointment
// ], $response->getData());
// $this->assertEquals(200, $response->getStatus());
// }
//
// public function testUpdateException():void {
// $data = [];
// $this->service->expects($this->once())
// ->method('update')
// ->with($data)
// ->willThrowException(new ServiceException());
//
// $response = $this->controller->update($data);
//
// $this->assertEquals(500, $response->getStatus());
// }
//
// public function testDelete():void {
// $id = 1;
//
// $this->service->expects($this->once())
// ->method('delete')
// ->with(1);
//
// $response = $this->controller->delete($id);
//
// $this->assertEquals(
// [
// 'status' => 'success',
// 'data' => null
// ], $response->getData());
// $this->assertEquals(200, $response->getStatus());
// }
//
// public function testDeleteException():void {
// $id = 1;
// $this->service->expects($this->once())
// ->method('delete')
// ->with(1)
// ->willThrowException(new ServiceException());
//
// $response = $this->controller->delete($id);
//
// $this->assertEquals(403, $response->getStatus());
// }
}

View File

@ -0,0 +1,251 @@
<?php
declare(strict_types=1);
/**
* Calendar App
*
* @copyright 2021 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Calendar\Service\Appointments;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\DB\Exception;
use OCA\Calendar\Db\AppointmentConfig;
use OCA\Calendar\Db\AppointmentConfigMapper;
use OCA\Calendar\Exception\ServiceException;
use PHPUnit\Framework\MockObject\MockObject;
class AppointmentServiceTest extends TestCase {
/** @var AppointmentConfigMapper|MockObject */
private $mapper;
/** @var AppointmentConfigService */
private $service;
protected function setUp(): void {
parent::setUp();
$this->mapper = $this->createMock(AppointmentConfigMapper::class);
$this->service = new AppointmentConfigService(
$this->mapper
);
}
// public function testGetAllConfigurations(){
// $user = 'testuser';
//
// $this->mapper->expects($this->once())
// ->method('findAllForUser')
// ->with($user)
// ->willReturn([new AppointmentConfig()]);
//
// $this->service->getAllAppointmentConfigurations($user);
// }
//
// public function testGetAllConfigurationsException(){
// $user = 'testuser';
//
// $this->mapper->expects($this->once())
// ->method('findAllForUser')
// ->with($user)
// ->willThrowException(new Exception());
//
// $this->expectException(ServiceException::class);
// $this->service->getAllAppointmentConfigurations($user);
// }
//
// public function testDelete(): void {
// $id = 1;
//
// $this->mapper->expects($this->once())
// ->method('deleteById')
// ->with($id);
//
// $this->service->delete($id);
// }
//
// public function testDeleteException(): void {
// $id = 1;
//
// $this->mapper->expects($this->once())
// ->method('deleteById')
// ->with($id)
// ->willThrowException(new Exception());
//
// $this->expectException(ServiceException::class);
// $this->service->delete($id);
// }
//
// public function testUpdate(): void {
// $data = [];
//
// $this->mapper->expects($this->once())
// ->method('updateFromData')
// ->with($data)
// ->willReturn(new AppointmentConfig());
//
// $this->service->update($data);
// }
//
// public function testUpdateException(): void {
// $data = [];
//
// $this->mapper->expects($this->once())
// ->method('updateFromData')
// ->with($data)
// ->willThrowException(new Exception());
//
// $this->expectException(ServiceException::class);
// $this->service->update($data);
// }
//
// public function testFindById(): void {
// $id = 1;
//
// $this->mapper->expects($this->once())
// ->method('findById')
// ->with($id)
// ->willReturn(new AppointmentConfig());
//
// $this->service->findById($id);
// }
//
// public function testFindByIdException(): void {
// $id = 1;
//
// $this->mapper->expects($this->once())
// ->method('findById')
// ->with($id)
// ->willThrowException(new Exception());
//
// $this->expectException(ServiceException::class);
// $this->service->findById($id);
// }
//
// public function testFindByIdDoesNotExistException(): void {
// $id = 1;
//
// $this->mapper->expects($this->once())
// ->method('findById')
// ->with($id)
// ->willThrowException(new DoesNotExistException(''));
//
// $this->expectException(ServiceException::class);
// $this->service->findById($id);
// }
//
// public function testFindByIdMultipleObjectsReturnedException(): void {
// $id = 1;
//
// $this->mapper->expects($this->once())
// ->method('findById')
// ->with($id)
// ->willThrowException(new MultipleObjectsReturnedException(''));
//
// $this->expectException(ServiceException::class);
// $this->service->findById($id);
// }
//
// public function testCreate(): void {
// $data = [];
//
// $this->mapper->expects($this->once())
// ->method('insertFromData')
// ->with($data)
// ->willReturn(new AppointmentConfig());
//
// $this->service->create($data);
// }
//
// public function testCreateException(): void {
// $data = [];
//
// $this->mapper->expects($this->once())
// ->method('insertFromData')
// ->with($data)
// ->willThrowException(new Exception());
//
// $this->expectException(ServiceException::class);
// $this->service->create($data);
// }
//
// public function testIsNotInFuture() : void {
// $id = 1;
// $startDate = strtotime('-1 day');
// $endDate = time();
//
// $this->mapper->expects($this->never())
// ->method('findById');
//
// $this->expectException(ServiceException::class);
// $this->service->getSlots($id, $startDate, $endDate, '');
// }
//
// public function testIdDoesNotExist():void {
// $id = 1;
// $appointment = new AppointmentConfig();
// $appointment->setLength(0);
// $appointment->setIncrement(0);
//
// // use one day in the future
// $startDate = strtotime('+1 day');
// $endDate = strtotime('+31 hours');
//
// $this->mapper->expects($this->once())
// ->method('findById')
// ->with($id)
// ->willThrowException(new ServiceException());
//
// $this->expectException(ServiceException::class);
// $this->service->getSlots($id, $startDate, $endDate, '');
// }
//
// public function testGetSlotsNoLength():void {
// $id = 1;
// $appointment = new AppointmentConfig();
// $appointment->setLength(0);
// $appointment->setIncrement(0);
//
// $startDate = strtotime('+1 day');
// $endDate = strtotime('+31 hours');// 7 hour timespan
//
// $this->mapper->expects($this->once())
// ->method('findById')
// ->with($id)
// ->willReturn($appointment);
//
// $this->expectException(ServiceException::class);
// $this->service->getSlots($id, $startDate, $endDate, '');
// }
// public function testGetSlots():void {
// $appointment = new AppointmentConfig();
// /** every 15 minutes */
// $appointment->setIncrement(15);
// /** 60 minutes long */
// $appointment->setLength(60);
//
// $startDate = strtotime('+1 day');
// $endDate = strtotime('+31 hours');// 7 hour timespan
//
//
// }
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
/**
* Calendar App
*
* @copyright 2021 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Calendar\Service\Appointments;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OC\Calendar\CalendarQuery;
use OC\Calendar\Manager;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\CalendarProvider;
use OCA\Calendar\Db\AppointmentConfig;
use OCA\Calendar\Db\AppointmentConfigMapper;
use PHPUnit\Framework\MockObject\MockObject;
class BookingServiceTest extends TestCase {
/** @var AppointmentConfigMapper|MockObject */
private $mapper;
/** @var BookingService */
private $service;
/** @var mixed|Manager|MockObject */
private $manager;
protected function setUp(): void {
parent::setUp();
$backend = \OC::$server->get(CalDavBackend::class);
$calendarProvider = new CalendarProvider($backend);
$this->manager = \OC::$server->get(Manager::class);
$this->mapper = $this->createMock(AppointmentConfigMapper::class);
$this->service = new BookingService(
$this->manager,
$this->mapper
);
}
public function testGetCalendarFreeTimeblocks() {
$appointmentConfig = new AppointmentConfig();
$appointmentConfig->setUserId('admin'); // or something else
$appointmentConfig->setTargetCalendarUri('calendars/admin/personal/');
$booking = new Booking($appointmentConfig, time(), (time()+3600) );
$calendarQuery = new CalendarQuery($appointmentConfig->getPrincipalUri());
$booking->setAppointmentConfig($appointmentConfig);
$this->manager->expects($this->once())
->method('newQuery')
->with($appointmentConfig->getPrincipalUri())
->willReturn($calendarQuery);
$this->manager->expects($this->once())
->method('searchForPrincipal')
->with($calendarQuery)
->willReturn(null);
$this->service->getCalendarFreeTimeblocks($booking);
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/**
* Calendar App
*
* @copyright 2021 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Calendar\Service\Appointments;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Calendar\Db\AppointmentConfig;
use Recurr\Exception\InvalidRRule;
use Recurr\Exception\InvalidWeekday;
use Recurr\Recurrence;
use Recurr\Rule;
use Recurr\Transformer\ArrayTransformer;
use Recurr\Transformer\ArrayTransformerConfig;
class BookingTest extends TestCase {
/** @var Booking */
private $booking;
/** @var AppointmentConfig */
private $appointmentConfig;
protected function setUp(): void {
parent::setUp();
$this->appointmentConfig = new AppointmentConfig();
$this->booking = new Booking($this->appointmentConfig,strtotime('midnight'), (strtotime('midnight') + 84000));
}
/**
* @covers Booking::getAppointmentConfig()
* @covers Booking::setAppointmentConfig()
*/
public function testSetAppointmentConfig(): void {
$this->appointmentConfig = new AppointmentConfig();
$this->booking->setAppointmentConfig($this->appointmentConfig);
$this->assertEquals($this->booking->getAppointmentConfig(), $this->appointmentConfig);
}
}