From e1036338cd7bfa9b7222e1106fe549cdeee34682 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Willms Date: Wed, 18 Mar 2026 16:42:45 +0100 Subject: studygroup proposals is now a vue app that loads the proposals via json api, fixes #6377 --- app/controllers/my_studygroups.php | 119 -------------------- app/views/my_studygroups/proposals.php | 31 ----- lib/classes/JsonApi/RouteMap.php | 2 + .../JsonApi/Routes/Studygroups/Proposals.php | 125 +++++++++++++++++++++ lib/classes/JsonApi/SchemaMap.php | 1 + lib/classes/JsonApi/Schemas/Course.php | 87 +++++++++----- lib/classes/JsonApi/Schemas/Tag.php | 39 +++++++ lib/modules/StudygroupWidget.php | 17 +-- resources/vue/apps/StudygroupProposals.vue | 72 ++++++++++++ templates/vue-app.php | 1 + 10 files changed, 308 insertions(+), 186 deletions(-) delete mode 100644 app/views/my_studygroups/proposals.php create mode 100644 lib/classes/JsonApi/Routes/Studygroups/Proposals.php create mode 100644 lib/classes/JsonApi/Schemas/Tag.php create mode 100644 resources/vue/apps/StudygroupProposals.vue diff --git a/app/controllers/my_studygroups.php b/app/controllers/my_studygroups.php index bc7c0bd..12aa5b0 100644 --- a/app/controllers/my_studygroups.php +++ b/app/controllers/my_studygroups.php @@ -26,14 +26,6 @@ class MyStudygroupsController extends AuthenticatedController } } - public function proposals_action() - { - PageLayout::setHelpKeyword('Basis.MeineStudiengruppen'); - PageLayout::setTitle(_('Meine Studiengruppen')); - URLHelper::removeLinkParam('cid'); - $this->proposed_studygroups = $this->proposeStudygroups(); - } - public function set_sidebar() { if ($GLOBALS['user']->perms === 'user') { @@ -57,115 +49,4 @@ class MyStudygroupsController extends AuthenticatedController } $sidebar->addWidget($actions); } - - public function proposeStudygroups($user_id = null, $amount = 4) - { - $user_id ??= User::findCurrent()->id; - $cache_id = 'core/studygroups/proposals/' . $user_id; - $cache = \Studip\Cache\Factory::getCache(); - $studygroup_ids = $cache->read($cache_id); - if ($studygroup_ids !== false) { - return Course::findMany($studygroup_ids); - } - - // Vorgeschlagen werden sollen Studiengruppen, - // a) in denen Personen sitzen, die auch in anderen Veranstaltungen sitzen, in denen der aktive Nutzer Mitglied ist - // b) die zu dem Studienbereich des Studierenden gehören - // c) die einfach neu sind - // und die zudem aktiv sind. Es wird eine Liste von 36 Studiengruppen gebaut, wovon drei alle 15 Minuten im Widget - // angezeigt werden. - - $studygroup_sem_types = array_filter( - array_keys($GLOBALS['SEM_TYPE']), - fn($sem_type_id) => (bool) $GLOBALS['SEM_CLASS'][$GLOBALS['SEM_TYPE'][$sem_type_id]['class']]['studygroup_mode'] - ); - - $query = "SELECT DISTINCT `Seminar_id` - FROM ( - SELECT `Seminar_id` FROM ( - -- Andere Personen aus meinen Veranstaltungen - SELECT `seminare`.`Seminar_id`, COUNT(`seminar_user`.`user_id`) AS `count_colleagues` - FROM ( - SELECT colleagues.`user_id` - FROM `seminar_user` AS colleagues - JOIN `seminar_user` AS mine USING (`Seminar_id`) - WHERE mine.`user_id` = :me - AND colleagues.`user_id` != mine.`user_id` - ) AS my_colleagues - JOIN `seminar_user` - ON (`my_colleagues`.`user_id` = `seminar_user`.`user_id`) - JOIN `seminare` - ON (`seminare`.`Seminar_id` = `seminar_user`.`Seminar_id`) - WHERE `seminare`.`status` IN (:studygroup_types) - AND NOT EXISTS( - SELECT 1 - FROM `seminar_user` - WHERE `seminar_user`.`Seminar_id` = `seminare`.`Seminar_id` - AND `seminar_user`.`user_id` = :me - ) - GROUP BY `seminare`.`seminar_id` - ORDER BY `count_colleagues` DESC - LIMIT 12 - ) AS `colleagues_groups` - - UNION ALL - - SELECT `Seminar_id` FROM ( - -- Andere Personen aus meinen Studiengängen - SELECT DISTINCT `seminare`.`Seminar_id` - FROM `user_studiengang` - STRAIGHT_JOIN `mvv_stgteil` - ON (`mvv_stgteil`.`fach_id` = `user_studiengang`.`fach_id`) - STRAIGHT_JOIN `mvv_stg_stgteil` - ON (`mvv_stg_stgteil`.`stgteil_id` = `mvv_stgteil`.`stgteil_id`) - STRAIGHT_JOIN `mvv_studiengang` - ON ( - `mvv_studiengang`.`studiengang_id` = `mvv_stg_stgteil`.`studiengang_id` - AND `mvv_studiengang`.`abschluss_id` = `user_studiengang`.`abschluss_id` - ) - STRAIGHT_JOIN `studygroup_stgteil` - ON (`studygroup_stgteil`.`stgteil_id` = `mvv_stgteil`.`stgteil_id`) - STRAIGHT_JOIN `seminare` - ON (`seminare`.`Seminar_id` = `studygroup_stgteil`.`studygroup_id`) - WHERE `seminare`.`status` IN (:studygroup_types) - AND `user_studiengang`.`user_id` = :me - AND NOT EXISTS( - SELECT 1 - FROM `seminar_user` - WHERE `seminar_user`.`Seminar_id` = `seminare`.`Seminar_id` - AND `seminar_user`.`user_id` = :me - ) - LIMIT 12 - ) AS `same_studyarea_groups` - - UNION ALL - - SELECT `Seminar_id` FROM ( - -- Neue Studiengruppen - SELECT `seminare`.`Seminar_id` - FROM `seminare` - WHERE `seminare`.`status` IN (:studygroup_types) - AND NOT EXISTS( - SELECT 1 - FROM `seminar_user` - WHERE `seminar_user`.`Seminar_id` = `seminare`.`Seminar_id` - AND `seminar_user`.`user_id` = :me - ) - ORDER BY `seminare`.`mkdate` DESC - LIMIT 12 - ) AS `new_groups` - ) AS `all_groups`"; - $group_ids = DBManager::get()->fetchFirst($query, [ - ':studygroup_types' => $studygroup_sem_types, - ':me' => $user_id, - ':amount' => $amount, - ]); - - // Zufällig sortieren ist in PHP schneller als in SQL - shuffle($group_ids); - $group_ids = array_slice($group_ids, 0, $amount); - - $cache->write($cache_id, $group_ids, 15 * 60); - return Course::findMany($group_ids); - } } diff --git a/app/views/my_studygroups/proposals.php b/app/views/my_studygroups/proposals.php deleted file mode 100644 index 7347849..0000000 --- a/app/views/my_studygroups/proposals.php +++ /dev/null @@ -1,31 +0,0 @@ -
- - -
- id)->getImageTag(Avatar::MEDIUM) ?> -
- - getFullname()) ?> - -
- members) - ), - count($course->members) - ) ?> -
-
-
- tags)) : ?> -
- tags as $tag) : ?> - name) ?> - -
- -
- -
diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index bd5a58a..8aaa14f 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -119,6 +119,8 @@ class RouteMap $group->get('/available-rooms', AvailableRooms::class); + $group->get('/studygroup-proposals', Routes\Studygroups\Proposals::class); + $this->addAuthenticatedAdmissionRoutes($group); $this->addAuthenticatedBlubberRoutes($group); $this->addAuthenticatedClipboardRoutes($group); diff --git a/lib/classes/JsonApi/Routes/Studygroups/Proposals.php b/lib/classes/JsonApi/Routes/Studygroups/Proposals.php new file mode 100644 index 0000000..e7867d2 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Studygroups/Proposals.php @@ -0,0 +1,125 @@ +getUser($request); + + [, $limit] = $this->getOffsetAndLimit(0, 4); + + $proposed_studygroup_ids = $this->getProposedStudygroupIds($user, $limit); + $proposed_studygroups = Course::findMany($proposed_studygroup_ids); + + return $this->getContentResponse($proposed_studygroups); + } + + private function getProposedStudygroupIds(User $user, int $amount = 4): array + { + $query = "SELECT DISTINCT `Seminar_id` + FROM ( + SELECT `Seminar_id` FROM ( + -- Andere Personen aus meinen Veranstaltungen + SELECT `seminare`.`Seminar_id`, COUNT(`seminar_user`.`user_id`) AS `count_colleagues` + FROM ( + SELECT colleagues.`user_id` + FROM `seminar_user` AS colleagues + JOIN `seminar_user` AS mine USING (`Seminar_id`) + WHERE mine.`user_id` = :me + AND colleagues.`user_id` != mine.`user_id` + ) AS my_colleagues + JOIN `seminar_user` + ON (`my_colleagues`.`user_id` = `seminar_user`.`user_id`) + JOIN `seminare` + ON (`seminare`.`Seminar_id` = `seminar_user`.`Seminar_id`) + WHERE `seminare`.`status` IN (:studygroup_types) + AND NOT EXISTS( + SELECT 1 + FROM `seminar_user` + WHERE `seminar_user`.`Seminar_id` = `seminare`.`Seminar_id` + AND `seminar_user`.`user_id` = :me + ) + GROUP BY `seminare`.`seminar_id` + ORDER BY `count_colleagues` DESC + LIMIT 12 + ) AS `colleagues_groups` + + UNION ALL + + SELECT `Seminar_id` FROM ( + -- Andere Personen aus meinen Studiengängen + SELECT DISTINCT `seminare`.`Seminar_id` + FROM `user_studiengang` + STRAIGHT_JOIN `mvv_stgteil` + ON (`mvv_stgteil`.`fach_id` = `user_studiengang`.`fach_id`) + STRAIGHT_JOIN `mvv_stg_stgteil` + ON (`mvv_stg_stgteil`.`stgteil_id` = `mvv_stgteil`.`stgteil_id`) + STRAIGHT_JOIN `mvv_studiengang` + ON ( + `mvv_studiengang`.`studiengang_id` = `mvv_stg_stgteil`.`studiengang_id` + AND `mvv_studiengang`.`abschluss_id` = `user_studiengang`.`abschluss_id` + ) + STRAIGHT_JOIN `studygroup_stgteil` + ON (`studygroup_stgteil`.`stgteil_id` = `mvv_stgteil`.`stgteil_id`) + STRAIGHT_JOIN `seminare` + ON (`seminare`.`Seminar_id` = `studygroup_stgteil`.`studygroup_id`) + WHERE `seminare`.`status` IN (:studygroup_types) + AND `user_studiengang`.`user_id` = :me + AND NOT EXISTS( + SELECT 1 + FROM `seminar_user` + WHERE `seminar_user`.`Seminar_id` = `seminare`.`Seminar_id` + AND `seminar_user`.`user_id` = :me + ) + LIMIT 12 + ) AS `same_studyarea_groups` + + UNION ALL + + SELECT `Seminar_id` FROM ( + -- Neue Studiengruppen + SELECT `seminare`.`Seminar_id` + FROM `seminare` + WHERE `seminare`.`status` IN (:studygroup_types) + AND NOT EXISTS( + SELECT 1 + FROM `seminar_user` + WHERE `seminar_user`.`Seminar_id` = `seminare`.`Seminar_id` + AND `seminar_user`.`user_id` = :me + ) + ORDER BY `seminare`.`mkdate` DESC + LIMIT 12 + ) AS `new_groups` + ) AS `all_groups`"; + $group_ids = DBManager::get()->fetchFirst($query, [ + ':studygroup_types' => $this->getStudygroupSemTypeIds(), + ':me' => $user->id, + ]); + + // Zufällig sortieren ist in PHP schneller als in SQL + shuffle($group_ids); + + return array_slice($group_ids, 0, $amount); + } + + private function getStudygroupSemTypeIds(): array + { + return array_filter( + array_keys($GLOBALS['SEM_TYPE']), + fn($sem_type_id) => (bool) $GLOBALS['SEM_CLASS'][$GLOBALS['SEM_TYPE'][$sem_type_id]['class']]['studygroup_mode'] + ); + } +} diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php index 89b0f9f..56184bb 100644 --- a/lib/classes/JsonApi/SchemaMap.php +++ b/lib/classes/JsonApi/SchemaMap.php @@ -86,6 +86,7 @@ class SchemaMap \StgteilAbschnitt::class => Schemas\ComponentSection::class, \Theme::class => Schemas\Theme::class, \ShortUrl::class => Schemas\ShortUrl::class, + \Tag::class => Schemas\Tag::class, \Courseware\Block::class => Schemas\Courseware\Block::class, \Courseware\BlockComment::class => Schemas\Courseware\BlockComment::class, diff --git a/lib/classes/JsonApi/Schemas/Course.php b/lib/classes/JsonApi/Schemas/Course.php index 5ffb6ee..2c45863 100644 --- a/lib/classes/JsonApi/Schemas/Course.php +++ b/lib/classes/JsonApi/Schemas/Course.php @@ -26,64 +26,75 @@ class Course extends SchemaProvider const REL_SEM_TYPE = 'sem-type'; const REL_START_SEMESTER = 'start-semester'; const REL_STATUS_GROUPS = 'status-groups'; - const REL_WIKI_PAGES = 'wiki-pages'; + const REL_TAGS = 'tags'; const REL_TOOLS = 'tools'; + const REL_WIKI_PAGES = 'wiki-pages'; - public function getId($course): ?string + /** + * @param \Course $resource + */ + public function getId($resource): ?string { - return $course->seminar_id; + return $resource->id; } - public function getAttributes($course, ContextInterface $context): iterable + /** + * @param \Course $resource + */ + public function getAttributes($resource, ContextInterface $context): iterable { $stringOrNull = function ($item) { return trim($item) != '' ? (string) $item : null; }; return [ - 'course-number' => $stringOrNull($course->veranstaltungsnummer), + 'course-number' => $stringOrNull($resource->veranstaltungsnummer), - 'title' => (string) $course->name, - 'subtitle' => $stringOrNull($course->untertitel), - 'course-type' => (int) $course->status, - 'description' => $stringOrNull($course->beschreibung), - 'location' => $stringOrNull($course->ort), - 'miscellaneous' => $stringOrNull($course->sonstiges), + 'title' => (string) $resource->name, + 'subtitle' => $stringOrNull($resource->untertitel), + 'course-type' => (int) $resource->status, + 'description' => $stringOrNull($resource->beschreibung), + 'location' => $stringOrNull($resource->ort), + 'miscellaneous' => $stringOrNull($resource->sonstiges), // 'read-access' => (int) $course->lesezugriff, // 'write-access' => (int) $course->schreibzugriff, ]; } - public function getRelationships($course, ContextInterface $context): iterable + /** + * @param \Course $resource + */ + public function getRelationships($resource, ContextInterface $context): iterable { $includeList = $context->getIncludePaths(); $relationships = []; - $relationships[self::REL_INSTITUTE] = $this->getInstitute($course, in_array(self::REL_INSTITUTE, $includeList)); + $relationships[self::REL_INSTITUTE] = $this->getInstitute($resource, in_array(self::REL_INSTITUTE, $includeList)); - if ($semester = $this->getStartSemester($course)) { + if ($semester = $this->getStartSemester($resource)) { $relationships[self::REL_START_SEMESTER] = $semester; } - if ($semester = $this->getEndSemester($course)) { + if ($semester = $this->getEndSemester($resource)) { $relationships[self::REL_END_SEMESTER] = $semester; } - $relationships = $this->getParticipatingInstitutes($relationships, $course, $includeList); - $relationships = $this->getFilesRelationship($relationships, $course); - $relationships = $this->getForumCategoriesRelationship($relationships, $course, $includeList); - $relationships = $this->getBlubberRelationship($relationships, $course, $includeList); - $relationships = $this->getCoursewareRelationship($relationships, $course, $includeList); - $relationships = $this->getEventsRelationship($relationships, $course, $includeList); - $relationships = $this->getFeedbackRelationship($relationships, $course, $includeList); - $relationships = $this->getMembershipsRelationship($relationships, $course, $includeList); - $relationships = $this->getNewsRelationship($relationships, $course, $includeList); - $relationships = $this->getSemClassRelationship($relationships, $course, $includeList); - $relationships = $this->getSemTypeRelationship($relationships, $course, $includeList); - $relationships = $this->getStatusGroupsRelationship($relationships, $course, $includeList); - $relationships = $this->getWikiPagesRelationship($relationships, $course, $includeList); - $relationships = $this->getToolsRelationship($relationships, $course, $includeList); + $relationships = $this->getParticipatingInstitutes($relationships, $resource, $includeList); + $relationships = $this->getFilesRelationship($relationships, $resource); + $relationships = $this->getForumCategoriesRelationship($relationships, $resource, $includeList); + $relationships = $this->getBlubberRelationship($relationships, $resource, $includeList); + $relationships = $this->getCoursewareRelationship($relationships, $resource, $includeList); + $relationships = $this->getEventsRelationship($relationships, $resource, $includeList); + $relationships = $this->getFeedbackRelationship($relationships, $resource, $includeList); + $relationships = $this->getMembershipsRelationship($relationships, $resource, $includeList); + $relationships = $this->getNewsRelationship($relationships, $resource, $includeList); + $relationships = $this->getSemClassRelationship($relationships, $resource, $includeList); + $relationships = $this->getSemTypeRelationship($relationships, $resource, $includeList); + $relationships = $this->getStatusGroupsRelationship($relationships, $resource, $includeList); + $relationships = $this->getTagsRelationship($relationships, $resource, $includeList); + $relationships = $this->getToolsRelationship($relationships, $resource, $includeList); + $relationships = $this->getWikiPagesRelationship($relationships, $resource, $includeList); return $relationships; } @@ -389,6 +400,23 @@ class Course extends SchemaProvider return array_merge($relationships, [self::REL_STATUS_GROUPS => $relation]); } + private function getTagsRelationship( + array $relationships, + \Course $course, + $includeData + ) { + $relation = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($course, self::REL_TAGS), + ] + ]; + if (in_array(self::REL_TAGS, $includeData)) { + $relation[self::RELATIONSHIP_DATA] = $course->tags; + } + + return array_merge($relationships, [self::REL_TAGS => $relation]); + } + /** * @inheritdoc */ @@ -414,6 +442,7 @@ class Course extends SchemaProvider 'medium' => $avatar->getURL(\Avatar::MEDIUM), 'normal' => $avatar->getURL(\Avatar::NORMAL), ], + 'members-count' => count($resource->members), ]; } diff --git a/lib/classes/JsonApi/Schemas/Tag.php b/lib/classes/JsonApi/Schemas/Tag.php new file mode 100644 index 0000000..ddc444b --- /dev/null +++ b/lib/classes/JsonApi/Schemas/Tag.php @@ -0,0 +1,39 @@ +id; + } + + /** + * @param \Tag $resource + */ + public function getAttributes($resource, ContextInterface $context): iterable + { + return [ + 'name' => $resource->name, + 'active' => (bool) $resource->active, + 'mkdate' => date('c', $resource->mkdate), + 'chdate' => date('c', $resource->chdate), + ]; + } + + /** + * @param \Tag $resource + */ + public function getRelationships($resource, ContextInterface $context): iterable + { + return []; + } +} diff --git a/lib/modules/StudygroupWidget.php b/lib/modules/StudygroupWidget.php index beddd4f..8a6db37 100644 --- a/lib/modules/StudygroupWidget.php +++ b/lib/modules/StudygroupWidget.php @@ -24,14 +24,17 @@ class StudygroupWidget extends CorePlugin implements PortalPlugin public function getPortalTemplate() { $template = $GLOBALS['template_factory']->open('start/studygroups'); + $template->proposals = Studip\VueApp::create('StudygroupProposals'); - $controller = app(\Trails\Dispatcher::class)->load_controller('my_studygroups'); - $response = $controller->relayWithRedirect('my_studygroups/proposals'); - $template->proposals = $response->body; - - $navigation = new Navigation('', 'dispatch.php/course/wizard?studygroup=1'); - $navigation->setImage(Icon::create('add', Icon::ROLE_CLICKABLE, ['title' => _('Neue Studiengruppe anlegen')])); - $navigation->setLinkAttributes(['data-dialog' => 'reload-on-close']); + $navigation = new Navigation( + _('Neue Studiengruppe anlegen'), + URLHelper::getURL('dispatch.php/course/wizard', ['studygroup' => 1]) + ); + $navigation->setImage(Icon::create('add')); + $navigation->setLinkAttributes([ + 'data-dialog' => 'reload-on-close', + 'title' => _('Neue Studiengruppe anlegen'), + ]); $template->icons = [$navigation]; return $template; diff --git a/resources/vue/apps/StudygroupProposals.vue b/resources/vue/apps/StudygroupProposals.vue new file mode 100644 index 0000000..b1bfcae --- /dev/null +++ b/resources/vue/apps/StudygroupProposals.vue @@ -0,0 +1,72 @@ + + diff --git a/templates/vue-app.php b/templates/vue-app.php index 7df6a01..271ed63 100644 --- a/templates/vue-app.php +++ b/templates/vue-app.php @@ -15,4 +15,5 @@ $data = [ ?>
+
-- cgit v1.0