diff --git a/appinfo/routes.php b/appinfo/routes.php index aade39a44..9f337bbc1 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -135,6 +135,7 @@ ['name' => 'ApiColumns#createTextColumn', 'url' => '/api/2/columns/text', 'verb' => 'POST'], ['name' => 'ApiColumns#createSelectionColumn', 'url' => '/api/2/columns/selection', 'verb' => 'POST'], ['name' => 'ApiColumns#createDatetimeColumn', 'url' => '/api/2/columns/datetime', 'verb' => 'POST'], + ['name' => 'ApiColumns#createUsergroupColumn', 'url' => '/api/2/columns/usergroup', 'verb' => 'POST'], ['name' => 'ApiFavorite#create', 'url' => '/api/2/favorites/{nodeType}/{nodeId}', 'verb' => 'POST', 'requirements' => ['nodeType' => '(\d+)', 'nodeId' => '(\d+)']], ['name' => 'ApiFavorite#destroy', 'url' => '/api/2/favorites/{nodeType}/{nodeId}', 'verb' => 'DELETE', 'requirements' => ['nodeType' => '(\d+)', 'nodeId' => '(\d+)']], diff --git a/lib/Capabilities.php b/lib/Capabilities.php index 1b8346fc4..493da10b9 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -62,7 +62,8 @@ public function getCapabilities(): array { 'version' => $this->appManager->getAppVersion('tables'), 'apiVersions' => [ '1.0', - '2.0' + '2.0', + '2.1', ], 'features' => [ 'favorite', @@ -81,6 +82,7 @@ public function getCapabilities(): array { 'datetime', 'datetime-date', 'datetime-time', + 'usergroup', ] ], ]; diff --git a/lib/Controller/AOCSController.php b/lib/Controller/AOCSController.php index 54cd1411f..4f3d8720b 100644 --- a/lib/Controller/AOCSController.php +++ b/lib/Controller/AOCSController.php @@ -56,7 +56,7 @@ protected function handlePermissionError(PermissionError $e): DataResponse { * @return DataResponse */ protected function handleNotFoundError(NotFoundError $e): DataResponse { - $this->logger->warning('A not found error occurred: ['. $e->getCode() . ']' . $e->getMessage()); + $this->logger->info('A not found error occurred: ['. $e->getCode() . ']' . $e->getMessage()); return new DataResponse(['message' => $this->n->t('A not found error occurred. More details can be found in the logs. Please reach out to your administration.')], Http::STATUS_NOT_FOUND); } diff --git a/lib/Controller/Api1Controller.php b/lib/Controller/Api1Controller.php index 7a2f33666..d70a0f649 100644 --- a/lib/Controller/Api1Controller.php +++ b/lib/Controller/Api1Controller.php @@ -155,7 +155,7 @@ public function showScheme(int $tableId): DataResponse { $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_FORBIDDEN); } catch (NotFoundError $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } catch (InternalError|Exception $e) { @@ -187,7 +187,7 @@ public function getTable(int $tableId): DataResponse { $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_FORBIDDEN); } catch (NotFoundError $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } catch (InternalError|Exception $e) { @@ -222,7 +222,7 @@ public function updateTable(int $tableId, ?string $title = null, ?string $emoji $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_FORBIDDEN); } catch (NotFoundError $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } catch (InternalError|Exception $e) { @@ -254,7 +254,7 @@ public function deleteTable(int $tableId): DataResponse { $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_FORBIDDEN); } catch (NotFoundError $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } catch (InternalError|Exception $e) { @@ -288,7 +288,7 @@ public function indexViews(int $tableId): DataResponse { $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_FORBIDDEN); } catch (NotFoundError $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } catch (InternalError|Exception $e) { @@ -355,7 +355,7 @@ public function getView(int $viewId): DataResponse { $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_INTERNAL_SERVER_ERROR); } catch (NotFoundError $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } @@ -416,7 +416,7 @@ public function deleteView(int $viewId): DataResponse { $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_INTERNAL_SERVER_ERROR); } catch (NotFoundError $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } @@ -450,7 +450,7 @@ public function getShare(int $shareId): DataResponse { $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_INTERNAL_SERVER_ERROR); } catch (NotFoundError $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } @@ -549,7 +549,7 @@ public function createShare( $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_INTERNAL_SERVER_ERROR); } catch (NotFoundError $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } @@ -581,7 +581,7 @@ public function deleteShare(int $shareId): DataResponse { $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_INTERNAL_SERVER_ERROR); } catch (NotFoundError $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } @@ -615,7 +615,7 @@ public function updateSharePermissions(int $shareId, string $permissionType, boo $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_INTERNAL_SERVER_ERROR); } catch (NotFoundError $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } @@ -660,7 +660,7 @@ public function updateShareDisplayMode(int $shareId, int $displayMode, string $t $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_INTERNAL_SERVER_ERROR); } catch (NotFoundError $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } catch (PermissionError $e) { @@ -729,7 +729,7 @@ public function indexViewColumns(int $viewId): DataResponse { $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_INTERNAL_SERVER_ERROR); } catch (NotFoundError $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } @@ -745,7 +745,7 @@ public function indexViewColumns(int $viewId): DataResponse { * @param int|null $tableId Table ID * @param int|null $viewId View ID * @param string $title Title - * @param 'text'|'number'|'datetime'|'select' $type Column main type + * @param 'text'|'number'|'datetime'|'select'|'usergroup' $type Column main type * @param string|null $subtype Column sub type * @param bool $mandatory Is the column mandatory * @param string|null $description Description @@ -761,6 +761,11 @@ public function indexViewColumns(int $viewId): DataResponse { * @param string|null $selectionOptions Options for a selection (json array{id: int, label: string}) * @param string|null $selectionDefault Default option IDs for a selection (json int[]) * @param string|null $datetimeDefault Default value, if column is datetime + * @param string|null $usergroupDefault Default value, if column is usergroup (json array{id: string, type: int}) + * @param bool|null $usergroupMultipleItems Can select multiple users or/and groups, if column is usergroup + * @param bool|null $usergroupSelectUsers Can select users, if column type is usergroup + * @param bool|null $usergroupSelectGroups Can select groups, if column type is usergroup + * @param bool|null $usergroupShowUserStatus Whether to show the user's status, if column type is usergroup * @param int[]|null $selectedViewIds View IDs where this column should be added to be presented * * @return DataResponse|DataResponse @@ -792,6 +797,13 @@ public function createColumn( ?string $selectionDefault = '', ?string $datetimeDefault = '', + + ?string $usergroupDefault = '', + ?bool $usergroupMultipleItems = null, + ?bool $usergroupSelectUsers = null, + ?bool $usergroupSelectGroups = null, + ?bool $usergroupShowUserStatus = null, + ?array $selectedViewIds = [] ): DataResponse { try { @@ -820,6 +832,13 @@ public function createColumn( $selectionDefault, $datetimeDefault, + + $usergroupDefault, + $usergroupMultipleItems, + $usergroupSelectUsers, + $usergroupSelectGroups, + $usergroupShowUserStatus, + $selectedViewIds )->jsonSerialize()); } catch (PermissionError $e) { @@ -831,7 +850,7 @@ public function createColumn( $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_INTERNAL_SERVER_ERROR); } catch (DoesNotExistException $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } @@ -861,6 +880,11 @@ public function createColumn( * @param string|null $selectionOptions Options for a selection (json array{id: int, label: string}) * @param string|null $selectionDefault Default option IDs for a selection (json int[]) * @param string|null $datetimeDefault Default value, if column is datetime + * @param string|null $usergroupDefault Default value, if column is usergroup + * @param bool|null $usergroupMultipleItems Can select multiple users or/and groups, if column is usergroup + * @param bool|null $usergroupSelectUsers Can select users, if column type is usergroup + * @param bool|null $usergroupSelectGroups Can select groups, if column type is usergroup + * @param bool|null $usergroupShowUserStatus Whether to show the user's status, if column type is usergroup * * @return DataResponse|DataResponse * @@ -887,7 +911,14 @@ public function updateColumn( ?string $selectionOptions, ?string $selectionDefault, - ?string $datetimeDefault + ?string $datetimeDefault, + + ?string $usergroupDefault, + ?bool $usergroupMultipleItems, + ?bool $usergroupSelectUsers, + ?bool $usergroupSelectGroups, + ?bool $usergroupShowUserStatus, + ): DataResponse { try { $item = $this->columnService->update( @@ -913,7 +944,13 @@ public function updateColumn( $selectionOptions, $selectionDefault, - $datetimeDefault + $datetimeDefault, + + $usergroupDefault, + $usergroupMultipleItems, + $usergroupSelectUsers, + $usergroupSelectGroups, + $usergroupShowUserStatus, ); return new DataResponse($item->jsonSerialize()); } catch (InternalError $e) { @@ -945,7 +982,7 @@ public function getColumn(int $columnId): DataResponse { $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_FORBIDDEN); } catch (NotFoundError $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } catch (InternalError|Exception $e) { @@ -977,7 +1014,7 @@ public function deleteColumn(int $columnId): DataResponse { $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_FORBIDDEN); } catch (NotFoundError $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } catch (InternalError|Exception $e) { @@ -1194,7 +1231,7 @@ public function getRow(int $rowId): DataResponse { $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_INTERNAL_SERVER_ERROR); } catch (NotFoundError $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } @@ -1270,7 +1307,7 @@ public function deleteRow(int $rowId): DataResponse { $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_INTERNAL_SERVER_ERROR); } catch (NotFoundError $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } @@ -1303,7 +1340,7 @@ public function deleteRowByView(int $rowId, int $viewId): DataResponse { $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_INTERNAL_SERVER_ERROR); } catch (NotFoundError $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } @@ -1336,7 +1373,7 @@ public function importInTable(int $tableId, string $path, bool $createMissingCol $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_INTERNAL_SERVER_ERROR); } catch (NotFoundError|DoesNotExistException $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } @@ -1369,7 +1406,7 @@ public function importInView(int $viewId, string $path, bool $createMissingColum $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_INTERNAL_SERVER_ERROR); } catch (NotFoundError|DoesNotExistException $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } @@ -1411,7 +1448,7 @@ public function createTableShare(int $tableId, string $receiver, string $receive $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_INTERNAL_SERVER_ERROR); } catch (NotFoundError $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } @@ -1426,7 +1463,7 @@ public function createTableShare(int $tableId, string $receiver, string $receive * * @param int $tableId Table ID * @param string $title Title - * @param 'text'|'number'|'datetime'|'select' $type Column main type + * @param 'text'|'number'|'datetime'|'select'|'usergroup' $type Column main type * @param string|null $subtype Column sub type * @param bool $mandatory Is the column mandatory * @param string|null $description Description @@ -1442,6 +1479,11 @@ public function createTableShare(int $tableId, string $receiver, string $receive * @param string|null $selectionOptions Options for a selection (json array{id: int, label: string}) * @param string|null $selectionDefault Default option IDs for a selection (json int[]) * @param string|null $datetimeDefault Default value, if column is datetime + * @param string|null $usergroupDefault Default value, if column is usergroup + * @param bool|null $usergroupMultipleItems Can select multiple users or/and groups, if column is usergroup + * @param bool|null $usergroupSelectUsers Can select users, if column type is usergroup + * @param bool|null $usergroupSelectGroups Can select groups, if column type is usergroup + * @param bool|null $usergroupShowUserStatus Whether to show the user's status, if column type is usergroup * @param int[]|null $selectedViewIds View IDs where this column should be added to be presented * * @return DataResponse|DataResponse @@ -1473,6 +1515,12 @@ public function createTableColumn( ?string $selectionDefault = '', ?string $datetimeDefault = '', + + ?string $usergroupDefault = '', + ?bool $usergroupMultipleItems = null, + ?bool $usergroupSelectUsers = null, + ?bool $usergroupSelectGroups = null, + ?bool $usergroupShowUserStatus = null, ?array $selectedViewIds = [] ): DataResponse { try { @@ -1501,6 +1549,13 @@ public function createTableColumn( $selectionDefault, $datetimeDefault, + + $usergroupDefault, + $usergroupMultipleItems, + $usergroupSelectUsers, + $usergroupSelectGroups, + $usergroupShowUserStatus, + $selectedViewIds ); return new DataResponse($item->jsonSerialize()); @@ -1513,7 +1568,7 @@ public function createTableColumn( $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_INTERNAL_SERVER_ERROR); } catch (DoesNotExistException $e) { - $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $this->logger->info('A not found error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); } diff --git a/lib/Controller/ApiColumnsController.php b/lib/Controller/ApiColumnsController.php index edaeb8422..163cf869b 100644 --- a/lib/Controller/ApiColumnsController.php +++ b/lib/Controller/ApiColumnsController.php @@ -140,6 +140,11 @@ public function createNumberColumn(int $baseNodeId, string $title, ?float $numbe null, null, null, + null, + null, + null, + null, + null, $selectedViewIds ); return new DataResponse($column->jsonSerialize()); @@ -195,6 +200,11 @@ public function createTextColumn(int $baseNodeId, string $title, ?string $textDe null, null, null, + null, + null, + null, + null, + null, $selectedViewIds ); return new DataResponse($column->jsonSerialize()); @@ -249,6 +259,11 @@ public function createSelectionColumn(int $baseNodeId, string $title, string $se $selectionOptions, $selectionDefault, null, + null, + null, + null, + null, + null, $selectedViewIds ); return new DataResponse($column->jsonSerialize()); @@ -302,6 +317,70 @@ public function createDatetimeColumn(int $baseNodeId, string $title, ?string $da null, null, $datetimeDefault, + null, + null, + null, + null, + null, + $selectedViewIds + ); + return new DataResponse($column->jsonSerialize()); + } + + /** + * [api v2] Create new usergroup column + * + * @NoAdminRequired + * + * @param int $baseNodeId Context of the column creation + * @param string $title Title + * @param string|null $usergroupDefault Json array{id: string, type: int}, eg [{"id": "admin", "type": 0}, {"id": "user1", "type": 0}] + * @param boolean $usergroupMultipleItems Whether you can select multiple users or/and groups + * @param boolean $usergroupSelectUsers Whether you can select users + * @param boolean $usergroupSelectGroups Whether you can select groups + * @param boolean $showUserStatus Whether to show the user's status + * @param string|null $description Description + * @param int[]|null $selectedViewIds View IDs where this columns should be added + * @param boolean $mandatory Is mandatory + * @param 'table'|'view' $baseNodeType Context type of the column creation + * @return DataResponse|DataResponse + * + * 200: Column created + * 403: No permission + * 404: Not found + * @throws InternalError + * @throws NotFoundError + * @throws PermissionError + */ + public function createUsergroupColumn(int $baseNodeId, string $title, ?string $usergroupDefault, bool $usergroupMultipleItems = null, bool $usergroupSelectUsers = null, bool $usergroupSelectGroups = null, bool $showUserStatus = null, string $description = null, ?array $selectedViewIds = [], bool $mandatory = false, string $baseNodeType = 'table'): DataResponse { + $tableId = $baseNodeType === 'table' ? $baseNodeId : null; + $viewId = $baseNodeType === 'view' ? $baseNodeId : null; + $column = $this->service->create( + $this->userId, + $tableId, + $viewId, + 'usergroup', + null, + $title, + $mandatory, + $description, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + $usergroupDefault, + $usergroupMultipleItems, + $usergroupSelectUsers, + $usergroupSelectGroups, + $showUserStatus, $selectedViewIds ); return new DataResponse($column->jsonSerialize()); diff --git a/lib/Controller/ApiTablesController.php b/lib/Controller/ApiTablesController.php index d246636d5..1ac5cd46e 100644 --- a/lib/Controller/ApiTablesController.php +++ b/lib/Controller/ApiTablesController.php @@ -157,6 +157,13 @@ public function createFromScheme(string $title, string $emoji, string $descripti $column['selectionDefault'], $column['datetimeDefault'], + + $column['usergroupDefault'], + $column['usergroupMultipleItems'], + $column['usergroupSelectUsers'], + $column['usergroupSelectGroups'], + $column['showUserStatus'], + [], ); }; diff --git a/lib/Controller/ColumnController.php b/lib/Controller/ColumnController.php index 41f49aa4f..8c150d698 100644 --- a/lib/Controller/ColumnController.php +++ b/lib/Controller/ColumnController.php @@ -92,6 +92,13 @@ public function create( ?string $selectionDefault, ?string $datetimeDefault, + + ?string $usergroupDefault, + ?bool $usergroupMultipleItems, + ?bool $usergroupSelectUsers, + ?bool $usergroupSelectGroups, + ?bool $showUserStatus, + ?array $selectedViewIds ): DataResponse { return $this->handleError(function () use ( @@ -118,6 +125,13 @@ public function create( $selectionDefault, $datetimeDefault, + + $usergroupDefault, + $usergroupMultipleItems, + $usergroupSelectUsers, + $usergroupSelectGroups, + $showUserStatus, + $selectedViewIds) { return $this->service->create( $this->userId, @@ -144,6 +158,13 @@ public function create( $selectionDefault, $datetimeDefault, + + $usergroupDefault, + $usergroupMultipleItems, + $usergroupSelectUsers, + $usergroupSelectGroups, + $showUserStatus, + $selectedViewIds); }); } @@ -174,7 +195,13 @@ public function update( ?string $selectionOptions, ?string $selectionDefault, - ?string $datetimeDefault + ?string $datetimeDefault, + + ?string $usergroupDefault, + ?bool $usergroupMultipleItems, + ?bool $usergroupSelectUsers, + ?bool $usergroupSelectGroups, + ?bool $showUserStatus ): DataResponse { return $this->handleError(function () use ( $id, @@ -199,7 +226,13 @@ public function update( $selectionOptions, $selectionDefault, - $datetimeDefault + $datetimeDefault, + + $usergroupDefault, + $usergroupMultipleItems, + $usergroupSelectUsers, + $usergroupSelectGroups, + $showUserStatus ) { return $this->service->update( $id, @@ -225,7 +258,14 @@ public function update( $selectionOptions, $selectionDefault, - $datetimeDefault); + $datetimeDefault, + + $usergroupDefault, + $usergroupMultipleItems, + $usergroupSelectUsers, + $usergroupSelectGroups, + $showUserStatus + ); }); } diff --git a/lib/Db/Column.php b/lib/Db/Column.php index a59fd586b..ddaae5b15 100644 --- a/lib/Db/Column.php +++ b/lib/Db/Column.php @@ -60,6 +60,16 @@ * @method setSelectionDefault(?string $selectionDefault) * @method getDatetimeDefault(): string * @method setDatetimeDefault(?string $datetimeDefault) + * @method getUsergroupDefault(): string + * @method setUsergroupDefault(?string $usergroupDefaultArray) + * @method getUsergroupMultipleItems(): bool + * @method setUsergroupMultipleItems(?bool $usergroupMultipleItems) + * @method getUsergroupSelectUsers(): bool + * @method setUsergroupSelectUsers(?bool $usergroupSelectUsers) + * @method getUsergroupSelectGroups(): bool + * @method setUsergroupSelectGroups(?bool $usergroupSelectGroups) + * @method getShowUserStatus(): bool + * @method setShowUserStatus(?bool $showUserStatus) */ class Column extends Entity implements JsonSerializable { // Meta column types @@ -73,6 +83,7 @@ class Column extends Entity implements JsonSerializable { public const TYPE_TEXT = 'text'; public const TYPE_NUMBER = 'number'; public const TYPE_DATETIME = 'datetime'; + public const TYPE_USERGROUP = 'usergroup'; protected ?string $title = null; protected ?int $tableId = null; @@ -108,6 +119,13 @@ class Column extends Entity implements JsonSerializable { // type datetime protected ?string $datetimeDefault = null; + // type usergroup + protected ?string $usergroupDefault = null; + protected ?bool $usergroupMultipleItems = null; + protected ?bool $usergroupSelectUsers = null; + protected ?bool $usergroupSelectGroups = null; + protected ?bool $showUserStatus = null; + public function __construct() { $this->addType('id', 'integer'); $this->addType('tableId', 'integer'); @@ -121,6 +139,26 @@ public function __construct() { // type text $this->addType('textMaxLength', 'integer'); + + // // type usergroup + $this->addType('usergroupMultipleItems', 'boolean'); + $this->addType('usergroupSelectUsers', 'boolean'); + $this->addType('usergroupSelectGroups', 'boolean'); + $this->addType('showUserStatus', 'boolean'); + } + + public function getUsergroupDefaultArray():array { + $default = $this->getUsergroupDefault(); + if ($default !== "" && $default !== null) { + return \json_decode($default, true) ?? []; + } else { + return []; + } + } + + public function setUsergroupDefaultArray(array $array):void { + $json = \json_encode($array); + $this->setUsergroup($json); } public function getSelectionOptionsArray():array { @@ -175,6 +213,13 @@ public function jsonSerialize(): array { // type datetime 'datetimeDefault' => $this->datetimeDefault, + + // type usergroup + 'usergroupDefault' => $this->getUsergroupDefaultArray(), + 'usergroupMultipleItems' => $this->usergroupMultipleItems, + 'usergroupSelectUsers' => $this->usergroupSelectUsers, + 'usergroupSelectGroups' => $this->usergroupSelectGroups, + 'showUserStatus' => $this->showUserStatus, ]; } } diff --git a/lib/Db/ColumnTypes/UsergroupColumnQB.php b/lib/Db/ColumnTypes/UsergroupColumnQB.php new file mode 100644 index 000000000..ccada8a50 --- /dev/null +++ b/lib/Db/ColumnTypes/UsergroupColumnQB.php @@ -0,0 +1,12 @@ +setParameter($searchValuePlaceHolder, $unformattedSearchValue, IQueryBuilder::PARAM_STR); + } +} diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index 06deb17cf..4e2b4f1cd 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -9,7 +9,7 @@ use OCA\Tables\Helper\UserHelper; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; -use OCP\AppFramework\Db\QBMapper; +use OCP\AppFramework\Db\TTransactional; use OCP\DB\Exception; use OCP\DB\IResult; use OCP\DB\QueryBuilder\IQueryBuilder; @@ -21,6 +21,8 @@ use Throwable; class Row2Mapper { + use TTransactional; + private RowSleeveMapper $rowSleeveMapper; private ?string $userId = null; private IDBConnection $db; @@ -54,22 +56,14 @@ public function delete(Row2 $row): Row2 { $this->db->beginTransaction(); try { foreach ($this->columnsHelper->columns as $columnType) { - $cellMapperClassName = 'OCA\Tables\Db\RowCell' . ucfirst($columnType) . 'Mapper'; - /** @var RowCellMapperSuper $cellMapper */ - try { - $cellMapper = Server::get($cellMapperClassName); - } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new Exception(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); - } - $cellMapper->deleteAllForRow($row->getId()); + $this->getCellMapperFromType($columnType)->deleteAllForRow($row->getId()); } $this->rowSleeveMapper->deleteById($row->getId()); $this->db->commit(); } catch (Throwable $e) { $this->db->rollBack(); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new Exception(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new Exception(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); } return $row; @@ -91,11 +85,11 @@ public function find(int $id, array $columns): Row2 { } elseif (count($rows) === 0) { $e = new Exception('Wanted row not found.'); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); } else { $e = new Exception('Too many results for one wanted row.'); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); } } @@ -105,9 +99,9 @@ public function find(int $id, array $columns): Row2 { public function findNextId(int $offsetId = -1): ?int { try { $rowSleeve = $this->rowSleeveMapper->findNext($offsetId); - } catch (MultipleObjectsReturnedException|Exception $e) { + } catch (MultipleObjectsReturnedException | Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); } catch (DoesNotExistException $e) { return null; } @@ -139,7 +133,7 @@ private function getWantedRowIds(string $userId, int $tableId, ?array $filter = $qb->select('id') ->from('tables_row_sleeves', 'sleeves') ->where($qb->expr()->eq('table_id', $qb->createNamedParameter($tableId, IQueryBuilder::PARAM_INT))); - if($filter) { + if ($filter) { $this->addFilterToQuery($qb, $filter, $userId); } if ($limit !== null) { @@ -194,8 +188,19 @@ private function getRows(array $rowIds, array $columnIds): array { foreach ($this->columnsHelper->columns as $columnType) { $qbTmp = $this->db->getQueryBuilder(); $qbTmp->select('row_id', 'column_id', 'last_edit_at', 'last_edit_by') - ->selectAlias($qb->expr()->castColumn('value', IQueryBuilder::PARAM_STR), 'value') - ->from('tables_row_cells_'.$columnType) + ->selectAlias($qb->expr()->castColumn('value', IQueryBuilder::PARAM_STR), 'value'); + + // This is not ideal but I cannot think of a good way to abstract this away into the mapper right now + // Ideally we dynamically construct this query depending on what additional selects the column type requires + // however the union requires us to match the exact number of selects for each column type + if ($columnType === Column::TYPE_USERGROUP) { + $qbTmp->selectAlias($qb->expr()->castColumn('value_type', IQueryBuilder::PARAM_STR), 'value_type'); + } else { + $qbTmp->selectAlias($qbTmp->createFunction('NULL'), 'value_type'); + } + + $qbTmp + ->from('tables_row_cells_' . $columnType) ->where($qb->expr()->in('column_id', $qb->createNamedParameter($columnIds, IQueryBuilder::PARAM_INT_ARRAY, ':columnIds'))) ->andWhere($qb->expr()->in('row_id', $qb->createNamedParameter($rowIds, IQueryBuilder::PARAM_INT_ARRAY, ':rowsIds'))); @@ -208,6 +213,8 @@ private function getRows(array $rowIds, array $columnIds): array { $qbSqlForColumnTypes .= ')'; $qb->select('row_id', 'column_id', 'created_by', 'created_at', 't1.last_edit_by', 't1.last_edit_at', 'value', 'table_id') + // Also should be more generic (see above) + ->addSelect('value_type') ->from($qb->createFunction($qbSqlForColumnTypes), 't1') ->innerJoin('t1', 'tables_row_sleeves', 'rs', 'rs.id = t1.row_id'); @@ -222,10 +229,17 @@ private function getRows(array $rowIds, array $columnIds): array { $sleeves = $this->rowSleeveMapper->findMultiple($rowIds); } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); + } + + try { + $columnTypes = $this->columnMapper->getColumnTypes($columnIds); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); } - return $this->parseEntities($result, $sleeves); + return $this->parseEntities($result, $sleeves, $columnTypes); } /** @@ -247,7 +261,7 @@ private function addFilterToQuery(IQueryBuilder &$qb, array $filters, string $us private function replacePlaceholderValues(array &$filters, string $userId): void { foreach ($filters as &$filterGroup) { foreach ($filterGroup as &$filter) { - if(substr($filter['value'], 0, 1) === '@') { + if (substr($filter['value'], 0, 1) === '@') { $filter['value'] = $this->resolveSearchValue($filter['value'], $userId); } } @@ -297,7 +311,7 @@ private function getFilter(IQueryBuilder &$qb, array $filterGroup): array { } else { $e = new Exception("Needed column (" . $columnId . ") not found."); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); } $filterExpressions[] = $sql; } @@ -309,7 +323,7 @@ private function getFilter(IQueryBuilder &$qb, array $filterGroup): array { */ private function getFilterExpression(IQueryBuilder $qb, Column $column, string $operator, string $value): IQueryBuilder { $paramType = $this->getColumnDbParamType($column); - $value = $this->formatValue($column, $value, 'in'); + $value = $this->getCellMapper($column)->filterValueToQueryParam($column, $value); // We try to match the requested value against the default before building the query // so we know if we shall include rows that have no entry in the column_TYPE tables upfront @@ -341,20 +355,20 @@ private function getFilterExpression(IQueryBuilder $qb, Column $column, string $ if ($column->getType() === 'selection' && $column->getSubtype() === 'multi') { $value = str_replace(['"', '\''], '', $value); $filterExpression = $qb2->expr()->orX( - $qb->expr()->like('value', $qb->createNamedParameter('['.$this->db->escapeLikeParameter($value).']')), - $qb->expr()->like('value', $qb->createNamedParameter('['.$this->db->escapeLikeParameter($value).',%')), - $qb->expr()->like('value', $qb->createNamedParameter('%,'.$this->db->escapeLikeParameter($value).']%')), - $qb->expr()->like('value', $qb->createNamedParameter('%,'.$this->db->escapeLikeParameter($value).',%')) + $qb->expr()->like('value', $qb->createNamedParameter('[' . $this->db->escapeLikeParameter($value) . ']')), + $qb->expr()->like('value', $qb->createNamedParameter('[' . $this->db->escapeLikeParameter($value) . ',%')), + $qb->expr()->like('value', $qb->createNamedParameter('%,' . $this->db->escapeLikeParameter($value) . ']%')), + $qb->expr()->like('value', $qb->createNamedParameter('%,' . $this->db->escapeLikeParameter($value) . ',%')) ); break; } - $filterExpression = $qb->expr()->like('value', $qb->createNamedParameter('%'.$this->db->escapeLikeParameter($value).'%', $paramType)); + $filterExpression = $qb->expr()->like('value', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%', $paramType)); break; case 'is-equal': $includeDefault = $defaultValue === $value; if ($column->getType() === 'selection' && $column->getSubtype() === 'multi') { $value = str_replace(['"', '\''], '', $value); - $filterExpression = $qb->expr()->eq('value', $qb->createNamedParameter('['.$this->db->escapeLikeParameter($value).']', $paramType)); + $filterExpression = $qb->expr()->eq('value', $qb->createNamedParameter('[' . $this->db->escapeLikeParameter($value) . ']', $paramType)); break; } $filterExpression = $qb->expr()->eq('value', $qb->createNamedParameter($value, $paramType)); @@ -380,7 +394,7 @@ private function getFilterExpression(IQueryBuilder $qb, Column $column, string $ $filterExpression = $qb->expr()->isNull('value'); break; default: - throw new InternalError('Operator '.$operator.' is not supported.'); + throw new InternalError('Operator ' . $operator . ' is not supported.'); } return $qb2->andWhere( @@ -435,11 +449,11 @@ private function getMetaFilterExpression(IQueryBuilder $qb, int $columnId, strin private function getSqlOperator(string $operator, IQueryBuilder $qb, string $columnName, $value, $paramType): string { switch ($operator) { case 'begins-with': - return $qb->expr()->like($columnName, $qb->createNamedParameter('%'.$this->db->escapeLikeParameter($value), $paramType)); + return $qb->expr()->like($columnName, $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($value), $paramType)); case 'ends-with': - return $qb->expr()->like($columnName, $qb->createNamedParameter($this->db->escapeLikeParameter($value).'%', $paramType)); + return $qb->expr()->like($columnName, $qb->createNamedParameter($this->db->escapeLikeParameter($value) . '%', $paramType)); case 'contains': - return $qb->expr()->like($columnName, $qb->createNamedParameter('%'.$this->db->escapeLikeParameter($value).'%', $paramType)); + return $qb->expr()->like($columnName, $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%', $paramType)); case 'is-equal': return $qb->expr()->eq($columnName, $qb->createNamedParameter($value, $paramType)); case 'is-greater-than': @@ -453,7 +467,7 @@ private function getSqlOperator(string $operator, IQueryBuilder $qb, string $col case 'is-empty': return $qb->expr()->isNull($columnName); default: - throw new InternalError('Operator '.$operator.' is not supported.'); + throw new InternalError('Operator ' . $operator . ' is not supported.'); } } @@ -492,7 +506,7 @@ private function resolveSearchValue(string $placeholder, string $userId): string * @return Row2[] * @throws InternalError */ - private function parseEntities(IResult $result, array $sleeves): array { + private function parseEntities(IResult $result, array $sleeves, array $columnTypes): array { $data = $result->fetchAll(); $rows = []; @@ -506,13 +520,37 @@ private function parseEntities(IResult $result, array $sleeves): array { $rows[$sleeve->getId()]->setTableId($sleeve->getTableId()); } + $rowValues = []; + $keyToColumnId = []; + $keyToRowId = []; + foreach ($data as $rowData) { if (!isset($rowData['row_id']) || !isset($rows[$rowData['row_id']])) { break; } - /* @var array $rowData */ - $rows[$rowData['row_id']]->addCell($rowData['column_id'], $this->formatValue($this->columns[$rowData['column_id']], $rowData['value'])); + $columnType = $this->columns[$rowData['column_id']]->getType(); + $cellClassName = 'OCA\Tables\Db\RowCell' . ucfirst($columnType); + $entity = call_user_func($cellClassName .'::fromRowData', $rowData); // >5.2.3 + $cellMapper = $this->getCellMapperFromType($columnType); + $value = $cellMapper->formatEntity($this->columns[$rowData['column_id']], $entity); + $compositeKey = (string)$rowData['row_id'] . ',' . (string)$rowData['column_id']; + + if ($cellMapper->hasMultipleValues()) { + if (array_key_exists($compositeKey, $rowValues)) { + $rowValues[$compositeKey][] = $value; + } else { + $rowValues[$compositeKey] = [$value]; + } + } else { + $rowValues[$compositeKey] = $value; + } + $keyToColumnId[$compositeKey] = $rowData['column_id']; + $keyToRowId[$compositeKey] = $rowData['row_id']; + } + + foreach ($rowValues as $compositeKey => $value) { + $rows[$keyToRowId[$compositeKey]]->addCell($keyToColumnId[$compositeKey], $value); } // format an array without keys @@ -540,7 +578,7 @@ public function isRowInViewPresent(int $rowId, View $view, string $userId): bool public function insert(Row2 $row, array $columns): Row2 { $this->setColumns($columns); - if($row->getId()) { + if ($row->getId()) { // if row has an id from migration or import etc. $rowSleeve = $this->createRowSleeveFromExistingData($row->getId(), $row->getTableId(), $row->getCreatedAt(), $row->getCreatedBy(), $row->getLastEditBy(), $row->getLastEditAt()); } else { @@ -564,7 +602,7 @@ public function insert(Row2 $row, array $columns): Row2 { * @throws InternalError */ public function update(Row2 $row, array $columns): Row2 { - if(!$columns) { + if (!$columns) { throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': columns are missing'); } $this->setColumns($columns); @@ -579,9 +617,9 @@ public function update(Row2 $row, array $columns): Row2 { $sleeve = $this->rowSleeveMapper->find($row->getId()); $this->updateMetaData($sleeve); $this->rowSleeveMapper->update($sleeve); - } catch (DoesNotExistException|MultipleObjectsReturnedException|Exception $e) { + } catch (DoesNotExistException | MultipleObjectsReturnedException | Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); } // write all changed cells to its db-table @@ -648,32 +686,37 @@ private function insertCell(int $rowId, int $columnId, $value, ?string $lastEdit throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } - $cellClassName = 'OCA\Tables\Db\RowCell'.ucfirst($this->columns[$columnId]->getType()); - /** @var RowCellSuper $cell */ - $cell = new $cellClassName(); - - $cell->setRowIdWrapper($rowId); - $cell->setColumnIdWrapper($columnId); - $this->updateMetaData($cell, false, $lastEditAt, $lastEditBy); // insert new cell - $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($this->columns[$columnId]->getType()).'Mapper'; - /** @var QBMapper $cellMapper */ - try { - $cellMapper = Server::get($cellMapperClassName); - } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } + $column = $this->columns[$columnId]; + $cellMapper = $this->getCellMapper($column); - $v = $this->formatValue($this->columns[$columnId], $value, 'in'); - $cell->setValueWrapper($v); + $column = $this->columns[$columnId]; try { - $cellMapper->insert($cell); + $cellClassName = 'OCA\Tables\Db\RowCell' . ucfirst($this->columns[$columnId]->getType()); + if ($cellMapper->hasMultipleValues()) { + foreach ($value as $val) { + /** @var RowCellSuper $cell */ + $cell = new $cellClassName(); + $cell->setRowIdWrapper($rowId); + $cell->setColumnIdWrapper($columnId); + $this->updateMetaData($cell, false, $lastEditAt, $lastEditBy); + $cellMapper->applyDataToEntity($column, $cell, $val); + $cellMapper->insert($cell); + } + } else { + /** @var RowCellSuper $cell */ + $cell = new $cellClassName(); + $cell->setRowIdWrapper($rowId); + $cell->setColumnIdWrapper($columnId); + $this->updateMetaData($cell, false, $lastEditAt, $lastEditBy); + $cellMapper->applyDataToEntity($column, $cell, $value); + $cellMapper->insert($cell); + } } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError('Failed to insert column: ' . $e->getMessage(), 0, $e); } } @@ -685,8 +728,7 @@ private function insertCell(int $rowId, int $columnId, $value, ?string $lastEdit * @throws InternalError */ private function updateCell(RowCellSuper $cell, RowCellMapperSuper $mapper, $value, Column $column): void { - $v = $this->formatValue($column, $value, 'in'); - $cell->setValueWrapper($v); + $this->getCellMapper($column)->applyDataToEntity($column, $cell, $value); $this->updateMetaData($cell); $mapper->updateWrapper($cell); } @@ -695,18 +737,18 @@ private function updateCell(RowCellSuper $cell, RowCellMapperSuper $mapper, $val * @throws InternalError */ private function insertOrUpdateCell(int $rowId, int $columnId, $value): void { - $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($this->columns[$columnId]->getType()).'Mapper'; - /** @var RowCellMapperSuper $cellMapper */ + $cellMapper = $this->getCellMapper($this->columns[$columnId]); try { - $cellMapper = Server::get($cellMapperClassName); - } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } - try { - $cell = $cellMapper->findByRowAndColumn($rowId, $columnId); - $this->updateCell($cell, $cellMapper, $value, $this->columns[$columnId]); - } catch (DoesNotExistException $e) { + if ($cellMapper->hasMultipleValues()) { + $this->atomic(function () use ($cellMapper, $rowId, $columnId, $value) { + $cellMapper->deleteAllForRow($rowId); + $this->insertCell($rowId, $columnId, $value); + }, $this->db); + } else { + $cell = $cellMapper->findByRowAndColumn($rowId, $columnId); + $this->updateCell($cell, $cellMapper, $value, $this->columns[$columnId]); + } + } catch (DoesNotExistException) { $this->insertCell($rowId, $columnId, $value); } catch (MultipleObjectsReturnedException|Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); @@ -729,61 +771,37 @@ private function setColumns(array $columns, array $tableColumns = []): void { } - /** - * @param Column $column - * @param mixed $value - * @param 'out'|'in' $mode Parse the value for incoming requests that get send to the db or outgoing, from the db to the services - * @return mixed - * @throws InternalError - */ - private function formatValue(Column $column, $value, string $mode = 'out') { - $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($column->getType()).'Mapper'; + private function getCellMapper(Column $column): RowCellMapperSuper { + return $this->getCellMapperFromType($column->getType()); + } + + private function getCellMapperFromType(string $columnType): RowCellMapperSuper { + $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($columnType).'Mapper'; /** @var RowCellMapperSuper $cellMapper */ try { - $cellMapper = Server::get($cellMapperClassName); + return Server::get($cellMapperClassName); } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } - if ($mode === 'out') { - return $cellMapper->parseValueOutgoing($column, $value); - } else { - return $cellMapper->parseValueIncoming($column, $value); - } } /** * @throws InternalError */ private function getColumnDbParamType(Column $column) { - $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($column->getType()).'Mapper'; - /** @var RowCellMapperSuper $cellMapper */ - try { - $cellMapper = Server::get($cellMapperClassName); - } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } - return $cellMapper->getDbParamType(); + return $this->getCellMapper($column)->getDbParamType(); } /** * @throws InternalError */ public function deleteDataForColumn(Column $column): void { - $cellMapperClassName = 'OCA\Tables\Db\RowCell' . ucfirst($column->getType()) . 'Mapper'; - /** @var RowCellMapperSuper $cellMapper */ try { - $cellMapper = Server::get($cellMapperClassName); - } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); - } - try { - $cellMapper->deleteAllForColumn($column->getId()); + $this->getCellMapper($column)->deleteAllForColumn($column->getId()); } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); } } @@ -834,19 +852,21 @@ private function getFormattedDefaultValue(Column $column) { $defaultValue = null; switch ($column->getType()) { case Column::TYPE_SELECTION: - $defaultValue = $this->formatValue($column, $column->getSelectionDefault(), 'in'); + $defaultValue = $this->getCellMapper($column)->filterValueToQueryParam($column, $column->getSelectionDefault()); break; case Column::TYPE_DATETIME: - $defaultValue = $this->formatValue($column, $column->getDatetimeDefault(), 'in'); + $defaultValue = $this->getCellMapper($column)->filterValueToQueryParam($column, $column->getDatetimeDefault()); break; case Column::TYPE_NUMBER: - $defaultValue = $this->formatValue($column, $column->getNumberDefault(), 'in'); + $defaultValue = $this->getCellMapper($column)->filterValueToQueryParam($column, $column->getNumberDefault()); break; case Column::TYPE_TEXT: - $defaultValue = $this->formatValue($column, $column->getTextDefault(), 'in'); + $defaultValue = $this->getCellMapper($column)->filterValueToQueryParam($column, $column->getTextDefault()); + break; + case Column::TYPE_USERGROUP: + $defaultValue = $this->getCellMapper($column)->filterValueToQueryParam($column, $column->getUsergroupDefault()); break; } return $defaultValue; } - } diff --git a/lib/Db/RowCellDatetime.php b/lib/Db/RowCellDatetime.php index 5ee46201b..fa4c0deaf 100644 --- a/lib/Db/RowCellDatetime.php +++ b/lib/Db/RowCellDatetime.php @@ -5,8 +5,9 @@ /** @template-extends RowCellSuper */ class RowCellDatetime extends RowCellSuper { protected ?string $value = null; + protected ?int $valueType = null; public function jsonSerialize(): array { - return parent::jsonSerializePreparation($this->value); + return parent::jsonSerializePreparation($this->value, $this->valueType); } } diff --git a/lib/Db/RowCellMapperSuper.php b/lib/Db/RowCellMapperSuper.php index 86f76509e..50bcbdab9 100644 --- a/lib/Db/RowCellMapperSuper.php +++ b/lib/Db/RowCellMapperSuper.php @@ -12,8 +12,8 @@ /** * @template-extends QBMapper * @template T of RowCellSuper - * @template TIncoming - * @template TOutgoing + * @template TIncoming Type the db is using to store the actual value + * @template TOutgoing Type the API is using */ class RowCellMapperSuper extends QBMapper { @@ -22,32 +22,39 @@ public function __construct(IDBConnection $db, string $table, string $class) { } /** - * Parse value for db results (after send request) - * eg for filtering + * Format a row cell entity to API response array * - * @param Column $column - * @param TOutgoing $value + * @param T $cell * @return TOutgoing */ - public function parseValueOutgoing(Column $column, $value) { + public function formatEntity(Column $column, RowCellSuper $cell) { + /** @var TOutgoing $value */ + $value = $cell->getValue(); return $value; } - - /** - * Parse value for db requests (before send request) - * - * @param Column $column - * @param TIncoming $value - * @return TIncoming + /* + * Transform value from a filter rule to the actual query parameter used + * for constructing the view filter query */ - public function parseValueIncoming(Column $column, $value) { + public function filterValueToQueryParam(Column $column, $value) { return $value; } + public function applyDataToEntity(Column $column, RowCellSuper $cell, $data): void { + $cell->setValue($data); + } + public function getDbParamType() { return IQueryBuilder::PARAM_STR; } + /* + * Indicating that the column can have multiple values represented by individual entities + */ + public function hasMultipleValues(): bool { + return false; + } + /** * @throws Exception */ diff --git a/lib/Db/RowCellNumber.php b/lib/Db/RowCellNumber.php index c1d33f7a6..45e0f1c96 100644 --- a/lib/Db/RowCellNumber.php +++ b/lib/Db/RowCellNumber.php @@ -5,8 +5,9 @@ /** @template-extends RowCellSuper */ class RowCellNumber extends RowCellSuper { protected ?float $value = null; + protected ?int $valueType = null; public function jsonSerialize(): array { - return parent::jsonSerializePreparation($this->value); + return parent::jsonSerializePreparation($this->value, $this->valueType); } } diff --git a/lib/Db/RowCellNumberMapper.php b/lib/Db/RowCellNumberMapper.php index 3a8867722..3b77f8d9f 100644 --- a/lib/Db/RowCellNumberMapper.php +++ b/lib/Db/RowCellNumberMapper.php @@ -13,10 +13,8 @@ public function __construct(IDBConnection $db) { parent::__construct($db, $this->table, RowCellNumber::class); } - /** - * @inheritDoc - */ - public function parseValueOutgoing(Column $column, $value) { + public function formatEntity(Column $column, RowCellSuper $cell) { + $value = $cell->getValue(); if($value === '') { return null; } @@ -28,14 +26,11 @@ public function parseValueOutgoing(Column $column, $value) { } } - /** - * @inheritDoc - */ - public function parseValueIncoming(Column $column, $value): ?float { - if(!is_numeric($value)) { - return null; + public function applyDataToEntity(Column $column, RowCellSuper $cell, $data): void { + if(!is_numeric($data)) { + $cell->setValueWrapper(null); } - return (float) $value; + $cell->setValueWrapper((float)$data); } public function getDbParamType() { diff --git a/lib/Db/RowCellSelection.php b/lib/Db/RowCellSelection.php index 42cc14202..9219944f3 100644 --- a/lib/Db/RowCellSelection.php +++ b/lib/Db/RowCellSelection.php @@ -5,8 +5,9 @@ /** @template-extends RowCellSuper */ class RowCellSelection extends RowCellSuper { protected ?string $value = null; + protected ?int $valueType = null; public function jsonSerialize(): array { - return parent::jsonSerializePreparation($this->value); + return parent::jsonSerializePreparation($this->value, $this->valueType); } } diff --git a/lib/Db/RowCellSelectionMapper.php b/lib/Db/RowCellSelectionMapper.php index 66d71dff0..36f280d95 100644 --- a/lib/Db/RowCellSelectionMapper.php +++ b/lib/Db/RowCellSelectionMapper.php @@ -14,10 +14,19 @@ public function __construct(IDBConnection $db) { parent::__construct($db, $this->table, RowCellSelection::class); } - /** - * @inheritDoc - */ - public function parseValueIncoming(Column $column, $value): string { + public function filterValueToQueryParam(Column $column, $value) { + return $this->valueToJsonDbValue($column, $value); + } + + public function applyDataToEntity(Column $column, RowCellSuper $cell, $data): void { + $cell->setValue($this->valueToJsonDbValue($column, $data)); + } + + public function formatEntity(Column $column, RowCellSuper $cell) { + return json_decode($cell->getValue()); + } + + private function valueToJsonDbValue(Column $column, $value): string { if ($column->getSubtype() === 'check') { return json_encode(ltrim($value, '"')); } @@ -28,11 +37,4 @@ public function parseValueIncoming(Column $column, $value): string { return json_encode($value); } - - /** - * @inheritDoc - */ - public function parseValueOutgoing(Column $column, $value) { - return json_decode($value); - } } diff --git a/lib/Db/RowCellSuper.php b/lib/Db/RowCellSuper.php index e2d5962ec..2c31be541 100644 --- a/lib/Db/RowCellSuper.php +++ b/lib/Db/RowCellSuper.php @@ -29,6 +29,8 @@ abstract class RowCellSuper extends Entity implements JsonSerializable { protected ?int $rowId = null; protected ?string $lastEditBy = null; protected ?string $lastEditAt = null; + protected ?string $createdBy = null; + protected ?string $createdAt = null; public function __construct() { $this->addType('id', 'integer'); @@ -36,17 +38,39 @@ public function __construct() { $this->addType('rowId', 'integer'); } + /** + * Same as Entity::fromRow but ignoring unknown properties + */ + public static function fromRowData(array $row): RowCellSuper { + $instance = new static(); + + foreach ($row as $key => $value) { + $property = $instance->columnToProperty($key); + $setter = 'set' . ucfirst($property); + ; + if (property_exists($instance, $property)) { + $instance->$setter($value); + } + } + + $instance->resetUpdatedFields(); + + return $instance; + } + /** * @param float|null|string $value + * @param int $value_type */ - public function jsonSerializePreparation(string|float|null $value): array { + public function jsonSerializePreparation(string|float|null $value, int $valueType): array { return [ 'id' => $this->id, 'columnId' => $this->columnId, 'rowId' => $this->rowId, 'lastEditBy' => $this->lastEditBy, 'lastEditAt' => $this->lastEditAt, - 'value' => $value + 'value' => $value, + 'valueType' => $valueType ]; } diff --git a/lib/Db/RowCellText.php b/lib/Db/RowCellText.php index 0650d7d8f..c64126f48 100644 --- a/lib/Db/RowCellText.php +++ b/lib/Db/RowCellText.php @@ -5,8 +5,9 @@ /** @template-extends RowCellSuper */ class RowCellText extends RowCellSuper { protected ?string $value = null; + protected ?int $valueType = null; public function jsonSerialize(): array { - return parent::jsonSerializePreparation($this->value); + return parent::jsonSerializePreparation($this->value, $this->valueType); } } diff --git a/lib/Db/RowCellUsergroup.php b/lib/Db/RowCellUsergroup.php new file mode 100644 index 000000000..65d3d2a0a --- /dev/null +++ b/lib/Db/RowCellUsergroup.php @@ -0,0 +1,33 @@ + + * @method setValueType(int $param) + * @method getValueType(): int + */ +class RowCellUsergroup extends RowCellSuper { + protected ?string $value = null; + protected ?int $valueType = null; + + public function jsonSerialize(): array { + return parent::jsonSerializePreparation($this->value, $this->valueType); + } + + public function setValueWrapper($value) { + $this->setValue((string)$value['id']); + $this->setValueType((int)$value['type']); + } + + public static function verifyUserGroupArray(array $data): bool { + if (!array_key_exists('id', $data)) { + return false; + } + if (!array_key_exists('type', $data)) { + return false; + } + + return true; + } +} diff --git a/lib/Db/RowCellUsergroupMapper.php b/lib/Db/RowCellUsergroupMapper.php new file mode 100644 index 000000000..5aa052cb6 --- /dev/null +++ b/lib/Db/RowCellUsergroupMapper.php @@ -0,0 +1,40 @@ + */ +class RowCellUsergroupMapper extends RowCellMapperSuper { + protected string $table = 'tables_row_cells_usergroup'; + + public function __construct(IDBConnection $db, private IUserManager $userManager) { + parent::__construct($db, $this->table, RowCellUsergroup::class); + } + + public function filterValueToQueryParam(Column $column, $value) { + return $value; + } + + public function applyDataToEntity(Column $column, RowCellSuper $cell, $data): void { + if (!RowCellUsergroup::verifyUserGroupArray($data)) { + throw new \InvalidArgumentException('Provided value is not valid user group data'); + } + + $cell->setValueWrapper($data); + } + + public function formatEntity(Column $column, RowCellSuper $cell) { + $displayName = $cell->getValueType() === 0 ? ($this->userManager->getDisplayName($cell->getValue()) ?? $cell->getValue()) : $cell->getValue(); + return [ + 'id' => $cell->getValue(), + 'type' => $cell->getValueType(), + 'displayName' => $displayName, + ]; + } + + public function hasMultipleValues(): bool { + return true; + } +} diff --git a/lib/Helper/ColumnsHelper.php b/lib/Helper/ColumnsHelper.php index 338ad07cd..8045422dc 100644 --- a/lib/Helper/ColumnsHelper.php +++ b/lib/Helper/ColumnsHelper.php @@ -8,6 +8,7 @@ class ColumnsHelper { 'text', 'number', 'datetime', - 'selection' + 'selection', + 'usergroup' ]; } diff --git a/lib/Migration/Version000000Date20210921000000.php b/lib/Migration/Version000000Date20210921000000.php index 0c5a71ca6..7867b6ef9 100644 --- a/lib/Migration/Version000000Date20210921000000.php +++ b/lib/Migration/Version000000Date20210921000000.php @@ -161,6 +161,28 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt 'notnull' => false, ]); + // type usergroup (added in Version000800Date20240712000000) + $table->addColumn('usergroup_default', Types::TEXT, [ + 'notnull' => false, + 'length' => 65535, + ]); + $table->addColumn('usergroup_multiple_items', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => 0, + ]); + $table->addColumn('usergroup_select_users', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => 0, + ]); + $table->addColumn('usergroup_select_groups', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => 0, + ]); + $table->addColumn('show_user_status', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => 0, + ]); + $table->setPrimaryKey(['id']); $table->addIndex(['table_id'], 'tables_columns_t_id'); diff --git a/lib/Migration/Version000800Date20240712000000.php b/lib/Migration/Version000800Date20240712000000.php new file mode 100644 index 000000000..726d7929e --- /dev/null +++ b/lib/Migration/Version000800Date20240712000000.php @@ -0,0 +1,91 @@ +createUserGroupTable($schema, 'usergroup', Types::TEXT); + $changes = $this->haveUserGroupColumnDefinitionFields($schema) ?? $changes; + + return $changes; + } + + private function createUserGroupTable(ISchemaWrapper $schema, string $name, string $type): ?ISchemaWrapper { + if (!$schema->hasTable('tables_row_cells_'.$name)) { + $table = $schema->createTable('tables_row_cells_'.$name); + $table->addColumn('id', Types::INTEGER, [ + 'autoincrement' => true, + 'notnull' => true, + ]); + $table->addColumn('column_id', Types::INTEGER, ['notnull' => true]); + $table->addColumn('row_id', Types::INTEGER, ['notnull' => true]); + $table->addColumn('value', $type, ['notnull' => false]); + $table->addColumn('value_type', Types::INTEGER, ['notnull' => false]); + $table->addColumn('last_edit_at', Types::DATETIME, ['notnull' => true]); + $table->addColumn('last_edit_by', Types::STRING, ['notnull' => true, 'length' => 64]); + $table->addIndex(['column_id', 'row_id']); + $table->setPrimaryKey(['id']); + return $schema; + } + + return null; + } + + /** + * Add column schema options for usergroup type to the tables_columns table + */ + private function haveUserGroupColumnDefinitionFields(ISchemaWrapper $schema) { + if ($schema->hasTable('tables_columns')) { + $table = $schema->getTable('tables_columns'); + if (!$table->hasColumn('usergroup_default')) { + $table->addColumn('usergroup_default', Types::TEXT, [ + 'notnull' => false, + 'length' => 65535, + ]); + } + if (!$table->hasColumn('usergroup_multiple_items')) { + $table->addColumn('usergroup_multiple_items', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => 0, + ]); + } + if (!$table->hasColumn('usergroup_select_users')) { + $table->addColumn('usergroup_select_users', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => 0, + ]); + } + if (!$table->hasColumn('usergroup_select_groups')) { + $table->addColumn('usergroup_select_groups', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => 0, + ]); + } + if (!$table->hasColumn('show_user_status')) { + $table->addColumn('show_user_status', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => 0, + ]); + } + return $schema; + } + + return null; + } +} diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index c53be07a1..de9c6adb4 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -108,16 +108,21 @@ * orderWeight: int, * numberDefault: float, * numberMin: float, - * numberMax: float, - * numberDecimals: int, - * numberPrefix: string, - * numberSuffix: string, - * textDefault: string, - * textAllowedPattern: string, - * textMaxLength: int, - * selectionOptions: string, - * selectionDefault: string, - * datetimeDefault: string, + * numberMax: float, + * numberDecimals: int, + * numberPrefix: string, + * numberSuffix: string, + * textDefault: string, + * textAllowedPattern: string, + * textMaxLength: int, + * selectionOptions: string, + * selectionDefault: string, + * datetimeDefault: string, + * usergroupDefault: string, + * usergroupMultipleItems: bool, + * usergroupSelectUsers: bool, + * usergroupSelectGroups: bool, + * showUserStatus: bool, * } * * @psalm-type TablesImportState = array{ @@ -144,6 +149,8 @@ * displayMode: int, * userId: string, * } + * + * @psalm-type TablesColumn */ class ResponseDefinitions { } diff --git a/lib/Service/ColumnService.php b/lib/Service/ColumnService.php index afa2e0a7d..338dbbbb9 100644 --- a/lib/Service/ColumnService.php +++ b/lib/Service/ColumnService.php @@ -154,6 +154,11 @@ public function find(int $id, ?string $userId = null): Column { * @param string|null $selectionOptions * @param string|null $selectionDefault * @param string|null $datetimeDefault + * @param string|null $usergroupDefault + * @param bool|null $usergroupMultipleItems + * @param bool|null $usergroupSelectUsers + * @param bool|null $usergroupSelectGroups + * @param bool|null $showUserStatus * @param array $selectedViewIds * @return Column * @@ -185,6 +190,13 @@ public function create( ?string $selectionDefault, ?string $datetimeDefault, + + ?string $usergroupDefault, + ?bool $usergroupMultipleItems, + ?bool $usergroupSelectUsers, + ?bool $usergroupSelectGroups, + ?bool $showUserStatus, + array $selectedViewIds = [] ):Column { // security @@ -267,6 +279,11 @@ public function create( $item->setSelectionOptions($selectionOptions); $item->setSelectionDefault($selectionDefault); $item->setDatetimeDefault($datetimeDefault); + $item->setUsergroupDefault($usergroupDefault); + $item->setUsergroupMultipleItems($usergroupMultipleItems); + $item->setUsergroupSelectUsers($usergroupSelectUsers); + $item->setUsergroupSelectGroups($usergroupSelectGroups); + $item->setShowUserStatus($showUserStatus); try { $entity = $this->mapper->insert($item); @@ -317,6 +334,11 @@ public function create( * @param string|null $selectionOptions * @param string|null $selectionDefault * @param string|null $datetimeDefault + * @param string|null $usergroupDefault + * @param bool|null $usergroupMultipleItems + * @param bool|null $usergroupSelectUsers + * @param bool|null $usergroupSelectGroups + * @param bool|null $showUserStatus * @return Column * @throws InternalError */ @@ -343,7 +365,14 @@ public function update( ?string $selectionOptions, ?string $selectionDefault, - ?string $datetimeDefault + + ?string $datetimeDefault, + + ?string $usergroupDefault, + ?bool $usergroupMultipleItems, + ?bool $usergroupSelectUsers, + ?bool $usergroupSelectGroups, + ?bool $showUserStatus, ):Column { try { /** @var Column $item */ @@ -391,6 +420,14 @@ public function update( } $item->setDatetimeDefault($datetimeDefault); + if ($usergroupDefault !== null) { + $item->setUsergroupDefault($usergroupDefault); + } + $item->setUsergroupMultipleItems($usergroupMultipleItems); + $item->setUsergroupSelectUsers($usergroupSelectUsers); + $item->setUsergroupSelectGroups($usergroupSelectGroups); + $item->setShowUserStatus($showUserStatus); + $this->updateMetadata($item, $userId); return $this->enhanceColumn($this->mapper->update($item)); } catch (Exception $e) { @@ -549,6 +586,11 @@ public function findOrCreateColumnsByTitleForTableAsArray(?int $tableId, ?int $v null, $dataTypes[$i]['selection_default'] ?? null, null, + null, + null, + null, + null, + null, [] ); $countCreatedColumns++; diff --git a/lib/Service/ColumnTypes/IColumnTypeBusiness.php b/lib/Service/ColumnTypes/IColumnTypeBusiness.php index d461b2ac9..3c93d82ad 100644 --- a/lib/Service/ColumnTypes/IColumnTypeBusiness.php +++ b/lib/Service/ColumnTypes/IColumnTypeBusiness.php @@ -7,7 +7,11 @@ interface IColumnTypeBusiness { /** - * try to parse a string for the given column type + * Parse frontend value and transform for using it in the database + * + * Used when inserting from API to the database + * + * FIXME: Why is this not using Mapper methods which should do the same thing * * @param mixed $value * @param Column|null $column diff --git a/lib/Service/ColumnTypes/UsergroupBusiness.php b/lib/Service/ColumnTypes/UsergroupBusiness.php new file mode 100644 index 000000000..e04debd51 --- /dev/null +++ b/lib/Service/ColumnTypes/UsergroupBusiness.php @@ -0,0 +1,61 @@ +logger->warning('No column given, but expected on '.__FUNCTION__.' within '.__CLASS__, ['exception' => new \Exception()]); + return json_encode([]); + } + + if($value === null) { + return json_encode([]); + } + + return json_encode($value); + } + + /** + * @param mixed $value parsable is json encoded array{id: string, type: int} + * @param Column|null $column + * @return bool + */ + public function canBeParsed($value, ?Column $column = null): bool { + if(!$column) { + $this->logger->warning('No column given, but expected on '.__FUNCTION__.' within '.__CLASS__, ['exception' => new \Exception()]); + return false; + } + + if($value === null) { + return true; + } + + if (is_string($value)) { + $value = json_decode($value, true); + } + + foreach ($value as $v) { + if((array_key_exists('id', $v) && !is_string($v['id'])) && (array_key_exists('type', $v) && !is_int($v['type']))) { + return false; + } + } + + return true; + } +} diff --git a/lib/Service/ImportService.php b/lib/Service/ImportService.php index 81e375705..a019b7ea6 100644 --- a/lib/Service/ImportService.php +++ b/lib/Service/ImportService.php @@ -432,6 +432,11 @@ private function getColumns(Row $firstRow, Row $secondRow): void { $this->columnsConfig[$index]['selectionOptions'] ?? '', $this->columnsConfig[$index]['selectionDefault'] ?? '', $this->columnsConfig[$index]['datetimeDefault'] ?? '', + $this->columnsConfig[$index]['usergroupDefault'] ?? null, + $this->columnsConfig[$index]['usergroupMultipleItems'] ?? null, + $this->columnsConfig[$index]['usergroupSelectUsers'] ?? null, + $this->columnsConfig[$index]['usergroupSelectGroups'] ?? null, + $this->columnsConfig[$index]['showUserStatus'] ?? null, $this->columnsConfig[$index]['selectedViewIds'] ?? [] ); $title = $column->getTitle(); diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index a835b3764..e4a95cbb0 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -275,7 +275,7 @@ private function parseValueByColumnType(Column $column, $value = null) { /** @var IColumnTypeBusiness $columnBusiness */ $columnBusiness = Server::get($businessClassName); if($columnBusiness->canBeParsed($value, $column)) { - return json_decode($columnBusiness->parseValue($value, $column)); + return json_decode($columnBusiness->parseValue($value, $column), true); } } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { $this->logger->debug('Column type business class not found', ['exception' => $e]); diff --git a/lib/Service/TableTemplateService.php b/lib/Service/TableTemplateService.php index 9f3d2213b..44becab60 100644 --- a/lib/Service/TableTemplateService.php +++ b/lib/Service/TableTemplateService.php @@ -853,6 +853,21 @@ private function createColumn(int $tableId, array $parameters): ?Column { // datetimeDefault (isset($parameters['datetimeDefault'])) ? $parameters['datetimeDefault'] : '', + // usergroupDefault + (isset($parameters['usergroupDefault'])) ? $parameters['usergroupDefault'] : '', + + // usergroupMultipleItems + (isset($parameters['usergroupMultipleItems'])) ? $parameters['usergroupMultipleItems'] : null, + + // usergroupSelectUsers + (isset($parameters['usergroupSelectUsers'])) ? $parameters['usergroupSelectUsers'] : null, + + // usergroupSelectGroups + (isset($parameters['usergroupSelectGroups'])) ? $parameters['usergroupSelectGroups'] : null, + + // showUserStatus + (isset($parameters['showUserStatus'])) ? $parameters['showUserStatus'] : null, + // additional view ids [] ); diff --git a/openapi.json b/openapi.json index 652d6d5cd..9e06758db 100644 --- a/openapi.json +++ b/openapi.json @@ -90,7 +90,12 @@ "textMaxLength", "selectionOptions", "selectionDefault", - "datetimeDefault" + "datetimeDefault", + "usergroupDefault", + "usergroupMultipleItems", + "usergroupSelectUsers", + "usergroupSelectGroups", + "showUserStatus" ], "properties": { "id": { @@ -172,6 +177,21 @@ }, "datetimeDefault": { "type": "string" + }, + "usergroupDefault": { + "type": "string" + }, + "usergroupMultipleItems": { + "type": "boolean" + }, + "usergroupSelectUsers": { + "type": "boolean" + }, + "usergroupSelectGroups": { + "type": "boolean" + }, + "showUserStatus": { + "type": "boolean" } } }, @@ -2858,7 +2878,8 @@ "text", "number", "datetime", - "select" + "select", + "usergroup" ], "description": "Column main type" }, @@ -2944,6 +2965,32 @@ "default": "", "description": "Default value, if column is datetime" }, + "usergroupDefault": { + "type": "string", + "nullable": true, + "default": "", + "description": "Default value, if column is usergroup" + }, + "usergroupMultipleItems": { + "type": "boolean", + "nullable": true, + "description": "Can select multiple users or/and groups, if column is usergroup" + }, + "usergroupSelectUsers": { + "type": "boolean", + "nullable": true, + "description": "Can select users, if column type is usergroup" + }, + "usergroupSelectGroups": { + "type": "boolean", + "nullable": true, + "description": "Can select groups, if column type is usergroup" + }, + "usergroupShowUserStatus": { + "type": "boolean", + "nullable": true, + "description": "Whether to show the user's status, if column type is usergroup" + }, "selectedViewIds": { "type": "array", "nullable": true, @@ -3180,7 +3227,8 @@ "text", "number", "datetime", - "select" + "select", + "usergroup" ], "description": "Column main type" }, @@ -3266,6 +3314,32 @@ "default": "", "description": "Default value, if column is datetime" }, + "usergroupDefault": { + "type": "string", + "nullable": true, + "default": "", + "description": "Default value, if column is usergroup (json array{id: string, type: int})" + }, + "usergroupMultipleItems": { + "type": "boolean", + "nullable": true, + "description": "Can select multiple users or/and groups, if column is usergroup" + }, + "usergroupSelectUsers": { + "type": "boolean", + "nullable": true, + "description": "Can select users, if column type is usergroup" + }, + "usergroupSelectGroups": { + "type": "boolean", + "nullable": true, + "description": "Can select groups, if column type is usergroup" + }, + "usergroupShowUserStatus": { + "type": "boolean", + "nullable": true, + "description": "Whether to show the user's status, if column type is usergroup" + }, "selectedViewIds": { "type": "array", "nullable": true, @@ -3454,6 +3528,31 @@ "type": "string", "nullable": true, "description": "Default value, if column is datetime" + }, + "usergroupDefault": { + "type": "string", + "nullable": true, + "description": "Default value, if column is usergroup" + }, + "usergroupMultipleItems": { + "type": "boolean", + "nullable": true, + "description": "Can select multiple users or/and groups, if column is usergroup" + }, + "usergroupSelectUsers": { + "type": "boolean", + "nullable": true, + "description": "Can select users, if column type is usergroup" + }, + "usergroupSelectGroups": { + "type": "boolean", + "nullable": true, + "description": "Can select groups, if column type is usergroup" + }, + "usergroupShowUserStatus": { + "type": "boolean", + "nullable": true, + "description": "Whether to show the user's status, if column type is usergroup" } } } @@ -7764,6 +7863,261 @@ } } }, + "/ocs/v2.php/apps/tables/api/2/columns/usergroup": { + "post": { + "operationId": "api_columns-create-usergroup-column", + "summary": "[api v2] Create new usergroup column", + "tags": [ + "api_columns" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "baseNodeId", + "title" + ], + "properties": { + "baseNodeId": { + "type": "integer", + "format": "int64", + "description": "Context of the column creation" + }, + "title": { + "type": "string", + "description": "Title" + }, + "usergroupDefault": { + "type": "string", + "nullable": true, + "description": "Json array{id: string, type: int}, eg [{\"id\": \"admin\", \"type\": 0}, {\"id\": \"user1\", \"type\": 0}]" + }, + "usergroupMultipleItems": { + "type": "boolean", + "description": "Whether you can select multiple users or/and groups" + }, + "usergroupSelectUsers": { + "type": "boolean", + "description": "Whether you can select users" + }, + "usergroupSelectGroups": { + "type": "boolean", + "description": "Whether you can select groups" + }, + "showUserStatus": { + "type": "boolean", + "description": "Whether to show the user's status" + }, + "description": { + "type": "string", + "nullable": true, + "description": "Description" + }, + "selectedViewIds": { + "type": "array", + "nullable": true, + "default": [], + "description": "View IDs where this columns should be added", + "items": { + "type": "integer", + "format": "int64" + } + }, + "mandatory": { + "type": "boolean", + "default": false, + "description": "Is mandatory" + }, + "baseNodeType": { + "type": "string", + "default": "table", + "enum": [ + "table", + "view" + ], + "description": "Context type of the column creation" + } + } + } + } + } + }, + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Column created", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/Column" + } + } + } + } + } + } + } + }, + "403": { + "description": "No permission", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + }, + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/tables/api/2/favorites/{nodeType}/{nodeId}": { "post": { "operationId": "api_favorite-create", diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index b64696958..b38fd9ea3 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -605,6 +605,23 @@ export type paths = { readonly patch?: never; readonly trace?: never; }; + readonly "/ocs/v2.php/apps/tables/api/2/columns/usergroup": { + readonly parameters: { + readonly query?: never; + readonly header?: never; + readonly path?: never; + readonly cookie?: never; + }; + readonly get?: never; + readonly put?: never; + /** [api v2] Create new usergroup column */ + readonly post: operations["api_columns-create-usergroup-column"]; + readonly delete?: never; + readonly options?: never; + readonly head?: never; + readonly patch?: never; + readonly trace?: never; + }; readonly "/ocs/v2.php/apps/tables/api/2/favorites/{nodeType}/{nodeId}": { readonly parameters: { readonly query?: never; @@ -760,6 +777,11 @@ export type components = { readonly selectionOptions: string; readonly selectionDefault: string; readonly datetimeDefault: string; + readonly usergroupDefault: string; + readonly usergroupMultipleItems: boolean; + readonly usergroupSelectUsers: boolean; + readonly usergroupSelectGroups: boolean; + readonly showUserStatus: boolean; }; readonly Context: { /** Format: int64 */ @@ -2120,7 +2142,7 @@ export interface operations { * @description Column main type * @enum {string} */ - readonly type: "text" | "number" | "datetime" | "select"; + readonly type: "text" | "number" | "datetime" | "select" | "usergroup"; /** @description Column sub type */ readonly subtype?: string | null; /** @description Is the column mandatory */ @@ -2175,6 +2197,19 @@ export interface operations { * @default */ readonly datetimeDefault?: string | null; + /** + * @description Default value, if column is usergroup + * @default + */ + readonly usergroupDefault?: string | null; + /** @description Can select multiple users or/and groups, if column is usergroup */ + readonly usergroupMultipleItems?: boolean | null; + /** @description Can select users, if column type is usergroup */ + readonly usergroupSelectUsers?: boolean | null; + /** @description Can select groups, if column type is usergroup */ + readonly usergroupSelectGroups?: boolean | null; + /** @description Whether to show the user's status, if column type is usergroup */ + readonly usergroupShowUserStatus?: boolean | null; /** * @description View IDs where this column should be added to be presented * @default [] @@ -2308,7 +2343,7 @@ export interface operations { * @description Column main type * @enum {string} */ - readonly type: "text" | "number" | "datetime" | "select"; + readonly type: "text" | "number" | "datetime" | "select" | "usergroup"; /** @description Column sub type */ readonly subtype?: string | null; /** @description Is the column mandatory */ @@ -2363,6 +2398,19 @@ export interface operations { * @default */ readonly datetimeDefault?: string | null; + /** + * @description Default value, if column is usergroup (json array{id: string, type: int}) + * @default + */ + readonly usergroupDefault?: string | null; + /** @description Can select multiple users or/and groups, if column is usergroup */ + readonly usergroupMultipleItems?: boolean | null; + /** @description Can select users, if column type is usergroup */ + readonly usergroupSelectUsers?: boolean | null; + /** @description Can select groups, if column type is usergroup */ + readonly usergroupSelectGroups?: boolean | null; + /** @description Whether to show the user's status, if column type is usergroup */ + readonly usergroupShowUserStatus?: boolean | null; /** * @description View IDs where this column should be added to be presented * @default [] @@ -2530,6 +2578,16 @@ export interface operations { readonly selectionDefault?: string | null; /** @description Default value, if column is datetime */ readonly datetimeDefault?: string | null; + /** @description Default value, if column is usergroup */ + readonly usergroupDefault?: string | null; + /** @description Can select multiple users or/and groups, if column is usergroup */ + readonly usergroupMultipleItems?: boolean | null; + /** @description Can select users, if column type is usergroup */ + readonly usergroupSelectUsers?: boolean | null; + /** @description Can select groups, if column type is usergroup */ + readonly usergroupSelectGroups?: boolean | null; + /** @description Whether to show the user's status, if column type is usergroup */ + readonly usergroupShowUserStatus?: boolean | null; }; }; }; @@ -4553,6 +4611,122 @@ export interface operations { }; }; }; + readonly "api_columns-create-usergroup-column": { + readonly parameters: { + readonly query?: never; + readonly header: { + /** @description Required to be true for the API request to pass */ + readonly "OCS-APIRequest": boolean; + }; + readonly path?: never; + readonly cookie?: never; + }; + readonly requestBody: { + readonly content: { + readonly "application/json": { + /** + * Format: int64 + * @description Context of the column creation + */ + readonly baseNodeId: number; + /** @description Title */ + readonly title: string; + /** @description Json array{id: string, type: int}, eg [{"id": "admin", "type": 0}, {"id": "user1", "type": 0}] */ + readonly usergroupDefault?: string | null; + /** @description Whether you can select multiple users or/and groups */ + readonly usergroupMultipleItems?: boolean; + /** @description Whether you can select users */ + readonly usergroupSelectUsers?: boolean; + /** @description Whether you can select groups */ + readonly usergroupSelectGroups?: boolean; + /** @description Whether to show the user's status */ + readonly showUserStatus?: boolean; + /** @description Description */ + readonly description?: string | null; + /** + * @description View IDs where this columns should be added + * @default [] + */ + readonly selectedViewIds?: readonly number[]; + /** + * @description Is mandatory + * @default false + */ + readonly mandatory?: boolean; + /** + * @description Context type of the column creation + * @default table + * @enum {string} + */ + readonly baseNodeType?: "table" | "view"; + }; + }; + }; + readonly responses: { + /** @description Column created */ + readonly 200: { + headers: { + readonly [name: string]: unknown; + }; + content: { + readonly "application/json": { + readonly ocs: { + readonly meta: components["schemas"]["OCSMeta"]; + readonly data: components["schemas"]["Column"]; + }; + }; + }; + }; + /** @description No permission */ + readonly 403: { + headers: { + readonly [name: string]: unknown; + }; + content: { + readonly "application/json": { + readonly ocs: { + readonly meta: components["schemas"]["OCSMeta"]; + readonly data: { + readonly message: string; + }; + }; + }; + }; + }; + /** @description Not found */ + readonly 404: { + headers: { + readonly [name: string]: unknown; + }; + content: { + readonly "application/json": { + readonly ocs: { + readonly meta: components["schemas"]["OCSMeta"]; + readonly data: { + readonly message: string; + }; + }; + }; + }; + }; + readonly 500: { + headers: { + readonly [name: string]: unknown; + }; + content: { + readonly "application/json": { + readonly ocs: { + readonly meta: components["schemas"]["OCSMeta"]; + readonly data: { + readonly message: string; + }; + }; + }; + readonly "text/plain": string; + }; + }; + }; + }; readonly "api_favorite-create": { readonly parameters: { readonly query?: never; diff --git a/tests/integration/features/APIv2.feature b/tests/integration/features/APIv2.feature index 6b478c46d..ada139bd0 100644 --- a/tests/integration/features/APIv2.feature +++ b/tests/integration/features/APIv2.feature @@ -73,6 +73,12 @@ Feature: APIv2 | subtype | date | | title | A single date | | datetimeDefault | today | + Then column from main type "usergroup" for node type "table" and node name "t2" exists with name "c7" and following properties via v2 + | title | Cool usergroup column | + | usergroupDefault | [{"id": "admin", "type": 0}] | + | usergroupMultipleItems | true | + | usergroupSelectUsers | true | + | usergroupSelectGroups | false | Then node with node type "table" and node name "t2" has the following columns via v2 | Beautiful text column | Rich is cool | Counter | Progress | Checking | A single date | Then print register @@ -93,8 +99,27 @@ Feature: APIv2 | sel single | sel multi | Then print register + @api2usergroup + Scenario: Create and modify usergroup columns + Given table "Table 5" with emoji "👋" exists for user "participant1-v2" as "t5" via v2 + Then column from main type "usergroup" for node type "table" and node name "t5" exists with name "ug-c1" and following properties via v2 + | title | ug column | + | usergroupMultipleItems | true | + | usergroupSelectUsers | true | + | usergroupSelectGroups | true | + Then node with node type "table" and node name "t5" has the following columns via v2 + | ug column | + Then print register + Then set following properties for last created column + | title | ug column renamed | + | mandatory | 0 | + | description | New description | + | usergroupDefault | [{"id":"admin","type":0}] | + Then table has at least following columns + | ug column renamed | + @api2transfer - Scenario: Create selection columns + Scenario: Transfer table Given table "Table 4" with emoji "👋" exists for user "participant1-v2" as "t4" via v2 Then table "t4" is owned by "participant1-v2" Then change owner for table "t4" from user "participant1-v2" to user "participant2-v2" @@ -543,6 +568,24 @@ Feature: APIv2 When user "participant3-v2" attempts to fetch Context "c1" Then the reported status is "404" + + @api1 @rows + Scenario: Create and modify usergroup row via v1 + Given table "Usergroup row check" with emoji "👋" exists for user "participant1-v2" as "base1" via v2 + Then column "one" exists with following properties + | type | usergroup | + | mandatory | 0 | + | description | New description | + | usergroupMultipleItems | true | + | usergroupSelectUsers | true | + | usergroupSelectGroups | true | + Then row exists with following values + | one | [{"id":"admin","type":0}] | + Then set following values for last created row + | one | [{"id":"admin","type":1}] | + Then user deletes last created row + Then user "participant1-v2" deletes table with keyword "Usergroup row check" + @api2 @rows Scenario: Create rows via v2 and check them Given table "Rows check" with emoji "👨🏻‍💻" exists for user "participant1-v2" as "base1" via v2 @@ -566,11 +609,19 @@ Feature: APIv2 | subtype | date | | mandatory | 0 | | description | This is a description! | + And column "five" exists with following properties + | type | usergroup | + | mandatory | 0 | + | description | This is a description! | + | usergroupMultipleItems | true | + | usergroupSelectUsers | true | + | usergroupSelectGroups | false | When row exists using v2 with following values | one | AHA | | two | 161 | | three | true | | four | 2023-12-24 | + | five | [{"id": "admin", "type": 0}] | Then the reported status is 200 @api2 @rows @@ -596,6 +647,13 @@ Feature: APIv2 | subtype | date | | mandatory | 0 | | description | This is a description! | + And column "five" exists with following properties + | type | usergroup | + | mandatory | 1 | + | description | This is a description! | + | usergroupMultipleItems | true | + | usergroupSelectUsers | true | + | usergroupSelectGroups | false | And user "participant1-v2" shares table with user "participant2-v2" And user "participant2-v2" has the following permissions | read | 1 | @@ -608,6 +666,7 @@ Feature: APIv2 | two | 161 | | three | true | | four | 2023-12-24 | + | five | [{"id": "admin", "type": 0}] | Then the reported status is 200 @api2 @rows @@ -633,6 +692,13 @@ Feature: APIv2 | subtype | date | | mandatory | 0 | | description | This is a description! | + And column "five" exists with following properties + | type | usergroup | + | mandatory | 1 | + | description | This is a description! | + | usergroupMultipleItems | true | + | usergroupSelectUsers | true | + | usergroupSelectGroups | false | And user "participant1-v2" shares table with user "participant2-v2" And user "participant1-v2" sets permission "create" to 0 And user "participant2-v2" has the following permissions @@ -646,6 +712,7 @@ Feature: APIv2 | two | 161 | | three | true | | four | 2023-12-24 | + | five | [{"id": "admin", "type": 0}] | Then the reported status is 403 @api2 @rows @@ -671,11 +738,19 @@ Feature: APIv2 | subtype | date | | mandatory | 0 | | description | This is a description! | + And column "five" exists with following properties + | type | usergroup | + | mandatory | 1 | + | description | This is a description! | + | usergroupMultipleItems | true | + | usergroupSelectUsers | true | + | usergroupSelectGroups | false | When user "participant2-v2" tries to create a row using v2 with following values | one | AHA | | two | 161 | | three | true | | four | 2023-12-24 | + | five | [{"id": "admin", "type": 0}] | Then the reported status is 404 @api2 @rows @views @@ -701,12 +776,20 @@ Feature: APIv2 | subtype | date | | mandatory | 0 | | description | This is a description! | + And column "five" exists with following properties + | type | usergroup | + | mandatory | 1 | + | description | This is a description! | + | usergroupMultipleItems | true | + | usergroupSelectUsers | true | + | usergroupSelectGroups | false | And user "participant1-v2" create view "v1" with emoji "⚡️" for "t1" as "v1" When user "participant1-v2" tries to create a row using v2 on "view" "v1" with following values | one | AHA | | two | 161 | | three | true | | four | 2023-12-24 | + | five | [{"id": "admin", "type": 0}] | Then the reported status is 200 @api2 @rows @views @@ -732,6 +815,13 @@ Feature: APIv2 | subtype | date | | mandatory | 0 | | description | This is a description! | + And column "five" exists with following properties + | type | usergroup | + | mandatory | 1 | + | description | This is a description! | + | usergroupMultipleItems | true | + | usergroupSelectUsers | true | + | usergroupSelectGroups | false | And user "participant1-v2" create view "v1" with emoji "⚡️" for "t1" as "v1" And user "participant1-v2" shares view "v1" with "participant2-v2" When user "participant2-v2" tries to create a row using v2 on "view" "v1" with following values @@ -739,6 +829,7 @@ Feature: APIv2 | two | 161 | | three | true | | four | 2023-12-24 | + | five | [{"id": "admin", "type": 0}] | Then the reported status is 200 @api2 @rows @views @@ -764,6 +855,13 @@ Feature: APIv2 | subtype | date | | mandatory | 0 | | description | This is a description! | + And column "five" exists with following properties + | type | usergroup | + | mandatory | 1 | + | description | This is a description! | + | usergroupMultipleItems | true | + | usergroupSelectUsers | true | + | usergroupSelectGroups | false | And user "participant1-v2" create view "v1" with emoji "⚡️" for "t1" as "v1" And user "participant1-v2" shares view "v1" with "participant2-v2" And user "participant1-v2" sets permission "create" to 0 @@ -772,6 +870,7 @@ Feature: APIv2 | two | 161 | | three | true | | four | 2023-12-24 | + | five | [{"id": "admin", "type": 0}] | Then the reported status is 403 @api2 @rows @views @@ -797,10 +896,18 @@ Feature: APIv2 | subtype | date | | mandatory | 0 | | description | This is a description! | + And column "five" exists with following properties + | type | usergroup | + | mandatory | 1 | + | description | This is a description! | + | usergroupMultipleItems | true | + | usergroupSelectUsers | true | + | usergroupSelectGroups | false | And user "participant1-v2" create view "v1" with emoji "⚡️" for "t1" as "v1" When user "participant2-v2" tries to create a row using v2 on "view" "v1" with following values | one | AHA | | two | 161 | | three | true | | four | 2023-12-24 | + | five | [{"id": "admin", "type": 0}] | Then the reported status is 404 diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index cec7fd4c2..0414aa325 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -413,6 +413,7 @@ public function createColumnV2(string $nodeType, string $nodeName, string $colum $newColumn = $this->getDataFromResponse($this->response)['ocs']['data']; $this->columnIds[$columnName] = $newColumn['id']; + $this->columnId = $newColumn['id']; Assert::assertEquals(200, $this->response->getStatusCode()); @@ -541,6 +542,10 @@ public function checkImportResults(TableNode $table): void { * */ public function printRegister(): void { + if (getenv('DEBUG') === '') { + return; + } + echo "REGISTER ========================\n"; echo "Tables --------------------\n"; print_r($this->tableIds); @@ -1202,7 +1207,14 @@ public function updateColumn(?TableNode $properties = null): void { Assert::assertEquals(200, $this->response->getStatusCode()); foreach ($props as $key => $value) { - Assert::assertEquals($column[$key], $value); + if (is_array($column[$key])) { + // I am afraid this is usergroupcolumn specific, but not generic + $retrieved = json_encode($column[$key], true); + } else { + $retrieved = $column[$key]; + } + + Assert::assertEquals($value, $retrieved, 'Failing key: ' . $key); } $this->sendRequest( @@ -1213,7 +1225,14 @@ public function updateColumn(?TableNode $properties = null): void { $columnToVerify = $this->getDataFromResponse($this->response); Assert::assertEquals(200, $this->response->getStatusCode()); foreach ($props as $key => $value) { - Assert::assertEquals($columnToVerify[$key], $value); + // I am afraid this is usergroupcolumn specific, but not generic + if (is_array($columnToVerify[$key])) { + $retrieved = json_encode($columnToVerify[$key], true); + } else { + $retrieved = $columnToVerify[$key]; + } + + Assert::assertEquals($value, $retrieved, 'Failing key: ' . $key); } } @@ -1253,7 +1272,17 @@ public function createRow(?TableNode $properties = null): void { $rowToVerify = $this->getDataFromResponse($this->response); Assert::assertEquals(200, $this->response->getStatusCode()); foreach ($rowToVerify['data'] as $cell) { - Assert::assertEquals($props[$cell['columnId']], $cell['value']); + // I am afraid this is usergroupcolumn specific, but not generic + if (is_array($cell['value'])) { + $retrieved = json_encode(array_reduce($cell['value'], function (array $carry, string $item): array { + $carry[] = json_decode($item, true); + return $carry; + }, [])); + } else { + $retrieved = $cell['value']; + } + + Assert::assertEquals($props[$cell['columnId']], $retrieved); } } @@ -1429,7 +1458,17 @@ public function updateRow(?TableNode $properties = null): void { $rowToVerify = $this->getDataFromResponse($this->response); Assert::assertEquals(200, $this->response->getStatusCode()); foreach ($rowToVerify['data'] as $cell) { - Assert::assertEquals($props[$cell['columnId']], $cell['value']); + // I am afraid this is usergroupcolumn specific, but not generic + if (is_array($cell['value'])) { + $retrieved = json_encode(array_reduce($cell['value'], function (array $carry, string $item): array { + $carry[] = json_decode($item, true); + return $carry; + }, [])); + } else { + $retrieved = $cell['value']; + } + + Assert::assertEquals($props[$cell['columnId']], $retrieved); } } diff --git a/tests/integration/run.sh b/tests/integration/run.sh index e105a8826..17b7b8183 100755 --- a/tests/integration/run.sh +++ b/tests/integration/run.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash APP_NAME=tables +SCENARIO=$1 APP_INTEGRATION_DIR=$PWD @@ -74,7 +75,7 @@ echo '' echo '#' echo '# Running tests' echo '#' -vendor/bin/behat --colors -f junit -f pretty +vendor/bin/behat --colors -f junit -f pretty $SCENARIO RESULT=$? echo ''