aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/controllers/admin/tags.php67
-rw-r--r--app/controllers/course/connectedcourses.php234
-rw-r--r--app/controllers/course/connectedstudygroups.php315
-rw-r--r--app/controllers/course/overview.php11
-rw-r--r--app/controllers/course/studygroup.php209
-rw-r--r--app/controllers/course/wizard.php11
-rw-r--r--app/controllers/my_courses.php34
-rw-r--r--app/controllers/my_studygroups.php100
-rw-r--r--app/controllers/search/angebot.php12
-rw-r--r--app/controllers/search/globalsearch.php28
-rw-r--r--app/controllers/search/studiengaenge.php14
-rw-r--r--app/views/admin/tags/index.php65
-rw-r--r--app/views/admin/tags/view_objects.php51
-rw-r--r--app/views/course/connectedcourses/_course_to_connect.php21
-rw-r--r--app/views/course/connectedcourses/connect.php78
-rw-r--r--app/views/course/connectedcourses/index.php112
-rw-r--r--app/views/course/connectedstudygroups/_studygroup_to_connect.php21
-rw-r--r--app/views/course/connectedstudygroups/connect.php79
-rw-r--r--app/views/course/connectedstudygroups/index.php110
-rw-r--r--app/views/course/overview/index.php4
-rw-r--r--app/views/course/studygroup/details.php19
-rw-r--r--app/views/course/studygroup/edit.php27
-rw-r--r--app/views/course/studygroup/widget.php88
-rw-r--r--app/views/course/wizard/step.php14
-rw-r--r--app/views/course/wizard/steps/basicdata/index_studygroup.php7
-rw-r--r--app/views/course/wizard/steps/studygroups/index.php110
-rw-r--r--app/views/my_studygroups/_course.php46
-rw-r--r--app/views/my_studygroups/index.php17
-rw-r--r--app/views/my_studygroups/proposals.php31
-rw-r--r--app/views/search/studiengaenge/verlauf.php55
-rw-r--r--app/views/studygroup/browse.php54
-rw-r--r--db/migrations/6.0.38_improved_studygroups.php305
-rw-r--r--lib/admissionrules/connectedcourseadmission/ConnectedcourseAdmission.class.php121
-rw-r--r--lib/admissionrules/connectedcourseadmission/rule.manifest2
-rw-r--r--lib/admissionrules/connectedcourseadmission/templates/configure.php5
-rw-r--r--lib/admissionrules/connectedcourseadmission/templates/info.php1
-rw-r--r--lib/classes/Avatar.php2
-rw-r--r--lib/classes/MyRealmModel.php95
-rw-r--r--lib/classes/StudipController.php6
-rw-r--r--lib/classes/StudygroupModel.php246
-rw-r--r--lib/classes/admission/AdmissionRule.php2
-rw-r--r--lib/classes/admission/CourseSet.php39
-rw-r--r--lib/classes/coursewizardsteps/BasicDataWizardStep.php50
-rw-r--r--lib/classes/forms/MultiquicksearchInput.php31
-rw-r--r--lib/classes/globalsearch/GlobalSearchStudygroups.php315
-rw-r--r--lib/classes/searchtypes/StudygroupSearch.class.php103
-rw-r--r--lib/cronjobs/expire_studygroups.class.php53
-rw-r--r--lib/cronjobs/studygroup_expiration.php97
-rw-r--r--lib/models/Course.php34
-rw-r--r--lib/models/StudiengangTeil.php26
-rw-r--r--lib/models/StudygroupCourse.php29
-rw-r--r--lib/models/StudygroupCourseProposal.php36
-rw-r--r--lib/models/StudygroupStgteil.php19
-rw-r--r--lib/models/Tag.php55
-rw-r--r--lib/models/TagRelation.php19
-rw-r--r--lib/modules/CoreAdmin.php5
-rw-r--r--lib/modules/CoreStudygroupAdmin.php1
-rw-r--r--lib/modules/MyStudygroupsWidget.php38
-rw-r--r--lib/modules/StudygroupWidget.php39
-rw-r--r--lib/navigation/AdminNavigation.php8
-rw-r--r--resources/assets/javascripts/lib/global_search.js7
-rw-r--r--resources/assets/javascripts/lib/search.js7
-rw-r--r--resources/assets/stylesheets/scss/forms.scss7
-rw-r--r--resources/assets/stylesheets/scss/studygroup.scss104
-rw-r--r--resources/assets/stylesheets/scss/tables.scss6
-rw-r--r--resources/vue/base-components.js1
-rw-r--r--resources/vue/components/Multiquicksearch.vue96
-rw-r--r--resources/vue/components/MyCoursesTables.vue5
-rw-r--r--resources/vue/components/Quicksearch.vue1
-rw-r--r--templates/forms/multiquicksearch_input.php17
-rw-r--r--templates/forms/quicksearch_input.php1
-rw-r--r--templates/header.php6
-rw-r--r--templates/start/my_studygroups.php3
-rw-r--r--templates/start/studygroups.php3
-rw-r--r--tests/_support/_generated/JsonapiTesterActions.php2
75 files changed, 3870 insertions, 222 deletions
diff --git a/app/controllers/admin/tags.php b/app/controllers/admin/tags.php
new file mode 100644
index 0000000..03819bd
--- /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 0000000..dcbc3ae
--- /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 0000000..9ef6b92
--- /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 e1da00e..4313cdc 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 981a152..c95c20e 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 b4244b6..ad289ed 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 c7a6771..a8d9797 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 579d47e..ac55cf4 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 0c0f402..4f749d4 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 b31663f..fd82103 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'
+ );
}
/**
@@ -171,6 +182,23 @@ class Search_GlobalsearchController extends AuthenticatedController
}
/**
+ * @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.
*/
private function addInfoText()
diff --git a/app/controllers/search/studiengaenge.php b/app/controllers/search/studiengaenge.php
index 8f08c32..1745ba6 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 0000000..4d5067a
--- /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 0000000..0d90f66
--- /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 0000000..0235785
--- /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 0000000..4c6a2e5
--- /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 0000000..fa8006b
--- /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 0000000..d438cf1
--- /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 0000000..9f7c094
--- /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 0000000..4611b6c
--- /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 e538a6f..03fc848 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 26a3ef5..7fbbc39 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 e3189f7..3891e94 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 0000000..fc0f133
--- /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 0fe1dfc..214515a 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 9f75da5..10368b9 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 0000000..b7327a9
--- /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 37c081e..e554ddb 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 6fd2d20..cfdd2e2 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 0000000..7347849
--- /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 4464e66..540f46e 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 6362d6a..cf8fe3e 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 0000000..2ed7124
--- /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 0000000..40f32ac
--- /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 0000000..a86c218
--- /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 0000000..4a60973
--- /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 0000000..123c7bc
--- /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 bb3a915..b958655 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 5425288..f3ab44c 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 e0cfd05..b7c55fd 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 9fd44b3..09cec16 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 cff3e35..9855165 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 d93cfb0..81bd17f 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 4b401ee..18892f0 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 0000000..c7a9a02
--- /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 0000000..a9f1398
--- /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 0000000..9bfef6c
--- /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 0000000..3ad9b93
--- /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 0000000..25ba288
--- /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 f07440c..4fc32e3 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 13bf7ce..b4a71eb 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 0000000..14450ac
--- /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 0000000..b746fbc
--- /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 0000000..7a0063e
--- /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 0000000..8614e5a
--- /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 0000000..168b7d3
--- /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 4c0cc3c..aded3ae 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 acf5973..5c74996 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 0000000..1709419
--- /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 0000000..beddd4f
--- /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 62796d7..fe31b72 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 19154a1..4c46c77 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 ff3a98d..5c0e3e8 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 0a707c8..5c8f3a4 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 8eb0c4c..18cde00 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 2c05362..32a26c2 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 d9bb1b7..2524642 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 0000000..19d8068
--- /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 e3b70ea..3041735 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 dcdd31d..1a61513 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 0000000..acb28ae
--- /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 6fbaff1..46680a1 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 fda7471..2f1d509 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 0000000..f2770cb
--- /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 0000000..aa7b6e6
--- /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 9cf6118..df93307 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;