diff options
| author | Elmar Ludwig <elmar.ludwig@uni-osnabrueck.de> | 2025-01-10 14:55:22 +0000 |
|---|---|---|
| committer | Elmar Ludwig <elmar.ludwig@uni-osnabrueck.de> | 2025-01-10 14:55:22 +0000 |
| commit | 339493dbd88f45eee9d044123d13717558047fca (patch) | |
| tree | b5fc6959aaae455e25873804109742d053f3ac5b /lib/modules | |
| parent | 10636268c2303409879014e01eadb3cbe05bd885 (diff) | |
add Vips as CorePlugin, re #4258
Merge request studip/studip!3432
Diffstat (limited to 'lib/modules')
| -rw-r--r-- | lib/modules/VipsModule.php | 471 |
1 files changed, 471 insertions, 0 deletions
diff --git a/lib/modules/VipsModule.php b/lib/modules/VipsModule.php new file mode 100644 index 0000000..9c37c3a --- /dev/null +++ b/lib/modules/VipsModule.php @@ -0,0 +1,471 @@ +<?php +/* + * VipsModule.php - Vips plugin class for Stud.IP + * Copyright (c) 2007-2021 Elmar Ludwig + * + * 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. + */ + +use Courseware\CoursewarePlugin; + +/** + * Vips plugin class for Stud.IP + */ +class VipsModule extends CorePlugin implements StudipModule, SystemPlugin, PrivacyPlugin, CoursewarePlugin +{ + public static ?bool $exam_mode = null; + public static ?VipsModule $instance = null; + public static ?Flexi\Factory $template_factory = null; + + public function __construct() + { + global $perm, $user; + + parent::__construct(); + + self::$instance = $this; + self::$template_factory = new Flexi\Factory($GLOBALS['STUDIP_BASE_PATH'] . '/app/views/vips'); + + NotificationCenter::addObserver($this, 'userDidDelete', 'UserDidDelete'); + NotificationCenter::addObserver($this, 'courseDidDelete', 'CourseDidDelete'); + NotificationCenter::addObserver($this, 'userDidLeaveCourse', 'UserDidLeaveCourse'); + NotificationCenter::addObserver($this, 'userDidMigrate', 'UserDidMigrate'); + NotificationCenter::addObserver($this, 'statusgruppeUserDidCreate', 'StatusgruppeUserDidCreate'); + NotificationCenter::addObserver($this, 'statusgruppeUserDidDelete', 'StatusgruppeUserDidDelete'); + + Exercise::addExerciseType(_('Single Choice'), SingleChoiceTask::class, ['choice-single', '']); + Exercise::addExerciseType(_('Multiple Choice'), MultipleChoiceTask::class, 'choice-multiple'); + Exercise::addExerciseType(_('Multiple Choice Matrix'), MatrixChoiceTask::class, 'choice-matrix'); + Exercise::addExerciseType(_('Freie Antwort'), TextLineTask::class, 'text-line'); + Exercise::addExerciseType(_('Textaufgabe'), TextTask::class, 'text-area'); + Exercise::addExerciseType(_('Lückentext'), ClozeTask::class, ['cloze-input', 'cloze-select', 'cloze-drag']); + Exercise::addExerciseType(_('Zuordnung'), MatchingTask::class, ['matching', 'matching-multiple']); + Exercise::addExerciseType(_('Reihenfolge'), SequenceTask::class, 'sequence'); + + if ($perm->have_perm('root')) { + $nav_item = new Navigation(_('Klausuren'), 'dispatch.php/vips/config'); + Navigation::addItem('/admin/config/vips', $nav_item); + } + + if (Navigation::hasItem('/contents')) { + $nav_item = new Navigation(_('Aufgaben')); + $nav_item->setImage(Icon::create('vips')); + $nav_item->setDescription(_('Erstellen und Verwalten von Aufgabenblättern')); + Navigation::addItem('/contents/vips', $nav_item); + + $sub_item = new Navigation(_('Aufgabenblätter'), 'dispatch.php/vips/pool/assignments'); + $nav_item->addSubNavigation('assignments', $sub_item); + + $sub_item = new Navigation(_('Aufgaben'), 'dispatch.php/vips/pool/exercises'); + $nav_item->addSubNavigation('exercises', $sub_item); + } + + // check for running exams + if (Config::get()->VIPS_EXAM_RESTRICTIONS && !isset(self::$exam_mode)) { + $courses = self::getCoursesWithRunningExams($user->id); + self::$exam_mode = count($courses) > 0; + + if (self::$exam_mode) { + $page = basename($_SERVER['PHP_SELF']); + $path_info = Request::pathInfo(); + $course_id = Context::getId(); + + // redirect page calls if necessary + if (match_route('dispatch.php/jsupdater/get')) { + // always allow jsupdater calls + UpdateInformation::setInformation('vips', ['exam_mode' => true]); + } else if (isset($course_id, $courses[$course_id])) { + // course with running exam is selected, allow all exam actions + if (!match_route('dispatch.php/vips/sheets')) { + header('Location: ' . URLHelper::getURL('dispatch.php/vips/sheets')); + sess()->save(); + die(); + } + } else if (count($courses) === 1) { + // only one course with running exam, redirect there + header('Location: ' . URLHelper::getURL('dispatch.php/vips/sheets', ['cid' => key($courses)])); + sess()->save(); + + die(); + } else if (!match_route('dispatch.php/vips/exam_mode')) { + // forward to overview of all running courses with exams + header('Location: ' . URLHelper::getURL('dispatch.php/vips/exam_mode')); + sess()->save(); + die(); + } + } else { + PageLayout::addHeadElement( + 'script', + [], + 'STUDIP.JSUpdater.register("vips", () => location.reload());' + ); + } + } + } + + /** + * Return whether or not the current user has the given status in a course. + * + * @param string $status status name: 'autor', 'tutor' or 'dozent' + * @param string $course_id course to check + */ + public static function hasStatus(string $status, string $course_id): bool + { + return $course_id && $GLOBALS['perm']->have_studip_perm($status, $course_id); + } + + /** + * Check whether or not the current user has the required status in a course. + * + * @param string $status required status: 'autor', 'tutor' or 'dozent' + * @param string $course_id course to check + * @throws AccessDeniedException if the requirement is not met, an exception is thrown + */ + public static function requireStatus(string $status, string $course_id): void + { + if (!VipsModule::hasStatus($status, $course_id)) { + throw new AccessDeniedException(_('Sie verfügen nicht über die notwendigen Rechte für diese Aktion.')); + } + } + + /** + * Checks whether or not the current user may view an assignment. + * + * @param VipsAssignment|null $assignment assignment to check + * @param int|null $exercise_id check that this exercise is on the assignment (optional) + * @throws AccessDeniedException If the current user doesn't have access, an exception is thrown + */ + public static function requireViewPermission(?VipsAssignment $assignment, ?int $exercise_id = null): void + { + if (!$assignment || !$assignment->checkViewPermission()) { + throw new AccessDeniedException(_('Sie haben keinen Zugriff auf dieses Aufgabenblatt!')); + } + + if ($exercise_id && !$assignment->hasExercise($exercise_id)) { + throw new AccessDeniedException(_('Sie haben keinen Zugriff auf diese Aufgabe!')); + } + } + + /** + * Checks whether or not the current user may edit an assignment. + * + * @param VipsAssignment|null $assignment assignment to check + * @param int|null $exercise_id check that this exercise is on the assignment (optional) + * @throws AccessDeniedException If the current user doesn't have access, an exception is thrown + */ + public static function requireEditPermission(?VipsAssignment $assignment, ?int $exercise_id = null): void + { + if (!$assignment || !$assignment->checkEditPermission()) { + throw new AccessDeniedException(_('Sie haben keinen Zugriff auf dieses Aufgabenblatt!')); + } + + if ($exercise_id && !$assignment->hasExercise($exercise_id)) { + throw new AccessDeniedException(_('Sie haben keinen Zugriff auf diese Aufgabe!')); + } + } + + /** + * Get all courses where the user is at least tutor and Vips is activated. + * + * @return array with all course ids, null if no courses + */ + public static function getActiveCourses(string $user_id): array + { + $plugin_manager = PluginManager::getInstance(); + $vips_plugin_id = VipsModule::$instance->getPluginId(); + + $sql = "JOIN seminar_user USING(Seminar_id) + WHERE user_id = ? AND seminar_user.status IN ('dozent', 'tutor') + ORDER BY (SELECT MIN(beginn) FROM semester_data + JOIN semester_courses USING(semester_id) + WHERE course_id = Seminar_id) DESC, Name"; + $courses = Course::findBySQL($sql, [$user_id]); + + // remove courses where Vips is not active + foreach ($courses as $key => $course) { + if (!$plugin_manager->isPluginActivated($vips_plugin_id, $course->id)) { + unset($courses[$key]); + } + } + + return $courses; + } + + /** + * Get all courses with currently running exams for the given user. + * + * @param string $user_id The user id + * + * @return array associative array of course ids and course names + */ + public static function getCoursesWithRunningExams(string $user_id): array + { + $db = DBManager::get(); + + $courses = []; + + $sql = "SELECT DISTINCT seminare.Seminar_id, seminare.Name, etask_assignments.id + FROM etask_assignments + JOIN seminar_user ON seminar_user.Seminar_id = etask_assignments.range_id + JOIN seminare USING(Seminar_id) + WHERE etask_assignments.type = 'exam' + AND etask_assignments.start <= UNIX_TIMESTAMP() + AND etask_assignments.end > UNIX_TIMESTAMP() + AND seminar_user.user_id = ? + AND seminar_user.status = 'autor' + ORDER BY seminare.Name"; + $stmt = $db->prepare($sql); + $stmt->execute([$user_id]); + + foreach ($stmt as $row) { + $assignment = VipsAssignment::find($row['id']); + $ip_range = $assignment->options['ip_range']; + + if ($assignment->isVisible($user_id)) { + if (strlen($ip_range) > 0 && $assignment->checkIPAccess($_SERVER['REMOTE_ADDR'])) { + $courses[$row['Seminar_id']] = $row['Name']; + } + } + } + + return $courses; + } + + public function setupExamNavigation() + { + $navigation = new Navigation(''); + + $start = Navigation::getItem('/start'); + $start->setURL('dispatch.php/vips/exam_mode'); + $navigation->addSubNavigation('start', $start); + + $course = new Navigation(_('Veranstaltung')); + $navigation->addSubNavigation('course', $course); + + $vips = new Navigation($this->getPluginName()); + $vips->setImage(Icon::create('vips')); + $course->addSubNavigation('vips', $vips); + + $nav_item = new Navigation(_('Aufgabenblätter'), 'dispatch.php/vips/sheets'); + $vips->addSubNavigation('sheets', $nav_item); + + $links = new Navigation('Links'); + $links->addSubNavigation('logout', new Navigation(_('Logout'), 'logout.php')); + $navigation->addSubNavigation('links', $links); + + Config::get()->PERSONAL_NOTIFICATIONS_ACTIVATED = 0; + PageLayout::addStyle('#navigation-level-1, #navigation-level-2, #context-title { display: none; }'); + PageLayout::addCustomQuicksearch('<div style="width: 64px;"></div>'); + Navigation::setRootNavigation($navigation); + } + + public function getIconNavigation($course_id, $last_visit, $user_id) + { + if (VipsModule::hasStatus('tutor', $course_id)) { + // find all uncorrected exercises in finished assignments in this course + // Added JOIN with seminar_user to filter out lecturer/tutor solutions. + $new_items = VipsSolution::countBySql( + "JOIN etask_assignments ON etask_responses.assignment_id = etask_assignments.id + LEFT JOIN seminar_user + ON seminar_user.Seminar_id = etask_assignments.range_id + AND seminar_user.user_id = etask_responses.user_id + WHERE etask_assignments.range_id = ? + AND etask_assignments.type IN ('exam', 'practice', 'selftest') + AND etask_assignments.end <= UNIX_TIMESTAMP() + AND etask_responses.state = 0 + AND IFNULL(seminar_user.status, 'autor') = 'autor'", + [$course_id] + ); + + $message = ngettext('%d unkorrigierte Lösung', '%d unkorrigierte Lösungen', $new_items); + } else { + // find all active assignments not yet seen by the student + $assignments = VipsAssignment::findBySQL( + "LEFT JOIN etask_assignment_attempts + ON etask_assignment_attempts.assignment_id = etask_assignments.id + AND etask_assignment_attempts.user_id = ? + WHERE etask_assignments.range_id = ? + AND etask_assignments.type IN ('exam', 'practice', 'selftest') + AND etask_assignments.start <= UNIX_TIMESTAMP() + AND (etask_assignments.end IS NULL OR etask_assignments.end > UNIX_TIMESTAMP()) + AND etask_assignment_attempts.user_id IS NULL", + [$user_id, $course_id] + ); + + $new_items = 0; + + foreach ($assignments as $assignment) { + if ($assignment->isVisible($user_id)) { + ++$new_items; + } + } + + $message = ngettext('%d neues Aufgabenblatt', '%d neue Aufgabenblätter', $new_items); + } + + $overview_message = $this->getPluginName(); + $icon = Icon::create('vips'); + + if ($new_items > 0) { + $overview_message = sprintf($message, $new_items); + $icon = Icon::create('vips', Icon::ROLE_NEW); + } + + $icon_navigation = new Navigation($this->getPluginName(), 'dispatch.php/vips/sheets'); + $icon_navigation->setImage($icon->copyWithAttributes(['title' => $overview_message])); + + return $icon_navigation; + } + + public function getInfoTemplate($course_id) + { + return null; + } + + public function getTabNavigation($course_id) + { + $navigation = new Navigation($this->getPluginName()); + $navigation->setImage(Icon::create('vips')); + + $nav_item = new Navigation(_('Aufgabenblätter'), 'dispatch.php/vips/sheets'); + $navigation->addSubNavigation('sheets', $nav_item); + + $nav_item = new Navigation(_('Ergebnisse'), 'dispatch.php/vips/solutions'); + $navigation->addSubNavigation('solutions', $nav_item); + + return ['vips' => $navigation]; + } + + public function getMetadata() + { + $metadata['category'] = _('Inhalte und Aufgabenstellungen'); + $metadata['displayname'] = _('Aufgaben und Prüfungen'); + $metadata['summary'] = + _('Erstellung und Durchführung von Übungen, Tests und Klausuren'); + $metadata['description'] = + _('Mit diesem Werkzeug können Übungen, Tests und Klausuren online vorbereitet und durchgeführt werden. ' . + 'Die Lehrenden erhalten eine Übersicht darüber, welche Teilnehmenden eine Übung oder einen ' . + 'Test mit welchem Ergebnis abgeschlossen haben. Im Gegensatz zu herkömmlichen Übungszetteln ' . + 'oder Klausurbögen sind in Stud.IP alle Texte gut lesbar und sortiert abgelegt. Lehrende ' . + 'erhalten sofort einen Überblick darüber, was noch zu korrigieren ist. Neben allgemein ' . + 'üblichen Fragetypen wie Multiple Choice und Freitextantwort verfügt das Werkzeug auch über ' . + 'ungewöhnlichere, aber didaktisch durchaus sinnvolle Fragetypen wie Lückentext und Zuordnung.'); + $metadata['keywords'] = + _('Einsatz bei Hausaufgaben und Präsenzprüfungen; Reduzierter Arbeitsaufwand bei der Auswertung; ' . + 'Sortierte Übersicht der eingereichten Ergebnisse; Single-, Multiple-Choice- und Textaufgaben, ' . + 'Lückentexte und Zuordnungen; Notwendige Korrekturen und erzielte Punktzahlen auf einen Blick'); + $metadata['icon'] = Icon::create('vips'); + + return $metadata; + } + + public function userDidDelete($event, $user) + { + // delete all personal assignments + VipsAssignment::deleteBySQL('range_id = ?', [$user->id]); + + // delete in etask_responses + VipsSolution::deleteBySQL('user_id = ?', [$user->id]); + + // delete start times and group memberships + VipsAssignmentAttempt::deleteBySQL('user_id = ?', [$user->id]); + VipsGroupMember::deleteBySQL('user_id = ?', [$user->id]); + } + + public function courseDidDelete($event, $course) + { + // delete all assignments in course + VipsAssignment::deleteBySQL('range_id = ?', [$course->id]); + + // delete other course related info + VipsBlock::deleteBySQL('range_id = ?', [$course->id]); + } + + public function userDidLeaveCourse($event, $course_id, $user_id) + { + // terminate group membership when leaving a course + $group_member = VipsGroupMember::findOneBySQL( + 'JOIN statusgruppen ON statusgruppe_id = group_id WHERE range_id = ? AND user_id = ? AND end IS NULL', + [$course_id, $user_id] + ); + + if ($group_member) { + $group_member->end = time(); + $group_member->store(); + } + } + + public function userDidMigrate($event, $user_id, $new_id) + { + $db = DBManager::get(); + + $db->execute('UPDATE IGNORE etask_assignment_attempts SET user_id = ? WHERE user_id = ?', [$new_id, $user_id]); + $db->execute('UPDATE etask_tasks SET user_id = ? WHERE user_id = ?', [$new_id, $user_id]); + + $db->execute('UPDATE IGNORE etask_responses SET user_id = ? WHERE user_id = ?', [$new_id, $user_id]); + $db->execute('UPDATE etask_tests SET user_id = ? WHERE user_id = ?', [$new_id, $user_id]); + } + + public function statusgruppeUserDidCreate($event, $statusgruppe_user) + { + VipsGroupMember::create([ + 'group_id' => $statusgruppe_user->statusgruppe_id, + 'user_id' => $statusgruppe_user->user_id, + 'start' => time() + ]); + } + + public function statusgruppeUserDidDelete($event, $statusgruppe_user) + { + $member = VipsGroupMember::findOneBySQL( + 'group_id = ? AND user_id = ? AND end IS NULL', + [$statusgruppe_user->statusgruppe_id, $statusgruppe_user->user_id] + ); + + if ($member) { + $member->end = time(); + $member->store(); + } + } + + /** + * Export available data of a given user into a storage object + * (an instance of the StoredUserData class) for that user. + * + * @param StoredUserData $store object to store data into + */ + public function exportUserData(StoredUserData $store) + { + $db = DBManager::get(); + + $data = $db->fetchAll('SELECT * FROM etask_group_members WHERE user_id = ?', [$store->user_id]); + $store->addTabularData(_('Aufgaben-Gruppenzuordnung'), 'etask_group_members', $data); + } + + /** + * Implement this method to register more block types. + * + * You get the current list of block types and return an updated list + * containing your own block types. + */ + public function registerBlockTypes(array $otherBlockTypes): array + { + $otherBlockTypes[] = Courseware\BlockTypes\TestBlock::class; + + return $otherBlockTypes; + } + + /** + * Implement this method to register more container types. + * + * You get the current list of container types and return an updated list + * containing your own container types. + */ + public function registerContainerTypes(array $otherContainerTypes): array + { + return $otherContainerTypes; + } +} |
