From 109bf5b76478e31e67b10ba6e50b3e4c5946f5a5 Mon Sep 17 00:00:00 2001 From: Ron Lucke Date: Fri, 1 Dec 2023 10:34:14 +0000 Subject: Lernmaterialien in Courseware sortieren Closes #3032 Merge request studip/studip!2052 --- db/migrations/5.5.8_add_pos_to_cw_units.php | 27 +++ lib/classes/JsonApi/RouteMap.php | 1 + .../JsonApi/Routes/Courseware/Authority.php | 5 + .../JsonApi/Routes/Courseware/UnitsCreate.php | 2 + .../JsonApi/Routes/Courseware/UnitsSort.php | 67 +++++++ .../JsonApi/Routes/Courseware/UnitsUpdate.php | 2 +- lib/classes/JsonApi/Schemas/Courseware/Unit.php | 1 + lib/models/Courseware/Unit.php | 45 +++++ .../scss/courseware/content-courses.scss | 2 +- .../stylesheets/scss/courseware/layouts/tile.scss | 12 ++ .../assets/stylesheets/scss/courseware/shelf.scss | 13 +- .../courseware/layouts/CoursewareTile.vue | 110 +++++------ .../courseware/unit/CoursewareUnitItem.vue | 10 +- .../courseware/unit/CoursewareUnitItems.vue | 204 +++++++++++++++++++-- .../store/courseware/courseware-shelf.module.js | 14 ++ 15 files changed, 445 insertions(+), 70 deletions(-) create mode 100644 db/migrations/5.5.8_add_pos_to_cw_units.php create mode 100644 lib/classes/JsonApi/Routes/Courseware/UnitsSort.php diff --git a/db/migrations/5.5.8_add_pos_to_cw_units.php b/db/migrations/5.5.8_add_pos_to_cw_units.php new file mode 100644 index 0000000..d3e3790 --- /dev/null +++ b/db/migrations/5.5.8_add_pos_to_cw_units.php @@ -0,0 +1,27 @@ +exec(" + ALTER TABLE `cw_units` + ADD COLUMN `position` INT(11) DEFAULT NULL AFTER `content_type` + "); + } + + public function down() + { + $db = DBManager::get(); + $db->exec(" + ALTER TABLE `cw_units` + DROP COLUMN `position` + "); + } +} diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index 2eb33a8..5542c31 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -515,6 +515,7 @@ class RouteMap $group->delete('/courseware-units/{id}', Routes\Courseware\UnitsDelete::class); // not a JSON route $group->post('/courseware-units/{id}/copy', Routes\Courseware\UnitsCopy::class); + $group->post('/{type:courses|users}/{id}/courseware-units/sort', Routes\Courseware\UnitsSort::class); $group->get('/courseware-clipboards', Routes\Courseware\ClipboardsIndex::class); $group->get('/users/{id}/courseware-clipboards', Routes\Courseware\UsersClipboardsIndex::class); diff --git a/lib/classes/JsonApi/Routes/Courseware/Authority.php b/lib/classes/JsonApi/Routes/Courseware/Authority.php index 0f837de..3df103d 100644 --- a/lib/classes/JsonApi/Routes/Courseware/Authority.php +++ b/lib/classes/JsonApi/Routes/Courseware/Authority.php @@ -493,6 +493,11 @@ class Authority return $GLOBALS['perm']->have_studip_perm('tutor', $range->id ,$user->id); } + public static function canSortUnit(User $user, \Range $range): bool + { + return self::canCreateUnit($user, $range); + } + public static function canUpdateUnit(User $user, Unit $resource): bool { return $resource->canEdit($user); diff --git a/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php b/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php index f8fb17b..6f88bca 100644 --- a/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php +++ b/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php @@ -2,6 +2,7 @@ namespace JsonApi\Routes\Courseware; +use Courseware\Unit; use JsonApi\Errors\AuthorizationFailedException; use JsonApi\Errors\RecordNotFoundException; use JsonApi\JsonApiController; @@ -103,6 +104,7 @@ class UnitsCreate extends JsonApiController 'range_type' => $range->getRangeType(), 'structural_element_id' => $struct->id, 'content_type' => 'courseware', + 'position' => Unit::getNewPosition($range->getRangeId()), 'creator_id' => $user->id, 'public' => self::arrayGet($json, 'data.attributes.public', '0'), 'release_date' => self::arrayGet($json, 'data.attributes.release-date'), diff --git a/lib/classes/JsonApi/Routes/Courseware/UnitsSort.php b/lib/classes/JsonApi/Routes/Courseware/UnitsSort.php new file mode 100644 index 0000000..c307910 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/UnitsSort.php @@ -0,0 +1,67 @@ + + * @license GPL2 or any later version + * + * @since Stud.IP 5.5 + */ + +class UnitsSort extends JsonApiController +{ + public function __invoke(Request $request, Response $response, $args) + { + $range = $this->getRange($args); + $user = $this->getUser($request); + + if (!Authority::canSortUnit($user, $range)) { + throw new AuthorizationFailedException(); + } + $data = $request->getParsedBody()['data']; + $positions = $data['positions']; + $unitCount = Unit::getNewPosition($range->id); + + if (count($positions) !== $unitCount) { + throw new BadRequestException('Fehler beim Sortieren der Lernmaterialien.'); + } + + Unit::updatePositions($range, $positions); + + $response = $response->withHeader('Content-Type', 'application/json'); + + return $response; + } + + private function getRange($args): ?\Range + { + try { + return \RangeFactory::createRange( + $this->getRangeType($args['type']), + $args['id'] + ); + } catch (\Exception $e) { + return null; + } + } + + private function getRangeType($type): ?string + { + $type_map = [ + 'courses' => 'course', + 'users' => 'user', + ]; + + return $type_map[$type] ?? null; + } +} \ No newline at end of file diff --git a/lib/classes/JsonApi/Routes/Courseware/UnitsUpdate.php b/lib/classes/JsonApi/Routes/Courseware/UnitsUpdate.php index 75956c4..446d61e 100644 --- a/lib/classes/JsonApi/Routes/Courseware/UnitsUpdate.php +++ b/lib/classes/JsonApi/Routes/Courseware/UnitsUpdate.php @@ -13,7 +13,7 @@ use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; /** - * Update one Block. + * Update one Unit. */ class UnitsUpdate extends JsonApiController { diff --git a/lib/classes/JsonApi/Schemas/Courseware/Unit.php b/lib/classes/JsonApi/Schemas/Courseware/Unit.php index 39d2013..84c6ca2 100644 --- a/lib/classes/JsonApi/Schemas/Courseware/Unit.php +++ b/lib/classes/JsonApi/Schemas/Courseware/Unit.php @@ -29,6 +29,7 @@ class Unit extends SchemaProvider { return [ 'content-type' => (string) $resource['content_type'], + 'position' => (int) $resource['position'], 'public' => (int) $resource['public'], 'release-date' => $resource['release_date'] ? date('c', $resource['release_date']) : null, 'withdraw-date' => $resource['withdraw_date'] ? date('c', $resource['withdraw_date']) : null, diff --git a/lib/models/Courseware/Unit.php b/lib/models/Courseware/Unit.php index e89b04d..6f3535d 100644 --- a/lib/models/Courseware/Unit.php +++ b/lib/models/Courseware/Unit.php @@ -59,6 +59,8 @@ class Unit extends \SimpleORMap implements \PrivacyObject 'foreign_key' => 'creator_id', ]; + $config['registered_callbacks']['after_delete'][] = 'updatePositionsAfterDelete'; + parent::configure($config); } @@ -127,4 +129,47 @@ class Unit extends \SimpleORMap implements \PrivacyObject } } + + public static function getNewPosition($range_id): int + { + return static::countBySQL('range_id = ?', [$range_id]); + } + + public function updatePositionsAfterDelete(): void + { + if (is_null($this->position)) { + return; + } + + $db = \DBManager::get(); + $stmt = $db->prepare(sprintf( + 'UPDATE + %s + SET + position = position - 1 + WHERE + range_id = :range_id AND + position > :position', + 'cw_units' + )); + $stmt->bindValue(':range_id', $this->range_id); + $stmt->bindValue(':position', $this->position); + $stmt->execute(); + } + + public static function updatePositions($range, $positions): void + { + $db = \DBManager::get(); + $query = sprintf( + 'UPDATE + %s + SET + position = FIND_IN_SET(id, ?) - 1 + WHERE + range_id = ?', + 'cw_units'); + $args = array(join(',', $positions), $range->id); + $stmt = $db->prepare($query); + $stmt->execute($args); + } } diff --git a/resources/assets/stylesheets/scss/courseware/content-courses.scss b/resources/assets/stylesheets/scss/courseware/content-courses.scss index 16d712c..0e64af9 100644 --- a/resources/assets/stylesheets/scss/courseware/content-courses.scss +++ b/resources/assets/stylesheets/scss/courseware/content-courses.scss @@ -6,7 +6,7 @@ font-size: 1.4em; } - ul.cw-tiles { + .cw-tiles { margin-bottom: 20px; } } \ No newline at end of file diff --git a/resources/assets/stylesheets/scss/courseware/layouts/tile.scss b/resources/assets/stylesheets/scss/courseware/layouts/tile.scss index f4c795f..d668e69 100644 --- a/resources/assets/stylesheets/scss/courseware/layouts/tile.scss +++ b/resources/assets/stylesheets/scss/courseware/layouts/tile.scss @@ -33,6 +33,18 @@ @include background-icon(courseware, clickable, 128); } + .overlay-handle { + @extend .drag-handle; + background-color: $white; + background-position: center !important; + height: 22px; + padding: 4px 8px; + margin-top: 3px; + float: left; + border-left: solid thin var(--content-color-20); + } + + .overlay-text { padding: 6px 7px; margin: 4px; diff --git a/resources/assets/stylesheets/scss/courseware/shelf.scss b/resources/assets/stylesheets/scss/courseware/shelf.scss index a7b3eab..0b6c93b 100644 --- a/resources/assets/stylesheets/scss/courseware/shelf.scss +++ b/resources/assets/stylesheets/scss/courseware/shelf.scss @@ -50,4 +50,15 @@ h2 { margin-top: 0; } -} \ No newline at end of file +} + +.cw-unit-items { + .unit-ghost { + background: var(--white); + border: dashed 2px var(--content-color-40); + } + .unit-ghost .cw-tile { + opacity: 0; + height: 416px; + } +} diff --git a/resources/vue/components/courseware/layouts/CoursewareTile.vue b/resources/vue/components/courseware/layouts/CoursewareTile.vue index f396769..dd7e173 100644 --- a/resources/vue/components/courseware/layouts/CoursewareTile.vue +++ b/resources/vue/components/courseware/layouts/CoursewareTile.vue @@ -1,10 +1,15 @@