Skip to content
Snippets Groups Projects
Commit c21817bf authored by Jan-Hendrik Willms's avatar Jan-Hendrik Willms Committed by David Siegfried
Browse files

implement jsonapi for clipboard and clipboard items and replace old clipboards...

implement jsonapi for clipboard and clipboard items and replace old clipboards route with new ones, fixes #4198

Closes #4198

Merge request studip/studip!3029
parent 98ee46ee
No related branches found
No related tags found
No related merge requests found
Showing
with 904 additions and 220 deletions
......@@ -5,8 +5,6 @@ namespace JsonApi;
use JsonApi\Contracts\JsonApiPlugin;
use JsonApi\Middlewares\Authentication;
use JsonApi\Middlewares\DangerousRouteHandler;
use JsonApi\Middlewares\JsonApi as JsonApiMiddleware;
use JsonApi\Middlewares\StudipMockNavigation;
use JsonApi\Routes\Holidays\HolidaysShow;
use Slim\Routing\RouteCollectorProxy;
......@@ -49,7 +47,6 @@ use Slim\Routing\RouteCollectorProxy;
*
* $this->app->post('/article/{id}/comments', MeineRoute::class);
*
* @see \JsonApi\Middlewares\JsonApi
* @see \JsonApi\Middlewares\Authentication
* @see \JsonApi\Contracts\JsonApiPlugin
* @see http://www.slimframework.com/docs/objects/router.html#how-to-create-routes
......@@ -118,6 +115,7 @@ class RouteMap
$group->get('/status-groups/{id}', Routes\StatusgroupShow::class);
$this->addAuthenticatedBlubberRoutes($group);
$this->addAuthenticatedClipboardRoutes($group);
$this->addAuthenticatedConsultationRoutes($group);
$this->addAuthenticatedContactsRoutes($group);
$this->addAuthenticatedCoursesRoutes($group);
......@@ -205,6 +203,21 @@ class RouteMap
);
}
private function addAuthenticatedClipboardRoutes(RouteCollectorProxy $group): void
{
$group->post('/clipboards', Routes\Clipboards\ClipboardsCreate::class);
$group->patch('/clipboards/{id}', Routes\Clipboards\ClipboardsUpdate::class);
$group->delete('/clipboards/{id}', Routes\Clipboards\ClipboardsDelete::class);
$group->get('/clipboard-items/{id}', Routes\Clipboards\ClipboardItemsShow::class);
$group->post('/clipboards/{id}/items', Routes\Clipboards\ClipboardItemsCreate::class);
$group->delete('/clipboards/{id}/items', Routes\Clipboards\ClipboardItemsDelete::class);
$group->delete('/clipboards/{id}/items/{itemId}', Routes\Clipboards\ClipboardItemsDelete::class);
$group->post('/clipboard-items', Routes\Clipboards\ClipboardItemsCreate::class);
$group->delete('/clipboard-items/{id}', Routes\Clipboards\ClipboardItemsDelete::class);
}
private function addAuthenticatedConsultationRoutes(RouteCollectorProxy $group): void
{
$group->get('/{type:courses|institutes|users}/{id}/consultations', Routes\Consultations\BlocksByRangeIndex::class);
......
<?php
namespace JsonApi\Routes\Clipboards;
use User;
final class Authority
{
public static function canCreateClipboard(User $user): bool
{
return true;
}
public static function canAccessClipboard(User $user, \Clipboard $clipboard): bool
{
return $user->id === $clipboard->user_id
|| $user->perms === 'root';
}
public static function canUpdateClipboard(User $user, \Clipboard $clipboard): bool
{
return self::canAccessClipboard($user, $clipboard);
}
public static function canDeleteClipboard(User $user, \Clipboard $clipboard): bool
{
return self::canUpdateClipboard($user, $clipboard);
}
}
<?php
namespace JsonApi\Routes\Clipboards;
use JsonApi\Errors\AuthorizationFailedException;
use JsonApi\JsonApiController;
use JsonApi\Routes\ValidationTrait;
use JsonApi\Schemas\Clipboard;
use Psr\Http\Message\{
ResponseInterface as Response,
ServerRequestInterface as Request
};
final class ClipboardItemsCreate extends JsonApiController
{
use ValidationTrait;
public function __invoke(Request $request, Response $response, $args): Response
{
$json = $this->validate($request, $args);
$clipboard_id = $args['id'] ?? $json['data']['relationships']['clipboard']['data']['id'];
$clipboard = \Clipboard::find($clipboard_id);
$user = $this->getUser($request);
if (!Authority::canUpdateClipboard($user, $clipboard)) {
throw new AuthorizationFailedException();
}
$range_id = $json['data']['attributes']['range_id'];
$range_type = $json['data']['attributes']['range_type'];
$item = \ClipboardItem::findOneBySql(
'clipboard_id = ? AND range_id = ? AND range_type = ?',
[$clipboard_id, $range_id, $range_type]
);
if ($item) {
return $this->getCodeResponse(302, [
'Location' => $this->getLinkToItem($item),
]);
}
$item = \ClipboardItem::create([
'clipboard_id' => $clipboard_id,
'range_id' => $range_id,
'range_type' => $range_type,
]);
return $this->getContentResponse($item);
}
protected function validateResourceDocument($json, $data)
{
$clipboardValidationError = $this->validateRequestContainsValidClipboard($json, $data);
if ($clipboardValidationError !== null) {
return $clipboardValidationError;
}
if (!self::arrayHas($json, 'data.attributes.range_id')) {
return 'No range_id defined';
}
if (!self::arrayHas($json, 'data.attributes.range_type')) {
return 'No range_type defined';
}
$range_type = self::arrayGet($json, 'data.attributes.range_type');
if (!is_a($range_type, \StudipItem::class, true)) {
return 'Range type must implement interface StudipItem';
}
return null;
}
private function validateRequestContainsValidClipboard($json, $data): ?string
{
if (isset($data['id'])) {
if (!\Clipboard::exists($data['id'])) {
return 'Provided clipboard id is invalid';
}
} else {
if (!self::arrayHas($json, 'data.relationships.clipboard')) {
return 'No clipboard relationship defined';
}
$clipboard = self::arrayGet($json, 'data.relationships.clipboard');
if (
!isset($clipboard['data']['type'], $clipboard['data']['id'])
|| $clipboard['data']['type'] !== Clipboard::TYPE
) {
return 'Defined clipboard relationship has invalid format.';
}
if (!\Clipboard::exists($clipboard['data']['id'])) {
return 'Related clipboard does not exist.';
}
}
return null;
}
private function getLinkToItem(\ClipboardItem $item): string
{
$json = $this->encoder->encodeData($item);
return json_decode($json, true)['data']['links']['self'];
}
}
<?php
namespace JsonApi\Routes\Clipboards;
use JsonApi\Errors\BadRequestException;
use JsonApi\Errors\RecordNotFoundException;
use JsonApi\JsonApiController;
use Psr\Http\Message\{
ResponseInterface as Response,
ServerRequestInterface as Request
};
final class ClipboardItemsDelete extends JsonApiController
{
protected $allowedFilteringParameters = ['range_id'];
public function __invoke(Request $request, Response $response, $args): Response
{
$clipboard = \Clipboard::find($args['id']);
if (!$clipboard) {
throw new RecordNotFoundException('Clipboard not found');
}
$user = $this->getUser($request);
if (!Authority::canUpdateClipboard($user, $clipboard)) {
throw new \AccessDeniedException();
}
$item = null;
if (isset($args['itemId'])) {
$item = \ClipboardItem::find($args['itemId']);
} else {
$filtering = iterator_to_array($this->getQueryParameters()->getFilters());
if (!isset($filtering['range_id'])) {
throw new BadRequestException('No range_id filter given');
}
$item = \ClipboardItem::findOneBySQL(
'clipboard_id = ? AND range_id = ?',
[$clipboard->id, $filtering['range_id']]
);
}
if (!$item) {
throw new RecordNotFoundException('Item not found');
}
if ($item->clipboard_id !== $clipboard->id) {
throw new BadRequestException('Item does not belong to clipboard');
}
$item->delete();
return $this->getCodeResponse(204);
}
}
<?php
namespace JsonApi\Routes\Clipboards;
use JsonApi\Errors\AuthorizationFailedException;
use JsonApi\Errors\RecordNotFoundException;
use JsonApi\JsonApiController;
use Psr\Http\Message\{
ResponseInterface as Response,
ServerRequestInterface as Request
};
final class ClipboardItemsShow extends JsonApiController
{
public function __invoke(Request $request, Response $response, $args): Response
{
$item = \ClipboardItem::find($args['id']);
if (!$item) {
throw new RecordNotFoundException();
}
$user = $this->getUser($request);
if (!Authority::canAccessClipboard($user, $item->clipboard)) {
throw new AuthorizationFailedException();
}
return $this->getContentResponse($item);
}
}
<?php
namespace JsonApi\Routes\Clipboards;
use JsonApi\Errors\AuthorizationFailedException;
use JsonApi\JsonApiController;
use JsonApi\Routes\ValidationTrait;
use Psr\Http\Message\{
ResponseInterface as Response,
ServerRequestInterface as Request
};
final class ClipboardsCreate extends JsonApiController
{
use ValidationTrait;
public function __invoke(Request $request, Response $response, $args): Response
{
$user = $this->getUser($request);
if (!Authority::canCreateClipboard($user)) {
throw new AuthorizationFailedException();
}
$json = $this->validate($request, $args);
$clipboard = \Clipboard::create([
'name' => $json['data']['attributes']['name'],
'user_id' => $user->id,
]);
return $this->getContentResponse($clipboard);
}
protected function validateResourceDocument($json, $data)
{
if (!self::arrayHas($json, 'data.attributes.name')) {
return 'No name for the clipboard defined';
}
if (!trim(self::arrayGet($json, 'data.attributes.name'))) {
return 'Name of the clipboard may not be empty';
}
return null;
}
}
<?php
namespace JsonApi\Routes\Clipboards;
use JsonApi\Errors\AuthorizationFailedException;
use JsonApi\Errors\RecordNotFoundException;
use JsonApi\JsonApiController;
use Psr\Http\Message\{
ResponseInterface as Response,
ServerRequestInterface as Request
};
final class ClipboardsDelete extends JsonApiController
{
public function __invoke(Request $request, Response $response, $args): Response
{
$clipboard = \Clipboard::find($args['id']);
if (!$clipboard) {
throw new RecordNotFoundException();
}
$user = $this->getUser($request);
if (!Authority::canDeleteClipboard($user, $clipboard)) {
throw new AuthorizationFailedException();
}
$clipboard->delete();
return $this->getCodeResponse(204);
}
}
<?php
namespace JsonApi\Routes\Clipboards;
use JsonApi\Errors\AuthorizationFailedException;
use JsonApi\Errors\RecordNotFoundException;
use JsonApi\JsonApiController;
use JsonApi\Routes\ValidationTrait;
use Psr\Http\Message\{
ResponseInterface as Response,
ServerRequestInterface as Request
};
final class ClipboardsUpdate extends JsonApiController
{
use ValidationTrait;
public function __invoke(Request $request, Response $response, $args): Response
{
$clipboard = \Clipboard::find($args['id']);
if (!$clipboard) {
throw new RecordNotFoundException();
}
$user = $this->getUser($request);
if (!Authority::canUpdateClipboard($user, $clipboard)) {
throw new AuthorizationFailedException();
}
$json = $this->validate($request, $args);
$clipboard->name = $json['data']['attributes']['name'];
$clipboard->store();
return $this->getContentResponse($clipboard);
}
protected function validateResourceDocument($json, $data)
{
if (!self::arrayHas($json, 'data.attributes.name')) {
return 'No name for the clipboard defined';
}
if (!trim(self::arrayGet($json, 'data.attributes.name'))) {
return 'Name of the clipboard may not be empty';
}
return null;
}
}
......@@ -2,6 +2,8 @@
namespace JsonApi;
use JsonApi\Schemas\Clipboard;
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
......@@ -19,6 +21,8 @@ class SchemaMap
\BlubberThread::class => Schemas\BlubberThread::class,
\CalendarDateAssignment::class => Schemas\CalendarDateAssignment::class,
\Clipboard::class => Schemas\Clipboard::class,
\ClipboardItem::class => Schemas\ClipboardItem::class,
\ConsultationBlock::class => Schemas\ConsultationBlock::class,
\ConsultationBooking::class => Schemas\ConsultationBooking::class,
\ConsultationSlot::class => Schemas\ConsultationSlot::class,
......
<?php
namespace JsonApi\Schemas;
use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
use Neomerx\JsonApi\Schema\Link;
final class Clipboard extends SchemaProvider
{
public const TYPE = 'clipboards';
public const REL_USER = 'user';
public const REL_ITEMS = 'clipboard-items';
/**
* @param \Clipboard $resource
*/
public function getId($resource): ?string
{
return (string) $resource->id;
}
/**
* @param \Clipboard $resource
*/
public function getAttributes($resource, ContextInterface $context): iterable
{
return [
'name' => $resource->name,
'handler' => $resource->handler,
'allows_item_class' => $resource->allowed_item_class,
'mkdate' => date('c', $resource->mkdate),
'chdate' => date('c', $resource->chdate),
];
}
/**
* @param \Clipboard $resource
*/
public function getRelationships($resource, ContextInterface $context): iterable
{
$relationships = [];
$isPrimary = $context->getPosition()->getLevel() === 0;
if ($isPrimary) {
$relationships = $this->getUserRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_USER));
$relationships = $this->getItemsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_ITEMS));
}
return $relationships;
}
private function getUserRelationship(array $relationships, \Clipboard $clipboard, bool $includeData): array
{
$relationships[self::REL_USER] = [
self::RELATIONSHIP_LINKS => [
Link::RELATED => $this->createLinkToResource($clipboard->user),
],
self::RELATIONSHIP_DATA => $includeData ? $clipboard->user : \User::build(['id' => $clipboard->user_id], false),
];
return $relationships;
}
private function getItemsRelationship(array $relationships, \Clipboard $clipboard, bool $includeData): array
{
if ($includeData) {
$relatedItems = $clipboard->items;
} else {
$relatedItems = $clipboard->items->map(fn($item) => \ClipboardItem::build(['id' => $item->id], false));
}
$relationships[self::REL_ITEMS] = [
self::RELATIONSHIP_LINKS => [
Link::RELATED => $this->getRelationshipRelatedLink($clipboard, self::REL_ITEMS),
],
self::RELATIONSHIP_DATA => $relatedItems,
];
return $relationships;
}
}
<?php
namespace JsonApi\Schemas;
use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
use Neomerx\JsonApi\Schema\Link;
final class ClipboardItem extends SchemaProvider
{
public const TYPE = 'clipboard-items';
public const REL_CLIPBOARD = 'clipboard';
/**
* @param \ClipboardItem $resource
*/
public function getId($resource): ?string
{
return (string) $resource->id;
}
/**
* @param \ClipboardItem $resource
*/
public function getAttributes($resource, ContextInterface $context): iterable
{
return [
'range_id' => $resource->range_id,
'range_type' => $resource->range_type,
'name' => $resource->name,
'mkdate' => date('c', $resource->mkdate),
'chdate' => date('c', $resource->chdate),
];
}
/**
* @param \ClipboardItem $resource
*/
public function getRelationships($resource, ContextInterface $context): iterable
{
$relationships = [];
$isPrimary = $context->getPosition()->getLevel() === 0;
if ($isPrimary) {
$relationships = $this->getClipboardRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_CLIPBOARD));
}
return $relationships;
}
private function getClipboardRelationship(array $relationships, \ClipboardItem $clipboardItem, bool $includeData): array
{
$relationships[self::REL_CLIPBOARD] = [
self::RELATIONSHIP_LINKS => [
Link::RELATED => $this->createLinkToResource($clipboardItem->clipboard),
],
self::RELATIONSHIP_DATA => $includeData ? $clipboardItem->clipboard : \User::build(['id' => $clipboardItem->clipboard_id], false),
];
return $relationships;
}
}
......@@ -23,6 +23,8 @@
* @property int $mkdate database column
* @property int $chdate database column
* @property Clipboard $clipboard belongs_to Clipboard
*
* @property-read string $name
*/
class ClipboardItem extends SimpleORMap
{
......@@ -36,36 +38,32 @@ class ClipboardItem extends SimpleORMap
'assoc_func' => 'find'
];
$config['additional_fields']['name'] = [
'get' => fn(ClipboardItem $item) => $item->__toString(),
];
parent::configure($config);
}
/**
* @returns string representation of this clipboard item.
*/
public function __toString()
{
//Get the class $range_type and the object with ID $range_id,
//if $range_type is a StudipItem:
$use_generic_name = true;
$object = null;
if (is_subclass_of($this->range_type, 'StudipItem', true)) {
// Get the class $range_type and the object with ID $range_id,
// if $range_type is a StudipItem:
if (is_subclass_of($this->range_type, StudipItem::class)) {
$range_class_name = $this->range_type;
$object = $range_class_name::find($this->range_id);
if ($object) {
$use_generic_name = false;
return $object->getItemName(false);
}
}
if ($use_generic_name) {
//$range_type is not a class name of a StudipItem class
//or no object of a StudipItem class could be found:
//We cannot determine the name and must therefore use
//a generic name:
return $this->range_type . '_' . $this->range_id;
} else {
return $object->getItemName(false);
}
// $range_type is not a class name of a StudipItem class
// or no object of a StudipItem class could be found:
// We cannot determine the name and must therefore use
// a generic name:
return $this->range_type . '_' . $this->range_id;
}
}
......@@ -25,7 +25,9 @@ STUDIP.domReady(function () {
jQuery(document).on('click', '.clipboard-remove-button', function (event) {
event.preventDefault();
STUDIP.Dialog.confirm($(this).data('confirm-message'), function() {
const message = $(this).data('confirm-message');
STUDIP.Dialog.confirm(message).done(() => {
STUDIP.Clipboard.handleRemoveClick(event.target);
});
});
......@@ -62,10 +64,11 @@ STUDIP.domReady(function () {
});
});
jQuery(document).on('submit', '.clipboard-widget .new-clipboard-form', function (event) {
event.preventDefault();
STUDIP.Clipboard.handleAddForm(event);
});
jQuery(document).on(
'submit',
'.clipboard-widget .new-clipboard-form',
STUDIP.Clipboard.handleAddForm
);
jQuery(document).on('click', '.clipboard-add-item-button', function (event) {
event.preventDefault();
......
......@@ -52,6 +52,8 @@ class AbstractAPI
var deferred;
const request = this.#createRequest(url, options);
if (options.async && this.request_count > 0) {
// Request should be sent asynchronous after every other request
// is finished. The configuration for this particular request is
......@@ -73,10 +75,10 @@ class AbstractAPI
this.total_requests += 1;
// Actual request
deferred = $.ajax(STUDIP.URLHelper.getURL(`${this.base_url}/${url}`, {}, true), {
deferred = $.ajax(request.url, {
contentType: options.contentType || 'application/x-www-form-urlencoded; charset=UTF-8',
method: options.method.toUpperCase(),
data: this.encodeData(options.data, options.method.toUpperCase()),
data: this.encodeData(request.data, options.method.toUpperCase()),
headers: options.headers
}).always(() => {
// Decrease request counter, remove overlay if neccessary
......@@ -93,6 +95,27 @@ class AbstractAPI
}
}).promise();
}
#createRequest(url, options) {
const hasBody = ['post', 'put', 'patch'].includes(options.method.toLowerCase());
const query = hasBody ? '' : `?${this.convertDataToRequestParameters(options.data)}`;
return {
url: STUDIP.URLHelper.getURL(`${this.base_url}/${url}${query}`, {}, true),
data: hasBody ? options.data : {},
};
}
convertDataToRequestParameters(data, prefix = '') {
return Object.entries(data).map(([key, value]) => {
const name = prefix ? `${prefix}[${key}]` : `${key}`;
if (value.constructor.name === 'Object') {
return this.convertDataToRequestParameters(value, name);
} else {
return `${name}=${value}`;
}
}).join('&');
}
}
// Create shortcut methods for easier access by method
......
import {$gettext} from './gettext';
function extractAttribute(node, attribute) {
return node.querySelector(`input[name="${attribute}"]`)?.value.trim();
}
function extractAttributes(node, attributes) {
const result = {};
for (const key of attributes) {
result[key] = extractAttribute(node, key);
}
return result;
}
const Clipboard = {
switchClipboard: function(event) {
......@@ -32,32 +42,30 @@ const Clipboard = {
}
},
handleAddForm: function(event) {
if (!event) {
return false;
}
handleAddForm(event) {
event.preventDefault();
const attributes = extractAttributes(event.target, ['name', 'allowed_item_class']);
//Check if a name is entered in the form:
let name_input = jQuery(event.target).find('input[type="text"][name="name"]');
const name_input = event.target.querySelector('input[name="name"]');
if (!name_input) {
//Something is wrong with the HTML:
return false;
}
let name = jQuery(name_input).val().trim();
if (!name) {
if (!attributes.name) {
//The name field is empty. Why send an empty field?
return false;
}
//Submit the form via AJAX:
STUDIP.api.POST(
'clipboard/add',
{
data: jQuery(event.target).serialize()
}
).done(STUDIP.Clipboard.add);
// Submit the form via AJAX:
STUDIP.jsonapi.POST('clipboards', {data: {data: {attributes}}}).done(({data}) => {
STUDIP.Clipboard.add({
id: data.id,
name: data.attributes.name,
widget_id: extractAttribute(event.target, 'widget_id')
});
});
},
add: function(data) {
......@@ -134,11 +142,9 @@ const Clipboard = {
jQuery(widget_node).find('#clipboard-group-container').removeClass('invisible');
//Call the droppable jQuery method on the new clipboard area:
jQuery(clipboard_node).droppable(
{
drop: STUDIP.Clipboard.handleItemDrop
}
);
jQuery(clipboard_node).droppable({
drop: STUDIP.Clipboard.handleItemDrop
});
//Clear the text input in the "add clipboard" form:
jQuery(widget_node).find(
......@@ -238,17 +244,19 @@ const Clipboard = {
}
//Add the item to the clipboard via AJAX:
STUDIP.api.POST(
'clipboard/' + clipboard_id + '/item',
{
STUDIP.jsonapi.POST(`clipboards/${clipboard_id}/items`, {
data: {
data: {
'range_id': range_id,
'range_type': range_type,
'widget_id': widget_id
attributes: { range_id, range_type }
}
}
).done(function(data) {
STUDIP.Clipboard.addDroppedItem(data);
}).done(({data}) => {
STUDIP.Clipboard.addDroppedItem({
id: data.id,
name: data.attributes.name,
range_id: data.attributes.range_id,
widget_id
});
});
},
......@@ -263,6 +271,7 @@ const Clipboard = {
let widget = jQuery('#ClipboardWidget_' + response_data['widget_id']);
let clipboard_id = jQuery(widget).find(".clipboard-selector").val();
if (!widget) {
//The widget with the speicified widget-ID
//is not present on the current page.
......@@ -325,25 +334,16 @@ const Clipboard = {
);
},
rename: function(widget_id) {
if (!widget_id) {
//Required data are missing!
return;
}
rename(widget_id) {
const widget = jQuery('#ClipboardWidget_' + widget_id);
const clipboard_id = widget.find('.clipboard-selector').val();
const name = widget.find('input.clipboard-name').val();
let widget = jQuery('#ClipboardWidget_' + widget_id);
let clipboard_id = jQuery(widget).find(".clipboard-selector").val();
let namer = jQuery(widget).find("input.clipboard-name");
STUDIP.api.PUT(
'clipboard/' + clipboard_id,
{
data: {
name: namer.val()
}
}
).done(function(data) {
STUDIP.Clipboard.update(data, widget_id)
STUDIP.jsonapi.PATCH(`clipboards/${clipboard_id}`, {data: {data: {attributes: {name}}}}).done(({data}) => {
STUDIP.Clipboard.update({
id: data.id,
name: data.attributes.name,
}, widget_id)
});
},
......@@ -358,7 +358,7 @@ const Clipboard = {
STUDIP.Clipboard.toggleEditButtons(widget_id);
},
remove: function(clipboard_id, widget_id) {
remove(clipboard_id, widget_id) {
if (!clipboard_id || !widget_id) {
//Required data are missing!
return;
......@@ -427,10 +427,6 @@ const Clipboard = {
},
handleRemoveClick: function(delete_icon) {
if (!delete_icon) {
return;
}
//Get the data of the clipboard:
let clipboard_select = jQuery(delete_icon).siblings('.clipboard-selector')[0];
if (!clipboard_select) {
......@@ -444,52 +440,42 @@ const Clipboard = {
//Another case where something is wrong with the HTML.
return;
}
let widget_id = jQuery(widget).data('widget_id');
STUDIP.api.DELETE(
'clipboard/' + clipboard_id,
{
data: {
widget_id: widget_id
}
}
).done(function() {
const widget_id = jQuery(widget).data('widget_id');
STUDIP.jsonapi.DELETE(`clipboards/${clipboard_id}`).done(() => {
STUDIP.Clipboard.remove(clipboard_id, widget_id);
});
},
removeItem: function(delete_icon) {
if (!delete_icon) {
return;
}
//Get the item-ID:
let item_html = jQuery(delete_icon).parents('tr');
let range_id = jQuery(item_html).data('range_id');
let clipboard_element = jQuery(item_html).parents('table');
let clipboard_id = jQuery(clipboard_element).data('id');
removeItem(delete_icon) {
// Get the item-ID:
const item_element = jQuery(delete_icon).parents('tr');
const range_id = jQuery(item_element).data('range_id');
const clipboard_element = jQuery(item_element).parents('table');
const clipboard_id = jQuery(clipboard_element).data('id');
if (!range_id || !clipboard_id) {
//We cannot proceed without the item-ID and the clipboard-ID!
return;
}
STUDIP.api.DELETE(
'clipboard/' + clipboard_id + '/item/' + range_id
).done(function() {
STUDIP.jsonapi.DELETE(`clipboards/${clipboard_id}/items`, {
data: {
filter: { range_id }
}
}).done(() => {
//Check if the item has siblings:
let siblings = jQuery(item_html).siblings();
let siblings = item_element.siblings();
if (siblings.length < 3) {
//Only the "no items" element and the template
//are siblings of the item.
//We must display the "no items" element:
jQuery(item_html).siblings(
'.empty-clipboard-message'
).removeClass('invisible');
item_element.siblings('.empty-clipboard-message').removeClass('invisible');
jQuery("#clipboard-group-container").find('.widget-links').addClass('invisible');
}
//Finally remove the item:
jQuery(item_html).remove();
item_element.remove();
});
},
......
......@@ -11,19 +11,11 @@ class StudipDb extends \Codeception\Module
{
/**
* @api
*
* @var
*/
public ?\StudipPdo $dbh;
/**
* @var array
*/
protected array $config = [];
/**
* @var array
*/
protected array $requiredFields = ['dsn', 'user', 'password'];
/**
......
<?php
use JsonApi\Routes\Clipboards\ClipboardItemsCreate;
use JsonApi\Routes\Clipboards\ClipboardItemsDelete;
use JsonApi\Routes\Clipboards\ClipboardsCreate;
use JsonApi\Routes\Clipboards\ClipboardsDelete;
use JsonApi\Routes\Clipboards\ClipboardsUpdate;
use JsonApi\Schemas\Clipboard as ClipboardSchema;
use JsonApi\Schemas\ClipboardItem as ClipboardItemSchema;
use WoohooLabs\Yang\JsonApi\Response\JsonApiResponse;
use WoohooLabs\Yang\JsonApi\Schema\Resource\ResourceObject;
require_once __DIR__ . '/JSONAPIHelperTrait.php';
class ClipboardRoutesTest extends Codeception\Test\Unit
{
use JSONAPIHelperTrait;
public function testCreateClipboard(): void
{
$resource = $this->createClipboard(
$this->tester->getCredentialsForTestDozent()
);
$this->assertHasRelations($resource, 'user', 'clipboard-items');
$this->assertEquals(ClipboardSchema::TYPE, $resource->type());
$this->assertEquals('Test-Clipboard', $resource->attribute('name'));
}
public function testUpdateClipboard(): void
{
$credentials = $this->tester->getCredentialsForTestDozent();
$resource = $this->createClipboard($credentials);
$response = $this->sendMockRequest(
"/clipboards/{id}",
ClipboardsUpdate::class,
$credentials,
['id' => $resource->id()],
[
'considered_successful' => [200],
'method' => 'PATCH',
'json_body' => [
'data' => [
'attributes' => ['name' => 'Foo Bar'],
],
],
],
);
$resource = $this->getResourceFromResponse($response);
$this->assertEquals('Foo Bar', $resource->attribute('name'));
}
public function testDeleteClipboard(): void
{
$credentials = $this->tester->getCredentialsForTestDozent();
$resource = $this->createClipboard($credentials);
$this->sendMockRequest(
"/clipboards/{id}",
ClipboardsDelete::class,
$credentials,
['id' => $resource->id()],
[
'considered_successful' => [204],
'method' => 'DELETE',
],
);
}
public function testAddItemToClipboard(): void
{
$credentials = $this->tester->getCredentialsForTestDozent();
$resource = $this->createClipboard($credentials);
$resource = $this->createClipboardItem(
$credentials,
$resource->id(),
'abcd1234',
'Room'
);
$this->assertHasRelations($resource, 'clipboard');
$this->assertEquals(ClipboardItemSchema::TYPE, $resource->type());
$this->assertEquals('abcd1234', $resource->attribute('range_id'));
$this->assertEquals('Room', $resource->attribute('range_type'));
}
public function testRemoveItemFromClipboard(): void
{
$credentials = $this->tester->getCredentialsForTestDozent();
$clipboard = $this->createClipboard($credentials);
$item = $this->createClipboardItem(
$credentials,
$clipboard->id(),
'abcd1234',
'Room'
);
$this->sendMockRequest(
"/clipboards/{id}/items/{itemId}",
ClipboardItemsDelete::class,
$credentials,
[
'id' => $clipboard->id(),
'itemId' => $item->id(),
],
[
'considered_successful' => [204],
'method' => 'DELETE',
],
);
}
protected function createClipboard(array $credentials, string $name = 'Test-Clipboard'): ResourceObject
{
$response = $this->sendMockRequest(
"/clipboards",
ClipboardsCreate::class,
$credentials,
[],
[
'considered_successful' => [200],
'method' => 'POST',
'json_body' => [
'data' => [
'type' => ClipboardSchema::TYPE,
'attributes' => ['name' => $name],
],
],
],
);
return $this->getResourceFromResponse($response);
}
protected function createClipboardItem(
array $credentials,
string $clipboard_id,
string $range_id,
string $range_type
): ResourceObject {
$response = $this->sendMockRequest(
"/clipboards/{id}/items",
ClipboardItemsCreate::class,
$credentials,
['id' => $clipboard_id],
[
'considered_successful' => [200],
'method' => 'POST',
'json_body' => [
'data' => [
'attributes' => [
'range_id' => $range_id,
'range_type' => $range_type,
],
],
],
],
);
return $this->getResourceFromResponse($response);
}
}
<?php
use WoohooLabs\Yang\JsonApi\Response\JsonApiResponse;
use WoohooLabs\Yang\JsonApi\Schema\Document;
use WoohooLabs\Yang\JsonApi\Schema\Resource\ResourceObject;
require_once __DIR__ . '/JSONAPIHelperTrait.php';
trait ConsultationHelper
{
/**
* @var \UnitTester
*/
protected $tester;
protected function _before()
{
\DBManager::getInstance()->setConnection('studip', $this->getModule('\\Helper\\StudipDb')->dbh);
}
use JSONAPIHelperTrait;
protected static $BLOCK_DATA = [
'room' => 'Testraum',
......@@ -88,23 +78,6 @@ trait ConsultationHelper
return $block->slots->first();
}
protected function withStudipEnv(array $credentials, callable $fn)
{
// Create global template factory if neccessary
$has_template_factory = isset($GLOBALS['template_factory']);
if (!$has_template_factory) {
$GLOBALS['template_factory'] = new Flexi\Factory($GLOBALS['STUDIP_BASE_PATH'] . '/templates');
}
$result = $this->tester->withPHPLib($credentials, $fn);
if (!$has_template_factory) {
unset($GLOBALS['template_factory']);
}
return $result;
}
protected function createBookingForSlot(array $credentials, ConsultationSlot $slot, User $user): ConsultationBooking
{
return $this->withStudipEnv(
......@@ -122,81 +95,4 @@ trait ConsultationHelper
}
);
}
protected function sendMockRequest(string $route, string $handler, array $credentials, array $variables = [], array $options = []): JsonApiResponse
{
$options = array_merge([
'method' => 'GET',
'considered_successful' => [200],
'json_body' => null,
], $options);
$app = $this->tester->createApp(
$credentials,
strtolower($options['method']),
$route,
$handler
);
$evaluated_route = preg_replace_callback(
'/\{(.+?)(:[^}]+)?}/',
function ($match) use ($variables) {
$key = $match[1];
if (!isset($variables[$key])) {
throw new Exception("No variable '{$key}' defined");
}
return $variables[$key];
},
$route
);
$requestBuilder = $this->tester->createRequestBuilder($credentials);
$requestBuilder->setUri($evaluated_route)->setMethod(strtoupper($options['method']));
if (isset($options['json_body'])) {
$requestBuilder->setJsonApiBody($options['json_body']);
}
/** @var JsonApiResponse $response */
$response = $this->withStudipEnv($credentials, function () use ($app, $requestBuilder) {
return $this->tester->sendMockRequest($app, $requestBuilder->getRequest());
});
if ($options['considered_successful']) {
$this->assertTrue(
$response->isSuccessful($options['considered_successful']),
'Actual status code is ' . $response->getStatusCode()
);
}
return $response;
}
protected function getSingleResourceDocument(JsonApiResponse $response): Document
{
$this->assertTrue($response->hasDocument());
$document = $response->document();
$this->assertTrue($document->isSingleResourceDocument());
return $document;
}
protected function getResourceCollectionDocument(JsonApiResponse $response): Document
{
$this->assertTrue($response->hasDocument());
$document = $response->document();
$this->assertTrue($document->isResourceCollectionDocument());
return $document;
}
protected function assertHasRelations(ResourceObject $resource, ...$relations)
{
foreach ($relations as $relation) {
$this->assertTrue($resource->hasRelationship($relation));
}
}
}
<?php
use WoohooLabs\Yang\JsonApi\Response\JsonApiResponse;
use WoohooLabs\Yang\JsonApi\Schema\Document;
use WoohooLabs\Yang\JsonApi\Schema\Resource\ResourceObject;
trait JSONAPIHelperTrait
{
protected JSONAPITester $tester;
protected function _before()
{
DBManager::getInstance()->setConnection(
'studip',
$this->getModule('\\Helper\\StudipDb')->dbh
);
}
protected function withStudipEnv(array $credentials, callable $fn)
{
// Create global template factory if neccessary
$has_template_factory = isset($GLOBALS['template_factory']);
if (!$has_template_factory) {
$GLOBALS['template_factory'] = new Flexi\Factory($GLOBALS['STUDIP_BASE_PATH'] . '/templates');
}
$result = $this->tester->withPHPLib($credentials, $fn);
if (!$has_template_factory) {
unset($GLOBALS['template_factory']);
}
return $result;
}
protected function sendMockRequest(string $route, string $handler, array $credentials, array $variables = [], array $options = []): JsonApiResponse
{
$options = array_merge([
'method' => 'GET',
'considered_successful' => [200],
'json_body' => null,
], $options);
$app = $this->tester->createApp(
$credentials,
strtolower($options['method']),
$route,
$handler
);
$evaluated_route = preg_replace_callback(
'/\{(.+?)(:[^}]+)?}/',
function ($match) use ($variables) {
$key = $match[1];
if (!isset($variables[$key])) {
throw new Exception("No variable '{$key}' defined");
}
return $variables[$key];
},
$route
);
$requestBuilder = $this->tester->createRequestBuilder($credentials);
$requestBuilder->setUri($evaluated_route)->setMethod(strtoupper($options['method']));
if (isset($options['json_body'])) {
$requestBuilder->setJsonApiBody($options['json_body']);
}
/** @var JsonApiResponse $response */
$response = $this->withStudipEnv($credentials, function () use ($app, $requestBuilder) {
return $this->tester->sendMockRequest($app, $requestBuilder->getRequest());
});
if ($options['considered_successful']) {
$this->assertTrue(
$response->isSuccessful($options['considered_successful']),
'Actual status code is ' . $response->getStatusCode()
);
}
return $response;
}
protected function getSingleResourceDocument(JsonApiResponse $response): Document
{
$this->assertTrue($response->hasDocument());
$document = $response->document();
$this->assertTrue($document->isSingleResourceDocument());
return $document;
}
protected function getResourceCollectionDocument(JsonApiResponse $response): Document
{
$this->assertTrue($response->hasDocument());
$document = $response->document();
$this->assertTrue($document->isResourceCollectionDocument());
return $document;
}
protected function assertHasRelations(ResourceObject $resource, ...$relations)
{
foreach ($relations as $relation) {
$this->assertTrue($resource->hasRelationship($relation));
}
}
protected function getResourceFromResponse(JsonApiResponse $response): ResourceObject
{
return $this->getSingleResourceDocument($response)->primaryResource();
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment