From 00b4e32c6dcff8ca9b038c59b81e93b11737ac25 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Willms <tleilax+studip@gmail.com> Date: Tue, 14 Jun 2022 12:47:13 +0000 Subject: [PATCH] add consultation routes Closes #1149 Merge request studip/studip!686 --- lib/classes/JsonApi/RouteMap.php | 5 +- .../Routes/Consultations/Authority.php | 54 +++++----- .../Routes/Consultations/BlockShow.php | 31 ++++++ .../Consultations/BlocksByRangeIndex.php | 65 ++++++++++++ .../Consultations/BlocksByUserIndex.php | 72 -------------- .../Consultations/BookingsBySlotIndex.php | 25 +++++ .../Routes/Consultations/BookingsCreate.php | 98 +++++++++++++++++++ .../Routes/Consultations/BookingsDelete.php | 39 ++++++++ .../Routes/Consultations/BookingsShow.php | 31 ++++++ .../Routes/Consultations/FilterTrait.php | 36 +++++++ .../JsonApi/Routes/Consultations/SlotShow.php | 31 ++++++ .../Consultations/SlotsByBlockIndex.php | 37 +++++++ lib/classes/JsonApi/SchemaMap.php | 3 + .../JsonApi/Schemas/ConsultationBlock.php | 34 ++++--- .../JsonApi/Schemas/ConsultationBooking.php | 24 +++-- .../JsonApi/Schemas/ConsultationSlot.php | 20 ++-- lib/classes/RangeFactory.php | 2 +- lib/models/ConsultationBooking.php | 20 ++++ lib/models/Course.class.php | 5 + lib/models/Institute.class.php | 5 + lib/navigation/ConsultationNavigation.php | 13 +-- 21 files changed, 503 insertions(+), 147 deletions(-) create mode 100644 lib/classes/JsonApi/Routes/Consultations/BlocksByRangeIndex.php delete mode 100644 lib/classes/JsonApi/Routes/Consultations/BlocksByUserIndex.php create mode 100644 lib/classes/JsonApi/Routes/Consultations/FilterTrait.php diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index 4db0b699962..0ebea653379 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -115,7 +115,7 @@ class RouteMap $group->patch('/config-values/{id}', Routes\ConfigValues\ConfigValuesUpdate::class); $this->addAuthenticatedBlubberRoutes($group); - // $this->addAuthenticatedConsultationRoutes($group); + $this->addAuthenticatedConsultationRoutes($group); $this->addAuthenticatedContactsRoutes($group); $this->addAuthenticatedCoursesRoutes($group); $this->addAuthenticatedCoursewareRoutes($group); @@ -180,7 +180,7 @@ class RouteMap private function addAuthenticatedConsultationRoutes(RouteCollectorProxy $group): void { - $group->get('/users/{id}/consultations', Routes\Consultations\BlocksByUserIndex::class); + $group->get('/{type:courses|institutes|users}/{id}/consultations', Routes\Consultations\BlocksByRangeIndex::class); $group->get('/consultation-blocks/{id}', Routes\Consultations\BlockShow::class); $group->get('/consultation-blocks/{id}/slots', Routes\Consultations\SlotsByBlockIndex::class); @@ -189,6 +189,7 @@ class RouteMap $group->get('/consultation-slots/{id}/bookings', Routes\Consultations\BookingsBySlotIndex::class); $group->post('/consultation-slots/{id}/bookings', Routes\Consultations\BookingsCreate::class); + $group->post('/consultation-bookings', Routes\Consultations\BookingsCreate::class); $group->get('/consultation-bookings/{id}', Routes\Consultations\BookingsShow::class); $group->delete('/consultation-bookings/{id}', Routes\Consultations\BookingsDelete::class); } diff --git a/lib/classes/JsonApi/Routes/Consultations/Authority.php b/lib/classes/JsonApi/Routes/Consultations/Authority.php index 476fcbab160..d3022a13820 100644 --- a/lib/classes/JsonApi/Routes/Consultations/Authority.php +++ b/lib/classes/JsonApi/Routes/Consultations/Authority.php @@ -2,55 +2,57 @@ namespace JsonApi\Routes\Consultations; -use ConsultationBlock; -use ConsultationBooking; -use ConsultationSlot; +use JsonApi\Schemas\ConsultationBooking; -class Authority +final class Authority { - // TODO - public static function canShowBlubberThread(User $user, BlubberThread $resource) + public static function canShowRange(\User $user, \Range $range): bool { - return self::userIsAuthor($user) && $resource->isReadable($user->id); + return $range->isAccessibleToUser($user->id); } - public static function canCreatePrivateBlubberThread(User $user) + public static function canEditRange(\User $user, \Range $range): bool { - return self::userIsAuthor($user); + return $range->isEditableByUser($user->id); } - public static function canCreateComment(User $user, BlubberThread $resource) + public static function canShowBlock(\User $user, \ConsultationBlock $block): bool { - return self::userIsAuthor($user) && $resource->isCommentable($user->id); + return self::canShowRange($user, $block->range); } - public static function canDeleteComment(User $user, BlubberComment $resource) + public static function canEditBlock(\User $user, \ConsultationBlock $block): bool { - return self::canEditComment($user, $resource); + return self::canEditRange($user, $block->range); } - public static function canEditComment(User $user, BlubberComment $resource) + public static function canShowSlot(\User $user, \ConsultationSlot $slot): bool { - return self::userIsAuthor($user) && $resource->isWritable($user->id); + return self::canShowBlock($user, $slot->block); } - public static function canIndexComments(User $user, BlubberThread $resource = null) + public static function canEditSlot(\User $user, \ConsultationSlot $slot): bool { - return isset($resource) - ? self::canShowBlubberThread($user, $resource) - : self::userIsAuthor($user); + return self::canEditBlock($user, $slot->block); } - public static function canShowComment(User $user, BlubberComment $resource) + public static function canBookSlot(\User $user, \ConsultationSlot $slot): bool { - return self::canShowBlubberThread($user, $resource->thread); + return \ConsultationBooking::userMayCreateBookingForRange( + $slot->block->range, + $user + ); } - /** - * @SuppressWarnings(PHPMD.Superglobals) - */ - private static function userIsAuthor(User $user) + public static function canShowBooking(\User $user, \ConsultationBooking $booking): bool { - return $GLOBALS['perm']->have_perm('autor', $user->id); + return self::canShowSlot($user, $booking->slot) + || $booking->user_id === $user->id; + } + + public static function canEditBooking(\User $user, \ConsultationBooking $booking): bool + { + return self::canEditSlot($user, $booking->slot) + || $booking->user_id === $user->id; } } diff --git a/lib/classes/JsonApi/Routes/Consultations/BlockShow.php b/lib/classes/JsonApi/Routes/Consultations/BlockShow.php index e69de29bb2d..09baea730ba 100644 --- a/lib/classes/JsonApi/Routes/Consultations/BlockShow.php +++ b/lib/classes/JsonApi/Routes/Consultations/BlockShow.php @@ -0,0 +1,31 @@ +<?php +namespace JsonApi\Routes\Consultations; + +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use JsonApi\Schemas\ConsultationBlock; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +class BlockShow extends JsonApiController +{ + protected $allowedIncludePaths = [ + ConsultationBlock::REL_SLOTS, + ConsultationBlock::REL_RANGE, + ]; + + public function __invoke(Request $request, Response $response, $args) + { + $block = \ConsultationBlock::find($args['id']); + if (!$block) { + throw new RecordNotFoundException(); + } + + if (!Authority::canShowBlock($this->getUser($request), $block)) { + throw new AuthorizationFailedException(); + } + + return $this->getContentResponse($block); + } +} diff --git a/lib/classes/JsonApi/Routes/Consultations/BlocksByRangeIndex.php b/lib/classes/JsonApi/Routes/Consultations/BlocksByRangeIndex.php new file mode 100644 index 00000000000..41474b763c5 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Consultations/BlocksByRangeIndex.php @@ -0,0 +1,65 @@ +<?php +namespace JsonApi\Routes\Consultations; + +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use JsonApi\Schemas\ConsultationBlock; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +/** + * Displays all consultation blocks of a range + */ +class BlocksByRangeIndex extends JsonApiController +{ + use FilterTrait; + + protected $allowedIncludePaths = [ + ConsultationBlock::REL_SLOTS, + ConsultationBlock::REL_RANGE, + ]; + protected $allowedPagingParameters = ['offset', 'limit']; + protected $allowedFilteringParameters = ['current', 'expired']; + + public function __invoke(Request $request, Response $response, $args) + { + $this->validateFilters(); + + $range_id = $args['id']; + $range_type = substr($args['type'], 0, -1); // Strips trailing plural s + + $range = \RangeFactory::createRange($range_type, $range_id); + if ($range->isNew()) { + throw new RecordNotFoundException(); + } + + if (!Authority::canShowRange($this->getUser($request), $range)) { + throw new AuthorizationFailedException(); + } + + [$offset, $limit] = $this->getOffsetAndLimit(); + + $filters = $this->getFilters(); + $blocks = $this->getBlocks($range, $filters); + + return $this->getPaginatedContentResponse( + $blocks->limit($offset, $limit)->getArrayCopy(), + count($blocks) + ); + } + + private function getBlocks(\Range $range, array $filters): \SimpleCollection + { + if (!$filters['current'] && !$filters['expired']) { + return \SimpleCollection::createFromArray([]); + } + + if ($filters['current'] && $filters['expired']) { + return $range->consultation_blocks; + } + + $blocks = \ConsultationBlock::findByRange($range, 'ORDER BY start', $filters['expired']); + return \SimpleCollection::createFromArray($blocks); + } +} diff --git a/lib/classes/JsonApi/Routes/Consultations/BlocksByUserIndex.php b/lib/classes/JsonApi/Routes/Consultations/BlocksByUserIndex.php deleted file mode 100644 index 0705ff275fc..00000000000 --- a/lib/classes/JsonApi/Routes/Consultations/BlocksByUserIndex.php +++ /dev/null @@ -1,72 +0,0 @@ -<?php -namespace JsonApi\Routes\Consultations; - -use JsonApi\Errors\AuthorizationFailedException; -use JsonApi\Errors\RecordNotFoundException; -use JsonApi\JsonApiController; -use JsonApi\Routes\TimestampTrait; -use Psr\Http\Message\ResponseInterface as Response; -use Psr\Http\Message\ServerRequestInterface as Request; - -/** - * Displays all consultation blocks of a user - */ -class BlocksByUserIndex extends JsonApiController -{ - use TimestampTrait, FilterTrait; - - protected $allowedFilteringParameters = ['since', 'before']; -// protected $allowedIncludePaths = ['author', 'mentions', 'thread']; - protected $allowedPagingParameters = ['offset', 'limit']; - - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function __invoke(Request $request, Response $response, $args) - { - $this->validateFilters(); - - if (!($user = \User::find($args['id']))) { - throw new RecordNotFoundException(); - } - - if (!Authority::canShowBlubberThread($this->getUser($request), $thread)) { - throw new AuthorizationFailedException(); - } - - $filters = $this->getFilters(); - list($total, $comments) = $this->getComments($thread, $filters); - - return $this->getPaginatedContentResponse($comments, $total); - } - - private function getComments(\BlubberThread $thread, array $filters) - { - list($offset, $limit) = $this->getOffsetAndLimit(); - - $query = 'thread_id = :thread_id'; - $params = ['thread_id' => $thread->id]; - - if (isset($filters['before'])) { - $query .= ' AND mkdate <= :before'; - $params['before'] = $filters['before']; - } - - if (isset($filters['since'])) { - $query .= ' AND mkdate >= :since'; - $params['since'] = $filters['since']; - } - - if (isset($filters['search'])) { - $query .= ' AND content LIKE :search'; - $params['search'] = '%' . $filters['search'] . '%'; - } - - $query .= ' ORDER BY mkdate ASC LIMIT :limit OFFSET :offset'; - $params['limit'] = $limit + 1; - $params['offset'] = $offset; - - $comments = \BlubberComment::findBySQL($query, $params); - return [count($comments) <= $limit ? count($comments) + $offset : null, array_slice($comments, 0, $limit)]; - } -} diff --git a/lib/classes/JsonApi/Routes/Consultations/BookingsBySlotIndex.php b/lib/classes/JsonApi/Routes/Consultations/BookingsBySlotIndex.php index e69de29bb2d..0bb846b1cf4 100644 --- a/lib/classes/JsonApi/Routes/Consultations/BookingsBySlotIndex.php +++ b/lib/classes/JsonApi/Routes/Consultations/BookingsBySlotIndex.php @@ -0,0 +1,25 @@ +<?php +namespace JsonApi\Routes\Consultations; + +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +class BookingsBySlotIndex extends JsonApiController +{ + public function __invoke(Request $request, Response $response, $args) + { + $slot = \ConsultationSlot::find($args['id']); + if (!$slot) { + throw new RecordNotFoundException(); + } + + if (!Authority::canShowSlot($this->getUser($request), $slot)) { + throw new AuthorizationFailedException(); + } + + return $this->getContentResponse($slot->bookings); + } +} diff --git a/lib/classes/JsonApi/Routes/Consultations/BookingsCreate.php b/lib/classes/JsonApi/Routes/Consultations/BookingsCreate.php index e69de29bb2d..a7347ab43b4 100644 --- a/lib/classes/JsonApi/Routes/Consultations/BookingsCreate.php +++ b/lib/classes/JsonApi/Routes/Consultations/BookingsCreate.php @@ -0,0 +1,98 @@ +<?php +namespace JsonApi\Routes\Consultations; + +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\ConflictException; +use JsonApi\JsonApiController; +use JsonApi\Routes\ValidationTrait; +use JsonApi\Schemas\ConsultationSlot; +use JsonApi\Schemas\User; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +class BookingsCreate extends JsonApiController +{ + use ValidationTrait; + + public function __invoke(Request $request, Response $response, $args) + { + $json = $this->validate($request, $args); + + $slot = $this->getBookingSlot($json, $args); + $booking_user = $this->getBookingUser($json); + + if (!Authority::canBookSlot($booking_user, $slot)) { + throw new AuthorizationFailedException(); + } + + if ($slot->isOccupied()) { + throw new ConflictException('The slot is already occupied'); + } + + $booking = \ConsultationBooking::create([ + 'slot_id' => $slot->id, + 'user_id' => $booking_user->id, + 'reason' => self::arrayGet($json, 'data.attributes.reason', ''), + ]); + + return $this->getCreatedResponse($booking); + } + + protected function validateResourceDocument($json, $data) + { + $user_validation_error = $this->validateRequestContainsValidUser($json, $data); + $slot_validation_error = $this->validateRequestContainsValidSlot($json, $data); + + return $user_validation_error ?? $slot_validation_error; + } + + protected function validateRequestContainsValidUser($json, $data) + { + if (!self::arrayHas($json, 'data.relationships.user')) { + return 'No user relationship defined for booking'; + } + + $booking_user = self::arrayGet($json, 'data.relationships.user'); + if (!isset($booking_user['data']['type'], $booking_user['data']['id']) || $booking_user['data']['type'] !== User::TYPE) { + return 'Defined booking user has invalid format.'; + } + if (!\User::exists($booking_user['data']['id'])) { + return 'Defined booking user does not exist.'; + } + + return null; + } + + protected function validateRequestContainsValidSlot($json, $data) + { + if (isset($data['id']) && \ConsultationSlot::exists($data['id'])) { + return null; + } + + if (!self::arrayHas($json, 'data.relationships.slot')) { + return 'No slot relationship defined for booking'; + } + + $booking_slot = self::arrayGet($json, 'data.relationships.slot'); + if (!isset($booking_slot['data']['type'], $booking_slot['data']['id']) || $booking_slot['data']['type'] !== ConsultationSlot::TYPE) { + return 'Defined slot for booking has invalid format.'; + } + if (!\ConsultationSlot::exists($booking_slot['data']['id'])) { + return 'Defined slot for booking does not exist.'; + } + + return null; + } + + protected function getBookingUser($json): \User + { + $user_id = self::arrayGet($json, 'data.relationships.user.data.id'); + return \User::find($user_id); + } + + protected function getBookingSlot($json, $data): \ConsultationSlot + { + $slot_id = self::arrayGet($json, 'data.relationships.slot.data.id', $data['id'] ?? null); + return \ConsultationSlot::find($slot_id); + } +} diff --git a/lib/classes/JsonApi/Routes/Consultations/BookingsDelete.php b/lib/classes/JsonApi/Routes/Consultations/BookingsDelete.php index e69de29bb2d..dadf3a3c90c 100644 --- a/lib/classes/JsonApi/Routes/Consultations/BookingsDelete.php +++ b/lib/classes/JsonApi/Routes/Consultations/BookingsDelete.php @@ -0,0 +1,39 @@ +<?php +namespace JsonApi\Routes\Consultations; + +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use JsonApi\Routes\ValidationTrait; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +class BookingsDelete extends JsonApiController +{ + use ValidationTrait; + + public function __invoke(Request $request, Response $response, $args) + { + $json = $this->validate($request); + + $booking = \ConsultationBooking::find($args['id']); + if (!$booking) { + throw new RecordNotFoundException(); + } + + $user = $this->getUser($request); + if (!Authority::canEditBooking($user, $booking)) { + throw new AuthorizationFailedException(); + } + + $reason = self::arrayGet($json, 'data.attributes.reason', ''); + + $booking->cancel($reason); + + return $this->getCodeResponse(204); + } + + protected function validateResourceDocument($json, $data) + { + } +} diff --git a/lib/classes/JsonApi/Routes/Consultations/BookingsShow.php b/lib/classes/JsonApi/Routes/Consultations/BookingsShow.php index e69de29bb2d..31df647ecad 100644 --- a/lib/classes/JsonApi/Routes/Consultations/BookingsShow.php +++ b/lib/classes/JsonApi/Routes/Consultations/BookingsShow.php @@ -0,0 +1,31 @@ +<?php +namespace JsonApi\Routes\Consultations; + +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use JsonApi\Schemas\ConsultationBooking; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +class BookingsShow extends JsonApiController +{ + protected $allowedIncludePaths = [ + ConsultationBooking::REL_SLOT, + ConsultationBooking::REL_USER, + ]; + + public function __invoke(Request $request, Response $response, $args) + { + $booking = \ConsultationBooking::find($args['id']); + if (!$booking) { + throw new RecordNotFoundException(); + } + + if (!Authority::canShowBooking($this->getUser($request), $booking)) { + throw new AuthorizationFailedException(); + } + + return $this->getContentResponse($booking); + } +} diff --git a/lib/classes/JsonApi/Routes/Consultations/FilterTrait.php b/lib/classes/JsonApi/Routes/Consultations/FilterTrait.php new file mode 100644 index 00000000000..8aed18ef1ce --- /dev/null +++ b/lib/classes/JsonApi/Routes/Consultations/FilterTrait.php @@ -0,0 +1,36 @@ +<?php +namespace JsonApi\Routes\Consultations; + +use JsonApi\Errors\BadRequestException; + +trait FilterTrait +{ + private function validateFilters() + { + $filtering = $this->getQueryParameters()->getFilteringParameters() ?? []; + + if (array_key_exists('current', $filtering)) { + if (!ctype_digit($filtering['current']) || !in_array($filtering['current'], [0, 1])) { + throw new BadRequestException('Filter "current" may only be 0 or 1.'); + } + } + + if (array_key_exists('expired', $filtering)) { + if (!ctype_digit($filtering['expired']) || !in_array($filtering['expired'], [0, 1])) { + throw new BadRequestException('Filter "expired" may only be 0 or 1.'); + } + } + } + + private function getFilters(): array + { + $filtering = $this->getQueryParameters()->getFilteringParameters() ?? []; + + $has_filter = isset($filtering['current']) || isset($filtering['expired']); + + $filters['current'] = $filtering['current'] ?? !$has_filter; + $filters['expired'] = $filtering['expired'] ?? !$has_filter; + + return $filters; + } +} diff --git a/lib/classes/JsonApi/Routes/Consultations/SlotShow.php b/lib/classes/JsonApi/Routes/Consultations/SlotShow.php index e69de29bb2d..1de10cf01de 100644 --- a/lib/classes/JsonApi/Routes/Consultations/SlotShow.php +++ b/lib/classes/JsonApi/Routes/Consultations/SlotShow.php @@ -0,0 +1,31 @@ +<?php +namespace JsonApi\Routes\Consultations; + +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use JsonApi\Schemas\ConsultationSlot; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +class SlotShow extends JsonApiController +{ + protected $allowedIncludePaths = [ + ConsultationSlot::REL_BLOCK, + ConsultationSlot::REL_BOOKINGS, + ]; + + public function __invoke(Request $request, Response $response, $args) + { + $slot = \ConsultationSlot::find($args['id']); + if (!$slot) { + throw new RecordNotFoundException(); + } + + if (!Authority::canShowSlot($this->getUser($request), $slot)) { + throw new AuthorizationFailedException(); + } + + return $this->getContentResponse($slot); + } +} diff --git a/lib/classes/JsonApi/Routes/Consultations/SlotsByBlockIndex.php b/lib/classes/JsonApi/Routes/Consultations/SlotsByBlockIndex.php index e69de29bb2d..7953f4517f3 100644 --- a/lib/classes/JsonApi/Routes/Consultations/SlotsByBlockIndex.php +++ b/lib/classes/JsonApi/Routes/Consultations/SlotsByBlockIndex.php @@ -0,0 +1,37 @@ +<?php +namespace JsonApi\Routes\Consultations; + +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use JsonApi\Schemas\ConsultationSlot; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +class SlotsByBlockIndex extends JsonApiController +{ + protected $allowedIncludePaths = [ + ConsultationSlot::REL_BLOCK, + ConsultationSlot::REL_BOOKINGS, + ]; + protected $allowedPagingParameters = ['offset', 'limit']; + + public function __invoke(Request $request, Response $response, $args) + { + $block = \ConsultationBlock::find($args['id']); + if (!$block) { + throw new RecordNotFoundException(); + } + + if (!Authority::canShowBlock($this->getUser($request), $block)) { + throw new AuthorizationFailedException(); + } + + [$offset, $limit] = $this->getOffsetAndLimit(); + + return $this->getPaginatedContentResponse( + $block->slots->limit($offset, $limit), + count($block->slots) + ); + } +} diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php index de6e24759ea..1af7a90b380 100644 --- a/lib/classes/JsonApi/SchemaMap.php +++ b/lib/classes/JsonApi/SchemaMap.php @@ -19,6 +19,9 @@ class SchemaMap \BlubberThread::class => Schemas\BlubberThread::class, \CalendarEvent::class => Schemas\CalendarEvent::class, + \ConsultationBlock::class => Schemas\ConsultationBlock::class, + \ConsultationBooking::class => Schemas\ConsultationBooking::class, + \ConsultationSlot::class => Schemas\ConsultationSlot::class, \ConfigValue::class => Schemas\ConfigValue::class, \CourseEvent::class => Schemas\CourseEvent::class, \ContentTermsOfUse::class => Schemas\ContentTermsOfUse::class, diff --git a/lib/classes/JsonApi/Schemas/ConsultationBlock.php b/lib/classes/JsonApi/Schemas/ConsultationBlock.php index 83ea3db3bd9..4e4e8d72e12 100644 --- a/lib/classes/JsonApi/Schemas/ConsultationBlock.php +++ b/lib/classes/JsonApi/Schemas/ConsultationBlock.php @@ -8,9 +8,13 @@ use Neomerx\JsonApi\Schema\Link; class ConsultationBlock extends SchemaProvider { const TYPE = 'consultation-blocks'; + const REL_SLOTS = 'slots'; const REL_RANGE = 'range'; + /** + * @param \ConsultationBlock $resource + */ public function getId($resource): ?string { return $resource->id; @@ -22,15 +26,16 @@ class ConsultationBlock extends SchemaProvider 'start' => date('c', $resource->start), 'end' => date('c', $resource->end), - 'room' => $resource->room, - 'size' => (int) $resource->size, - + 'size' => (int) $resource->size, 'show-participants' => (bool) $resource->show_participants, 'require-reason' => $resource->require_reason, 'confirmation-text' => $resource->confirmation_text ?: null, 'confirmation-text-html' => formatLinks($resource->confirmation_text) ?: null, + 'room' => $resource->room, + 'room-html' => formatLinks($resource->room), + 'note' => $resource->note, 'note-html' => formatLinks($resource->note), @@ -49,28 +54,27 @@ class ConsultationBlock extends SchemaProvider public function getRelationships($resource, ContextInterface $context): iterable { $relationships = []; - $relationships = $this->getSlotsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_SLOTS)); $isPrimary = $context->getPosition()->getLevel() === 0; - if (!$isPrimary) { - return $relationships; + if ($isPrimary) { + $relationships = $this->getSlotsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_SLOTS)); + $relationships = $this->getRangeRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_RANGE)); } - $relationships = $this->getRangeRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_RANGE)); return $relationships; } // #### PRIVATE HELPERS #### - private function getSlotsRelationship(array $relationships, \BlubberComment $resource, $includeData) + private function getSlotsRelationship(array $relationships, \ConsultationBlock $resource, $includeData) { if ($includeData) { $relatedSlots = $resource->slots; } else { - $relatedSlots = array_map(function ($slot) { + $relatedSlots = $resource->slots->map(function ($slot) { return \ConsultationSlot::build(['id' => $slot->id], false); - }, $resource->slots); + }); } $relationships[self::REL_SLOTS] = [ @@ -97,7 +101,7 @@ class ConsultationBlock extends SchemaProvider return $relationships; } - private function getLinkForRange(Range $range) + private function getLinkForRange(\Range $range) { if ( $range instanceof \Course || @@ -110,18 +114,18 @@ class ConsultationBlock extends SchemaProvider throw new \Exception('Unknown range type'); } - private function getMinimalRange(Range $range) + private function getMinimalRange(\Range $range) { if ($range instanceof \Course) { - return Course::build(['id' => $range->id], false); + return \Course::build(['id' => $range->id], false); } if ($range instanceof \Institute) { - return Institute::build(['id' => $range->id], false); + return \Institute::build(['id' => $range->id], false); } if ($range instanceof \User) { - return User::build(['id' => $range->id], false); + return \User::build(['id' => $range->id], false); } throw new \Exception('Unknown range type'); diff --git a/lib/classes/JsonApi/Schemas/ConsultationBooking.php b/lib/classes/JsonApi/Schemas/ConsultationBooking.php index 0368ac566ed..5aa96310c96 100644 --- a/lib/classes/JsonApi/Schemas/ConsultationBooking.php +++ b/lib/classes/JsonApi/Schemas/ConsultationBooking.php @@ -8,14 +8,21 @@ use Neomerx\JsonApi\Schema\Link; class ConsultationBooking extends SchemaProvider { const TYPE = 'consultation-bookings'; + const REL_SLOT = 'slot'; const REL_USER = 'user'; + /** + * @param \ConsultationBooking $resource + */ public function getId($resource): ?string { return $resource->id; } + /** + * @param \ConsultationBooking $resource + */ public function getAttributes($resource, ContextInterface $context): iterable { $attributes = [ @@ -28,23 +35,22 @@ class ConsultationBooking extends SchemaProvider return $attributes; } - /** - * In dieser Methode können Relationships zu anderen Objekten - * spezifiziert werden. - * {@inheritdoc} - */ public function getRelationships($resource, ContextInterface $context): iterable { $relationships = []; - $relationships = $this->getSlotRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_SLOT)); - $relationships = $this->getUserRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_USER)); + + $isPrimary = $context->getPosition()->getLevel() === 0; + if ($isPrimary) { + $relationships = $this->getSlotRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_SLOT)); + $relationships = $this->getUserRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_USER)); + } return $relationships; } // #### PRIVATE HELPERS #### - private function getSlotRelationship(array $relationships, \BlubberComment $resource, $includeData) + private function getSlotRelationship(array $relationships, \ConsultationBooking $resource, $includeData) { $slot = $resource->slot; @@ -58,7 +64,7 @@ class ConsultationBooking extends SchemaProvider return $relationships; } - private function getRangeRelationship($relationships, $resource, $includeData) + private function getUserRelationship($relationships, \ConsultationBooking $resource, $includeData) { $user = $resource->user; diff --git a/lib/classes/JsonApi/Schemas/ConsultationSlot.php b/lib/classes/JsonApi/Schemas/ConsultationSlot.php index a630bb6a42f..92678f55322 100644 --- a/lib/classes/JsonApi/Schemas/ConsultationSlot.php +++ b/lib/classes/JsonApi/Schemas/ConsultationSlot.php @@ -8,11 +8,10 @@ use Neomerx\JsonApi\Schema\Link; class ConsultationSlot extends SchemaProvider { const TYPE = 'consultation-slots'; + const REL_BLOCK = 'block'; const REL_BOOKINGS = 'bookings'; - - public function getId($resource): ?string { return $resource->id; @@ -42,21 +41,20 @@ class ConsultationSlot extends SchemaProvider public function getRelationships($resource, ContextInterface $context): iterable { $relationships = []; - $relationships = $this->getBlockRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_BLOCK)); $isPrimary = $context->getPosition()->getLevel() === 0; - if (!$isPrimary) { - return $relationships; + if ($isPrimary) { + $relationships = $this->getBlockRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_BLOCK)); + $relationships = $this->getBookingsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_BOOKINGS)); } - $relationships = $this->getBookingsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_BOOKINGS)); return $relationships; } // #### PRIVATE HELPERS #### - private function getBlockRelationship($relationships, $resource, $includeData) + private function getBlockRelationship($relationships, \ConsultationSlot $resource, $includeData) { $block = $resource->block; @@ -70,14 +68,14 @@ class ConsultationSlot extends SchemaProvider return $relationships; } - private function getBookingsRelationship(array $relationships, \BlubberComment $resource, $includeData) + private function getBookingsRelationship(array $relationships, \ConsultationSlot $resource, $includeData) { if ($includeData) { $relatedBookings = $resource->bookings; } else { - $relatedBookings = array_map(function ($booking) { - return \ConsultationBooking::build(['slot_id' => $booking->slot_id], false); - }, $resource->bookings); + $relatedBookings = $resource->bookings->map(function ($booking) { + return \ConsultationBooking::build(['booking_id' => $booking->id], false); + }); } $relationships[self::REL_BOOKINGS] = [ diff --git a/lib/classes/RangeFactory.php b/lib/classes/RangeFactory.php index 73cf11e9b7b..8da592a28a1 100644 --- a/lib/classes/RangeFactory.php +++ b/lib/classes/RangeFactory.php @@ -31,7 +31,7 @@ final class RangeFactory * @param string $type Range type * @param mixed $id Range id * @return mixed any of the supported range types - * @throws Exception when an invalid range type was given + * @throws Exception when an invalid range type was given * * @todo Should this be more dynamic in case any more ranges are added? */ diff --git a/lib/models/ConsultationBooking.php b/lib/models/ConsultationBooking.php index 3e02ba014ce..1903249d6a2 100644 --- a/lib/models/ConsultationBooking.php +++ b/lib/models/ConsultationBooking.php @@ -94,6 +94,26 @@ class ConsultationBooking extends SimpleORMap implements PrivacyObject parent::configure($config); } + /** + * Returns whether a user may create a booking for the given range. + * + * @param User $user + * @return bool + */ + public static function userMayCreateBookingForRange(\Range $range, \User $user): bool + { + if (!($range instanceof \User)) { + return true; + } + + $allowed_perms = ['user', 'autor', 'tutor']; + if (Config::get()->CONSULTATION_ALLOW_DOCENTS_RESERVING) { + $allowed_perms[] = 'dozent'; + } + + return in_array($user->perms, $allowed_perms); + } + public function cancel($reason = '') { if ($GLOBALS['user']->id !== $this->user_id) { diff --git a/lib/models/Course.class.php b/lib/models/Course.class.php index ba36ee9fb90..10969a4a877 100644 --- a/lib/models/Course.class.php +++ b/lib/models/Course.class.php @@ -142,6 +142,11 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe 'on_delete' => 'delete', 'on_store' => 'store', ]; + $config['has_many']['consultation_blocks'] = [ + 'class_name' => ConsultationBlock::class, + 'assoc_foreign_key' => 'range_id', + 'on_delete' => 'delete', + ]; $config['has_and_belongs_to_many']['semesters'] = [ 'class_name' => Semester::class, diff --git a/lib/models/Institute.class.php b/lib/models/Institute.class.php index 435fd988103..b25e3597e42 100644 --- a/lib/models/Institute.class.php +++ b/lib/models/Institute.class.php @@ -107,6 +107,11 @@ class Institute extends SimpleORMap implements Range 'on_delete' => 'delete', 'on_store' => 'store', ]; + $config['has_many']['consultation_blocks'] = [ + 'class_name' => ConsultationBlock::class, + 'assoc_foreign_key' => 'range_id', + 'on_delete' => 'delete', + ]; $config['has_many']['tools'] = [ 'class_name' => ToolActivation::class, 'assoc_foreign_key' => 'range_id', diff --git a/lib/navigation/ConsultationNavigation.php b/lib/navigation/ConsultationNavigation.php index 73aaf5497f5..e68982e3b5f 100644 --- a/lib/navigation/ConsultationNavigation.php +++ b/lib/navigation/ConsultationNavigation.php @@ -55,17 +55,8 @@ class ConsultationNavigation extends Navigation return; } - if ($this->range instanceof User) { - // Permissions that are allowed to book reservervations - $allowed = ['user', 'autor', 'tutor']; - if (Config::get()->CONSULTATION_ALLOW_DOCENTS_RESERVING) { - $allowed[] = 'dozent'; - } - - // User does not have required permissions - if (!in_array($GLOBALS['user']->perms, $allowed)) { - return null; - } + if (!ConsultationBooking::userMayCreateBookingForRange($this->range, User::findCurrent())) { + return; } // Create visitor navigation -- GitLab