From 39745c9aa8bb099e8bda1f4d775ed229dbe97be4 Mon Sep 17 00:00:00 2001 From: Rasmus Fuhse <fuhse@data-quest.de> Date: Fri, 10 Jan 2025 12:13:24 +0000 Subject: [PATCH] Resolve "Studiengruppen erweitern" - Hauptbronch Closes #3616 Merge request studip/studip!2509 --- app/controllers/admin/tags.php | 67 ++++ app/controllers/course/connectedcourses.php | 234 +++++++++++++ .../course/connectedstudygroups.php | 315 ++++++++++++++++++ app/controllers/course/overview.php | 11 + app/controllers/course/studygroup.php | 209 ++++++++++-- app/controllers/course/wizard.php | 11 +- app/controllers/my_courses.php | 34 +- app/controllers/my_studygroups.php | 100 +++++- app/controllers/search/angebot.php | 12 + app/controllers/search/globalsearch.php | 28 ++ app/controllers/search/studiengaenge.php | 14 +- app/views/admin/tags/index.php | 65 ++++ app/views/admin/tags/view_objects.php | 51 +++ .../connectedcourses/_course_to_connect.php | 21 ++ app/views/course/connectedcourses/connect.php | 78 +++++ app/views/course/connectedcourses/index.php | 112 +++++++ .../_studygroup_to_connect.php | 21 ++ .../course/connectedstudygroups/connect.php | 79 +++++ .../course/connectedstudygroups/index.php | 110 ++++++ app/views/course/overview/index.php | 4 + app/views/course/studygroup/details.php | 19 ++ app/views/course/studygroup/edit.php | 27 +- app/views/course/studygroup/widget.php | 88 +++++ app/views/course/wizard/step.php | 14 +- .../steps/basicdata/index_studygroup.php | 7 + .../course/wizard/steps/studygroups/index.php | 110 ++++++ app/views/my_studygroups/_course.php | 46 +-- app/views/my_studygroups/index.php | 17 +- app/views/my_studygroups/proposals.php | 31 ++ app/views/search/studiengaenge/verlauf.php | 55 +++ app/views/studygroup/browse.php | 54 +-- db/migrations/6.0.38_improved_studygroups.php | 305 +++++++++++++++++ .../ConnectedcourseAdmission.class.php | 121 +++++++ .../connectedcourseadmission/rule.manifest | 2 + .../templates/configure.php | 5 + .../templates/info.php | 1 + lib/classes/Avatar.php | 2 +- lib/classes/MyRealmModel.php | 95 +----- lib/classes/StudipController.php | 6 + lib/classes/StudygroupModel.php | 246 ++++++++++++-- lib/classes/admission/AdmissionRule.php | 2 +- lib/classes/admission/CourseSet.php | 39 +++ .../coursewizardsteps/BasicDataWizardStep.php | 50 ++- lib/classes/forms/MultiquicksearchInput.php | 31 ++ .../globalsearch/GlobalSearchStudygroups.php | 315 ++++++++++++++++++ .../searchtypes/StudygroupSearch.class.php | 103 ++++++ lib/cronjobs/expire_studygroups.class.php | 53 +++ lib/cronjobs/studygroup_expiration.php | 97 ++++++ lib/models/Course.php | 34 ++ lib/models/StudiengangTeil.php | 26 ++ lib/models/StudygroupCourse.php | 29 ++ lib/models/StudygroupCourseProposal.php | 36 ++ lib/models/StudygroupStgteil.php | 19 ++ lib/models/Tag.php | 55 +++ lib/models/TagRelation.php | 19 ++ lib/modules/CoreAdmin.php | 5 + lib/modules/CoreStudygroupAdmin.php | 1 + lib/modules/MyStudygroupsWidget.php | 38 +++ lib/modules/StudygroupWidget.php | 39 +++ lib/navigation/AdminNavigation.php | 8 +- .../assets/javascripts/lib/global_search.js | 7 + resources/assets/javascripts/lib/search.js | 7 + resources/assets/stylesheets/scss/forms.scss | 7 + .../assets/stylesheets/scss/studygroup.scss | 104 ++++++ resources/assets/stylesheets/scss/tables.scss | 6 +- resources/vue/base-components.js | 1 + resources/vue/components/Multiquicksearch.vue | 96 ++++++ resources/vue/components/MyCoursesTables.vue | 5 - resources/vue/components/Quicksearch.vue | 1 + templates/forms/multiquicksearch_input.php | 17 + templates/forms/quicksearch_input.php | 1 + templates/header.php | 6 +- templates/start/my_studygroups.php | 3 + templates/start/studygroups.php | 3 + .../_generated/JsonapiTesterActions.php | 2 +- 75 files changed, 3870 insertions(+), 222 deletions(-) create mode 100644 app/controllers/admin/tags.php create mode 100644 app/controllers/course/connectedcourses.php create mode 100644 app/controllers/course/connectedstudygroups.php create mode 100644 app/views/admin/tags/index.php create mode 100644 app/views/admin/tags/view_objects.php create mode 100644 app/views/course/connectedcourses/_course_to_connect.php create mode 100644 app/views/course/connectedcourses/connect.php create mode 100644 app/views/course/connectedcourses/index.php create mode 100644 app/views/course/connectedstudygroups/_studygroup_to_connect.php create mode 100644 app/views/course/connectedstudygroups/connect.php create mode 100644 app/views/course/connectedstudygroups/index.php create mode 100644 app/views/course/studygroup/widget.php create mode 100644 app/views/course/wizard/steps/studygroups/index.php create mode 100644 app/views/my_studygroups/proposals.php create mode 100644 db/migrations/6.0.38_improved_studygroups.php create mode 100644 lib/admissionrules/connectedcourseadmission/ConnectedcourseAdmission.class.php create mode 100644 lib/admissionrules/connectedcourseadmission/rule.manifest create mode 100644 lib/admissionrules/connectedcourseadmission/templates/configure.php create mode 100644 lib/admissionrules/connectedcourseadmission/templates/info.php create mode 100644 lib/classes/forms/MultiquicksearchInput.php create mode 100644 lib/classes/globalsearch/GlobalSearchStudygroups.php create mode 100644 lib/classes/searchtypes/StudygroupSearch.class.php create mode 100644 lib/cronjobs/expire_studygroups.class.php create mode 100644 lib/cronjobs/studygroup_expiration.php create mode 100644 lib/models/StudygroupCourse.php create mode 100644 lib/models/StudygroupCourseProposal.php create mode 100644 lib/models/StudygroupStgteil.php create mode 100644 lib/models/Tag.php create mode 100644 lib/models/TagRelation.php create mode 100644 lib/modules/MyStudygroupsWidget.php create mode 100644 lib/modules/StudygroupWidget.php create mode 100644 resources/vue/components/Multiquicksearch.vue create mode 100644 templates/forms/multiquicksearch_input.php create mode 100644 templates/start/my_studygroups.php create mode 100644 templates/start/studygroups.php diff --git a/app/controllers/admin/tags.php b/app/controllers/admin/tags.php new file mode 100644 index 00000000000..03819bd51d5 --- /dev/null +++ b/app/controllers/admin/tags.php @@ -0,0 +1,67 @@ +<?php + +class Admin_TagsController extends AuthenticatedController +{ + /** + * Common tasks for all actions. + */ + public function before_filter(&$action, &$args) + { + parent::before_filter($action, $args); + + $GLOBALS['perm']->check('root'); + Navigation::activateItem('/admin/locations/tags'); + PageLayout::setTitle(_('Schlagwortverwaltung')); + } + + public function index_action() + { + Tag::deleteBySQL('LEFT JOIN `tags_relations` ON (`tags`.`id` = `tags_relations`.`tag_id`) WHERE `tags_relations`.`range_id` IS NULL'); + $this->page = Request::int('page', 0); + $this->tags = Tag::findBySQL('1 ORDER BY `name` ASC LIMIT :offset, :limit', [ + 'offset' => $this->page * Config::get()->ENTRIES_PER_PAGE, + 'limit' => Config::get()->ENTRIES_PER_PAGE + ]); + $this->all_tags = Tag::countBySql('1'); + } + + public function edit_action(Tag $tag) + { + PageLayout::setTitle(sprintf(_('Schlagwort „%s“ bearbeiten'), $tag->name)); + $form = \Studip\Forms\Form::fromSORM( + $tag, + [ + 'legend' => _('Grunddaten'), + 'fields' => [ + 'name' => [ + 'label' =>_('Name'), + 'validate' => function ($value) use ($tag) { + $output = ''; + if ($value !== mb_strtolower($value)) { + $output .= _('Schlagwörter sollen keine Großbuchstaben entahlten').' '; + } + foreach (['\n', '#', '|', ' '] as $forbidden) { + if (str_contains($value, $forbidden)) { + $output .= _('Schlagwörter dürfen keine Zeilenumbrüche, Leerzeichen, Doppelkreuze (#) oder Pipe-Zeichen (|) enthalten.').' '; + break; + } + } + if (Tag::findOneByName($value) && $value !== $tag->name) { + $output .= _('Dieses Schlagwort ist schon vergeben.').' '; + } + return $output !== '' ? $output : true; + } + ], + 'active' => _('Aktiv') + ] + ] + )->autoStore()->setURL($this->indexURL()); + $this->render_form($form); + } + + public function view_objects_action(Tag $tag) + { + $this->tag = $tag; + PageLayout::setTitle(sprintf(_("Verknüpfte Objekte mit Schlagwort „%s“"), $tag->name)); + } +} diff --git a/app/controllers/course/connectedcourses.php b/app/controllers/course/connectedcourses.php new file mode 100644 index 00000000000..dcbc3ae87a0 --- /dev/null +++ b/app/controllers/course/connectedcourses.php @@ -0,0 +1,234 @@ +<?php + +class Course_ConnectedcoursesController extends AuthenticatedController +{ + + public function before_filter(&$action, &$args) + { + parent::before_filter($action, $args); + + if (!$GLOBALS['perm']->have_studip_perm('tutor', Context::getId())) { + throw new AccessDeniedException(); + } + } + + public function index_action() + { + Navigation::activateItem('/course/admin/connectedcourses'); + $this->connected = StudygroupCourse::findBySQL( + 'INNER JOIN seminare ON (seminare.Seminar_id = studygroup_courses.course_id) WHERE studygroup_courses.studygroup_id = ? ORDER BY seminare.name ASC', + [ + Context::getId() + ] + ); + $this->proposals = StudygroupCourseProposal::findBySQL( + 'INNER JOIN seminare ON (seminare.Seminar_id = studygroup_courses_proposals.studygroup_id) WHERE studygroup_courses_proposals.studygroup_id = ? ORDER BY seminare.name ASC', + [ + Context::getId() + ] + ); + $this->buildSidebar(); + + } + + public function connect_action($course_id = null) + { + + Navigation::activateItem('/course/admin/connectedcourses'); + PageLayout::setTitle(_('Veranstaltung suchen und zur Verknüpfung vorschlagen')); + + if (Request::isPost() && (Request::option('course_id') || $course_id)) { + CSRFProtection::verifySecurityToken(); + $course_id = $course_id ?? Request::option('course_id'); + $status = StudygroupModel::proposeAsStudygroupTo(Context::get(), $course_id); + if ($status === 'connected') { + PageLayout::postSuccess(_('Veranstaltung wurde verknüpft.')); + } + if ($status === 'proposed') { + PageLayout::postSuccess(_('Vorschlag wurde eingereicht.')); + } + $this->redirect('course/connectedcourses/index'); + return; + } + + $connected = StudygroupCourse::findBySQL( + 'INNER JOIN seminare ON (seminare.Seminar_id = studygroup_courses.course_id) WHERE studygroup_courses.studygroup_id = ? ORDER BY seminare.name ASC', + [ + Context::getId() + ] + ); + $proposals = StudygroupCourseProposal::findBySQL( + 'INNER JOIN seminare ON (seminare.Seminar_id = studygroup_courses_proposals.studygroup_id) WHERE studygroup_courses_proposals.studygroup_id = ? ORDER BY seminare.name ASC', + [ + Context::getId() + ] + ); + $already_covered = array_map(function($c) { return $c->course_id; }, $connected); + $already_covered = $already_covered + array_map(function($c) { return $c->course_id; }, $proposals); + + + + + $studygroup_ids = []; + foreach (SemClass::getClasses() as $sem_class) { + if ($sem_class['studygroup_mode'] > 0) { + foreach ($sem_class->getSemTypes() as $sem_type) { + $studygroup_ids[] = $sem_type['id']; + } + } + } + $this->my_courses = []; + if (!$GLOBALS['perm']->have_perm('admin')) { + $this->my_courses = Course::findBySQL('INNER JOIN `seminar_user` USING (`Seminar_id`) + LEFT JOIN `semester_courses` ON (`seminare`.`Seminar_id` = `semester_courses`.`course_id`) + WHERE `seminar_user`.`user_id` = :user_id + AND `seminare`.`status` NOT IN (:studygroup_sem_types) + AND (`semester_courses`.`semester_id` IS NULL OR `semester_courses`.`semester_id` = :semester_id) + AND `seminare`.`Seminar_id` NOT IN (:ignore) + ORDER BY `seminare`.`name` ASC ', + [ + 'user_id' => User::findCurrent()->id, + 'studygroup_sem_types' => $studygroup_ids, + 'semester_id' => Request::get('semester_id') ?? Semester::findCurrent()->id, + 'ignore' => count($already_covered) ? $already_covered : '' + ]); + foreach ($this->my_courses as $my_course) { + $already_covered[] = $my_course->id; + } + } + + if (Request::get('search') && Request::get('search') != 1) { + //do the search: + $query = SQLQuery::table('seminare') + ->where('search', + '`name` LIKE :search OR `VeranstaltungsNummer` LIKE :search', + ['search' => '%' . Request::get('search') . '%'] + ) + ->where( + 'studygroups', + '`seminare`.`status` NOT IN (:sem_type_ids)', + ['sem_type_ids' => $studygroup_ids] + ) + ->groupBy('`seminare`.`Seminar_id`'); + if (count($already_covered) > 0) { + $query->where( + 'ignore', + '`seminare`.`Seminar_id` NOT IN (:ignore)', + ['ignore' => $already_covered] + ); + } + if (!empty(Request::get('semester_id'))) { + $query->join( + 'semester_courses', + 'semester_courses', + '`semester_courses`.`course_id` = `seminare`.`Seminar_id`', + 'LEFT JOIN' + ); + $query->where( + 'semester_id', + 'semester_courses.semester_id = :semester_id OR semester_courses.semester_id IS NULL', + ['semester_id' => Request::get('semester_id')] + ); + } + $this->searchresults = $query->fetchAll(Course::class); + } else { + //get up to 10 courses with a lot of members of the current studygroup: + $statement = DBManager::get()->prepare(" + SELECT `seminare`.* + FROM `seminar_user` + INNER JOIN `seminare` ON (`seminare`.`Seminar_id` = `seminar_user`.`Seminar_id`) + INNER JOIN `seminar_user` AS `su2` ON (`su2`.`user_id` = `seminar_user`.`user_id` AND `su2`.`Seminar_id` = :course_id) + LEFT JOIN `studygroup_courses` ON (`studygroup_courses`.`course_id` = `seminare`.`Seminar_id` AND `studygroup_courses`.`studygroup_id` = `su2`.`Seminar_id`) + WHERE `seminare`.`status` NOT IN (:studygroup_sem_types) + AND `studygroup_courses`.`id` IS NULL + AND `seminare`.`Seminar_id` NOT IN (:ignore) + GROUP BY `seminare`.`Seminar_id` + HAVING COUNT(`seminar_user`.`user_id`) > 1 + ORDER BY COUNT(`seminar_user`.`user_id`) DESC + LIMIT 20 + "); + $statement->execute([ + 'course_id' => Context::getId(), + 'studygroup_sem_types' => $studygroup_ids, + 'ignore' => count($already_covered) ? $already_covered : '' + ]); + $suggestions = $statement->fetchAll(PDO::FETCH_ASSOC); + $this->suggestions = array_map(function ($d) { + return Course::buildExisting($d); + }, $suggestions); + } + + + } + + public function remove_action($course_id) + { + if (Request::isPost() && $course_id) { + CSRFProtection::verifySecurityToken(); + StudygroupCourse::deleteBySQL('course_id = ? AND studygroup_id = ?', [ + $course_id, + Context::getId() + ]); + PageLayout::postSuccess(_('Verknüpfung zu der Veranstaltung wurde aufgehoben.')); + } + $this->redirect('course/connectedcourses/index'); + } + + public function decline_action(StudygroupCourseProposal $proposal) + { + if (Request::isPost()) { + CSRFProtection::verifySecurityToken(); + if ($GLOBALS['perm']->have_studip_perm('tutor', $proposal['course_id']) || $GLOBALS['perm']->have_studip_perm('tutor', $proposal['studygroup_id'])) { + if ($proposal['proposed_from'] === 'course') { + PageLayout::postSuccess(_('Vorschlag wurde abgewiesen.')); + $statement = DBManager::get()->prepare(" + SELECT `username`, `user_id` + FROM `auth_user_md5` + INNER JOIN `seminar_user` USING (`user_id`) + WHERE `seminar_user`.`Seminar_id` = ? AND `seminar_user`.`status` IN ('tutor', 'dozent') + "); + $statement->execute([$proposal['course_id']]); + $messaging = new messaging(); + + foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $user_data) { + setTempLanguage($user_data['user_id']); + $messaging->insert_message( + sprintf( + _('Ihr Vorschlag, die Studiengruppe „%1$s“ mit der Veranstaltung „%2$s“ zu verknüpfen, wurde leider abgewiesen.'), + Context::get()->getFullname(), + $proposal->studygroup->getFullname() + ), + $user_data['username'], + '____%system%____', + '', + '', + '', + '', + _('Verknüpfungsvorschlag abgewiesen'), + '', + 'normal', + ['Studiengruppe'] + ); + restoreLanguage(); + } + } else { + PageLayout::postSuccess(_('Vorschlag wurde zurückgezogen.')); + } + $proposal->delete(); + } + } + $this->redirect('course/connectedcourses/index'); + } + + protected function buildSidebar() + { + $actions = new ActionsWidget(); + $actions->addLink( + _('Verknüpfung vorschlagen'), + $this->url_for('course/connectedcourses/connect'), + Icon::create('add'), + ['data-dialog' => 1] + ); + Sidebar::Get()->addWidget($actions); + } +} diff --git a/app/controllers/course/connectedstudygroups.php b/app/controllers/course/connectedstudygroups.php new file mode 100644 index 00000000000..9ef6b920087 --- /dev/null +++ b/app/controllers/course/connectedstudygroups.php @@ -0,0 +1,315 @@ +<?php + +class Course_ConnectedstudygroupsController extends AuthenticatedController +{ + + public function before_filter(&$action, &$args) + { + parent::before_filter($action, $args); + + if (!$GLOBALS['perm']->have_studip_perm('tutor', Context::getId())) { + throw new AccessDeniedException(); + } + } + + public function index_action() + { + Navigation::activateItem('/course/admin/connectedstudygroups'); + $this->connected = StudygroupCourse::findBySQL( + 'INNER JOIN seminare ON (seminare.Seminar_id = studygroup_courses.studygroup_id) WHERE studygroup_courses.course_id = ? ORDER BY seminare.name ASC', + [ + Context::getId() + ] + ); + $this->proposals = StudygroupCourseProposal::findBySQL( + 'INNER JOIN seminare ON (seminare.Seminar_id = studygroup_courses_proposals.studygroup_id) WHERE studygroup_courses_proposals.course_id = ? ORDER BY seminare.name ASC', + [ + Context::getId() + ] + ); + $this->buildSidebar(); + + } + + public function connect_action($course_id = null) + { + Navigation::activateItem('/course/admin/connectedstudygroups'); + PageLayout::setTitle(_('Studiengruppe suchen und verknüpfen')); + if (Request::isPost() && (Request::option('course_id') || $course_id)) { + CSRFProtection::verifySecurityToken(); + $course_id = $course_id ?? Request::option('course_id'); + $proposal = StudygroupCourseProposal::findOneBySQL('course_id = ? AND studygroup_id = ?', [ + Context::getId(), + $course_id + ]); + if ($GLOBALS['perm']->have_studip_perm('tutor', $course_id) || $proposal['proposed_from'] === 'studygroup') { + $connection = StudygroupCourse::findOneBySQL('course_id = ? AND studygroup_id = ?', [ + Context::getId(), + $course_id + ]); + if (!$connection) { + $connection = new StudygroupCourse(); + $connection['course_id'] = Context::getId(); + $connection['studygroup_id'] = $course_id; + $connection->store(); + } + if ($proposal) { + if ($proposal['proposed_from'] === 'studygroup') { + $statement = DBManager::get()->prepare(" + SELECT `username`, `user_id` + FROM `auth_user_md5` + INNER JOIN `seminar_user` USING (`user_id`) + WHERE `seminar_user`.`Seminar_id` = ? AND `seminar_user`.`status` IN ('tutor', 'dozent') + "); + $statement->execute([$course_id]); + $messaging = new messaging(); + + foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $user_data) { + setTempLanguage($user_data['user_id']); + $messaging->insert_message( + sprintf( + _('Ihr Vorschlag, die Studiengruppe „%1$s“ mit der Veranstaltung „%2$s“ zu verknüpfen, wurde angenommen.'), + Context::get()->getFullname(), + Course::find($course_id)->getFullname() + ), + $user_data['username'], + '____%system%____', + '', + '', + '', + '', + _('Verknüpfungsvorschlag angenommen'), + '', + 'normal', + ['Studiengruppe'] + ); + restoreLanguage(); + } + } + $proposal->delete(); + } + PageLayout::postSuccess(_('Veranstaltung wurde verknüpft.')); + } else { + //send message: + if (!$proposal) { + $proposal = new StudygroupCourseProposal(); + $proposal['course_id'] = Context::getId(); + $proposal['studygroup_id'] = $course_id; + $proposal['proposed_from'] = 'course'; + $proposal['user_id'] = User::findCurrent()->id; + $proposal->store(); + + $statement = DBManager::get()->prepare(" + SELECT `username`, `user_id` + FROM `auth_user_md5` + INNER JOIN `seminar_user` USING (`user_id`) + WHERE `seminar_user`.`Seminar_id` = ? AND `seminar_user`.`status` IN ('tutor', 'dozent') + "); + $statement->execute([$course_id]); + $messaging = new messaging(); + $oldbase = URLHelper::setBaseURL($GLOBALS['ABSOLUTE_URI_STUDIP']); + + foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $user_data) { + setTempLanguage($user_data['user_id']); + $messaging->insert_message( + sprintf( + _('Es wurde vorgeschlagen, die Veranstaltung „%1$s“ mit Ihrer Studiengruppe „%2$s“ zu verknüpfen. Sie können den Vorschlag unter folgendem Link annehmen oder ablehnen:'), + Context::get()->getFullname(), + Course::find($course_id)->getFullname() + )."\n\n".URLHelper::getURL('dispatch.php/course/connectedcourses/index', ['cid' => $course_id]), + $user_data['username'], + '____%system%____', + '', + '', + '', + '', + _('Verknüpfung Ihrer Studiengruppe zu einer Veranstaltung'), + '', + 'normal', + ['Studiengruppe'] + ); + restoreLanguage(); + } + + URLHelper::setBaseURL($oldbase); + } + PageLayout::postSuccess(_('Antrag wurde gestellt.')); + } + $this->redirect('course/connectedstudygroups/index'); + return; + } + + $connected = StudygroupCourse::findBySQL( + 'INNER JOIN seminare ON (seminare.Seminar_id = studygroup_courses.studygroup_id) WHERE studygroup_courses.course_id = ? ORDER BY seminare.name ASC', + [ + Context::getId() + ] + ); + $proposals = StudygroupCourseProposal::findBySQL( + 'INNER JOIN seminare ON (seminare.Seminar_id = studygroup_courses_proposals.course_id) WHERE studygroup_courses_proposals.course_id = ? ORDER BY seminare.name ASC', + [ + Context::getId() + ] + ); + $already_covered = array_map(function ($c) { + return $c->course_id; + }, $connected); + $already_covered = $already_covered + array_map(function ($c) { + return $c->course_id; + }, $proposals); + + $studygroup_ids = []; + foreach (SemClass::getClasses() as $sem_class) { + if ($sem_class['studygroup_mode'] > 0) { + foreach ($sem_class->getSemTypes() as $sem_type) { + $studygroup_ids[] = $sem_type['id']; + } + } + } + + if (Request::get('search') && Request::get('search') != 1) { + $query = SQLQuery::table('seminare') + ->where('search', '`name` LIKE :search', ['search' => '%' . Request::get('search') . '%']) + ->where( + 'studygroups', + '`seminare`.`status` IN (:sem_type_ids)', + ['sem_type_ids' => $studygroup_ids] + ) + ->groupBy('`seminare`.`Seminar_id`'); + if (count($already_covered) > 0) { + $query->where( + 'ignore', + '`seminare`.`Seminar_id` NOT IN (:ignore)', + ['ignore' => $already_covered] + ); + } + if (!empty(Request::get('semester_id'))) { + $query->join( + 'semester_courses', + 'semester_courses', + '`semester_courses`.`course_id` = `seminare`.`Seminar_id`', + 'LEFT JOIN' + ); + $query->where( + 'semester_id', + 'semester_courses.semester_id = :semester_id OR semester_courses.semester_id IS NULL', + ['semester_id' => Request::get('semester_id')] + ); + } + $this->searchresults = $query->fetchAll(Course::class); + } else { + + $this->my_studygroups = []; + if (!$GLOBALS['perm']->have_perm('admin')) { + $this->my_studygroups = Course::findBySQL('INNER JOIN `seminar_user` USING (`Seminar_id`) + WHERE `seminar_user`.`user_id` = :user_id + AND `seminare`.`status` IN (:studygroup_sem_types) + AND `seminare`.`Seminar_id` NOT IN (:ignore) + ORDER BY `seminare`.`name` ASC ', + [ + 'user_id' => User::findCurrent()->id, + 'studygroup_sem_types' => $studygroup_ids, + 'ignore' => count($already_covered) ? $already_covered : '' + ]); + foreach ($this->my_studygroups as $my_studygroup) { + $already_covered[] = $my_studygroup->id; + } + } + + //get all studygroups with a lot of members in the current course: + $statement = DBManager::get()->prepare(" + SELECT `seminare`.* + FROM `seminar_user` + INNER JOIN `seminare` ON (`seminare`.`Seminar_id` = `seminar_user`.`Seminar_id`) + LEFT JOIN `seminar_user` AS `su2` ON (`su2`.`user_id` = `seminar_user`.`user_id` AND `su2`.`Seminar_id` = :course_id) + LEFT JOIN `studygroup_courses` ON (`studygroup_courses`.`studygroup_id` = `seminare`.`Seminar_id` AND `studygroup_courses`.`course_id` = `su2`.`Seminar_id`) + WHERE `seminare`.`status` IN (:studygroup_sem_types) + AND `studygroup_courses`.`id` IS NULL + GROUP BY `seminare`.`Seminar_id` + HAVING COUNT(`seminar_user`.`user_id`) > 1 + ORDER BY COUNT(`seminar_user`.`user_id`) DESC + LIMIT 20 + "); + $statement->execute([ + 'course_id' => Context::getId(), + 'studygroup_sem_types' => $studygroup_ids + ]); + $this->suggestions = $statement->fetchAll(PDO::FETCH_ASSOC); + $this->suggestions = array_map(function ($d) { + return Course::buildExisting($d); + }, $this->suggestions); + } + } + + public function remove_action($course_id) + { + if (Request::isPost() && $course_id) { + CSRFProtection::verifySecurityToken(); + $connection = StudygroupCourse::deleteBySQL('course_id = ? AND studygroup_id = ?', [ + Context::getId(), + $course_id + ]); + PageLayout::postSuccess(_('Verknüpfung zu der Studiengruppe wurde aufgehoben.')); + } + $this->redirect('course/connectedstudygroups/index'); + } + + public function decline_action(StudygroupCourseProposal $proposal) + { + if (Request::isPost()) { + CSRFProtection::verifySecurityToken(); + if ($GLOBALS['perm']->have_studip_perm('tutor', $proposal['course_id']) || $GLOBALS['perm']->have_studip_perm('tutor', $proposal['studygroup_id'])) { + if ($proposal['proposed_from'] === 'studygroup') { + PageLayout::postSuccess(_('Vorschlag wurde abgewiesen.')); + $statement = DBManager::get()->prepare(" + SELECT `username`, `user_id` + FROM `auth_user_md5` + INNER JOIN `seminar_user` USING (`user_id`) + WHERE `seminar_user`.`Seminar_id` = ? AND `seminar_user`.`status` IN ('tutor', 'dozent') + "); + $statement->execute([$proposal['studygroup_id']]); + $messaging = new messaging(); + + foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $user_data) { + setTempLanguage($user_data['user_id']); + $messaging->insert_message( + sprintf( + _('Ihr Vorschlag, die Studiengruppe „%1$s“ mit der Veranstaltung „%2$s“ zu verknüpfen, wurde leider abgewiesen.'), + $proposal->studygroup->getFullname(), + Context::get()->getFullname() + ), + $user_data['username'], + '____%system%____', + '', + '', + '', + '', + _('Verknüpfungsvorschlag abgewiesen'), + '', + 'normal', + ['Studiengruppe'] + ); + + restoreLanguage(); + } + } else { + PageLayout::postSuccess(_('Vorschlag wurde zurückgezogen.')); + } + $proposal->delete(); + } + } + $this->redirect('course/connectedstudygroups/index'); + } + + protected function buildSidebar() + { + $actions = new ActionsWidget(); + $actions->addLink( + _('Studiengruppe verknüpfen'), + $this->url_for('course/connectedstudygroups/connect'), + Icon::create('add'), + ['data-dialog' => 1] + ); + Sidebar::Get()->addWidget($actions); + } +} diff --git a/app/controllers/course/overview.php b/app/controllers/course/overview.php index e1da00e303f..4313cdc71a1 100644 --- a/app/controllers/course/overview.php +++ b/app/controllers/course/overview.php @@ -108,6 +108,17 @@ class Course_OverviewController extends AuthenticatedController $this->avatar = StudygroupAvatar::getAvatar($this->course_id); } + $connections = StudygroupCourse::countBySql( + "`studygroup_id` = :cid OR `course_id` = :cid", + [ + 'cid' => $this->course_id + ] + ); + if ($connections > 0) { + $response = $this->relay('course/studygroup/widget/' . $this->course_id); + $this->connectedstudygroups = $response->body; + } + $this->plugins = PluginEngine::getPlugins(StandardPlugin::class, $this->course_id); $sidebar = Sidebar::get(); diff --git a/app/controllers/course/studygroup.php b/app/controllers/course/studygroup.php index 981a152f2a5..c95c20e89d6 100644 --- a/app/controllers/course/studygroup.php +++ b/app/controllers/course/studygroup.php @@ -170,43 +170,168 @@ class Course_StudygroupController extends AuthenticatedController */ public function edit_action() { - global $perm; - - $id = Context::getId(); - + PageLayout::setTitle(Context::getHeaderLine() . ' - ' . _('Studiengruppe bearbeiten')); + Navigation::activateItem('/course/admin/main'); PageLayout::setHelpKeyword('Basis.StudiengruppenBearbeiten'); - // if we are permitted to edit the studygroup get some data... - if ($id && $perm->have_studip_perm('dozent', $id)) { - $this->course = Course::find($id); + $course = Context::get(); - PageLayout::setTitle(Context::getHeaderLine() . ' - ' . _('Studiengruppe bearbeiten')); - Navigation::activateItem('/course/admin/main'); + $expiration_date = CourseConfig::get($course->id)->STUDYGROUP_EXPIRATION_DATE; + if (!$expiration_date) { + $expiration_date = _('Unbegrenzt'); + } - $this->course_id = $id; - $this->sem_class = $GLOBALS['SEM_CLASS'][$GLOBALS['SEM_TYPE'][$this->course->status]['class']]; - $this->tutors = CourseMember::findByCourseAndStatus($this->course->id, 'tutor'); - $this->founders = StudygroupModel::getFounders($id); + $zugang_options = [ + 'all' => _('Offen für alle'), + 'invite' => _('Auf Anfrage'), + 'connectedcourse' => _('Für Mitglieder der zugehörigen Lehrveranstaltung') + ]; + if (Config::get()->STUDYGROUPS_INVISIBLE_ALLOWED) { + $zugang_options['invisible'] = _('Unsichtbar'); + } - $actions = new ActionsWidget(); + $form = \Studip\Forms\Form::fromSORM(Context::get(), [ + 'legend' => _('Grunddaten'), + 'fields' => [ + 'name' => [ + 'label' => _('Name'), + 'required' => true + ], + 'beschreibung' => _('Beschreibung'), + 'zugang' => [ + 'label' => _('Zugang'), + 'type' => 'select', + 'options' => $zugang_options, + 'value' => function () use ($course) { + $courseset = CourseSet::getSetForCourse($course->id); + if ($courseset && $courseset->getId() === CourseSet::getConnectedcourseAdmissionSetId()) { + return 'connectedcourse'; + } elseif (!$course->visible) { + return 'invisible'; + } else { + return $course->admission_prelim > 0 ? 'invite' : 'all'; + } + }, + 'store' => function ($value, $input) { + $course = $input->getContextObject(); + switch ($value) { + case 'connectedcourse': + CourseSet::addCourseToSet(CourseSet::getConnectedcourseAdmissionSetId(), $course->id); + $course->visible = 1; + break; + case 'invisible': + CourseSet::removeCourseFromSet(CourseSet::getConnectedcourseAdmissionSetId(), $course->id); + if (Config::get()->STUDYGROUPS_INVISIBLE_ALLOWED) { + $course->visible = 0; + break; + } + case 'invite': + CourseSet::removeCourseFromSet(CourseSet::getConnectedcourseAdmissionSetId(), $course->id); + $course->visible = 1; + $course->admission_prelim = 1; + $course->admission_prelim_txt = _('Die Moderator:innen der Studiengruppe können Ihren Aufnahmewunsch bestätigen oder ablehnen. Erst nach Bestätigung erhalten Sie vollen Zugriff auf die Gruppe.'); + break; + case 'all': + CourseSet::removeCourseFromSet(CourseSet::getConnectedcourseAdmissionSetId(), $course->id); + $course->visible = 1; + $course->admission_prelim = 0; + break; + } + $course->store(); + } + ] + ] + ])->addSORM( + Context::get(), [ + 'legend' => _('Erweiterte Einstellungen'), + 'fields' => [ + 'ablaufdatum' => [ + 'label' => _('Ablaufdatum / Löschdatum'), + 'type' => 'datetimepicker', + 'value' => $expiration_date, + 'store' => function ($value) { + CourseConfig::get(Context::getId())->store('STUDYGROUP_EXPIRATION_DATE', $value); + } + ], + 'tags' => [ + 'label' => _('Schlagwörter'), + 'type' => 'multiquicksearch', + 'addlabel' => _('Schlagwort hinzufügen'), + 'value' => function () { + $course = Context::get(); + $tags = Tag::getByRange($course->id, 'course'); + return array_map(function ($t) { return $t->name; }, $tags); + }, + 'searchtype' => (string) SQLSearch::get('SELECT `name`, `name` FROM `tags` WHERE `active` = 1 AND `name` LIKE :input', _('Schlagwort suchen')), + 'autocomplete' => true, + 'mapper' => function ($value, $obj) { + $tags = []; + foreach ($value as $name) { + if ($name) { + if ($tag = Tag::findOneByName($name)) { + if ($tag->active) { + $tags[] = $tag; + } + } else { + $tag = new Tag(); + $tag->name = $name; + $tag->store(); + $tags[] = $tag; + } + } + } + return $tags; + }, + 'store' => function ($tags, $input) { + $course = $input->getContextObject(); + $tag_ids = []; + foreach ($tags as $tag) { + $tag_ids[] = $tag->id; + $relation = TagRelation::findOneBySQL( + "`range_id` = :course_id AND `range_type` = 'course' AND `tag_id` = :tag_id", + [ + 'tag_id' => $tag->id, + 'course_id' => $course->id + ] + ); + if (!$relation) { + $relation = TagRelation::create([ + 'range_id' => $course->id, + 'range_type' => 'course', + 'tag_id' => $tag->id, + ]); + } + } + TagRelation::deleteBySQL( + "`range_id` = :course_id AND `range_type` = 'course' AND `tag_id` NOT IN (:ids)", + [ + 'ids' => $tag_ids, + 'course_id' => $course->id + ] + ); + } + ] + ] + ] + )->setURL($this->editURL()) + ->autoStore(); - $actions->addLink( - _('Neue Studiengruppe anlegen'), - $this->url_for('course/wizard?studygroup=1'), - Icon::create('add') - ); + $actions = new ActionsWidget(); - $actions->addLink( - _('Diese Studiengruppe löschen'), - $this->deleteURL(), - Icon::create('trash') - ); + $actions->addLink( + _('Neue Studiengruppe anlegen'), + $this->url_for('course/wizard?studygroup=1'), + Icon::create('add') + ); + $actions->addLink( + _('Diese Studiengruppe löschen'), + $this->deleteURL(), + Icon::create('trash') + ); - Sidebar::get()->addWidget($actions); - } // ... otherwise redirect us to the seminar - else { - $this->redirect(URLHelper::getURL('dispatch.php/course/go?to=' . $id)); - } + Sidebar::get()->addWidget($actions); + + $this->render_form($form); } /** @@ -255,14 +380,20 @@ class Course_StudygroupController extends AuthenticatedController $course->schreibzugriff = 1; $course->visible = 1; - if (Request::get('groupaccess') == 'all') { + $cs_id = CourseSet::getConnectedcourseAdmissionSetId(); + if (Request::get('groupaccess') === 'all') { $course->admission_prelim = 0; + CourseSet::removeCourseFromSet($cs_id, $id); + } elseif(Request::get('groupaccess') === 'top-course') { + CourseSet::addCourseToSet($cs_id, $id); } else { $course->admission_prelim = 1; - if (Config::get()->STUDYGROUPS_INVISIBLE_ALLOWED && Request::get('groupaccess') == 'invisible') { + if (Config::get()->STUDYGROUPS_INVISIBLE_ALLOWED && Request::get('groupaccess') === 'invisible') { $course->visible = 0; } $course->admission_prelim_txt = _('Die für die Moderation zuständigen Personen der Studiengruppe können Ihren Aufnahmewunsch bestätigen oder ablehnen. Erst nach Bestätigung erhalten Sie vollen Zugriff auf die Gruppe.'); + + CourseSet::removeCourseFromSet($cs_id, $id); } $course->store(); } @@ -783,4 +914,20 @@ class Course_StudygroupController extends AuthenticatedController $this->avatar_url = $avatar->getURL(Avatar::NORMAL); } + public function widget_action($range_id) + { + if (get_class($this->parent_controller) === __CLASS__) { + throw new RuntimeException('widget_action must be relayed'); + } + $this->course = Course::find($range_id); + + if ($this->course->isStudygroup()) { + $sql = "INNER JOIN `seminare` ON (`seminare`.`Seminar_id` = `studygroup_courses`.`course_id`) WHERE `studygroup_id` = ? ORDER BY `seminare`.`name` ASC"; + } else { + $sql = "INNER JOIN `seminare` ON (`seminare`.`Seminar_id` = `studygroup_courses`.`studygroup_id`) WHERE `course_id` = ? ORDER BY `seminare`.`name` ASC "; + } + $this->connections = StudygroupCourse::findBySQL($sql, [$range_id]); + } + + } diff --git a/app/controllers/course/wizard.php b/app/controllers/course/wizard.php index b4244b61bea..ad289edc5c3 100644 --- a/app/controllers/course/wizard.php +++ b/app/controllers/course/wizard.php @@ -64,7 +64,7 @@ class Course_WizardController extends AuthenticatedController */ public function index_action() { - $this->redirect('course/wizard/step/0' . ($this->studygroup ? '?studygroup=1' : '')); + $this->redirect('course/wizard/step/0' . ($this->studygroup ? '?studygroup=1&stgteil_id='.Request::option('stgteil_id') : '')); } /** @@ -93,7 +93,10 @@ class Course_WizardController extends AuthenticatedController // Add special studygroup flag to set values. $this->setStepValues( get_class($step), - array_merge($this->getValues(get_class($step)), ['studygroup' => 1]) + array_merge($this->getValues(get_class($step)), [ + 'studygroup' => 1, + 'stgteil_id' => Request::option('stgteil_id') + ]) ); } $this->values = $this->getValues(); @@ -193,11 +196,13 @@ class Course_WizardController extends AuthenticatedController } // A studygroup has been created. if (in_array($this->course->status, studygroup_sem_types())) { + $message = MessageBox::success(sprintf( - _('Die Studien-/Arbeitsgruppe "%s" wurde angelegt. ' + _('Die Studien-/Arbeitsgruppe „%s“ wurde angelegt. ' . 'Sie können sie direkt hier weiter verwalten.'), htmlReady($this->course->name) )); + $target = $this->url_for('course/studygroup/edit', ['cid' => $this->course->id]); // "Normal" course. diff --git a/app/controllers/my_courses.php b/app/controllers/my_courses.php index c7a6771aa6f..a8d9797eedb 100644 --- a/app/controllers/my_courses.php +++ b/app/controllers/my_courses.php @@ -704,17 +704,19 @@ class MyCoursesController extends AuthenticatedController } } - $groups[] = [ - 'id' => $_outer_index, - 'name' => (string) $sem_data[$_outer_index]['name'], - 'data' => [ - [ - 'id' => md5($_outer_index), - 'label' => false, - 'ids' => array_keys($_courses), + if ($_outer_index) { + $groups[] = [ + 'id' => $_outer_index, + 'name' => (string)$sem_data[$_outer_index]['name'], + 'data' => [ + [ + 'id' => md5($_outer_index), + 'label' => false, + 'ids' => array_keys($_courses), + ], ], - ], - ]; + ]; + } $temp_courses = array_merge($temp_courses, $_courses); } else { $count = 1; @@ -747,11 +749,13 @@ class MyCoursesController extends AuthenticatedController $temp_courses = array_merge($temp_courses, $_courses); } - $groups[] = [ - 'id' => $_outer_index, - 'name' => (string) $sem_data[$_outer_index]['name'], - 'data' => $_groups, - ]; + if ($_outer_index) { + $groups[] = [ + 'id' => $_outer_index, + 'name' => (string)$sem_data[$_outer_index]['name'], + 'data' => $_groups, + ]; + } } } } diff --git a/app/controllers/my_studygroups.php b/app/controllers/my_studygroups.php index 579d47ecc4d..ac55cf40245 100644 --- a/app/controllers/my_studygroups.php +++ b/app/controllers/my_studygroups.php @@ -10,15 +10,28 @@ class MyStudygroupsController extends AuthenticatedController } } - public function index_action() + public function index_action($is_widget = false) { PageLayout::setHelpKeyword('Basis.MeineStudiengruppen'); PageLayout::setTitle(_('Meine Studiengruppen')); URLHelper::removeLinkParam('cid'); - $this->studygroups = MyRealmModel::getStudygroups(); + $this->is_widget = (bool)$is_widget; + $this->studygroups = StudygroupModel::getStudygroups(); $this->nav_elements = MyRealmModel::calc_single_navigation($this->studygroups); - $this->set_sidebar(); + + // do not render sidebar if this is the widget + if (!$this->is_widget) { + $this->set_sidebar(); + } + } + + public function proposals_action() + { + PageLayout::setHelpKeyword('Basis.MeineStudiengruppen'); + PageLayout::setTitle(_('Meine Studiengruppen')); + URLHelper::removeLinkParam('cid'); + $this->proposed_studygroups = $this->proposeStudygroups(); } public function set_sidebar() @@ -44,4 +57,85 @@ 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']), + function ($sem_type_id) { + return (bool) $GLOBALS['SEM_CLASS'][$GLOBALS['SEM_TYPE'][$sem_type_id]['class']]['studygroup_mode']; + } + ); + + $statement = DBManager::get()->prepare(" + SELECT `Seminar_id` FROM ( + SELECT `seminare`.`Seminar_id`, COUNT(`seminar_user`.`user_id`) AS `count_colleages` + FROM `seminar_user` AS `my_courses` + LEFT JOIN `seminar_user` AS `my_colleages` ON (`my_colleages`.`Seminar_id` = `my_courses`.`Seminar_id`) + LEFT JOIN `seminar_user` ON (`my_colleages`.`user_id` = `seminar_user`.`user_id`) + LEFT JOIN `seminar_user` AS `am_i_connected` ON (`seminar_user`.`Seminar_id` = `am_i_connected`.`Seminar_id` AND `am_i_connected`.`user_id` = :me) + LEFT JOIN `seminare` ON (`seminare`.`Seminar_id` = `seminar_user`.`Seminar_id`) + WHERE `seminare`.`status` IN (:studygroup_types) + AND `am_i_connected`.`user_id` IS NULL + AND `my_courses`.`user_id` = :me + GROUP BY `seminare`.`seminar_id` + ORDER BY `count_colleages` DESC + LIMIT 12 + ) AS `colleages_groups` + + UNION SELECT `Seminar_id` FROM ( + SELECT `seminare`.`Seminar_id` + FROM `seminare` + LEFT JOIN `seminar_user` AS `am_i_connected` ON (`am_i_connected`.`Seminar_id` = `seminare`.`Seminar_id` AND `am_i_connected`.`user_id` = :me) + INNER JOIN `studygroup_stgteil` ON (`studygroup_stgteil`.`studygroup_id` = `seminare`.`Seminar_id`) + INNER JOIN `mvv_stgteil` ON (`studygroup_stgteil`.`stgteil_id` = `mvv_stgteil`.`stgteil_id`) + INNER JOIN `user_studiengang` ON (`user_studiengang`.`fach_id` = `mvv_stgteil`.`fach_id`) + INNER JOIN `mvv_stg_stgteil` ON (`mvv_stg_stgteil`.`stgteil_id` = `mvv_stgteil`.`stgteil_id`) + INNER JOIN `mvv_studiengang` ON (`mvv_studiengang`.`studiengang_id` = `mvv_stg_stgteil`.`studiengang_id` + AND `mvv_studiengang`.`abschluss_id` = `user_studiengang`.`abschluss_id`) + WHERE `am_i_connected`.`user_id` IS NULL + AND `seminare`.`status` IN (:studygroup_types) + AND `user_studiengang`.`user_id` = :me + ORDER BY rand() + LIMIT 12 + ) AS `same_studyarea_groups` + + UNION SELECT `Seminar_id` FROM ( + SELECT `seminare`.`Seminar_id` + FROM `seminare` + LEFT JOIN `seminar_user` AS `am_i_connected` ON (`am_i_connected`.`Seminar_id` = `seminare`.`Seminar_id` AND `am_i_connected`.`user_id` = :me) + WHERE `am_i_connected`.`user_id` IS NULL + AND `seminare`.`status` IN (:studygroup_types) + ORDER BY `seminare`.`mkdate` DESC + LIMIT 12 + ) AS `new_groups` + + GROUP BY `Seminar_id` + ORDER BY rand() + LIMIT :amount + "); + $statement->execute([ + 'studygroup_types' => $studygroup_sem_types, + 'me' => $user_id, + 'amount' => $amount + ]); + $group_ids = $statement->fetchAll(PDO::FETCH_COLUMN); + $cache->write($cache_id, $group_ids, 15 * 60); + return Course::findMany($group_ids); + } } diff --git a/app/controllers/search/angebot.php b/app/controllers/search/angebot.php index 0c0f402f866..4f749d4e2a4 100644 --- a/app/controllers/search/angebot.php +++ b/app/controllers/search/angebot.php @@ -179,4 +179,16 @@ class Search_AngebotController extends MVVController $this->content = $response->body; $this->render_template('shared/content', $this->layout); } + + public function remove_studygroup_action($course_id, $stgteil_id) + { + CSRFProtection::verifyUnsafeRequest(); + if (!$GLOBALS['perm']->have_studip_perm('tutor', $course_id) && !$GLOBALS['perm']->have_perm('admin')) { + throw new AccessDeniedException(); + } + StudygroupStgteil::deleteBySQL('`studygroup_id` = ? AND `stgteil_id` = ?', [$course_id, $stgteil_id]); + PageLayout::postSuccess(_('Zuordnung wurde aufgehoben.')); + $stgteil = StudiengangTeil::find($stgteil_id); + $this->redirect('search/angebot/studiengang/'.$stgteil->studiengang[0]->id); + } } diff --git a/app/controllers/search/globalsearch.php b/app/controllers/search/globalsearch.php index b31663f7ed7..fd82103901b 100644 --- a/app/controllers/search/globalsearch.php +++ b/app/controllers/search/globalsearch.php @@ -107,6 +107,17 @@ class Search_GlobalsearchController extends AuthenticatedController ), 'institute_filter' ); + + $filter_widget->addElement( + new SelectListElement( + _('Studiengang'), + 'study_course', + $this->getStudyCourses(), + '', + ['id' => 'study_course_select'] + ), + 'study_course_filter' + ); } /** @@ -170,6 +181,23 @@ class Search_GlobalsearchController extends AuthenticatedController return $sem_classes; } + /** + * @return array + */ + private function getStudyCourses() + { + + $this->user = User::findCurrent(); + $study_courses = []; + + foreach ($this->user->studycourses as $usc) + { + $study_courses[] = $usc->studycourse->name; + } + + return $study_courses; + } + /** * Add some information on how to use the search. */ diff --git a/app/controllers/search/studiengaenge.php b/app/controllers/search/studiengaenge.php index 8f08c329967..1745ba61a7a 100644 --- a/app/controllers/search/studiengaenge.php +++ b/app/controllers/search/studiengaenge.php @@ -200,12 +200,12 @@ class Search_StudiengaengeController extends MVVController $this->with_courses = Request::option('with_courses', $_SESSION['MVV_SEARCH_SEQUENCE_WITH_COURSES'] ?? null); $_SESSION['MVV_SEARCH_SEQUENCE_WITH_COURSES'] = $this->with_courses; - $studiengangTeil = StudiengangTeil::find($stgteil_id); + $this->studiengangTeil = StudiengangTeil::find($stgteil_id); $versionen = StgteilVersion::findByStgteil($stgteil_id, 'start', 'DESC')->filter(function ($version) { $public = $GLOBALS['MVV_STGTEILVERSION']['STATUS']['values'][$version->stat]['public']; return (bool) $public; }); - if (!$studiengangTeil || count($versionen) === 0) { + if (!$this->studiengangTeil || count($versionen) === 0) { PageLayout::postInfo(_('Kein Verlaufsplan im gewählten Bereich verfügbar.')); } else { $version_id = Request::option('version', $this->sessGet('selected_version')); @@ -307,26 +307,24 @@ class Search_StudiengaengeController extends MVVController if ($studiengang_id) { if ($stgteil_bez_id) { $this->stgTeilBez = StgteilBezeichnung::get($stgteil_bez_id); - $this->breadcrumb->append([$this->stgTeilBez, $studiengangTeil], 'verlauf'); + $this->breadcrumb->append([$this->stgTeilBez, $this->studiengangTeil], 'verlauf'); } else { - $this->breadcrumb->append($studiengangTeil, 'verlauf'); + $this->breadcrumb->append($this->studiengangTeil, 'verlauf'); } $this->studiengang = Studiengang::get($studiengang_id); } $this->setVersionSelectWidget( $versionen, - $this->action_url('verlauf', $studiengangTeil->id, $stgteil_bez_id, $studiengang_id) + $this->action_url('verlauf', $this->studiengangTeil->id, $stgteil_bez_id, $studiengang_id) ); ksort($fachsemesterData); $this->fachsemesterData = $fachsemesterData; $this->abschnitteData = $abschnitteData; $this->versionen = $versionen; - // Augsburg // Ausgabe des Namens ohne Fach (dieses ist im Zusatz bereits enthalten) - // $this->studiengangTeilName = $studiengangTeil->getDisplayName(0); - $this->studiengangTeilName = $studiengangTeil->getDisplayName(); + $this->studiengangTeilName = $this->studiengangTeil->getDisplayName(); // add option widget to show only modules with courses in the // selected semester diff --git a/app/views/admin/tags/index.php b/app/views/admin/tags/index.php new file mode 100644 index 00000000000..4d5067af5e6 --- /dev/null +++ b/app/views/admin/tags/index.php @@ -0,0 +1,65 @@ +<?php +/** + * @var Admin_TagsController $controller + * @var Tag[] $tags + * @var integer $all_tags + * @var integer $page + * */ +?> +<table class="default"> + <caption> + <?= _('Schlagwörter') ?> + <span class="actions"> + <?= sprintf(_('%s Schlagwörter'), $all_tags) ?> + </span> + </caption> + <thead> + <tr> + <th><?= _('Schlagwort') ?></th> + <th><?= _('Verknüpfte Objekte') ?></th> + <th><?= _('Aktiv') ?></th> + <th class="actions"> + <?= _('Aktion') ?> + </th> + </tr> + </thead> + <tbody> + <? foreach ($tags as $tag) : ?> + <tr> + <td> + <?= htmlReady($tag['name']) ?> + </td> + <td> + <a href="<?= URLHelper::getLink('dispatch.php/admin/tags/view_objects/'.$tag->id) ?>" data-dialog> + <?= TagRelation::countBySql('`tag_id` = ?', [$tag->id]) ?> + </a> + </td> + <td> + <?= $tag['active'] + ? Icon::create('checkbox-checked', Icon::ROLE_INFO) + : Icon::create('checkbox-unchecked', Icon::ROLE_INFO) ?> + </td> + <td class="actions"> + <a href="<?= $controller->edit($tag) ?>" data-dialog> + <?= Icon::create('edit') ?> + </a> + </td> + </tr> + <? endforeach ?> + <? if (count($tags) === 0) : ?> + <tr> + <td colspan="2"> + <?= _('Noch keine Schlagwörter vorhanden.') ?> + </td> + </tr> + <? endif ?> + </tbody> + + <tfoot> + <tr> + <td colspan="4" class="actions"> + <?= Pagination::create($all_tags, $page)->asLinks() ?> + </td> + </tr> + </tfoot> +</table> diff --git a/app/views/admin/tags/view_objects.php b/app/views/admin/tags/view_objects.php new file mode 100644 index 00000000000..0d90f661a6b --- /dev/null +++ b/app/views/admin/tags/view_objects.php @@ -0,0 +1,51 @@ +<?php +/** + * @var Admin_TagsController $controller + * @var Tag $tag + * */ +?> +<table class="default"> + <thead> + <tr> + <th><?= _('Objekt') ?></th> + <th><?= _('Typ') ?></th> + <th></th> + </tr> + </thead> + <tbody> + <? foreach ($tag->related_objects as $relation) : ?> + <tr> + <td> + <? + switch ($relation->range_type) { + case 'course': + $course = Course::find($relation->range_id); + if ($course) { + echo '<a href="'.URLHelper::getLink($course->isStudygroup() ? 'dispatch.php/course/studygroup/details/' . $relation->range_id : 'dispatch.php/course/details/index/' . $relation->range_id) . '">'; + echo htmlReady($course->getFullName()); + echo '</a>'; + } else { + echo $relation->range_id; + } + break; + default: + echo $relation->range_id; + break; + } + ?> + </td> + <td><? + switch ($relation->range_type) { + case 'course': + echo _('Veranstaltung'); + break; + default: + echo $relation->range_type; + break; + } + ?></td> + <td></td> + </tr> + <? endforeach ?> + </tbody> +</table> diff --git a/app/views/course/connectedcourses/_course_to_connect.php b/app/views/course/connectedcourses/_course_to_connect.php new file mode 100644 index 00000000000..023578546c2 --- /dev/null +++ b/app/views/course/connectedcourses/_course_to_connect.php @@ -0,0 +1,21 @@ +<tr> + <td> + <?= CourseAvatar::getAvatar($course->id)->getImageTag(Avatar::SMALL) ?> + <?= htmlReady($course->getFullName()) ?> + </td> + <td> + <? if ($course->start_semester) : ?> + <?= htmlReady($course->start_semester->name) ?> + <? if ($course->end_semester && $course->end_semester->id !== $course->start_semester->id) : ?> + - <?= htmlReady($course->end_semester->name) ?> + <? endif ?> + <? endif ?> + </td> + <td class="actions"> + <?= Icon::create('add')->asInput([ + 'title' => _('Verknüpfung mit dieser Veranstaltung vorschlagen.'), + 'formaction' => $controller->connectURL($course->id), + 'formmethod' => 'post' + ]) ?> + </td> +</tr> diff --git a/app/views/course/connectedcourses/connect.php b/app/views/course/connectedcourses/connect.php new file mode 100644 index 00000000000..4c6a2e54a7a --- /dev/null +++ b/app/views/course/connectedcourses/connect.php @@ -0,0 +1,78 @@ +<form method="get" + action="<?= $controller->link_for('course/connectedcourses/connect', ['search' => 1]) ?>"> + <?= CSRFProtection::tokenTag() ?> + <table class="default" style="margin-top: 20px;"> + <caption> + <?= _('Lehrveranstaltungen') ?> + <span class="actions"> + <? if (Request::get('search')) : ?> + <select name="semester_id" aria-label="<?= _('Filtern Sie optional nach einem Semester') ?>"> + <option value=""><?= _('In Semester') ?></option> + <? foreach (array_reverse(Semester::getAll()) as $semester) : ?> + <option value="<?= htmlReady($semester->id) ?>"<?= $semester->id === Request::option('semester_id') ? ' selected' : '' ?>> + <?= htmlReady($semester->name) ?> + </option> + <? endforeach ?> + </select> + + <input type="text" + name="search" + id="search_connectable_courses" + autofocus + placeholder="<?= _('Veranstaltung suchen ...') ?>" + value="<?= htmlReady(Request::get('search') != 1 ? Request::get('search') : '') ?>"> + <?= Icon::create('search')->asInput([ + 'title' => _('Suchen Sie nach beliebigen Veranstaltungen'), + 'data-dialog' => 1 + ]) ?> + <a href="<?= $controller->connect() ?>" data-dialog title="<?= _('Suche schließen') ?>"> + <?= Icon::create('decline') ?> + </a> + <? else : ?> + <?= Icon::create('search')->asInput([ + 'title' => _('Suchen Sie nach beliebigen Veranstaltungen'), + 'data-dialog' => 1, + 'formaction' => $controller->connectURL(['search' => 1]) + ]) ?> + <? endif ?> + </span> + </caption> + <thead> + <tr> + <th><?= _('Name') ?></th> + <th><?= _('Semester') ?></th> + <th class="actions"><?= _('Aktion') ?></th> + </tr> + </thead> + <tbody> + <? if (!Request::get('search') || Request::get('search') == 1) : ?> + <? if (count($my_courses) + count($suggestions) > 0) : ?> + <? foreach ($my_courses as $my_course) : ?> + <?= $this->render_partial('course/connectedcourses/_course_to_connect', ['course' => $my_course]) ?> + <? endforeach ?> + <? foreach ($suggestions as $suggested_course) : ?> + <?= $this->render_partial('course/connectedcourses/_course_to_connect', ['course' => $suggested_course]) ?> + <? endforeach ?> + <? else : ?> + <tr> + <td colspan="3"> + <?= _('Suchen Sie nach Veranstaltungen.') ?> + </td> + </tr> + <? endif ?> + <? else : ?> + <? if (isset($searchresults) && count($searchresults)) : ?> + <? foreach ($searchresults as $course) : ?> + <?= $this->render_partial('course/connectedcourses/_course_to_connect', ['course' => $course]) ?> + <? endforeach ?> + <? else : ?> + <tr> + <td colspan="3"> + <?= _('Keine passenden Ergebnisse gefunden.') ?> + </td> + </tr> + <? endif ?> + <? endif ?> + </tbody> + </table> +</form> diff --git a/app/views/course/connectedcourses/index.php b/app/views/course/connectedcourses/index.php new file mode 100644 index 00000000000..fa8006bae9c --- /dev/null +++ b/app/views/course/connectedcourses/index.php @@ -0,0 +1,112 @@ +<? if (count($connected) + count($proposals) > 0) : ?> + <? if (count($connected) > 0) : ?> + <form method="post"> + <?= CSRFProtection::tokenTag() ?> + <table class="default"> + <caption> + <?= _('Verknüpfte Veranstaltungen') ?> + <thead> + <tr> + <th><?= _('Name') ?></th> + <th class="actions"><?= _('Aktion') ?></th> + </tr> + </thead> + <tbody> + <? foreach ($connected as $connection) : ?> + <tr> + <td> + <a href="<?= URLHelper::getLink('dispatch.php/course/details/' . $connection['course_id']) ?>" target="_blank"> + <?= CourseAvatar::getAvatar($connection['course_id'])->getImageTag(Avatar::SMALL) ?> + <?= htmlReady($connection->course->getFullName()) ?> + </a> + </td> + <td class="actions"> + <?= Icon::create('trash')->asInput([ + 'title' => _('Verknüpfung aufheben'), + 'data-confirm' => _('Wirklich die Zuweisung zu der Veranstaltung aufheben?'), + 'formaction' => $controller->url_for('course/connectedcourses/remove/'.$connection['course_id']) + ]) ?> + </td> + </tr> + <? endforeach ?> + </tbody> + </caption> + </table> + </form> + <? endif ?> + + <? if (count($proposals) > 0) : ?> + <form method="post"> + <?= CSRFProtection::tokenTag() ?> + <table class="default"> + <caption> + <?= _('Eingereichte Vorschläge') ?> + </caption> + <thead> + <tr> + <th><?= _('Name') ?></th> + <th><?= _('Vorgeschlagen von') ?></th> + <th class="actions"><?= _('Aktion') ?></th> + </tr> + </thead> + <tbody> + <? foreach ($proposals as $proposal) : ?> + <tr> + <td> + <a href="<?= URLHelper::getLink('dispatch.php/course/details/' . $connection['course_id']) ?>" target="_blank"> + <?= CourseAvatar::getAvatar($proposal['course_id'])->getImageTag(Avatar::SMALL) ?> + <?= htmlReady($proposal->course->getFullName()) ?> + </a> + </td> + <td> + <?= htmlReady($proposal->user->getFullName()) ?> + </td> + <td class="actions"> + <? if ($proposal['proposed_from'] === 'course') : ?> + <?= Icon::create('accept')->asInput([ + 'title' => _('Vorschlag annehmen'), + 'data-confirm' => _('Wirklich die Veranstaltung mit dieser Studiengruppe verknüpfen?'), + 'formaction' => $controller->connectURL($proposal['course_id']) + ]) ?> + <? endif ?> + <? if ($proposal['proposed_from'] === 'course') : ?> + <?= Icon::create('decline')->asInput([ + 'title' => _('Vorschlag ablehnen'), + 'data-confirm' => _('Wirklich den Vorschlag ablehnen?'), + 'formaction' => $controller->declineURL($proposal->id) + ]) ?> + <? else : ?> + <?= Icon::create('decline')->asInput([ + 'title' => _('Vorschlag zurückziehen'), + 'data-confirm' => _('Wirklich den Vorschlag zurückziehen?'), + 'formaction' => $controller->declineURL($proposal->id) + ]) ?> + <? endif ?> + </td> + </tr> + <? endforeach ?> + </tbody> + </table> + </form> + <? endif ?> + +<? else : ?> + + <div class="studip-contents-overview-teaser"> + <div class="teaser-content"> + + <div> + <header><?= _('Verknüpfung zu Lehrveranstaltungen') ?></header> + <?= _('Verknüpfen Sie diese Studiengruppen mit Lehrveranstaltungen, mit deren Inhalten sich diese Studiengruppe beschäftigt. Dadurch machen Sie diese Studiengruppe sichtbarer für andere Teilnehmende der Veranstaltung.') ?> + </div> + + <?= \Studip\LinkButton::create( + _('Verknüpfung zu Lehrveranstaltung vorschlagen'), + $controller->connect(), + ['data-dialog' => 1] + )?> + </div> + </div> + +<? endif ?> + diff --git a/app/views/course/connectedstudygroups/_studygroup_to_connect.php b/app/views/course/connectedstudygroups/_studygroup_to_connect.php new file mode 100644 index 00000000000..d438cf1eafa --- /dev/null +++ b/app/views/course/connectedstudygroups/_studygroup_to_connect.php @@ -0,0 +1,21 @@ +<tr> + <td> + <?= StudygroupAvatar::getAvatar($course->id)->getImageTag(Avatar::SMALL) ?> + <?= htmlReady($course->getFullName()) ?> + </td> + <td> + <? if ($course->start_semester) : ?> + <?= htmlReady($course->start_semester->name) ?> + <? if ($course->end_semester && $course->end_semester->id !== $course->start_semester->id) : ?> + - <?= htmlReady($course->end_semester->name) ?> + <? endif ?> + <? endif ?> + </td> + <td class="actions"> + <?= Icon::create('add')->asInput([ + 'title' => _('Verknüpfung mit dieser Studiengruppe vorschlagen.'), + 'formaction' => $controller->connectURL($course->id), + 'formmethod' => 'post' + ]) ?> + </td> +</tr> diff --git a/app/views/course/connectedstudygroups/connect.php b/app/views/course/connectedstudygroups/connect.php new file mode 100644 index 00000000000..9f7c0948f10 --- /dev/null +++ b/app/views/course/connectedstudygroups/connect.php @@ -0,0 +1,79 @@ +<form method="get" action="<?= $controller->connect(['search' => 1]) ?>"> + <?= CSRFProtection::tokenTag() ?> + <table class="default" style="margin-top: 20px;"> + <caption> + <?= _('Studiengruppen') ?> + <span class="actions"> + <? if (Request::get('search') || Request::get('semester_id')) : ?> + <select name="semester_id" aria-label="<?= _('Filtern Sie optional nach einem Semester') ?>"> + <option value=""><?= _('In Semester') ?></option> + <? foreach (array_reverse(Semester::getAll()) as $semester) : ?> + <option value="<?= htmlReady($semester->id) ?>"<?= $semester->id === Request::option('semester_id') ? ' selected' : '' ?>><?= htmlReady($semester->name) ?></option> + <? endforeach ?> + </select> + + <input type="text" + name="search" + id="search_connectable_courses" + autofocus + placeholder="<?= _('Veranstaltung suchen ...') ?>" + value="<?= htmlReady(Request::get('search') != 1 ? Request::get('search') : '') ?>"> + <?= Icon::create('search')->asInput([ + 'title' => _('Suchen Sie nach beliebigen Veranstaltungen'), + 'data-dialog' => 1 + ]) ?> + <a href="<?= $controller->connect() ?>" data-dialog title="<?= _('Suche schließen') ?>"> + <?= Icon::create('decline') ?> + </a> + <? else : ?> + <form action="<?= $controller->connect(['search' => 1]) ?>" + method="get" + class="default" + data-dialog> + <?= Icon::create('search')->asInput([ + 'title' => _('Suchen Sie nach beliebiger Studiengruppe'), + 'data-dialog' => 1 + ]) ?> + </form> + <? endif ?> + </span> + </caption> + <thead> + <tr> + <th><?= _('Name') ?></th> + <th><?= _('Semester') ?></th> + <th class="actions"><?= _('Aktion') ?></th> + </tr> + </thead> + <tbody> + <? if (!Request::get('search') || Request::get('search') == 1) : ?> + <? if (count($my_studygroups) + count($suggestions) > 0) : ?> + <? foreach ($my_studygroups as $my_course) : ?> + <?= $this->render_partial('course/connectedstudygroups/_studygroup_to_connect', ['course' => $my_course]) ?> + <? endforeach ?> + <? foreach ($suggestions as $suggested_course) : ?> + <?= $this->render_partial('course/connectedstudygroups/_studygroup_to_connect', ['course' => $suggested_course]) ?> + <? endforeach ?> + <? else : ?> + <tr> + <td colspan="3"> + <?= _('Suchen Sie nach Studiengruppen.') ?> + </td> + </tr> + <? endif ?> + <? else : ?> + <? if (isset($searchresults) && count($searchresults)) : ?> + <? foreach ($searchresults as $course) : ?> + <?= $this->render_partial('course/connectedstudygroups/_studygroup_to_connect', ['course' => $course]) ?> + <? endforeach ?> + <? else : ?> + <tr> + <td colspan="3"> + <?= _('Keine passenden Ergebnisse gefunden.') ?> + </td> + </tr> + <? endif ?> + <? endif ?> + </tbody> + </table> +</form> diff --git a/app/views/course/connectedstudygroups/index.php b/app/views/course/connectedstudygroups/index.php new file mode 100644 index 00000000000..4611b6cbe28 --- /dev/null +++ b/app/views/course/connectedstudygroups/index.php @@ -0,0 +1,110 @@ +<? if (count($connected) + count($proposals) > 0) : ?> + <? if (count($connected) > 0) : ?> + <form method="post"> + <table class="default"> + <caption> + <?= _('Verknüpfte Studiengruppen') ?> + <thead> + <tr> + <th><?= _('Name') ?></th> + <th class="actions"><?= _('Aktion') ?></th> + </tr> + </thead> + <tbody> + <? foreach ($connected as $connection) : ?> + <tr> + <td> + <a href="<?= URLHelper::getLink('dispatch.php/course/studygroup/details/' . $connection['studygroup_id'], [], true) ?>"> + <?= StudygroupAvatar::getAvatar($connection['studygroup_id'])->getImageTag(Avatar::SMALL) ?> + <?= htmlReady($connection->studygroup->getFullName()) ?> + </a> + </td> + <td class="actions"> + <?= CSRFProtection::tokenTag() ?> + <?= Icon::create('trash')->asInput([ + 'title' => _('Verknüpfung aufheben'), + 'data-confirm' => _('Wirklich die Zuweisung zu der Studiengruppe aufheben?'), + 'formaction' => $controller->removeURL($connection['studygroup_id']) + ]) ?> + </td> + </tr> + <? endforeach ?> + </tbody> + </caption> + </table> + </form> + <? endif ?> + + <? if (count($proposals) > 0) : ?> + <form method="post"> + <table class="default"> + <?= CSRFProtection::tokenTag() ?> + <caption> + <?= _('Eingereichte Vorschläge') ?> + </caption> + <thead> + <tr> + <th><?= _('Name') ?></th> + <th><?= _('Vorgeschlagen von') ?></th> + <th class="actions"><?= _('Aktion') ?></th> + </tr> + </thead> + <tbody> + <? foreach ($proposals as $proposal) : ?> + <tr> + <td> + <a href="<?= URLHelper::getLink('dispatch.php/course/studygroup/details/' . $proposal['studygroup_id']) ?>" target="_blank"> + <?= StudygroupAvatar::getAvatar($proposal['studygroup_id'])->getImageTag(Avatar::SMALL) ?> + <?= htmlReady($proposal->studygroup->getFullName()) ?> + </a> + </td> + <td> + <?= htmlReady($proposal->user->getFullName()) ?> + </td> + <td class="actions"> + <? if ($proposal['proposed_from'] === 'studygroup') : ?> + <?= Icon::create('accept')->asInput([ + 'title' => _('Vorschlag annehmen'), + 'data-confirm' => _('Wirklich die Studiengruppe mit dieser Veranstaltung verknüpfen?'), + 'formaction' => $controller->connectURL($proposal['studygroup_id']) + ]) ?> + <? endif ?> + <? if ($proposal['proposed_from'] === 'studycourse') : ?> + <?= Icon::create('decline')->asInput([ + 'title' => _('Vorschlag ablehnen'), + 'data-confirm' => _('Wirklich den Vorschlag ablehnen?'), + 'formaction' => $controller->declineURL($proposal->id) + ]) ?> + <? else : ?> + <?= Icon::create('decline')->asInput([ + 'title' => _('Vorschlag zurückziehen'), + 'data-confirm' => _('Wirklich den Vorschlag zurückziehen?'), + 'formaction' => $controller->declineURL($proposal->id) + ]) ?> + <? endif ?> + </td> + </tr> + <? endforeach ?> + </tbody> + </table> + </form> + <? endif ?> +<? else : ?> + + <div class="studip-contents-overview-teaser"> + <div class="teaser-content"> + + <div> + <header><?= _('Verknüpfung zu Studiengruppen') ?></header> + <?= _('Verknüpfen Sie Studiengruppen, die sich mit den Inhalten dieser Veranstaltung beschäftigen.') ?> + </div> + + <?= \Studip\LinkButton::create( + _('Verknüpfung zu Studiengruppe vorschlagen'), + $controller->connect(), + ['data-dialog' => 1] + )?> + </div> + </div> + +<? endif ?> diff --git a/app/views/course/overview/index.php b/app/views/course/overview/index.php index e538a6f3605..03fc848001e 100644 --- a/app/views/course/overview/index.php +++ b/app/views/course/overview/index.php @@ -68,6 +68,10 @@ if (!empty($questionnaires)) { echo $questionnaires; } +if (!empty($connectedstudygroups)) { + echo $connectedstudygroups; +} + // display plugins if (!empty($plugins)) { diff --git a/app/views/course/studygroup/details.php b/app/views/course/studygroup/details.php index 26a3ef5bcbc..7fbbc39dd78 100644 --- a/app/views/course/studygroup/details.php +++ b/app/views/course/studygroup/details.php @@ -12,6 +12,10 @@ <dl style="margin: 0"> <dt><?= _('Name der Studiengruppe') ?></dt> <dd><?= htmlReady($studygroup->name) ?></dd> + <? if ((string) $studygroup->Beschreibung): ?> + <dt><?= _('Beschreibung') ?></dt> + <dd><?= formatLinks($studygroup->Beschreibung) ?></dd> + <? endif; ?> <? if ((string) $studygroup->beschreibung): ?> <dt><?= _('Beschreibung') ?></dt> @@ -33,6 +37,21 @@ </section> </article> +<? if (count($studygroup->tags) > 0) : ?> +<article class="studip"> + <header> + <h1><?= _('Schlagwörter') ?></h1> + </header> + <section> + <? foreach ($studygroup->tags as $tag) : ?> + <a href="<?= URLHelper::getLink('dispatch.php/studygroup/browse', ['q' => $tag['name']]) ?>"> + <?= htmlReady('#'.$tag['name']) ?> + </a> + <? endforeach ?> + </section> +</article> +<? endif ?> + <div class="hidden-medium-up"> <? foreach ($sidebarActions as $action) : ?> <?= Studip\LinkButton::create($action->label, $action->url) ?> diff --git a/app/views/course/studygroup/edit.php b/app/views/course/studygroup/edit.php index e3189f746e5..3891e9446ce 100644 --- a/app/views/course/studygroup/edit.php +++ b/app/views/course/studygroup/edit.php @@ -4,7 +4,7 @@ <?= CSRFProtection::tokenTag() ?> <fieldset> <legend> - <?= _('Studiengruppe bearbeiten') ?> + <?= _('Grunddaten') ?> </legend> <input type='submit' class="invisible" name="<?=_('Änderungen übernehmen') ?>" aria-hidden="true"> @@ -35,12 +35,37 @@ <option value="invisible" <? if (!$course->visible) echo 'selected'; ?> <? if (!Config::get()->STUDYGROUPS_INVISIBLE_ALLOWED) echo 'disabled'; ?>> <?= _('Unsichtbar') ?> </option> + <? endif; ?> + <? if (true) : ?> + <? $courseset = CourseSet::getSetForCourse($sem_id) ?> + <option value="top-course"<?= $courseset && $courseset->getId() === CourseSet::getConnectedcourseAdmissionSetId() ? ' selected' : '' ?>> + <?= _('Für Mitglieder der zugehörigen Lehrveranstaltung') ?> + </option> + <? endif ?> + <? if (true) : ?> + <? $courseset = CourseSet::getSetForCourse($sem_id) ?> + <option value="top-course"<?= $courseset && $courseset->getId() === CourseSet::getConnectedcourseAdmissionSetId() ? ' selected' : '' ?>> + <?= _('Für Mitglieder der zugehörigen Lehrveranstaltung') ?> + </option> <? endif ?> </select> </label> </fieldset> + <fieldset> + <legend><?= _('Erweiterte Einstellungen') ?></legend> + + <label> + <?= _('Ablaufdatum') ?> + <input type="text" name="expiration_date"> + </label> + + <label> + <?= _('Schlagwörter') ?> + </label> + </fieldset> + <footer> <?= Studip\Button::createAccept(_('Übernehmen'), ['title' => _("Änderungen übernehmen")]); ?> <?= Studip\LinkButton::createCancel(_('Abbrechen'), URLHelper::getURL('dispatch.php/course/go')); ?> diff --git a/app/views/course/studygroup/widget.php b/app/views/course/studygroup/widget.php new file mode 100644 index 00000000000..fc0f133ea96 --- /dev/null +++ b/app/views/course/studygroup/widget.php @@ -0,0 +1,88 @@ +<article class="studip connectedcourses_widget"> + <header> + <h1> + + <? if ($course->isStudygroup()) : ?> + <?= Icon::create('seminar', Icon::ROLE_INFO)->asimg(['class' => "text-bottom"]) ?> + <?= _('Zugehörige Veranstaltung') ?> + <? else : ?> + <?= Icon::create('studygroup', Icon::ROLE_INFO)->asimg(['class' => "text-bottom"]) ?> + <?= _('Verknüpfte Studiengruppen') ?> + <? endif ?> + </h1> + + </header> + + <section> + <? if ($course->isStudygroup()) : ?> + <ul> + <? foreach ($connections as $connection) : ?> + <li> + <? $link = $connection->course->isAccessibleToUser() + ? URLHelper::getLink('seminar_main.php', ['auswahl' => $connection->course->id]) + : URLHelper::getLink('dispatch.php/course/details', ['cid' => $connection->course->id]) ?> + <a href="<?= $link ?>"> + <?= htmlReady($connection->course->getFullname()) ?> + </a> + </li> + <? endforeach ?> + </ul> + <? else : ?> + <table class="default"> + <colgroup> + <col style="width: 60px;"> + </colgroup> + <thead> + <tr> + <th><?= _('Avatar') ?></th> + <th><?= _('Name / Beschreibung') ?></th> + <th><?= _('Mitglieder') ?></th> + <th><?= _('Gründer:in') ?></th> + </tr> + </thead> + <tbody> + <? foreach ($connections as $connection) : ?> + <tr> + <td> + <? $link = $connection->studygroup->isAccessibleToUser() + ? URLHelper::getLink('seminar_main.php', ['auswahl' => $connection->studygroup->id]) + : URLHelper::getLink('dispatch.php/course/studygroup/details/'.$connection->studygroup->id) ?> + <a href="<?= $link ?>"> + <?= CourseAvatar::getAvatar($connection->studygroup->id)->getImageTag(Avatar::SMALL) ?> + </a> + </td> + <td> + <a href="<?= $link ?>"> + <?= htmlReady($connection->studygroup->getFullname()) ?> + </a> + <? if ($connection->studygroup->beschreibung) : ?> + <div> + <?= htmlReady($connection->studygroup->beschreibung) ?> + </div> + <? endif ?> + </td> + <td> + <?= count($connection->studygroup->members) ?> + </td> + <td> + <? + $founders = $connection->studygroup->members->filter(function ($m) { return $m['status'] === 'dozent'; }); + foreach ($founders as $index => $founder) : ?> + <? if ($index > 0) : ?> + , + <? endif ?> + <a href="<?= URLHelper::getLink('dispatch.php/profile', ['username' => $founder->user->username]) ?>"> + <?= Avatar::getAvatar($founder->user->id)->getImageTag(Avatar::SMALL) ?> + <?= htmlReady($founder->user->getFullname()) ?> + </a> + <? endforeach ?> + </td> + </tr> + <? endforeach ?> + </tbody> + </table> + <? endif ?> + + </section> + +</article> diff --git a/app/views/course/wizard/step.php b/app/views/course/wizard/step.php index 0fe1dfcdb09..214515a70ce 100644 --- a/app/views/course/wizard/step.php +++ b/app/views/course/wizard/step.php @@ -10,10 +10,14 @@ ?> <? if ($content) : ?> <form class="default course-wizard-step-<?= $stepnumber ?>" action="<?= $controller->link_for('course/wizard/process', $stepnumber, $temp_id) ?>" method="post" data-secure> - <fieldset> - <?= $content ?> - </fieldset> + <? if (!$studygroup) : ?> + <fieldset> + <? endif; ?> + <?= $content ?> + <? if (!$studygroup) : ?> + </fieldset> + <? endif; ?> <footer data-dialog-button> <input type="hidden" name="step" value="<?= $stepnumber ?>"> <? if (empty($first_step)): ?> @@ -23,11 +27,15 @@ !empty($dialog) ? ['data-dialog' => 'size=50%'] : [] ) ?> <? endif; ?> + <? if (!$studygroup) : ?> <?= Studip\Button::create( _('Weiter'), 'next', !empty($dialog) ? ['data-dialog' => 'size=50%'] : [] ) ?> + <? else : ?> + <?= Studip\Button::createAccept(_('Studiengruppe anlegen'), 'create') ?> + <? endif; ?> </footer> </form> <? else : ?> diff --git a/app/views/course/wizard/steps/basicdata/index_studygroup.php b/app/views/course/wizard/steps/basicdata/index_studygroup.php index 9f75da56436..10368b97369 100644 --- a/app/views/course/wizard/steps/basicdata/index_studygroup.php +++ b/app/views/course/wizard/steps/basicdata/index_studygroup.php @@ -34,6 +34,12 @@ rows="4"><?= htmlReady($values['description'] ?? '') ?></textarea> </label> +<label class="col-3"> + <?= _('Bezieht sich auf Lehrveranstaltung (optional)') ?> + <?= QuickSearch::get('lv_course_id', new StandardSearch('Seminar_id')) + ->defaultValue($values['lv_course_id'], $values['lv_course_id'] ? Course::find($values['lv_course_id'])->getFullname() : '') + ->render() ?> +</label> <label class="col-3"> <span class="required"><?= _('Zugang') ?></span> @@ -74,6 +80,7 @@ <input type="hidden" name="institute" value="<?= $values['institute'] ?>"/> <input type="hidden" name="start_semester" value="<?= htmlReady($values['start_semester']) ?>"> <input type="hidden" name="studygroup" value="1"/> +<input type="hidden" name="stgteil_id" value="<?= htmlReady($values['stgteil_id']) ?>"/> <?php foreach ($values['lecturers'] as $id => $assigned) : ?> <input type="hidden" name="lecturers[<?= $id ?>]" value="1"/> <?php endforeach ?> diff --git a/app/views/course/wizard/steps/studygroups/index.php b/app/views/course/wizard/steps/studygroups/index.php new file mode 100644 index 00000000000..b7327a9dc48 --- /dev/null +++ b/app/views/course/wizard/steps/studygroups/index.php @@ -0,0 +1,110 @@ +<fieldset> +<legend> + <?= _('Grunddaten') ?> +</legend> + +<label class=""> + <span class="required"><?= _('Name') ?></span> + <input type="text" name="name" id="wizard-name" maxlength="254" value="<?= htmlReady($values['name'] ?? '') ?>" required> +</label> + +<? if(count($types) > 1) : ?> + <label class=""> + <span class="required"><?= _('Typ') ?></span> + <select name="coursetype" id="wizard-coursetype"> + <?php foreach ($types as $class => $subtypes) : ?> + <optgroup label="<?= htmlReady($class) ?>"> + <?php foreach ($subtypes as $type) : ?> + <option value="<?= $type['id'] ?>"<?= $type['id'] == $values['coursetype'] ? ' selected="selected"' : '' ?>> + <?= htmlReady($type['name']) ?> + </option> + <?php endforeach ?> + </optgroup> + <?php endforeach ?> + </select> + </label> +<? else : ?> + <? $type = array_values($types)[0]; ?> + <input type="hidden" name="coursetype" value="<?= htmlReady($type[0]['id']) ?>"> +<? endif ?> + + +<label class=""> + <?= _('Beschreibung') ?> + <textarea name="description" id="wizard-description" + rows="4"><?= htmlReady($values['description'] ?? '') ?></textarea> +</label> + + +<label class=""> + <span class="required"><?= _('Zugang') ?></span> + + <select name="access" id="wizard-access"> + <option value="all" + <? if (isset($values['access']) && $values['access'] === 'all') echo 'selected'; ?>> + <?= _('offen für alle') ?> + </option> + <option value="invite" + <? if (isset($values['access']) && $values['access'] === 'invite') echo 'selected'; ?>> + <?= _('auf Anfrage') ?> + </option> + <?php if (Config::get()->STUDYGROUPS_INVISIBLE_ALLOWED) : ?> + <option value="invisible" + <? if (isset($values['access']) && $values['access'] === 'invisible') echo 'selected'; ?>> + <?= _('unsichtbar') ?> + </option> + <?php endif ?> + </select> +</label> + + +<label><span class="required"><?= _('Nutzungsbedingungen')?></span></label> + +<? if ($GLOBALS['perm']->have_perm('admin')) : ?> + <p style="font-weight: bold;"> + <?= _('Ich habe die eingetragenen Personen darüber informiert, dass in Ihrem Namen eine Studiengruppe angelegt wird und versichere, dass Sie mit folgenden Nutzungsbedingungen einverstandenen sind:') ?> + </p> +<? endif ?> +<?= formatReady(Config::Get()->STUDYGROUP_TERMS) ?> + +<label> + <input type="checkbox" name="accept" id="wizard-accept" required> + <?= _('Einverstanden') ?> +</label> +</fieldset> + +<fieldset> + <legend> + <?= _('Erweiterte Einstellungen') ?> + </legend> + + <label> + <?= _('Ablaufdatum / Löschdatum') ?> + <input type="text" aria-label="<?= _('Ablaufdatum / Löschdatum') ?>" title="<?= _('Ablaufdatum / Löschdatum') ?>" + data-date-picker + name="exp_date" + value="<?= date('d.m.Y H:i', time() + 86400 * 365 * 2) ?>" + class="hasDatePicker"> + </label> + + <label> + <?= _('Schlagwörter') ?> + <?= Studip\VueApp::create('Multiquicksearch') + ->withProps([ + 'name' => 'tags[]', + 'searchtype' => (string) SQLSearch::get('SELECT `name`, `name` FROM `tags` WHERE `active` = 1 AND `name` LIKE :input', _('Schlagwort suchen')), + 'autocomplete' => true, + 'addlabel' => _('Schlagwort hinzufügen') + ]) + ?> + </label> + +</fieldset> + + +<input type="hidden" name="institute" value="<?= $values['institute'] ?>"> +<input type="hidden" name="studygroup" value="1"> +<input type="hidden" name="stgteil_id" value="<?= htmlReady($values['stgteil_id']) ?>"> +<?php foreach ($values['lecturers'] as $id => $assigned) : ?> + <input type="hidden" name="lecturers[<?= $id ?>]" value="1"> +<?php endforeach ?> diff --git a/app/views/my_studygroups/_course.php b/app/views/my_studygroups/_course.php index 37c081e14b2..e554ddb2a73 100644 --- a/app/views/my_studygroups/_course.php +++ b/app/views/my_studygroups/_course.php @@ -4,6 +4,7 @@ <td> <?= StudygroupAvatar::getAvatar($group['seminar_id'])->getImageTag(Avatar::SMALL, ['title' => $group['name']]) ?> </td> + <td style="text-align: left"> <a href="<?= URLHelper::getLink('dispatch.php/course/go', ['to' => $group['seminar_id']]) ?>" <?= $group['last_visitdate'] < $group['chdate'] ? 'style="color: red;"' : '' ?>> @@ -21,6 +22,9 @@ <?= tooltipicon($infotext) ?> <? endif ?> </td> + <td data-sort-value="<?= $group['mkdate'] ?>"> + <?= htmlReady(date('d.m.Y', $group['mkdate'])) ?> + </td> <td style="text-align: left; white-space: nowrap;"> <? if (!empty($group['navigation'])) : ?> <ul class="my-courses-navigation" style="flex-wrap: nowrap"> @@ -43,28 +47,30 @@ </ul> <? endif ?> </td> - <td style="text-align: right"> - <? if (in_array($group["user_status"], ["dozent", "tutor"])) : ?> - <? $adminmodule = $group["sem_class"]->getAdminModuleObject(); ?> - <? if ($adminmodule) : ?> - <? $adminnavigation = $adminmodule->getIconNavigation($group['seminar_id'], 0, $GLOBALS['user']->id); ?> - <? endif ?> - <? if ($adminnavigation) : ?> - <a href="<?= URLHelper::getLink($adminnavigation->getURL(), ['cid' => $group['seminar_id']]) ?>"> - <?= $adminnavigation->getImage()->asImg($adminnavigation->getLinkAttributes())?> + <? if (!$is_widget) : ?> + <td style="text-align: right"> + <? if (in_array($group["user_status"], ["dozent", "tutor"])) : ?> + <? $adminmodule = $group["sem_class"]->getAdminModuleObject(); ?> + <? if ($adminmodule) : ?> + <? $adminnavigation = $adminmodule->getIconNavigation($group['seminar_id'], 0, $GLOBALS['user']->id); ?> + <? endif ?> + <? if ($adminnavigation) : ?> + <a href="<?= URLHelper::getLink($adminnavigation->getURL(), ['cid' => $group['seminar_id']]) ?>"> + <?= $adminnavigation->getImage()->asImg($adminnavigation->getLinkAttributes())?> + </a> + <? endif ?> + + <? elseif (!empty($group['binding'])) : ?> + <a href="<?= URLHelper::getLink('', ['to' => $group['seminar_id'], 'cmd' => 'no_kill']) ?>"> + <?= Icon::create('door-leave', Icon::ROLE_INACTIVE)->asImg(['title' => _('Die Teilnahme ist bindend. Bitte wenden Sie sich an die Lehrenden.')]) ?> + </a> + <? + else : ?> + <a href="<?= URLHelper::getLink("dispatch.php/my_courses/decline/{$group['seminar_id']}", ['cmd' => 'suppose_to_kill']) ?>"> + <?= Icon::create('door-leave', Icon::ROLE_INACTIVE)->asImg(['title' => _('aus der Studiengruppe abmelden')]) ?> </a> <? endif ?> - - <? elseif (!empty($group['binding'])) : ?> - <a href="<?= URLHelper::getLink('', ['to' => $group['seminar_id'], 'cmd' => 'no_kill']) ?>"> - <?= Icon::create('door-leave', Icon::ROLE_INACTIVE)->asImg(['title' => _('Die Teilnahme ist bindend. Bitte wenden Sie sich an die Lehrenden.')]) ?> - </a> - <? - else : ?> - <a href="<?= URLHelper::getLink("dispatch.php/my_courses/decline/{$group['seminar_id']}", ['cmd' => 'suppose_to_kill']) ?>"> - <?= Icon::create('door-leave', Icon::ROLE_INACTIVE)->asImg(['title' => _('aus der Studiengruppe abmelden')]) ?> - </a> + </td> <? endif ?> - </td> </tr> <? endforeach ?> diff --git a/app/views/my_studygroups/index.php b/app/views/my_studygroups/index.php index 6fd2d20f685..cfdd2e28102 100644 --- a/app/views/my_studygroups/index.php +++ b/app/views/my_studygroups/index.php @@ -1,5 +1,5 @@ <? if (!empty($studygroups)) : ?> - <table class="default" id="my_seminars"> + <table class="default sortable-table" id="my_seminars"> <caption> <?= _('Meine Studiengruppen') ?> </caption> @@ -7,20 +7,27 @@ <col width="10px"> <col width="25px"> <col> + <col> <col width="<?= $nav_elements * 27 ?>px"> - <col width="45px"> + <? if (!$is_widget) : ?> + <col width="45px"> + <? endif ?> </colgroup> <thead> - <tr> + <tr class="sortable" title="<?= _('Klicken, um die Sortierung zu ändern') ?>"> + <th colspan="2" nowrap align="center"> <a href="<?= URLHelper::getLink('dispatch.php/my_courses/groups/all/true') ?>" data-dialog="size=normal"> <?= Icon::create('group')->asImg(['title' => _('Gruppe ändern'), 'class' => 'middle']) ?> </a> </th> - <th><?= _('Name') ?></th> + <th data-sort="text"><?= _('Name') ?></th> + <th data-sort="digit"><?= _('gegründet') ?></th> <th><?= _('Inhalt') ?></th> - <th></th> + <? if (!$is_widget) : ?> + <th><?= _('Aktionen') ?></th> + <? endif ?> </tr> </thead> <?= $this->render_partial('my_studygroups/_course', compact('studygroups')) ?> diff --git a/app/views/my_studygroups/proposals.php b/app/views/my_studygroups/proposals.php new file mode 100644 index 00000000000..7347849e788 --- /dev/null +++ b/app/views/my_studygroups/proposals.php @@ -0,0 +1,31 @@ +<section class="studip-tiles"> + <? foreach ($proposed_studygroups as $course) : ?> + <a href="<?= URLHelper::getLink('dispatch.php/course/studygroup/details/'.$course->id) ?>"> + <div> + <?= StudygroupAvatar::getAvatar($course->id)->getImageTag(Avatar::MEDIUM) ?> + <div> + <strong> + <?= htmlReady($course->getFullname()) ?> + </strong> + <div> + <?= sprintf( + ngettext( + '1 Mitglied', + '%s Mitglieder', + count($course->members) + ), + count($course->members) + ) ?> + </div> + </div> + </div> + <? if (count($course->tags)) : ?> + <div> + <? foreach ($course->tags as $tag) : ?> + <?= '#'.htmlReady($tag->name) ?> + <? endforeach ?> + </div> + <? endif ?> + </a> + <? endforeach ?> +</section> diff --git a/app/views/search/studiengaenge/verlauf.php b/app/views/search/studiengaenge/verlauf.php index 4464e66c6e7..540f46eac5b 100644 --- a/app/views/search/studiengaenge/verlauf.php +++ b/app/views/search/studiengaenge/verlauf.php @@ -143,4 +143,59 @@ <? endforeach ?> </tbody> </table> + + <h2><?= _('Studentische Arbeitsgruppen') ?></h2> + + <section class="studip-tiles"> + <? foreach ($studiengangTeil->studygroups as $course) : ?> + <div> + <div class="with-action-menu"> + <div> + <a href="<?= URLHelper::getLink('dispatch.php/course/studygroup/details/'.$course->id) ?>"> + <?= CourseAvatar::getAvatar($course->id)->getImageTag(Avatar::MEDIUM) ?> + </a> + <a href="<?= URLHelper::getLink('dispatch.php/course/studygroup/details/'.$course->id) ?>"> + <strong> + <?= htmlReady($course->name) ?> + </strong> + <div> + <?= sprintf( + ngettext( + '1 Mitglied', + '%s Mitglieder', + count($course->members) + ), + $course->members + ) ?> + </div> + </a> + </div> + <? if ($GLOBALS['perm']->have_perm('admin')) : ?> + <form method="post"> + <?= CSRFProtection::tokenTag() ?> + <button class="undecorated" + data-confirm="<?= sprintf(_('Wirklich diese Studiengruppe aus dem Studiengang %s entfernen?'), $studiengangTeilName) ?>" + formaction="<?= $controller->remove_studygroup($course->id, $studiengangTeil->id) ?>"> + <?= Icon::create('trash') ?> + </button> + </form> + <? endif ?> + </div> + <? if (count($course->tags)) : ?> + <div> + <? foreach ($course->tags as $tag) : ?> + <?= '#'.htmlReady($tag->name) ?> + <? endforeach ?> + </div> + <? endif ?> + </div> + <? endforeach ?> + + <a href="<?= URLHelper::getLink('dispatch.php/course/wizard', ['studygroup' => 1, 'stgteil_id' => $studiengangTeil->id] )?>"> + <div> + <?= Icon::create('add')->asImg(50) ?> + <strong><?= _('Neue Studiengruppe erstellen') ?></strong> + </div> + </a> + </section> <? endif ?> diff --git a/app/views/studygroup/browse.php b/app/views/studygroup/browse.php index 6362d6af7ec..cf8fe3e0c04 100644 --- a/app/views/studygroup/browse.php +++ b/app/views/studygroup/browse.php @@ -18,16 +18,16 @@ <?php $headers = [ - 'name' => _('Name'), - 'founded' => _('gegründet'), - 'member' => _('Mitglieder'), - 'founder' => _('GründerIn'), - 'ismember' => _('Mitglied'), + 'name' => _('Name'), + 'tags' => _('Schlagwörter'), + 'last_activity' => _('Letzte Aktivität'), + 'member' => _('Mitglieder'), + 'founder' => _('Gründer:in') ]; ?> <? if ($anzahl > 0): ?> - <table class="default studygroup-browse"> + <table class="default studygroup-browse sortable-table" data-sortlist="[[3, 1]]"> <caption> <?= sprintf(ngettext('%u Studiengruppe', '%u Studiengruppen', $anzahl), $anzahl)?> </caption> @@ -36,19 +36,30 @@ $headers = [ <col> <col style="width: 10%"> <col style="width: 10%"> - <col style="width: 20%"> <col style="width: 10%"> + <col style="width: 20%"> </colgroup> <thead> <tr class="sortable" title="<?= _('Klicken, um die Sortierung zu ändern') ?>"> <th class="nosort hidden-small-down"></th> - <? foreach ($headers as $key => $label): ?> - <th <? if ($sort_type === $key) echo 'class="sort' . $sort_order . '"'; ?>> - <a href="<?= $controller->link_for("studygroup/browse/1/{$key}_" . ($sort_order === 'asc' ? 'desc' : 'asc'), compact('q', 'closed')) ?>"> - <?= htmlReady($label) ?> - </a> - </th> - <? endforeach; ?> + <? foreach ($headers as $key => $label): ?> + <? if ($key !== 'last_activity' && $key !== 'tags') : ?> + <th <? if ($sort_type === $key) echo 'class="sort' . $sort_order . '"'; ?>> + <a href="<?= $controller->link_for("studygroup/browse/1/{$key}_" . ($sort_order === 'asc' ? 'desc' : 'asc'), compact('q', 'closed')) ?>"> + <?= htmlReady($label) ?> + </a> + </th> + <? elseif($key !== 'tags') : ?> + <th data-sort="htmldata"> + <?= htmlReady($label) ?> + </th> + <? else : ?> + <th> + <?= htmlReady($label) ?> + </th> + <? endif; ?> + <? endforeach; ?> + <th></th> </tr> </thead> <tbody> @@ -71,7 +82,15 @@ $headers = [ <? } ?> </a> </td> - <td><?= strftime('%x', $group['mkdate']) ?> + <td> + <? foreach ($group['course']->tags as $tag) : ?> + <a href="<?= $controller->browse(['q' => $tag['name']]) ?>"> + <?= htmlReady('#'.$tag['name']) ?> + </a> + <? endforeach ?> + </td> + <td data-sort-value="<?= htmlReady($group['last_visit_date']) ?>"> + <?= htmlReady(date('d.m.Y', $group['last_visit_date'])) ?> </td> <td align="center"> <?= StudygroupModel::countMembers($group['Seminar_id']) ?> @@ -88,11 +107,6 @@ $headers = [ <br> <? endforeach; ?> </td> - <td align="center"> - <? if ($is_member) : ?> - <?= Icon::create('person', Icon::ROLE_INACTIVE, ['title' => _('Sie sind Mitglied in dieser Gruppe')])->asImg() ?> - <? endif; ?> - </td> </tr> <? endforeach; ?> </tbody> diff --git a/db/migrations/6.0.38_improved_studygroups.php b/db/migrations/6.0.38_improved_studygroups.php new file mode 100644 index 00000000000..2ed71248a32 --- /dev/null +++ b/db/migrations/6.0.38_improved_studygroups.php @@ -0,0 +1,305 @@ +<?php + +class ImprovedStudygroups extends Migration +{ + + public function description() + { + return 'Improve studygroups.'; + } + + public function up() + { + DBManager::get()->exec(" + CREATE TABLE `studygroup_courses` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `studygroup_id` char(32) NOT NULL, + `course_id` char(32) DEFAULT NULL, + `mkdate` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `studygroup_id` (`studygroup_id`,`course_id`), + KEY `studygroup_id_2` (`studygroup_id`), + KEY `course_id` (`course_id`) + ) + "); + DBManager::get()->exec(" + CREATE TABLE `studygroup_courses_proposals` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `studygroup_id` char(32) NOT NULL, + `course_id` char(32) NOT NULL, + `proposed_from` enum('course','studygroup') NOT NULL, + `user_id` char(32) DEFAULT NULL, + `mkdate` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `studygroup_id` (`studygroup_id`,`course_id`), + KEY `course_id` (`course_id`), + KEY `studygroup_id_2` (`studygroup_id`) + ) + "); + DBManager::get()->exec(" + CREATE TABLE `tags` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(128) NOT NULL, + `active` tinyint(1) DEFAULT 1, + `chdate` int(11) DEFAULT NULL, + `mkdate` int(11) DEFAULT NULL, + PRIMARY KEY (`id`) + ) + "); + DBManager::get()->exec(" + CREATE TABLE `tags_relations` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `tag_id` int(11) DEFAULT NULL, + `range_id` varchar(32) DEFAULT NULL, + `range_type` varchar(100) DEFAULT NULL, + `mkdate` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `tag_id` (`tag_id`), + KEY `range_id` (`range_id`), + KEY `range_type` (`range_type`) + ) + "); + DBManager::get()->exec(" + ALTER TABLE `seminare` + ADD COLUMN `expires` int(11) DEFAULT NULL + "); + DBManager::get()->exec(" + CREATE TABLE `studygroup_stgteil` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `studygroup_id` char(32) NOT NULL, + `stgteil_id` varchar(32) NULL NULL, + `mkdate` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `studygroup_id` (`studygroup_id`,`stgteil_id`), + KEY `studygroup_id_2` (`studygroup_id`), + KEY `stgteil_id` (`stgteil_id`) + ) + "); + DBManager::get()->exec( + "INSERT IGNORE INTO `config` + (`field`, `type`, `range`, `value`, `section`, `description`, `mkdate`, `chdate`) + VALUES + ( + 'STUDYGROUP_ON_STGTEIL_ENABLE', 'boolean', 'global', '1', 'studygroups', 'Are studygroups allowed to get attached to study course parts?', + UNIX_TIMESTAMP(), UNIX_TIMESTAMP() + )" + ); + DBManager::get()->exec(" + INSERT INTO `admissionrules` (`ruletype`, `active`, `mkdate`, `path`) + VALUES + ('ConnectedcourseAdmission', 1, UNIX_TIMESTAMP(), 'lib/admissionrules/connectedcourseadmission'); + "); + + + $statement = DBManager::get()->prepare(" + SELECT * + FROM config + WHERE field = 'GLOBALSEARCH_MODULES' + "); + $statement->execute(); + $config = $statement->fetch(PDO::FETCH_ASSOC); + $config['value'] = json_decode($config['value'], true); + $config['value']['GlobalSearchStudygroups'] = [ + 'order' => 15, + 'active' => true, + 'fulltext' => false + ]; + + //Adding to the global search: + + $statement = DBManager::get()->prepare(" + UPDATE config + SET `value` = :json + WHERE field = 'GLOBALSEARCH_MODULES' + "); + $statement->execute([ + 'json' => json_encode($config['value']) + ]); + + $statement = DBManager::get()->prepare(" + SELECT * + FROM config_values + WHERE field = 'GLOBALSEARCH_MODULES' + "); + $statement->execute(); + $config = $statement->fetch(PDO::FETCH_ASSOC); + if ($config) { + $config['value'] = json_decode($config['value'], true); + $config['value']['GlobalSearchStudygroups'] = [ + 'order' => 15, + 'active' => true, + 'fulltext' => true + ]; + + $statement = DBManager::get()->prepare(" + UPDATE config_values + SET `value` = :json + WHERE field = 'GLOBALSEARCH_MODULES' + "); + $statement->execute([ + 'json' => json_encode($config['value']) + ]); + } + + $db = DBManager::get(); + + // get position + $pos = $db->fetchColumn("SELECT MAX(navigationpos) + 1 FROM plugins WHERE plugintype = 'PortalPlugin'"); + + // install as portal plugin + $sql = "INSERT INTO plugins (pluginclassname, pluginname, plugintype, enabled, navigationpos) VALUES (?)"; + $db->execute($sql, [['StudygroupWidget', 'StudygroupWidget', 'PortalPlugin', 'yes', $pos]]); + + $sql = "INSERT INTO roles_plugins (roleid, pluginid) + SELECT roleid, ? FROM roles WHERE `system` = 'y' AND rolename != 'Nobody'"; + $db->execute($sql, [$db->lastInsertId()]); + + + // Add default cron tasks and schedules + $new_job = [ + 'filename' => 'lib/cronjobs/studygroup_expiration.class.php', + 'class' => 'StudygroupExpirationJob', + 'priority' => 'normal' + ]; + + $query = "INSERT IGNORE INTO `cronjobs_tasks` + (`task_id`, `filename`, `class`, `active`) + VALUES (:task_id, :filename, :class, 1)"; + $task_statement = DBManager::get()->prepare($query); + + $query = "INSERT IGNORE INTO `cronjobs_schedules` + (`schedule_id`, `task_id`, `parameters`, + `minute`, `hour`, `mkdate`, `chdate`, + `last_result`) + VALUES (:schedule_id, :task_id, '[]', + :minute, :hour, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), + NULL)"; + $schedule_statement = DBManager::get()->prepare($query); + + + $task_id = md5(uniqid('expirestudygroups', true)); + + $task_statement->execute([ + ':task_id' => $task_id, + ':filename' => $new_job['filename'], + ':class' => $new_job['class'], + ]); + + $schedule_id = md5(uniqid('schedule', true)); + $schedule_statement->execute([ + ':schedule_id' => $schedule_id, + ':task_id' => $task_id, + ':hour' => $new_job['hour'], + ':minute' => $new_job['minute'], + ]); + + // get position + $pos = $db->fetchColumn("SELECT MAX(navigationpos) + 1 FROM plugins WHERE plugintype = 'PortalPlugin'"); + + // install as portal plugin + $sql = "INSERT INTO plugins (pluginclassname, pluginname, plugintype, enabled, navigationpos) VALUES (?)"; + $db->execute($sql, [['MyStudygroupsWidget', 'MyStudygroupsWidget', 'PortalPlugin', 'yes', $pos]]); + + $sql = "INSERT INTO roles_plugins (roleid, pluginid) + SELECT roleid, ? FROM roles WHERE `system` = 'y' AND rolename != 'Nobody'"; + $db->execute($sql, [$db->lastInsertId()]); + } + + public function down() + { + $db = DBManager::get(); + + $plugin_id = $db->fetchColumn('SELECT pluginid FROM plugins WHERE pluginclassname = ?', ['MyStudygroupsWidget']); + + $db->execute('DELETE FROM widget_default WHERE pluginid = ?', [$plugin_id]); + $db->execute('DELETE FROM widget_user WHERE pluginid = ?', [$plugin_id]); + $db->execute('DELETE FROM roles_plugins WHERE pluginid = ?', [$plugin_id]); + $db->execute('DELETE FROM plugins WHERE pluginid = ?', [$plugin_id]); + + $db->exec(" + DELETE `cronjobs_schedules`.* FROM `cronjobs_schedules` + INNER JOIN `cronjobs_tasks` USING (`task_id`) + WHERE `cronjobs_tasks`.`class` = 'StudygroupExpirationJob' + "); + $db->exec(" + DELETE FROM `cronjobs_tasks` + WHERE `cronjobs_tasks`.`class` = 'StudygroupExpirationJob' + "); + + $plugin_id = $db->fetchColumn('SELECT pluginid FROM plugins WHERE pluginclassname = ?', ['StudygroupWidget']); + + $db->execute('DELETE FROM widget_default WHERE pluginid = ?', [$plugin_id]); + $db->execute('DELETE FROM widget_user WHERE pluginid = ?', [$plugin_id]); + $db->execute('DELETE FROM roles_plugins WHERE pluginid = ?', [$plugin_id]); + $db->execute('DELETE FROM plugins WHERE pluginid = ?', [$plugin_id]); + + $statement = DBManager::get()->prepare(" + SELECT * + FROM config_values + WHERE field = 'GLOBALSEARCH_MODULES' + "); + $statement->execute(); + $config = $statement->fetch(PDO::FETCH_ASSOC); + if ($config) { + $config['value'] = json_decode($config['value'], true); + unset($config['value']['GlobalSearchStudygroups']); + $statement = DBManager::get()->prepare(" + UPDATE config_values + SET `value` = :json + WHERE field = 'GLOBALSEARCH_MODULES' + "); + $statement->execute([ + 'json' => json_encode($config['value']) + ]); + } + + $statement = DBManager::get()->prepare(" + SELECT * + FROM config + WHERE field = 'GLOBALSEARCH_MODULES' + "); + $statement->execute(); + $config = $statement->fetch(PDO::FETCH_ASSOC); + $config['value'] = json_decode($config['value'], true); + unset($config['value']['GlobalSearchStudygroups']); + $statement = DBManager::get()->prepare(" + UPDATE config + SET `value` = :json + WHERE field = 'GLOBALSEARCH_MODULES' + "); + $statement->execute([ + 'json' => json_encode($config['value']) + ]); + + + DBManager::get()->exec(" + DELETE FROM `admissionrules` WHERE `ruletype` = 'ConnectedcourseAdmission' + "); + DBManager::get()->exec(" + DELETE `config`, `config_values` + FROM `config` + LEFT JOIN `config_values` USING (`field`) + WHERE `config`.`field` = 'STUDYGROUP_ON_STGTEIL_ENABLE' + "); + DBManager::get()->exec(" + DROP TABLE `studygroup_stgteil` + "); + DBManager::get()->exec(" + ALTER TABLE `seminare` + DROP COLUMN `expires` + "); + DBManager::get()->exec(" + DROP TABLE `tags_relations` + "); + DBManager::get()->exec(" + DROP TABLE `tags` + "); + DBManager::get()->exec(" + DROP TABLE `studygroup_courses_proposals` + "); + DBManager::get()->exec(" + DROP TABLE `studygroup_courses` + "); + } + +} diff --git a/lib/admissionrules/connectedcourseadmission/ConnectedcourseAdmission.class.php b/lib/admissionrules/connectedcourseadmission/ConnectedcourseAdmission.class.php new file mode 100644 index 00000000000..40f32acdbbf --- /dev/null +++ b/lib/admissionrules/connectedcourseadmission/ConnectedcourseAdmission.class.php @@ -0,0 +1,121 @@ +<?php + +/** + * ConnectedAdmission.class.php + * + * Represents a rule for access only for members of connected courses. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * @author Rasmus Fuhse <fuhse@data-quest.de> + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + */ + +class ConnectedcourseAdmission extends AdmissionRule +{ + /** + * Standard constructor. + * + * @param String ruleId + */ + public function __construct($ruleId = '', $courseSetId = '') + { + parent::__construct($ruleId, $courseSetId); + $this->default_message = _('Die Anmeldung ist nur für Mitglieder der dazu gehörigen Lehrveranstaltung möglich.'); + } + + + /** + * Gets some text that describes what this AdmissionRule (or respective + * subclass) does. + */ + public static function getDescription() + { + return _('Diese Art von Anmelderegel erlaubt die Anmeldung an bestimmte Studiengruppen. Nur wer in einer verknüpften Lehrveranstaltung eingetragen ist, darf sich auch in die Studiengruppe anmelden.'); + } + + /** + * Return this rule's name. + */ + public static function getName() + { + return _('Anmeldung nur über verknüpfte Lehrveranstaltung'); + } + + /** + * Gets the template that provides a configuration GUI for this rule. + * + * @return String + */ + public function getTemplate() + { + $factory = new Flexi\Factory(dirname(__FILE__).'/templates/'); + // Now open specific template for this rule and insert base template. + $tpl = $factory->open('configure'); + $tpl->set_attribute('rule', $this); + return $tpl->render(); + } + + /** + * Internal helper function for loading rule definition from database. + */ + public function load() + { + + } + + /** + * Does the current rule allow the given user to register as participant + * in the given course? Never happens here as admission is completely + * locked. + * + * @param String userId + * @param String courseId + * @return Array Any errors that occurred on admission. + */ + public function ruleApplies($userId, $courseId) + { + $errors = []; + $statement = DBManager::get()->prepare(" + SELECT 1 + FROM `studygroup_courses` + INNER JOIN `seminar_user` ON (`seminar_user`.`Seminar_id` = `studygroup_courses`.`course_id`) + WHERE `studygroup_courses`.`studygroup_id` = :studygroup_id + AND `seminar_user`.`user_id` = :user_id + LIMIT 1 + "); + $statement->execute([ + 'user_id' => $userId, + 'studygroup_id' => $courseId + ]); + if (!$statement->fetch(PDO::FETCH_COLUMN)) { + $errors[] = $this->getMessage(); + } + return $errors; + } + + /** + * Helper function for storing data to DB. + */ + public function store() + { + + } + + /** + * A textual description of the current rule. + * + * @return String + */ + public function toString() { + $factory = new Flexi\Factory(dirname(__FILE__).'/templates/'); + $tpl = $factory->open('info'); + $tpl->set_attribute('rule', $this); + return $tpl->render(); + } + +} diff --git a/lib/admissionrules/connectedcourseadmission/rule.manifest b/lib/admissionrules/connectedcourseadmission/rule.manifest new file mode 100644 index 00000000000..a86c2184daa --- /dev/null +++ b/lib/admissionrules/connectedcourseadmission/rule.manifest @@ -0,0 +1,2 @@ +# ConnectedcourseAdmission +classname=ConnectedcourseAdmission diff --git a/lib/admissionrules/connectedcourseadmission/templates/configure.php b/lib/admissionrules/connectedcourseadmission/templates/configure.php new file mode 100644 index 00000000000..4a60973d09b --- /dev/null +++ b/lib/admissionrules/connectedcourseadmission/templates/configure.php @@ -0,0 +1,5 @@ +<h3><?= $rule->getName() ?></h3> +<label for="message" class="caption"> + <?= _('Nachricht bei fehlgeschlagener Anmeldung') ?>: +</label> +<textarea name="message" rows="4" cols="50"><?= $rule->getMessage() ?></textarea> \ No newline at end of file diff --git a/lib/admissionrules/connectedcourseadmission/templates/info.php b/lib/admissionrules/connectedcourseadmission/templates/info.php new file mode 100644 index 00000000000..123c7bc906f --- /dev/null +++ b/lib/admissionrules/connectedcourseadmission/templates/info.php @@ -0,0 +1 @@ +<?= _('Die Anmeldung ist gesperrt.') ?> diff --git a/lib/classes/Avatar.php b/lib/classes/Avatar.php index bb3a91564e7..b9586550c5c 100644 --- a/lib/classes/Avatar.php +++ b/lib/classes/Avatar.php @@ -509,7 +509,7 @@ class Avatar $output_file = $this->getCustomAvatarPath($size); $directory = dirname($output_file); - if (!is_dir($directory) && !mkdir($directory)) { + if (!is_dir($directory) && !@mkdir($directory)) { throw new Exception(_('Das Verzeichnis zum Speichern der Datei konnte nicht angelegt werden.')); } diff --git a/lib/classes/MyRealmModel.php b/lib/classes/MyRealmModel.php index 54252881a1d..f3ab44c975e 100644 --- a/lib/classes/MyRealmModel.php +++ b/lib/classes/MyRealmModel.php @@ -281,6 +281,14 @@ class MyRealmModel $children = []; $semester_assign = []; + $courses2 = $courses; + foreach ($courses2 as $course) { + foreach ($course->studygroups as $studygroup) { + $courses[] = $studygroup; + } + } + $courses = $courses2; + foreach ($courses as $course) { // export object to array for simple handling $_course = $course->toArray($param_array); @@ -326,9 +334,7 @@ class MyRealmModel if ($show_semester_name && count($course->semesters) !== 1 && !$course->getSemClass()['studygroup_mode']) { $_course['name'] .= ' (' . $course->getTextualSemester() . ')'; } - if ($course->parent_course) { - $_course['parent_course'] = $course->parent_course; - } + $_course['parent_course'] = $course->parent_course ?? null; $_course['is_group'] = $course->getSemClass()->isGroup(); $_course['navigation'] = self::getAdditionalNavigations( $_course['seminar_id'], @@ -340,7 +346,7 @@ class MyRealmModel // add the the course to the correct semester - if (empty($_course['parent_course'])) { + if (empty($_course['parent_course']) && !$course->isStudygroup()) { if ($course->isOpenEnded()) { if ($current_semester_nr >= $min_sem_key && $current_semester_nr <= $max_sem_key) { $sem_courses[$current_semester_nr][$course->id] = $_course; @@ -357,9 +363,16 @@ class MyRealmModel } } } - } else { + } elseif(!empty($_course['parent_course'])) { $children[$_course['parent_course']][] = $_course; } + if ($course->isStudygroup()) { + foreach ($course->connectedcourses as $connectedcourse) { + if ($GLOBALS['perm']->have_studip_perm('user', $course->id)) { + $children[$connectedcourse->id][] = $_course; + } + } + } } // Now sort children directly under their parent. @@ -787,78 +800,6 @@ class MyRealmModel $sem_courses = $_tmp_courses; } - /** - * Retrieves all study groups for the current user. - * - * @returns array A two-dimensional array. The second dimension contains - * data for each study group. Most fields of the Course model are - * present in the second dimension and there are additional fields - * like the colour (gruppe) or the start and end semester. - */ - public static function getStudygroups() - { - $studygroup_sem_types = array_filter( - array_keys($GLOBALS['SEM_TYPE']), - function ($sem_type_id) { - return (bool) $GLOBALS['SEM_CLASS'][$GLOBALS['SEM_TYPE'][$sem_type_id]['class']]['studygroup_mode']; - } - ); - $studygroup_memberships = CourseMember::findBySQL( - 'INNER JOIN `seminare` USING (`seminar_id`) - WHERE `seminar_user`.`user_id` = :me - AND `seminare`.`status` IN (:studygroup_semtypes) - GROUP BY `seminar_id` - ORDER BY `seminar_user`.`gruppe` ASC, `seminare`.`name` ASC', - [ - 'me' => User::findCurrent()->id, - 'studygroup_semtypes' => $studygroup_sem_types - ] - ); - $studygroups = []; - Course::findEachMany( - function ($studygroup) use (&$studygroups) { - $studygroups[$studygroup->id] = $studygroup; - }, - array_map( - function ($membership) { - return $membership->seminar_id; - }, - $studygroup_memberships - ) - ); - - $data_fields = 'name seminar_id visible veranstaltungsnummer status visible ' - . 'chdate admission_binding admission_prelim'; - $studygroup_data = []; - foreach ($studygroup_memberships as $membership) { - if (!isset($studygroups[$membership->seminar_id])) { - continue; - } - $studygroup = $studygroups[$membership->seminar_id]; - $visit_data = get_objects_visits([$studygroup->id], 0, null, null, $studygroup->tools->pluck('plugin_id')); - $data = $studygroup->toArray($data_fields); - $data['tools'] = $studygroup->tools; - $data['sem_class'] = $studygroup->getSemClass(); - $data['start_semester'] = $studygroup->start_semester->name; - $data['end_semester'] = $studygroup->end_semester->name ?? ''; - $data['obj_type'] = 'sem'; - $data['user_status'] = $membership->status; - $data['gruppe'] = $membership->gruppe; - $data['visitdate'] = $visit_data[$studygroup->id][0]['visitdate']; - $data['last_visitdate'] = $visit_data[$studygroup->id][0]['last_visitdate']; - $data['navigation'] = self::getAdditionalNavigations( - $studygroup->id, - $data, - $data['sem_class'], - $GLOBALS['user']->id, - $visit_data[$studygroup->id] - ); - $studygroup_data[$studygroup->id] = $data; - } - - return $studygroup_data; - } - /** * Calc nav elements to get the table-column-width diff --git a/lib/classes/StudipController.php b/lib/classes/StudipController.php index e0cfd0584c8..b7c55fdbb95 100644 --- a/lib/classes/StudipController.php +++ b/lib/classes/StudipController.php @@ -571,6 +571,11 @@ abstract class StudipController extends Trails\Controller ); } + /** + * Renders a stud.ip form object. + * + * @param \Studip\Forms\Form $form the form that should be rendered. + */ public function render_form(\Studip\Forms\Form $form) { \NotificationCenter::postNotification('FormWillRender', $form); @@ -591,6 +596,7 @@ abstract class StudipController extends Trails\Controller $this->render_template($app->getTemplate(), $this->layout); } + /** * relays current request to another controller and returns the response * the other controller is given all assigned properties, additional parameters are passed diff --git a/lib/classes/StudygroupModel.php b/lib/classes/StudygroupModel.php index 9fd44b3eeab..09cec1647d6 100644 --- a/lib/classes/StudygroupModel.php +++ b/lib/classes/StudygroupModel.php @@ -168,8 +168,9 @@ class StudygroupModel */ public static function countGroups($search = null, $closed_groups = null) { - $conditions = ['status IN (?)']; - $parameters = [studygroup_sem_types()]; + $conditions = ['status IN (:studygroup_sem_types)']; + $parameters['studygroup_sem_types'] = studygroup_sem_types(); + $joins = ''; // Only root may see hidden studygroups if (!$GLOBALS['perm']->have_perm('root')) { @@ -178,8 +179,10 @@ class StudygroupModel // Search by name? if (isset($search)) { - $conditions[] = "Name LIKE CONCAT('%', ?, '%')"; - $parameters[] = $search; + $joins = "LEFT JOIN `tags_relations` ON (`tags_relations`.`range_id` = seminare.Seminar_id AND `tags_relations`.`range_type` = 'course') + LEFT JOIN `tags` ON (`tags`.`id` = `tags_relations`.`tag_id` AND `tags`.`active` = 1) "; + $conditions[] = "(seminare.`Name` LIKE :search OR `tags`.`name` LIKE :search) "; + $parameters['search'] = '%' . $search . '%'; } // Show closed groups @@ -188,7 +191,9 @@ class StudygroupModel } return Course::countBySQL( - implode(' AND ', $conditions), + ($joins ? $joins.' WHERE ' : '') . + implode(' AND ', $conditions) . + ' GROUP BY Seminar_id ', $parameters ); } @@ -209,25 +214,27 @@ class StudygroupModel $elements_per_page = Config::get()->ENTRIES_PER_PAGE; } - $sql = "SELECT * - FROM seminare AS s"; + $sql = "SELECT s.* + FROM seminare AS s + LEFT JOIN `tags_relations` ON (`tags_relations`.`range_id` = s.Seminar_id AND `tags_relations`.`range_type` = 'course') + LEFT JOIN `tags` ON (`tags`.`id` = `tags_relations`.`tag_id` AND `tags`.`active` = 1) "; $sql_additional = ''; $conditions = []; $parameters = []; - $conditions[] = 's.status IN (?)'; - $parameters[] = studygroup_sem_types(); + $conditions[] = 's.status IN (:studygroup_sem_types)'; + $parameters['studygroup_sem_types'] = studygroup_sem_types(); if (!$GLOBALS['perm']->have_perm('root')) { $conditions[] = 's.visible = 1'; } if (isset($search)) { - $conditions[] = "Name LIKE CONCAT('%', ?, '%')"; - $parameters[] = $search; + $conditions[] = "(s.`Name` LIKE :search OR `tags`.`name` LIKE :search) "; + $parameters['search'] = '%' . $search . '%'; } if (isset($closed_groups) && !$closed_groups) { - $conditions[] = 'admission_prelim = 0'; + $conditions[] = 's.admission_prelim = 0'; } list($sort_by, $sort_order) = explode('_', $sort); @@ -235,35 +242,37 @@ class StudygroupModel // add here the sortings if ($sort_by === 'name') { - $sort_by = 'Name'; + $sort_by = 's.Name'; } elseif ($sort_by === 'founded') { - $sort_by = 'mkdate'; + $sort_by = 's.mkdate'; } elseif ($sort_by === 'member') { $sort_by = 'members'; $sql = "SELECT s.*, COUNT(su.user_id) AS members FROM seminare AS s - LEFT JOIN seminar_user AS su USING (Seminar_id)"; + LEFT JOIN `tags_relations` ON (`tags_relations`.`range_id` = s.Seminar_id AND `tags_relations`.`range_type` = 'course') + LEFT JOIN `tags` ON (`tags`.`id` = `tags_relations`.`tag_id` AND `tags`.`active` = 1) + LEFT JOIN seminar_user AS su USING (Seminar_id)"; - $sql_additional = 'GROUP BY s.Seminar_id'; } elseif ($sort_by === 'founder') { $sort_by = "GROUP_CONCAT(aum.Nachname ORDER BY su.status, su.position, aum.Nachname, aum.Vorname SEPARATOR ',')"; $sql = "SELECT s.* FROM seminare AS s - LEFT JOIN seminar_user AS su ON (s.Seminar_id = su.Seminar_id AND su.status = 'dozent') - LEFT JOIN auth_user_md5 AS aum ON (su.user_id = aum.user_id)"; + LEFT JOIN `tags_relations` ON (`tags_relations`.`range_id` = s.Seminar_id AND `tags_relations`.`range_type` = 'course') + LEFT JOIN `tags` ON (`tags`.`id` = `tags_relations`.`tag_id` AND `tags`.`active` = 1) LEFT JOIN seminar_user AS su ON (s.Seminar_id = su.Seminar_id AND su.status = 'dozent') + LEFT JOIN auth_user_md5 AS aum ON (su.user_id = aum.user_id)"; - $sql_additional = 'GROUP BY s.Seminar_id'; } elseif ($sort_by === 'ismember') { $sort_by = 'is_member'; $sql = "SELECT s.*, COUNT(su.user_id) AS is_member FROM seminare AS s - LEFT JOIN seminar_user AS su ON s.Seminar_id = su.Seminar_id AND su.user_id = ?"; - array_unshift($parameters, $GLOBALS['user']->id); + LEFT JOIN `tags_relations` ON (`tags_relations`.`range_id` = s.Seminar_id AND `tags_relations`.`range_type` = 'course') + LEFT JOIN `tags` ON (`tags`.`id` = `tags_relations`.`tag_id` AND `tags`.`active` = 1) + LEFT JOIN seminar_user AS su ON s.Seminar_id = su.Seminar_id AND su.user_id = :user_id"; + $parameters['user_id'] = $GLOBALS['user']->id; - $sql_additional = 'GROUP BY s.Seminar_id'; } elseif ($sort_by == 'access') { $sort_by = 'admission_prelim'; } else { @@ -274,13 +283,22 @@ class StudygroupModel $sql .= ' WHERE ' . implode(' AND ', $conditions); } $sql .= ' ' . $sql_additional; + $sql .= ' GROUP BY s.Seminar_id '; $sql .= " ORDER BY {$sort_by} {$sort_order}"; - $sql .= ", name {$sort_order} LIMIT " . (int) $lower_bound . ',' . (int) $elements_per_page; + $sql .= ", s.`name` {$sort_order} LIMIT " . (int) $lower_bound . ',' . (int) $elements_per_page; $statement = DBManager::get()->prepare($sql); $statement->execute($parameters); $groups = $statement->fetchAll(); + foreach ($groups as $key => $studygroup) + { + $visit_data = get_objects_visits([$studygroup['Seminar_id']], 0); + $studygroup['last_visit_date'] = $visit_data[$studygroup['Seminar_id']]; + $groups[$key]['last_visit_date'] = $studygroup['last_visit_date']; + $groups[$key]['course'] = Course::buildExisting($studygroup); + } + return $groups; } @@ -562,4 +580,186 @@ class StudygroupModel return $msging->insert_message($message, $recipients, '', '', '', '1', '', $subject); } + + /** + * @param Course $studygroup + * @param $course_id + * @return false|string + */ + public static function proposeAsStudygroupTo(Course $studygroup, $course_id) + { + if (!$GLOBALS['perm']->have_studip_perm('tutor', $studygroup->id) && !$GLOBALS['perm']->have_studip_perm('tutor')) { + return false; + } + $proposal = StudygroupCourseProposal::findOneBySQL('course_id = ? AND studygroup_id = ?', [ + $course_id, + $studygroup->id + ]); + if ($GLOBALS['perm']->have_studip_perm('tutor', $course_id) || $proposal['proposed_from'] === 'course') { + $connection = StudygroupCourse::findOneBySQL('course_id = ? AND studygroup_id = ?', [ + $course_id, + $studygroup->id + ]); + if (!$connection) { + $connection = StudygroupCourse::create([ + 'course_id' => $course_id, + 'studygroup_id' => $studygroup->id + ]); + } + if ($proposal) { + if ($proposal['proposed_from'] === 'course') { + $statement = DBManager::get()->prepare(" + SELECT `username`, `user_id` + FROM `auth_user_md5` + INNER JOIN `seminar_user` USING (`user_id`) + WHERE `seminar_user`.`Seminar_id` = ? AND `seminar_user`.`status` IN ('tutor', 'dozent') + "); + $statement->execute([$course_id]); + $messaging = new messaging(); + + foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $user_data) { + setTempLanguage($user_data['user_id']); + $messaging->insert_message( + sprintf( + _('Ihr Vorschlag, die Studiengruppe "%s" mit der Veranstaltung "%s" zu verknüpfen, wurde angenommen.'), + $studygroup->getFullname(), + Course::find($course_id)->getFullname() + ), + $user_data['username'], + '____%system%____', + '', + '', + '', + '', + _('Verknüpfungsvorschlag angenommen'), + '', + 'normal', + ['Studiengruppe'] + ); + restoreLanguage(); + } + } + $proposal->delete(); + } + PageLayout::postSuccess(_('Veranstaltung wurde verknüpft.')); + return 'connected'; + } else { + if (!$proposal) { + $proposal = StudygroupCourseProposal::create([ + 'course_id' => $course_id, + 'studygroup_id' => $studygroup->id, + 'proposed_from' => 'studygroup', + 'user_id' => User::findCurrent()->id + ]); + //send message: + $statement = DBManager::get()->prepare(" + SELECT `username`, `user_id` + FROM `auth_user_md5` + INNER JOIN `seminar_user` USING (`user_id`) + WHERE `seminar_user`.`Seminar_id` = ? AND `seminar_user`.`status` IN ('tutor', 'dozent') + "); + $statement->execute([$course_id]); + $messaging = new messaging(); + $oldbase = URLHelper::setBaseURL($GLOBALS['ABSOLUTE_URI_STUDIP']); + + foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $user_data) { + setTempLanguage($user_data['user_id']); + $messaging->insert_message( + sprintf( + _('Es wurde vorgeschlagen, die Studiengruppe „%1$s“ mit Ihrer Veranstaltung „%2$s“ zu verknüpfen. Sie können den Vorschlag unter folgendem Link annehmen oder ablehnen:'), + $studygroup->getFullname(), + Course::find($course_id)->getFullname() + )."\n\n".URLHelper::getURL('dispatch.php/course/connectedstudygroups/index', ['cid' => $course_id]), + $user_data['username'], + '____%system%____', + '', + '', + '', + '', + _('Verknüpfung Ihrer Veranstaltung zu einer Studiengruppe'), + '', + 'normal', + ['Studiengruppe'] + ); + restoreLanguage(); + } + URLHelper::setBaseURL($oldbase); + return 'proposed'; + } + } + return false; + } + + /** + * Retrieves all study groups for the current user. + * + * @returns array A two-dimensional array. The second dimension contains + * data for each study group. Most fields of the Course model are + * present in the second dimension and there are additional fields + * like the colour (gruppe) or the start and end semester. + */ + public static function getStudygroups() + { + $studygroup_sem_types = array_filter( + array_keys($GLOBALS['SEM_TYPE']), + function ($sem_type_id) { + return (bool) $GLOBALS['SEM_CLASS'][$GLOBALS['SEM_TYPE'][$sem_type_id]['class']]['studygroup_mode']; + } + ); + $studygroup_memberships = CourseMember::findBySQL( + 'INNER JOIN `seminare` USING (`seminar_id`) + WHERE `seminar_user`.`user_id` = :me + AND `seminare`.`status` IN (:studygroup_semtypes) + GROUP BY `seminar_id` + ORDER BY `seminar_user`.`gruppe` ASC, `seminare`.`name` ASC', + [ + 'me' => User::findCurrent()->id, + 'studygroup_semtypes' => $studygroup_sem_types + ] + ); + $studygroups = []; + Course::findEachMany( + function ($studygroup) use (&$studygroups) { + $studygroups[$studygroup->id] = $studygroup; + }, + array_map( + function ($membership) { + return $membership->seminar_id; + }, + $studygroup_memberships + ) + ); + + $data_fields = 'name seminar_id visible veranstaltungsnummer duration_time status visible ' + . 'chdate admission_binding admission_prelim'; + $studygroup_data = []; + foreach ($studygroup_memberships as $membership) { + if (!isset($studygroups[$membership->seminar_id])) { + continue; + } + $studygroup = $studygroups[$membership->seminar_id]; + $visit_data = get_objects_visits([$studygroup->id], 0, null, null, $studygroup->tools->pluck('plugin_id')); + $data = $studygroup->toArray($data_fields); + $data['tools'] = $studygroup->tools; + $data['sem_class'] = $studygroup->getSemClass(); + $data['start_semester'] = $studygroup->start_semester->name; + $data['end_semester'] = $studygroup->end_semester->name ?? ''; + $data['obj_type'] = 'sem'; + $data['user_status'] = $membership->status; + $data['gruppe'] = $membership->gruppe; + $data['mkdate'] = $membership->mkdate; + $data['visitdate'] = $visit_data[$studygroup->id][0]['visitdate']; + $data['last_visitdate'] = $visit_data[$studygroup->id][0]['last_visitdate']; + $data['navigation'] = MyRealmModel::getAdditionalNavigations( + $studygroup->id, + $data, + $data['sem_class'], + $GLOBALS['user']->id, + $visit_data[$studygroup->id] + ); + $studygroup_data[$studygroup->id] = $data; + } + + return $studygroup_data; + } } diff --git a/lib/classes/admission/AdmissionRule.php b/lib/classes/admission/AdmissionRule.php index cff3e356e47..9855165f03a 100644 --- a/lib/classes/admission/AdmissionRule.php +++ b/lib/classes/admission/AdmissionRule.php @@ -57,6 +57,7 @@ abstract class AdmissionRule 'active' => (bool) $row['active'], ]; } catch (Exception $e) { + throw $e; } } ); @@ -521,5 +522,4 @@ abstract class AdmissionRule $this->id = md5(uniqid(get_class($this))); $this->courseSetId = null; } - } diff --git a/lib/classes/admission/CourseSet.php b/lib/classes/admission/CourseSet.php index d93cfb0e2ac..81bd17f04ca 100644 --- a/lib/classes/admission/CourseSet.php +++ b/lib/classes/admission/CourseSet.php @@ -1150,6 +1150,45 @@ class CourseSet implements UserFilterRange return $locked_set_id; } + public static function getConnectedcourseAdmissionSetId() + { + $db = DBManager::get(); + $locked_set_id = $db->fetchColumn(" + SELECT `courseset_rule`.`set_id` + FROM `courseset_rule` + INNER JOIN `coursesets` USING (`set_id`) + WHERE `type` = 'ConnectedcourseAdmission' + AND `private` = 1 + AND `user_id` = '' + LIMIT 1 + "); + if (!$locked_set_id) { + $cs_insert = $db->prepare(" + INSERT INTO coursesets (set_id, user_id, name, infotext, algorithm, private, mkdate, chdate) + VALUES (?, ?, ?, ?, '', ?, ?, ?) + "); + $cs_r_insert = $db->prepare(" + INSERT INTO `courseset_rule` (`set_id`, `rule_id`, `type`, `mkdate`) + VALUES (?, ?, ?, UNIX_TIMESTAMP()) + "); + $locked_insert = $db->prepare(" + INSERT INTO `lockedadmissions` (`rule_id`, `message`, `mkdate`, `chdate`) + VALUES (?,'Die Anmeldung ist gesperrt', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) + "); + $locked_set_id = md5(uniqid('coursesets_connected_course',1)); + $name = 'Verknüpfte Veranstaltung (global)'; + $cs_insert->execute([$locked_set_id,'',$name,'',1,time(),time()]); + $locked_rule_id = md5(uniqid('connectedcourse',1)); + $locked_insert->execute([$locked_rule_id]); + $cs_r_insert->execute([ + $locked_set_id, + $locked_rule_id, + 'ConnectedcourseAdmission' + ]); + } + return $locked_set_id; + } + public static function addCourseToSet($set_id, $course_id) { $db = DBManager::get(); diff --git a/lib/classes/coursewizardsteps/BasicDataWizardStep.php b/lib/classes/coursewizardsteps/BasicDataWizardStep.php index 4b401ee1042..18892f0febc 100644 --- a/lib/classes/coursewizardsteps/BasicDataWizardStep.php +++ b/lib/classes/coursewizardsteps/BasicDataWizardStep.php @@ -30,7 +30,7 @@ class BasicDataWizardStep implements CourseWizardStep // Load template from step template directory. $factory = new Flexi\Factory($GLOBALS['STUDIP_BASE_PATH'] . '/app/views/course/wizard/steps'); if (!empty($values[__CLASS__]['studygroup'])) { - $tpl = $factory->open('basicdata/index_studygroup'); + $tpl = $factory->open('studygroups/index'); $values[__CLASS__]['lecturers'][$GLOBALS['user']->id] = 1; } else { $tpl = $factory->open('basicdata/index'); @@ -428,7 +428,7 @@ class BasicDataWizardStep implements CourseWizardStep $course->name = new I18NString($values['name'], $values['name_i18n'] ?? []); $course->veranstaltungsnummer = $values['number'] ?? null; $course->beschreibung = new I18NString($values['description'], $values['description_i18n'] ?? []); - $course->start_semester = Semester::find($values['start_semester']); + $course->start_semester = isset($values['start_semester']) ? Semester::find($values['start_semester']) : Semester::findCurrent(); $course->institut_id = $values['institute']; $semclass = $course->getSemClass(); @@ -455,6 +455,7 @@ class BasicDataWizardStep implements CourseWizardStep break; } } + if (!$course->store()) { return false; } @@ -506,6 +507,51 @@ class BasicDataWizardStep implements CourseWizardStep self::copyParticipantsAndGroups($course, $source_id, $copy_participants, $copy_groups, $copy_members); } + if (in_array($values['coursetype'], studygroup_sem_types())) { + if (!empty($values['lv_course_id'])) { + StudygroupModel::proposeAsStudygroupTo($course, $values['lv_course_id']); + } + } + + if (!empty($values['exp_date'])) { + $exp_date = strtotime($values['exp_date']); + if ($exp_date) { + CourseConfig::get($course->id)->store('STUDYGROUP_EXPIRATION_DATE', $exp_date); + } + } + if (!empty($values['tags'])) { + foreach ($values['tags'] as $name) { + if ($tag = Tag::findOneByName($name)) { + if (!$tag->active) { + continue; + } + } else { + $tag = Tag::create(['name' => $name]); + } + + $relation = TagRelation::findOneBySQL( + "`range_id` = :course_id AND `range_type` = 'course' AND `tag_id` = :tag_id", + [ + 'tag_id' => $tag->id, + 'course_id' => $course->id + ] + ); + if (!$relation) { + $relation = TagRelation::create([ + 'range_id' => $course->id, + 'range_type' => 'course', + 'tag_id' => $tag->id + ]); + } + } + } + + + if (!empty($values['stgteil_id'])) { + $studiengangteil = StudiengangTeil::find($values['stgteil_id']); + $studiengangteil->addStudygroup($course); + } + return $course; } diff --git a/lib/classes/forms/MultiquicksearchInput.php b/lib/classes/forms/MultiquicksearchInput.php new file mode 100644 index 00000000000..c7a9a02fa29 --- /dev/null +++ b/lib/classes/forms/MultiquicksearchInput.php @@ -0,0 +1,31 @@ +<?php + +namespace Studip\Forms; + +class MultiquicksearchInput extends Input +{ + public function render() + { + $options = $this->extractOptionsFromAttributes($this->attributes); + + $name = $this->name; + if (substr($name, -2) === '[]') { + $name .= substr($name, 0, -2); + } + + $template = $GLOBALS['template_factory']->open('forms/multiquicksearch_input'); + $template->title = $this->title; + $template->name = $name; + $template->value = $this->getValue(); + $template->id = md5(uniqid()); + $template->required = $this->required; + $template->attributes = arrayToHtmlAttributes($this->attributes); + $template->options = $options; + return $template->render(); + } + + public function getRequestValue() + { + return \Request::getArray($this->name); + } +} diff --git a/lib/classes/globalsearch/GlobalSearchStudygroups.php b/lib/classes/globalsearch/GlobalSearchStudygroups.php new file mode 100644 index 00000000000..a9f13981b70 --- /dev/null +++ b/lib/classes/globalsearch/GlobalSearchStudygroups.php @@ -0,0 +1,315 @@ +<?php +/** + * Global search module for study groups + * + * @author Michaela Brückner <brueckner@data-quest.de> + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + * @since 6.0 + */ +class GlobalSearchStudygroups extends GlobalSearchModule implements GlobalSearchFulltext +{ + /** + * Returns the displayname for this module + * + * @return string + */ + public static function getName() + { + return _('Studiengruppen'); + } + + /** + * Returns the filters that are displayed in the sidebar of the global search. + * + * @return array Filters for this class. + */ + public static function getFilters() + { + return ['semester', 'study_course']; + } + + /** + * Transforms the search request into an sql statement, that provides the id (same as getId) as type and + * the object id, that is later passed to the filter. + * + * This function is required to make use of the mysql union parallelism + * + * @param string $search the input query string + * @param array $filter an array with search limiting filter information (e.g. 'category', 'semester', etc.) + * @return string SQL Query to discover elements for the search + */ + public static function getSQL($search, $filter, $limit) + { + if (!$search) { + return null; + } + $search = str_replace(' ', '% ', $search); + $query = DBManager::get()->quote("%{$search}%"); + + $language_name = 'courses.`Name`'; + $language_join = ''; + if (I18N::isEnabled() && $_SESSION['_language'] !== I18NString::getDefaultLanguage()) { + $language_name = 'IFNULL(`i18n`.`value`, courses.`Name`)'; + $language_join = "LEFT JOIN `i18n` + ON `i18n`.`object_id` = courses.`Seminar_id` + AND `i18n`.`table` = 'seminare' + AND `i18n`.`field` = 'name' + AND `lang` = " . DBManager::get()->quote($_SESSION['_language']); + } + + $visibility = ''; + $seminaruser = ''; + $semester_join = ''; + $institute_condition = ''; + $seminar_type_condition = ''; + $semester_condition = ''; + + // visibility + //if (!$GLOBALS['perm']->have_perm('admin')) { + $visibility = "courses.`visible` = 1 AND "; + $seminaruser = " AND EXISTS ( + SELECT 1 FROM `seminar_user` + WHERE `seminar_id` = `courses`.`Seminar_id` + AND `user_id` = " . DBManager::get()->quote($GLOBALS['user']->id) . " + ) "; + //} + + // generate SQL for the given sidebar filter (semester, institute, seminar_type) + if ($filter['category'] === self::class || $filter['category'] === 'show_all_categories') { + if (!empty($filter['semester'])) { + if ($filter['semester'] === 'future') { + $semester = Semester::findCurrent(); + $next_semester = Semester::findNext(); + + $semester_ids = [$semester->id]; + if ($next_semester) { + $semester_ids[] = $next_semester->id; + } + } else { + $semester = Semester::findByTimestamp($filter['semester']); + $semester_ids = [$semester->id]; + } + $semester_join = "LEFT JOIN semester_courses ON (courses.Seminar_id = semester_courses.course_id) "; + $semester_condition = " + AND ( + semester_courses.semester_id IS NULL OR semester_courses.semester_id IN (" . join(',', array_map([DBManager::get(), 'quote'], $semester_ids)) . ") + ) "; + } + $seminar_type_condition = " AND `courses`.`status` = '99' "; + } + + $tags_join = "LEFT JOIN tags_relations ON courses.Seminar_id = tags_relations.range_id LEFT JOIN tags on tags_relations.tag_id = tags.id"; + $tags_name = "tags.name"; + + $sql = "SELECT SQL_CALC_FOUND_ROWS courses.`Seminar_id`, + {$language_name} AS `Name`, + courses.`VeranstaltungsNummer`, courses.`status`, + {$tags_name} AS `Tag` + FROM `seminare` AS courses + {$language_join} + JOIN `seminar_user` u ON (u.`Seminar_id` = courses.`Seminar_id` AND u.`status` = 'dozent') + JOIN `auth_user_md5` a ON (a.`user_id` = u.`user_id`) + {$semester_join} + {$tags_join} + WHERE {$visibility} + ( + {$language_name} LIKE {$query} + OR {$tags_name} LIKE {$query} + OR courses.`VeranstaltungsNummer` LIKE {$query} + OR CONCAT(a.`Nachname`, ', ', a.`Vorname`, ' ', a.`Nachname`) LIKE {$query} + ) + {$seminaruser} + {$institute_condition} + {$seminar_type_condition} + {$semester_condition} + GROUP BY courses.Seminar_id"; + + if (Config::get()->IMPORTANT_SEMNUMBER) { + $sql .= ", courses.`VeranstaltungsNummer`"; + } + + $sql .= ", `Name`"; + $sql .= " LIMIT " . $limit; + + + return $sql; + } + + /** + * Returns an array of information for the found element. Following informations (key: description) are necessary + * + * - name: The name of the object + * - url: The url to send the user to when he clicks the link + * + * Additional informations are: + * + * - additional: Subtitle for the hit + * - expand: Url if the user further expands the search + * - img: Avatar for the + * + * @param array $data + * @param string $search + * @return array + */ + public static function filter($data, $search) + { + $course = Course::buildExisting($data); + $turnus_string = implode(' ', $course->getAllDatesInSemester()->toStringArray()); + //Shorten, if string too long (add link for details.php) + if (mb_strlen($turnus_string) > 70) { + $turnus_string = htmlReady(mb_substr($turnus_string, 0, mb_strpos(mb_substr($turnus_string, 70, mb_strlen($turnus_string)), ',') + 71)); + $turnus_string .= ' ... <a href="' . URLHelper::getURL("dispatch.php/course/details/index/{$course->id}") . '">(' . _('mehr') . ')</a>'; + } else { + $turnus_string = htmlReady($turnus_string); + } + $lecturers = $course->getMembersWithStatus('dozent'); + $semester = $course->start_semester; + + // If you are not root, perhaps not all available subcourses are visible. + $visibleChildren = $course->children; + if (!$GLOBALS['perm']->have_perm(Config::get()->SEM_VISIBILITY_PERM)) { + $visibleChildren = $visibleChildren->filter(function($c) { + return $c->visible; + }); + } + $result_children = []; + foreach($visibleChildren as $child) { + $result_children[] = self::filter($child, $search); + } + + //admission state + $admission_state = ""; + if (Config::get()->COURSE_SEARCH_SHOW_ADMISSION_STATE) { + switch (self::getStatusCourseAdmission($course->id, + $course->admission_prelim)) { + case 1: + $admission_state = Icon::create( + 'decline-circle', + Icon::ROLE_STATUS_YELLOW, + tooltip2(_('Eingeschränkter Zugang')) + )->asImg(); + break; + case 2: + $admission_state = Icon::create( + 'decline-circle', + Icon::ROLE_STATUS_RED, + tooltip2(_('Kein Zugang')) + )->asImg(); + break; + default: + $admission_state = Icon::create( + 'check-circle', + Icon::ROLE_STATUS_GREEN, + tooltip2(_('Uneingeschränkter Zugang')) + )->asImg(); + } + } + + $tags = array_map(function ($t) { + return '#' . $t->name; + }, $course->tags->getArrayCopy()); + + $result = [ + 'id' => $course->id, + 'number' => self::mark($course->veranstaltungsnummer, $search), + 'name' => self::mark($course->getFullName(), $search), + 'url' => URLHelper::getURL("dispatch.php/course/details/index/{$course->id}", [], true), + 'date' => htmlReady($semester->short_name), + 'dates' => $turnus_string, + 'has_children' => count($course->children) > 0, + 'children' => $result_children, + 'additional' => implode(', ', + array_filter( + array_map( + function ($lecturer, $index) use ($search, $course) { + if ($index < 3) { + return self::mark($lecturer->getUserFullname(), $search); + } else if ($index == 3) { + return '... (' . _('mehr') . ')'; + } + }, + $lecturers, + array_keys($lecturers) + ) + ) + ), + 'expand' => self::getSearchURL($search), + 'admission_state' => $admission_state, + 'found_tag' => self::mark(implode(' ', $tags), $search) + ]; + if ($course->getSemClass()->offsetGet('studygroup_mode')) { + $avatar = StudygroupAvatar::getAvatar($course->id); + } else { + $avatar = CourseAvatar::getAvatar($course->id); + } + $result['img'] = $avatar->getUrl(Avatar::MEDIUM); + return $result; + } + + /** + * Enables fulltext (MATCH AGAINST) search by creating the corresponding indices. + */ + public static function enable() + { + DBManager::get()->exec("ALTER TABLE `seminare` ADD FULLTEXT INDEX globalsearch (`VeranstaltungsNummer`, `Name`)"); + DBManager::get()->exec("ALTER TABLE `sem_types` ADD FULLTEXT INDEX globalsearch (`Name`)"); + } + + /** + * Disables fulltext (MATCH AGAINST) search by removing the corresponding indices. + */ + public static function disable() + { + DBManager::get()->exec("DROP INDEX globalsearch ON `seminare`"); + DBManager::get()->exec("DROP INDEX globalsearch ON `sem_types`"); + } + + /** + * Returns the URL that can be called for a full search. + * + * @param string $searchterm what to search for? + * @return string URL to the full search, containing the searchterm and the category + */ + public static function getSearchURL($searchterm): string + { + return URLHelper::getURL('dispatch.php/search/globalsearch', [ + 'q' => $searchterm, + 'category' => self::class + ]); + } + + /** + * Returns the admission status for a course. + * + * @param string $seminar_id Id of the course + * @param bool $prelim State of preliminary setting + * @return int + */ + public static function getStatusCourseAdmission($seminar_id, $prelim): int + { + $sql = "SELECT COUNT(`type`) AS `types`, + SUM(IF(`type` = 'LockedAdmission', 1, 0)) AS `type_locked` + FROM `seminar_courseset` + INNER JOIN `courseset_rule` USING (`set_id`) + WHERE `seminar_id` = ? + GROUP BY `set_id`"; + + $stmt = DBManager::get()->prepare($sql); + $stmt->execute([$seminar_id]); + $result = $stmt->fetch(); + + if (!empty($result['types'])) { + if ($result['type_locked']) { + return 2; + } + return 1; + } + + if ($prelim) { + return 1; + } + return 0; + } + +} diff --git a/lib/classes/searchtypes/StudygroupSearch.class.php b/lib/classes/searchtypes/StudygroupSearch.class.php new file mode 100644 index 00000000000..9bfef6ca73c --- /dev/null +++ b/lib/classes/searchtypes/StudygroupSearch.class.php @@ -0,0 +1,103 @@ +<?php +# Lifter010: TODO +/** + * StudygroupSearch.class.php + * class to add Studygroup search to Quicksearch + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * @author Michaela Brückner <brueckner@data-quest> + * @copyright 2024 Stud.IP Core-Group + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + */ + +class StudygroupSearch extends SearchType +{ + + /** + * title of the search like "search for courses" or just "courses" + * @return string + */ + public function getTitle(): string + { + return _('Studiengruppen suchen'); + } + + /** + * Returns the results to a given keyword. To get the results is the + * job of this routine and it does not even need to come from a database. + * The results should be an array in the form + * array ( + * array($key, $name), + * array($key, $name), + * ... + * ) + * where $key is an identifier like user_id and $name is a displayed text + * that should appear to represent that ID. + * @param keyword: string + * @param array $contextual_data an associative array with more variables + * @param int $limit maximum number of results (default: all) + * @param int $offset return results starting from this row (default: 0) + * @return array + */ + public function getResults($keyword, $contextual_data = [], $limit = PHP_INT_MAX, $offset = 0): array + { + $search_helper = new StudipSemSearchHelper(); + $search_helper->setParams( + [ + 'quick_search' => $keyword, + 'qs_choose' => $contextual_data['search_sem_qs_choose'] ?? 'all', + 'sem' => $contextual_data['search_sem_sem'] ?? 'all', + 'category' => $contextual_data['search_sem_category'] ?? null, + 'scope_choose' => $contextual_data['search_sem_scope_choose'] ?? null, + 'range_choose' => $contextual_data['search_sem_range_choose'] ?? null, + ], + !(is_object($GLOBALS['perm']) + && $GLOBALS['perm']->have_perm( + Config::Get()->SEM_VISIBILITY_PERM))); + $search_helper->doSearch(); + $result = $search_helper->getSearchResultAsArray(); + + if (empty($result)) { + return []; + } + + $query = "SELECT s.Seminar_id, CONCAT_WS(' ', s.VeranstaltungsNummer, s.name, CONCAT(' (', + IF(semester_courses.semester_id IS NULL, '" . _('unbegrenzt') . "', + IF(COUNT(DISTINCT semester_courses.semester_id) > 1, CONCAT_WS(' - ', (SELECT start_semester.name FROM `semester_data` AS start_semester WHERE start_semester.semester_id = semester_courses.semester_id ORDER BY `beginn` ASC LIMIT 1), (SELECT end_semester.name FROM `semester_data` AS end_semester WHERE end_semester.semester_id = semester_courses.semester_id ORDER BY `beginn` DESC LIMIT 1)), sem1.name)), ')')) AS Name + FROM seminare AS s + LEFT JOIN semester_courses ON (semester_courses.course_id = s.Seminar_id) + LEFT JOIN `semester_data` sem1 ON (semester_courses.semester_id = sem1.semester_id) + LEFT JOIN seminar_user AS su ON (su.Seminar_id = s.Seminar_id AND su.status='dozent') + LEFT JOIN auth_user_md5 USING (user_id) + WHERE s.Seminar_id IN (?) + GROUP BY s.Seminar_id"; + if (Config::get()->IMPORTANT_SEMNUMBER) { + $query .= " ORDER BY s.VeranstaltungsNummer, s.Name"; + } else { + $query .= " ORDER BY s.Name"; + } + $statement = DBManager::get()->prepare($query); + $statement->execute([ + array_slice($result, $offset, $limit) ?: '' + ]); + return $statement->fetchAll(PDO::FETCH_NUM); + } + + + /** + * Returns the path to this file, so that this class can be autoloaded and is + * always available when necessary. + * Should be: "return __file__;" + * + * @return string path to this file + */ + public function includePath(): string + { + return studip_relative_path(__FILE__); + } +} diff --git a/lib/cronjobs/expire_studygroups.class.php b/lib/cronjobs/expire_studygroups.class.php new file mode 100644 index 00000000000..3ad9b934696 --- /dev/null +++ b/lib/cronjobs/expire_studygroups.class.php @@ -0,0 +1,53 @@ +<?php +/** + * expire_studygroups.class.php - Removes studygroups that have reached their expiration date + * and also notifies about upcoming expirations to the founders of the studygroups. + * + * @author Rasmus Fuhse <fuhse@data-quest.de> + * @access public + * @since 6.0 + */ + +class ExpireStudygroups extends CronJob +{ + + public static function getName() + { + return _('Studiengruppen abräumen'); + } + + public static function getDescription() + { + return _('Löscht ablaufende Studiengruppen und benachrichtigt einen Monat vor Ablauf der Studiengruppe über die Löschung.'); + } + + public function execute($last_result, $parameters = []) + { + $statement = DBManager::get()->prepare(" + SELECT `seminare`.* + FROM `seminare` + INNER JOIN `config_values` ON (`config_values`.`range_id` = `seminare`.`Seminar_id` AND `config_values`.`field` = 'STUDYGROUP_EXPIRATION_DATE') + WHERE `config_values`.`value` >= UNIX_TIMESTAMP() + "); + $statement->execute(); + while ($course = Course::buildExisting($statement->fetch(PDO::FETCH_ASSOC))) { + $course->delete(); + } + + //now the notifications + $messaging = new messaging(); + $statement = DBManager::get()->prepare(" + SELECT `seminare`.* + FROM `seminare` + INNER JOIN `config_values` ON (`config_values`.`range_id` = `seminare`.`Seminar_id` AND `config_values`.`field` = 'STUDYGROUP_EXPIRATION_DATE') + WHERE `config_values`.`value` >= UNIX_TIMESTAMP() - 86400 * 31 + AND `config_values`.`value` < UNIX_TIMESTAMP() - 86400 * 30 + "); + $statement->execute([ + 'last_time' => $last_result + ]); + while ($course = Course::buildExisting($statement->fetch(PDO::FETCH_ASSOC))) { + + } + } +} diff --git a/lib/cronjobs/studygroup_expiration.php b/lib/cronjobs/studygroup_expiration.php new file mode 100644 index 00000000000..25ba288b6aa --- /dev/null +++ b/lib/cronjobs/studygroup_expiration.php @@ -0,0 +1,97 @@ +<?php +/** + * studygroup_expiration.php - Delete expired study groups + * + * @author Rasmus Fuhse <fuhse@data-quest.de> + * @author Michaela Brückner <brueckner@data-quest.de> + * @access public + * @since 6.0 + */ + +require_once 'lib/classes/CronJob.php'; + +class StudygroupExpirationJob extends CronJob +{ + /** + * Returns the name of the cronjob. + */ + public static function getName() + { + return _('Studiengruppen aufräumen'); + } + + /** + * Returns the description of the cronjob. + */ + public static function getDescription() + { + return _('Studiengruppen, die abgelaufen sind, werden gelöscht. Zusätzlich werden Gruppengründer:innen benachrichtigt, wenn ihre Studiengruppen in einem Monat ablaufen.'); + } + + /** + * Return the paremeters for this cronjob. + * + * @return Array Parameters. + */ + public static function getParameters() + { + return []; + } + + /** + * Executes the cronjob. + * + * @param mixed $last_result What the last execution of this cronjob + * returned. + * @param Array $parameters Parameters for this cronjob instance which + * were defined during scheduling. + */ + public function execute($last_result, $parameters = []) + { + $statement = DBManager::get()->prepare(" + SELECT `range_id` + FROM `config_values` + WHERE `field` = 'STUDYGROUP_EXPIRATION_DATE' + AND `value` > 0 AND `value` < UNIX_TIMESTAMP() + "); + $statement->execute(); + while ($course_id = $statement->fetch(PDO::FETCH_COLUMN)) { + $course = Course::find($course_id); + $course->delete(); + } + + //now the notifications + $messaging = new messaging(); + $statement = DBManager::get()->prepare(" + SELECT `seminare`.* + FROM `seminare` + INNER JOIN `config_values` ON (`config_values`.`range_id` = `seminare`.`Seminar_id` AND `config_values`.`field` = 'STUDYGROUP_EXPIRATION_DATE') + WHERE `config_values`.`value` >= UNIX_TIMESTAMP() - 86400 * 31 + AND `config_values`.`value` < UNIX_TIMESTAMP() - 86400 * 30 + "); + $statement->execute([ + 'last_time' => $last_result + ]); + while ($course = Course::buildExisting($statement->fetch(PDO::FETCH_ASSOC))) { + foreach ($course->getTeachers() as $course_member) { + setTempLanguage($course_member->user_id); + $message = sprintf( + _('Ihre Studiengruppe %s wird in einem Monat ablaufen und dann automatisch gelöscht werden. Falls Sie die Studiengruppe noch benötigen, ändern Sie in der Verwaltung der Studiengruppe das Ablaufdatum.'), + $course->getFullName() + ); + $subject = _('Ablauf Ihrer Studiengruppe'); + $messaging->insert_message( + $message, + $course_member->user->username, + '____%system%____', + '', + '', + '', + '', + $subject + ); + restoreLanguage(); + } + } + } +} diff --git a/lib/models/Course.php b/lib/models/Course.php index f07440ccb85..4fc32e3bb75 100644 --- a/lib/models/Course.php +++ b/lib/models/Course.php @@ -47,6 +47,7 @@ * @property int $admission_disable_waitlist_move database column * @property int $completion database column * @property string|null $parent_course database column + * @property string|null $expires database column * @property SimpleORMapCollection|CourseTopic[] $topics has_many CourseTopic * @property SimpleORMapCollection|CourseDate[] $dates has_many CourseDate * @property SimpleORMapCollection|CourseExDate[] $ex_dates has_many CourseExDate @@ -247,6 +248,20 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe 'assoc_foreign_key' => 'parent_course', 'order_by' => 'GROUP BY seminar_id ORDER BY VeranstaltungsNummer, Name' ]; + $config['has_and_belongs_to_many']['studygroups'] = [ + 'class_name' => Course::class, + 'thru_table' => 'studygroup_courses', + 'thru_key' => 'course_id', + 'thru_assoc_key' => 'studygroup_id', + 'order_by' => 'ORDER BY VeranstaltungsNummer, Name' + ]; + $config['has_and_belongs_to_many']['connectedcourses'] = [ + 'class_name' => Course::class, + 'thru_table' => 'studygroup_courses', + 'thru_key' => 'studygroup_id', + 'thru_assoc_key' => 'course_id', + 'order_by' => 'ORDER BY VeranstaltungsNummer, Name' + ]; $config['has_many']['tools'] = [ 'class_name' => ToolActivation::class, 'assoc_foreign_key' => 'range_id', @@ -270,6 +285,25 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe 'assoc_foreign_key' => 'range_id', 'on_delete' => 'delete', ]; + $config['has_and_belongs_to_many']['tags'] = [ + 'class_name' => Tag::class, + 'thru_table' => 'tags_relations', + 'thru_key' => 'range_id', + 'thru_assoc_key' => 'tag_id', + 'order_by' => 'ORDER BY `name` ASC', + 'on_delete' => 'delete', + 'on_store' => 'store', + ]; + $config['has_many']['studygroup_proposals'] = [ + 'class_name' => StudygroupCourseProposal::class, + 'assoc_foreign_key' => 'studygroup_id', + 'on_delete' => 'delete', + ]; + $config['has_many']['course_proposals'] = [ + 'class_name' => StudygroupCourseProposal::class, + 'assoc_foreign_key' => 'course_id', + 'on_delete' => 'delete', + ]; $config['default_values']['lesezugriff'] = 1; $config['default_values']['schreibzugriff'] = 1; diff --git a/lib/models/StudiengangTeil.php b/lib/models/StudiengangTeil.php index 13bf7ce37a4..b4a71ebefdb 100644 --- a/lib/models/StudiengangTeil.php +++ b/lib/models/StudiengangTeil.php @@ -70,6 +70,15 @@ class StudiengangTeil extends ModuleManagementModelTreeItem 'on_delete' => 'delete', 'on_store' => 'store' ]; + $config['has_and_belongs_to_many']['studygroups'] = [ + 'class_name' => Course::class, + 'thru_table' => 'studygroup_stgteil', + 'thru_key' => 'stgteil_id', + 'thru_assoc_key' => 'studygroup_id', + 'order_by' => 'ORDER BY `name` ASC', + 'on_delete' => 'delete', + 'on_store' => 'store', + ]; $config['additional_fields']['count_versionen']['get'] = @@ -479,4 +488,21 @@ class StudiengangTeil extends ModuleManagementModelTreeItem }, self::getAssignedFachbereiche('name', 'ASC', ['mvv_stgteil.stgteil_id' => $this->getId()])); } + public function addStudygroup(Course $course) + { + if (in_array($course->status, studygroup_sem_types())) { + if (!StudygroupStgteil::findOneBySQL('`studygroup_id` = ? AND `stgteil_id` = ?', [$course->id, $this->id])) { + $connection = StudygroupStgteil::create([ + 'studygroup_id' => $course->id, + 'stgteil_id' => $this->id + ]); + } + } + } + + public function removeStudygroup(Course $course) + { + StudygroupStgteil::deleteBySQL('`studygroup_id` = ? AND `stgteil_id` = ?', [$course->id, $this->id]); + } + } diff --git a/lib/models/StudygroupCourse.php b/lib/models/StudygroupCourse.php new file mode 100644 index 00000000000..14450acc2a6 --- /dev/null +++ b/lib/models/StudygroupCourse.php @@ -0,0 +1,29 @@ +<?php + +/** + * @license GPL2 or any later version + * + * @property string $id alias column for tag_hash + * @property string $studygroup_id database column + * @property string $course_id database column + * @property int $mkdate database column + * + */ +class StudygroupCourse extends SimpleORMap +{ + protected static function configure($config = []) + { + $config['db_table'] = 'studygroup_courses'; + $config['belongs_to']['course'] = [ + 'class_name' => Course::class, + 'foreign_key' => 'course_id', + 'assoc_foreign_key' => 'seminar_id', + ]; + $config['belongs_to']['studygroup'] = [ + 'class_name' => Course::class, + 'foreign_key' => 'studygroup_id', + 'assoc_foreign_key' => 'seminar_id', + ]; + parent::configure($config); + } +} diff --git a/lib/models/StudygroupCourseProposal.php b/lib/models/StudygroupCourseProposal.php new file mode 100644 index 00000000000..b746fbcbf79 --- /dev/null +++ b/lib/models/StudygroupCourseProposal.php @@ -0,0 +1,36 @@ +<?php + +/** + * @license GPL2 or any later version + * + * @property string $id alias column for tag_hash + * @property string $studygroup_id database column + * @property string $course_id database column + * @property string $proposed_from database column 'course' or 'studygroup' + * @property string $user_id database column + * @property int $mkdate database column + * + */ +class StudygroupCourseProposal extends SimpleORMap +{ + protected static function configure($config = []) + { + $config['db_table'] = 'studygroup_courses_proposals'; + $config['belongs_to']['course'] = [ + 'class_name' => Course::class, + 'foreign_key' => 'course_id', + 'assoc_foreign_key' => 'seminar_id', + ]; + $config['belongs_to']['studygroup'] = [ + 'class_name' => Course::class, + 'foreign_key' => 'studygroup_id', + 'assoc_foreign_key' => 'seminar_id', + ]; + $config['belongs_to']['user'] = [ + 'class_name' => User::class, + 'foreign_key' => 'user_id' + ]; + parent::configure($config); + } + +} diff --git a/lib/models/StudygroupStgteil.php b/lib/models/StudygroupStgteil.php new file mode 100644 index 00000000000..7a0063eb685 --- /dev/null +++ b/lib/models/StudygroupStgteil.php @@ -0,0 +1,19 @@ +<?php + +/** + * @license GPL2 or any later version + * + * @property string $id alias column for tag_hash + * @property string $studygroup_id database column + * @property string $stgteil_id database column + * @property int $mkdate database column + * + */ +class StudygroupStgteil extends SimpleORMap +{ + protected static function configure($config = []) + { + $config['db_table'] = 'studygroup_stgteil'; + parent::configure($config); + } +} diff --git a/lib/models/Tag.php b/lib/models/Tag.php new file mode 100644 index 00000000000..8614e5a7572 --- /dev/null +++ b/lib/models/Tag.php @@ -0,0 +1,55 @@ +<?php + +/** + * @license GPL2 or any later version + * + * @property string $id alias column for tag_hash + * @property string $name database column + * @property int $active database column + * @property int $chdate database column + * @property int $mkdate database column + * + */ +class Tag extends SimpleORMap +{ + protected static function configure($config = []) + { + $config['db_table'] = 'tags'; + $config['has_many']['related_objects'] = [ + 'class_name' => TagRelation::class, + 'assoc_foreign_key' => 'tag_id', + 'order_by' => 'ORDER BY `range_type` ASC, `range_id` ASC', + 'on_store' => 'store', + 'on_delete' => 'delete' + ]; + parent::configure($config); + } + + public static function isActive($tag_name) + { + $tag_name = self::normalizeName($tag_name); + $tag = static::findOneByName($tag_name); + return $tag === false || $tag['active'] > 0; + } + + public static function getByRange($range_id, $range_type) + { + return Tag::findBySQL('INNER JOIN `tags_relations` ON (`tags_relations`.`tag_id` = `tags`.`id`) + WHERE `tags_relations`.`range_id` = :range_id + AND `tags_relations`.`range_type` = :range_type AND `tags`.`active` = 1 ORDER BY `tags`.`name` ASC', [ + 'range_id' => $range_id, + 'range_type' => $range_type + ]); + } + + public static function normalizeName($name) + { + $name = mb_strtolower($name); + $name = str_replace( + [' ', "\n", '|', '#'], + ['-', '-', '-', ''], + $name + ); + return $name; + } +} diff --git a/lib/models/TagRelation.php b/lib/models/TagRelation.php new file mode 100644 index 00000000000..168b7d3f676 --- /dev/null +++ b/lib/models/TagRelation.php @@ -0,0 +1,19 @@ +<?php + +/** + * @license GPL2 or any later version + * + * @property string $id alias column for tag_hash + * @property int $tag_id database column + * @property string $range_id database column + * @property string $range_type database column + * @property int $mkdate database column + */ +class TagRelation extends SimpleORMap +{ + protected static function configure($config = []) + { + $config['db_table'] = 'tags_relations'; + parent::configure($config); + } +} diff --git a/lib/modules/CoreAdmin.php b/lib/modules/CoreAdmin.php index 4c0cc3c1fcd..aded3aeb8dc 100644 --- a/lib/modules/CoreAdmin.php +++ b/lib/modules/CoreAdmin.php @@ -93,6 +93,11 @@ class CoreAdmin extends CorePlugin implements StudipModule $item->setDescription(_('Vorlagen zur Erhebung weiterer Angaben von Teilnehmenden auswählen.')); $navigation->addSubNavigation('additional_data', $item); + $item = new Navigation(_('Verknüpfte Studiengruppen'), 'dispatch.php/course/connectedstudygroups'); + $item->setImage(Icon::create('studygroup')); + $item->setDescription(_('Studiengruppen verknüpfen bzw. verwalten')); + $navigation->addSubNavigation('connectedstudygroups', $item); + } // endif modules only seminars if (Config::get()->VOTE_ENABLE) { diff --git a/lib/modules/CoreStudygroupAdmin.php b/lib/modules/CoreStudygroupAdmin.php index acf59737330..5c74996eca9 100644 --- a/lib/modules/CoreStudygroupAdmin.php +++ b/lib/modules/CoreStudygroupAdmin.php @@ -39,6 +39,7 @@ class CoreStudygroupAdmin extends CorePlugin implements StudipModule $navigation->addSubNavigation('contentmodules', new Navigation(_('Werkzeuge'), "dispatch.php/course/contentmodules?cid={$course_id}")); $navigation->addSubNavigation('main', new Navigation(_('Verwaltung'), "dispatch.php/course/studygroup/edit/?cid={$course_id}")); $navigation->addSubNavigation('avatar', new Navigation(_(' Studiengruppenbild'), "dispatch.php/course/studygroup/avatar?cid={$course_id}")); + $navigation->addSubNavigation('connectedcourses', new Navigation(_('Verknüpfte Veranstaltungen'), "dispatch.php/course/connectedcourses?cid={$course_id}")); if (!$GLOBALS['perm']->have_perm('admin') && Config::get()->VOTE_ENABLE) { $item = new Navigation(_('Fragebögen'), 'dispatch.php/questionnaire/courseoverview'); diff --git a/lib/modules/MyStudygroupsWidget.php b/lib/modules/MyStudygroupsWidget.php new file mode 100644 index 00000000000..170941914b1 --- /dev/null +++ b/lib/modules/MyStudygroupsWidget.php @@ -0,0 +1,38 @@ +<?php +/** + * My Study group widget. Displays a list of own study groups + * + * @author + * @license GPL2 or any later version + * @since Stud.IP 6.0 + */ +class MyStudygroupsWidget extends CorePlugin implements PortalPlugin +{ + public function getPluginName() + { + return _('Meine Studiengruppen'); + } + + public function getMetadata() + { + return [ + 'description' => _('Dieses Widget zeigt eine Liste Ihrer Studiengruppen an.') + ]; + } + + public function getPortalTemplate() + { + $template = $GLOBALS['template_factory']->open('start/my_studygroups'); + + $controller = app(\Trails\Dispatcher::class)->load_controller('my_studygroups'); + $response = $controller->relayWithRedirect('my_studygroups/index/true'); + $template->content = $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']); + $template->icons = [$navigation]; + + return $template; + } +} diff --git a/lib/modules/StudygroupWidget.php b/lib/modules/StudygroupWidget.php new file mode 100644 index 00000000000..beddd4f777f --- /dev/null +++ b/lib/modules/StudygroupWidget.php @@ -0,0 +1,39 @@ +<?php +/** + * Study group widget. Displays a list of possibly interesting study groups + * + * @author + * @license GPL2 or any later version + * @since Stud.IP 6.0 + */ + +class StudygroupWidget extends CorePlugin implements PortalPlugin +{ + public function getPluginName() + { + return _('Für dich vorgeschlagene Studiengruppen'); + } + + public function getMetadata() + { + return [ + 'description' => _('Dieses Widget zeigt eine Liste von Vorschlägen interessanter Studiengruppen an.') + ]; + } + + public function getPortalTemplate() + { + $template = $GLOBALS['template_factory']->open('start/studygroups'); + + $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']); + $template->icons = [$navigation]; + + return $template; + } +} diff --git a/lib/navigation/AdminNavigation.php b/lib/navigation/AdminNavigation.php index 62796d7abcf..fe31b72015e 100644 --- a/lib/navigation/AdminNavigation.php +++ b/lib/navigation/AdminNavigation.php @@ -169,7 +169,13 @@ class AdminNavigation extends Navigation 'dispatch.php/admin/login_style' ) ); - + $navigation->addSubNavigation( + 'tags', + new Navigation( + _('Schlagwortverwaltung'), + 'dispatch.php/admin/tags/index' + ) + ); } $this->addSubNavigation('locations', $navigation); diff --git a/resources/assets/javascripts/lib/global_search.js b/resources/assets/javascripts/lib/global_search.js index 19154a1b0e8..4c46c775bf7 100644 --- a/resources/assets/javascripts/lib/global_search.js +++ b/resources/assets/javascripts/lib/global_search.js @@ -131,6 +131,13 @@ const GlobalSearch = { .appendTo(details); } + // Details: Descriptional text + if (result.found_tag !== null) { + $('<div class="globalsearch-result-description">') + .html(result.found_tag) + .appendTo(details); + } + // Details: Additional information if (result.additional !== null) { $('<div class="globalsearch-result-additional">') diff --git a/resources/assets/javascripts/lib/search.js b/resources/assets/javascripts/lib/search.js index ff3a98d8c46..5c0e3e84b11 100644 --- a/resources/assets/javascripts/lib/search.js +++ b/resources/assets/javascripts/lib/search.js @@ -245,6 +245,13 @@ const Search = { .appendTo(details); } + // Details: Tags + if (result.found_tag !== null) { + $('<div class="search-result-description">') + .html(result.found_tag) + .appendTo(details); + } + if (result.dates !== null) { $('<div class="search-result-dates">') .html(result.dates) diff --git a/resources/assets/stylesheets/scss/forms.scss b/resources/assets/stylesheets/scss/forms.scss index 0a707c8f17c..5c8f3a4d68c 100644 --- a/resources/assets/stylesheets/scss/forms.scss +++ b/resources/assets/stylesheets/scss/forms.scss @@ -562,6 +562,13 @@ form.default { margin-top: 0.5ex; } } + .multiquicksearch > li { + display: flex; + align-items: center; + a.delete_item { + margin-left: 5px; + } + } } form.narrow { diff --git a/resources/assets/stylesheets/scss/studygroup.scss b/resources/assets/stylesheets/scss/studygroup.scss index 8eb0c4ce81b..18cde0008b1 100644 --- a/resources/assets/stylesheets/scss/studygroup.scss +++ b/resources/assets/stylesheets/scss/studygroup.scss @@ -52,3 +52,107 @@ ul.studygroup-gallery { } } } + +.connectedcourses { + .teaser { + font-size: 24px; + } + .connectedstudygroups-empty-background { + @include empty-placeholder-image('network2', false); + } + footer { + background-color: var(--content-color-20); + border-top: 1px solid var(--brand-color-darker); + clear: both; + padding: 0; + height: 58px; + } +} + +.studip-tiles { + display: flex; + align-items: stretch; + flex-wrap: wrap; + > * { + display: flex; + flex-direction: column; + width: 270px; + border: 1px solid var(--content-color-20); + padding: 10px; + margin-bottom: 10px; + align-items: stretch; + margin-right: 10px; + > * { + display: flex; + flex-direction: row; + margin-bottom: 10px; + + &.with-action-menu { + justify-content: space-between; + > * { + margin-right: 0px; + &:first-child { + display: flex; + flex-direction: row; + > * { + margin-right: 10px; + } + } + } + } + .actions { + text-align: right; + } + &:last-child { + margin-bottom: 0px; + } + > * { + margin-right: 10px; + } + } + } +} + +.studip-contents-overview-teaser { + max-width: 782px; + background-color: var(--content-color-20); + background-image: url('#{$image-path}/courseware-keyvisual-negative.svg'); + background-repeat: no-repeat; + background-size: 196px; + background-position-y: 50%; + background-position-x: 24px; + padding: 24px; + margin-bottom: 10px; + + .teaser-content { + padding-left: 220px; + + header { + font-size: 1.5em; + margin-bottom: 0.5em; + } + } +} + +.responsive-display { + .cw-contents-overview-teaser { + max-width: 782px; + background-size: 60%; + background-position-y: 24px; + background-position-x: 50%; + padding: 24px; + margin-bottom: 10px; + + .teaser-content { + padding-top: 28%; + padding-left: 0; + text-align: justify; + + header { + font-size: 1.5em; + margin: 1em 0 0.5em 0; + text-align: center; + } + } + } +} diff --git a/resources/assets/stylesheets/scss/tables.scss b/resources/assets/stylesheets/scss/tables.scss index 2c05362995c..32a26c29038 100644 --- a/resources/assets/stylesheets/scss/tables.scss +++ b/resources/assets/stylesheets/scss/tables.scss @@ -623,10 +623,14 @@ table.default { font-size: $font-size-base; border-left: 1px solid var(--color--table-border); margin-bottom: -2px; - min-height: 26px; + min-height: 30px; padding-bottom: 3px; padding-left: 0.5em; padding-top: 4px; + input[type=text] { + width: auto; // otherwise it can be 100%, so there is no space left for other parts of the actions-area + max-height: 30px; + } } td.actions, th.actions { diff --git a/resources/vue/base-components.js b/resources/vue/base-components.js index d9bb1b79aff..25246429e5a 100644 --- a/resources/vue/base-components.js +++ b/resources/vue/base-components.js @@ -10,6 +10,7 @@ const BaseComponents = { EditableList: defineAsyncComponent(() => import('./components/EditableList.vue')), FileUpload: defineAsyncComponent(() => import('./components/form_inputs/FileUpload.vue')), I18nTextarea: defineAsyncComponent(() => import("./components/I18nTextarea.vue")), + Multiquicksearch: defineAsyncComponent(() => import('./components/Multiquicksearch.vue')), Multiselect: defineAsyncComponent(() => import('./components/Multiselect.vue')), MyCoursesColouredTable: defineAsyncComponent(() => import('./components/form_inputs/MyCoursesColouredTable.vue')), Quicksearch: defineAsyncComponent(() => import('./components/Quicksearch.vue')), diff --git a/resources/vue/components/Multiquicksearch.vue b/resources/vue/components/Multiquicksearch.vue new file mode 100644 index 00000000000..19d8068a462 --- /dev/null +++ b/resources/vue/components/Multiquicksearch.vue @@ -0,0 +1,96 @@ +<template> + <div> + <ul class="clean multiquicksearch"> + <li v-for="(item, index) in items" :key="index"> + <quicksearch :name="name" + :searchtype="searchtype" + :autocomplete="autocomplete" + :modelValue="autocomplete ? item.item_name : item.item_id" + :needle="item.item_name" + :ref="'qs_' + index" + @update:modelValue="(new_id, new_item_name) => editItem(new_id, new_item_name, index)"></quicksearch> + <a href="" class="delete_item" @click.prevent="deleteItem(index)"> + <studip-icon shape="trash" class="text-bottom"></studip-icon> + </a> + </li> + </ul> + <a href="#" @click.prevent="addItem"> + <studip-icon shape="add" class="text-bottom"></studip-icon> + {{ addlabel }} + </a> + </div> +</template> + +<script> +export default { + name: 'multiquicksearch', + inheritAttrs: false, + props: { + name: { + type: String, + required: false + }, + value: { + type: Object, + required: false, + default: [] + }, + searchtype: { + type: String, + required: true + }, + autocomplete: { + type: Boolean, + required: false, + default: false + }, + addlabel: { + type: String, + required: false, + default: "" + } + }, + data () { + return { + items: [] + }; + }, + mounted () { + for (let i in this.value) { + this.items.push({ + item_id: this.autocomplete ? this.value[i] : i, + item_name: this.value[i] + }); + } + }, + watch: { + items: { + handler(newValue, oldValue) { + let new_val = {}; + for (let i in newValue) { + new_val[newValue[i].item_id] = newValue[i].item_name; + } + this.$emit('update:modelValue', new_val); + }, + deep: true + } + }, + methods: { + addItem: function () { + this.items.push({ + item_id: '', + item_name: '' + }); + }, + editItem: function (item_id, item_name, index) { + this.items[index].item_id = item_id; + this.items[index].item_name = item_name; + }, + deleteItem: function (index) { + if (this.items.length > 0) { + this.items.splice(index, 1); + } + } + } +} +</script> diff --git a/resources/vue/components/MyCoursesTables.vue b/resources/vue/components/MyCoursesTables.vue index e3b70ea59fb..30417351871 100644 --- a/resources/vue/components/MyCoursesTables.vue +++ b/resources/vue/components/MyCoursesTables.vue @@ -141,11 +141,6 @@ export default { if (!this.isChild(course)) { courses.push(course); } - if (this.isParent(course)) { - this.getCourses(course.children).forEach(c => { - courses.push(c); - }); - } }); return courses; diff --git a/resources/vue/components/Quicksearch.vue b/resources/vue/components/Quicksearch.vue index dcdd31dfb73..1a61513a77f 100644 --- a/resources/vue/components/Quicksearch.vue +++ b/resources/vue/components/Quicksearch.vue @@ -8,6 +8,7 @@ :name="autocomplete ? name : null" v-model="inputValue" autocomplete="off" + ref="text_input" @blur="reset()" @keydown.up="selectUp" @keydown.down="selectDown" diff --git a/templates/forms/multiquicksearch_input.php b/templates/forms/multiquicksearch_input.php new file mode 100644 index 00000000000..acb28ae2b20 --- /dev/null +++ b/templates/forms/multiquicksearch_input.php @@ -0,0 +1,17 @@ +<div class="formpart" data-form-input-for="<?= htmlReady($name) ?>"> + <label<?= ($this->required ? ' class="studiprequired"' : '') ?> for="<?= $id ?>"> + <span class="textlabel"> + <?= htmlReady($this->title) ?> + </span> + <? if ($this->required) : ?> + <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span> + <? endif ?> + </label> + <multiquicksearch name="<?= htmlReady($name) ?>" + v-model="<?= htmlReady($name) ?>" + <?= ($required ? 'required aria-required="true"' : '') ?> + :value="<?= htmlReady(json_encode($value)) ?>" + id="<?= $id ?>" + <?= $attributes ?>> + </multiquicksearch> +</div> diff --git a/templates/forms/quicksearch_input.php b/templates/forms/quicksearch_input.php index 6fbaff15b0f..46680a1f766 100644 --- a/templates/forms/quicksearch_input.php +++ b/templates/forms/quicksearch_input.php @@ -10,6 +10,7 @@ <span> <quicksearch value="<?= htmlReady($value) ?>" name="<?= htmlReady($name) ?>" + @update:modelValue="(new_id, new_item_name) => { console.log(new_id); this.<?= htmlReady($name) ?> = new_id; }" id="<?= $id ?>" <?= ($this->required ? 'required aria-required="true"' : '') ?> <?= $attributes ?>> diff --git a/templates/header.php b/templates/header.php index fda7471171e..2f1d50992f4 100644 --- a/templates/header.php +++ b/templates/header.php @@ -317,7 +317,11 @@ if ($navigation) { <? endif ?> <? endif ?> <? if (Context::isCourse()) : ?> - <?= CourseAvatar::getAvatar(Context::get()->id)->getImageTag(Avatar::NORMAL, ['class' => 'context-avatar']) ?> + <? if (Context::get()->isStudygroup()) : ?> + <?= StudygroupAvatar::getAvatar(Context::getId())->getImageTag(Avatar::NORMAL, ['class' => 'context-avatar']) ?> + <? else : ?> + <?= CourseAvatar::getAvatar(Context::getId())->getImageTag(Avatar::NORMAL, ['class' => 'context-avatar']) ?> + <? endif ?> <span class="course-type"><?= htmlReady(Context::get()->getFullName('type')) ?>:</span> <span class="course-name"><?= htmlReady(Context::get()->getFullName('name')) ?></span> <? if ($GLOBALS['user']->config->SHOWSEM_ENABLE && !Context::get()->isOpenEnded()): ?> <span class="course-semester">(<?= htmlReady(Context::get()->getTextualSemester()) ?>)</span> diff --git a/templates/start/my_studygroups.php b/templates/start/my_studygroups.php new file mode 100644 index 00000000000..f2770cb03c4 --- /dev/null +++ b/templates/start/my_studygroups.php @@ -0,0 +1,3 @@ +<article class="studip"> + <?= $content ?> +</article> diff --git a/templates/start/studygroups.php b/templates/start/studygroups.php new file mode 100644 index 00000000000..aa7b6e61480 --- /dev/null +++ b/templates/start/studygroups.php @@ -0,0 +1,3 @@ +<article class="studip"> + <?= $proposals ?> +</article> diff --git a/tests/_support/_generated/JsonapiTesterActions.php b/tests/_support/_generated/JsonapiTesterActions.php index 9cf61187d47..df933073ab4 100644 --- a/tests/_support/_generated/JsonapiTesterActions.php +++ b/tests/_support/_generated/JsonapiTesterActions.php @@ -1,4 +1,4 @@ -<?php //[STAMP] 9a0bbbffd781acee72848c128782d162 +<?php //[STAMP] d235e554f01db85e147592e96c21bd04 // phpcs:ignoreFile namespace _generated; -- GitLab