aboutsummaryrefslogtreecommitdiff
path: root/app/controllers/vips
diff options
context:
space:
mode:
authorElmar Ludwig <elmar.ludwig@uni-osnabrueck.de>2025-01-10 14:55:22 +0000
committerElmar Ludwig <elmar.ludwig@uni-osnabrueck.de>2025-01-10 14:55:22 +0000
commit339493dbd88f45eee9d044123d13717558047fca (patch)
treeb5fc6959aaae455e25873804109742d053f3ac5b /app/controllers/vips
parent10636268c2303409879014e01eadb3cbe05bd885 (diff)
add Vips as CorePlugin, re #4258
Merge request studip/studip!3432
Diffstat (limited to 'app/controllers/vips')
-rw-r--r--app/controllers/vips/admin.php208
-rw-r--r--app/controllers/vips/api.php256
-rw-r--r--app/controllers/vips/config.php95
-rw-r--r--app/controllers/vips/exam_mode.php29
-rw-r--r--app/controllers/vips/pool.php473
-rw-r--r--app/controllers/vips/sheets.php2305
-rw-r--r--app/controllers/vips/solutions.php2521
7 files changed, 5887 insertions, 0 deletions
diff --git a/app/controllers/vips/admin.php b/app/controllers/vips/admin.php
new file mode 100644
index 0000000..92c6cc7
--- /dev/null
+++ b/app/controllers/vips/admin.php
@@ -0,0 +1,208 @@
+<?php
+/**
+ * vips/admin.php - course administration controller
+ *
+ * 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 Elmar Ludwig
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ */
+
+class Vips_AdminController extends AuthenticatedController
+{
+ /**
+ * Edit or create a block in the course.
+ */
+ public function edit_block_action()
+ {
+ Navigation::activateItem('/course/vips/sheets');
+ PageLayout::setHelpKeyword('Basis.Vips');
+
+ $block_id = Request::int('block_id');
+
+ if ($block_id) {
+ $block = VipsBlock::find($block_id);
+ } else {
+ $block = new VipsBlock();
+ $block->range_id = Context::getId();
+ }
+
+ VipsModule::requireStatus('tutor', $block->range_id);
+
+ $this->block = $block;
+ $this->groups = Statusgruppen::findBySeminar_id($block->range_id);
+ }
+
+ /**
+ * Store changes to a block.
+ */
+ public function store_block_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $block_id = Request::int('block_id');
+ $group_id = Request::option('group_id');
+
+ if ($block_id) {
+ $block = VipsBlock::find($block_id);
+ } else {
+ $block = new VipsBlock();
+ $block->range_id = Context::getId();
+ }
+
+ VipsModule::requireStatus('tutor', $block->range_id);
+
+ $block->name = Request::get('block_name');
+ $block->group_id = $group_id ?: null;
+ $block->visible = $group_id !== '';
+
+ if (!Request::int('block_grouped')) {
+ $block->weight = null;
+ } else if ($block->weight === null) {
+ $block->weight = 0;
+
+ if ($block_id) {
+ // sum up individual assignment weights for total block weight
+ foreach (VipsAssignment::findByBlock_id($block_id) as $assignment) {
+ $block->weight += $assignment->weight;
+ }
+ }
+ }
+
+ $block->store();
+
+ PageLayout::postSuccess(sprintf(_('Der Block „%s“ wurde gespeichert.'), htmlReady($block->name)));
+
+ $this->redirect($this->url_for('vips/sheets', ['group' => 1]));
+ }
+
+ /**
+ * Delete a block from the course.
+ */
+ public function delete_block_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $block_id = Request::int('block_id');
+ $block = VipsBlock::find($block_id);
+ $block_name = $block->name;
+
+ VipsModule::requireStatus('tutor', $block->range_id);
+
+ if ($block->delete()) {
+ PageLayout::postSuccess(sprintf(_('Der Block „%s“ wurde gelöscht.'), htmlReady($block_name)));
+ }
+
+ $this->redirect('vips/sheets');
+ }
+
+ /**
+ * Stores the weights of blocks, sheets and exams
+ */
+ public function store_weight_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment_weight = Request::floatArray('assignment_weight');
+ $block_weight = Request::floatArray('block_weight');
+
+ foreach ($assignment_weight as $assignment_id => $weight) {
+ $assignment = VipsAssignment::find($assignment_id);
+ VipsModule::requireEditPermission($assignment);
+
+ $assignment->weight = $weight;
+ $assignment->store();
+ }
+
+ foreach ($block_weight as $block_id => $weight) {
+ $block = VipsBlock::find($block_id);
+ VipsModule::requireStatus('tutor', $block->range_id);
+
+ $block->weight = $weight;
+ $block->store();
+ }
+
+ $this->redirect('vips/solutions');
+ }
+
+ /**
+ * Edit the grade distribution settings.
+ */
+ public function edit_grades_action()
+ {
+ Navigation::activateItem('/course/vips/solutions');
+ PageLayout::setHelpKeyword('Basis.VipsErgebnisse');
+
+ $course_id = Context::getId();
+ VipsModule::requireStatus('tutor', $course_id);
+
+ $grades = ['1,0', '1,3', '1,7', '2,0', '2,3', '2,7', '3,0', '3,3', '3,7', '4,0'];
+ $percentages = array_fill(0, count($grades), '');
+ $comments = array_fill(0, count($grades), '');
+ $settings = CourseConfig::get($course_id);
+
+ foreach ($settings->VIPS_COURSE_GRADES as $value) {
+ $index = array_search($value['grade'], $grades);
+
+ if ($index !== false) {
+ $percentages[$index] = $value['percent'];
+ $comments[$index] = $value['comment'];
+ }
+ }
+
+ $this->grades = $grades;
+ $this->grade_settings = $settings->VIPS_COURSE_GRADES;
+ $this->percentages = $percentages;
+ $this->comments = $comments;
+ }
+
+ /**
+ * Stores the distribution of grades
+ */
+ public function store_grades_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $course_id = Context::getId();
+ VipsModule::requireStatus('tutor', $course_id);
+
+ $grades = ['1,0', '1,3', '1,7', '2,0', '2,3', '2,7', '3,0', '3,3', '3,7', '4,0'];
+ $percentages = Request::floatArray('percentage');
+ $comments = Request::getArray('comment');
+ $grade_settings = [];
+ $percent_last = 101;
+ $error = false;
+
+ foreach ($percentages as $i => $percent) {
+ if ($percent) {
+ $grade_settings[] = [
+ 'grade' => $grades[$i],
+ 'percent' => $percent,
+ 'comment' => trim($comments[$i])
+ ];
+
+ if ($percent < 0 || $percent > 100) {
+ PageLayout::postError(_('Die Notenwerte müssen zwischen 0 und 100 liegen!'));
+ $error = true;
+ } else if ($percent_last <= $percent) {
+ PageLayout::postError(sprintf(_('Die Notenwerte müssen monoton absteigen (%s > %s)!'), $percent_last, $percent));
+ $error = true;
+ }
+
+ $percent_last = $percent;
+ }
+ }
+
+ if (!$error) {
+ $settings = CourseConfig::get($course_id);
+ $settings->store('VIPS_COURSE_GRADES', $grade_settings);
+
+ PageLayout::postSuccess(_('Die Notenwerte wurden eingetragen.'));
+ }
+
+ $this->redirect('vips/solutions');
+ }
+}
diff --git a/app/controllers/vips/api.php b/app/controllers/vips/api.php
new file mode 100644
index 0000000..8c2dbe0
--- /dev/null
+++ b/app/controllers/vips/api.php
@@ -0,0 +1,256 @@
+<?php
+/**
+ * vips/api.php - API controller for Vips
+ *
+ * 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 Elmar Ludwig
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ */
+
+class Vips_ApiController extends AuthenticatedController
+{
+ public function assignments_action($range_id)
+ {
+ if ($range_id !== $GLOBALS['user']->id) {
+ VipsModule::requireStatus('tutor', $range_id);
+ }
+
+ $assignments = VipsAssignment::findByRangeId($range_id);
+
+ $data = [];
+
+ foreach ($assignments as $assignment) {
+ if ($assignment->type !== 'exam') {
+ $data[] = [
+ 'id' => (string) $assignment->id,
+ 'title' => $assignment->test->title,
+ 'type' => $assignment->type,
+ 'icon' => $assignment->getTypeIcon()->getShape(),
+ 'start' => date('d.m.Y, H:i', $assignment->start),
+ 'end' => date('d.m.Y, H:i', $assignment->end),
+ 'active' => $assignment->active,
+ 'block' => $assignment->block_id ? $assignment->block->name : null
+ ];
+ }
+ }
+
+ $this->render_json($data);
+ }
+
+ public function assignment_action($assignment_id)
+ {
+ $assignment = VipsAssignment::find($assignment_id);
+ $user_id = $GLOBALS['user']->id;
+
+ VipsModule::requireViewPermission($assignment);
+
+ $released = $assignment->releaseStatus($user_id);
+
+ if ($assignment->type === 'exam') {
+ throw new AccessDeniedException(_('Sie haben keinen Zugriff auf dieses Aufgabenblatt!'));
+ }
+
+ if (
+ !$assignment->checkAccess($user_id)
+ && $released < VipsAssignment::RELEASE_STATUS_CORRECTIONS
+ ) {
+ throw new AccessDeniedException(_('Das Aufgabenblatt kann zur Zeit nicht bearbeitet werden.'));
+ }
+
+ // enter user start time the moment he/she first clicks on any exercise
+ if (!$assignment->checkEditPermission()) {
+ $assignment->recordAssignmentAttempt($user_id);
+ }
+
+ $data = [
+ 'id' => (string) $assignment->id,
+ 'title' => $assignment->test->title,
+ 'type' => $assignment->type,
+ 'icon' => $assignment->getTypeIcon()->getShape(),
+ 'start' => date('d.m.Y, H:i', $assignment->start),
+ 'end' => date('d.m.Y, H:i', $assignment->end),
+ 'active' => $assignment->active,
+ 'block' => $assignment->block_id ? $assignment->block->name : null,
+ 'reset_allowed' => $assignment->isRunning($user_id) && $assignment->isResetAllowed(),
+ 'points' => $assignment->test->getTotalPoints(),
+ 'release_status' => $released,
+ 'exercises' => []
+ ];
+
+ foreach ($assignment->getExerciseRefs($user_id) as $exercise_ref) {
+ $template = $this->courseware_template($assignment, $exercise_ref, $released);
+ $exercise = $exercise_ref->exercise;
+
+ $data['exercises'][] = [
+ 'id' => $exercise->id,
+ 'type' => $exercise->type,
+ 'title' => $exercise->title,
+ 'template' => $template->render(),
+ 'item_count' => $exercise->itemCount(),
+ 'show_solution' => $template->show_solution
+ ];
+ }
+
+ $this->render_json($data);
+ }
+
+ public function exercise_action($assignment_id, $exercise_id)
+ {
+ $assignment = VipsAssignment::find($assignment_id);
+ $user_id = $GLOBALS['user']->id;
+
+ VipsModule::requireViewPermission($assignment, $exercise_id);
+
+ $released = $assignment->releaseStatus($user_id);
+
+ if ($assignment->type === 'exam') {
+ throw new AccessDeniedException(_('Sie haben keinen Zugriff auf dieses Aufgabenblatt!'));
+ }
+
+ if (
+ !$assignment->checkAccess($user_id)
+ && $released < VipsAssignment::RELEASE_STATUS_CORRECTIONS
+ ) {
+ throw new AccessDeniedException(_('Das Aufgabenblatt kann zur Zeit nicht bearbeitet werden.'));
+ }
+
+ // enter user start time the moment he/she first clicks on any exercise
+ if (!$assignment->checkEditPermission()) {
+ $assignment->recordAssignmentAttempt($user_id);
+ }
+
+ $exercise_ref = VipsExerciseRef::find([$assignment->test_id, $exercise_id]);
+ $template = $this->courseware_template($assignment, $exercise_ref, $released);
+ $exercise = $exercise_ref->exercise;
+
+ $data = [
+ 'id' => $exercise->id,
+ 'type' => $exercise->type,
+ 'title' => $exercise->title,
+ 'template' => $template->render(),
+ 'item_count' => $exercise->itemCount(),
+ 'show_solution' => $template->show_solution
+ ];
+
+ $this->render_json($data);
+ }
+
+ private function courseware_template($assignment, $exercise_ref, $released)
+ {
+ $user_id = $GLOBALS['user']->id;
+ $exercise = $exercise_ref->exercise;
+ $solution = $assignment->getSolution($user_id, $exercise->id);
+ $max_tries = $assignment->getMaxTries();
+ $max_points = $exercise_ref->points;
+ $sample_solution = false;
+ $show_solution = false;
+ $tries_left = null;
+
+ if ($assignment->isRunning($user_id)) {
+ // if a solution has been submitted during a selftest
+ if ($max_tries && $solution) {
+ $tries_left = $max_tries - $solution->countTries();
+
+ if (
+ $solution->points == $max_points
+ || !$solution->state
+ || $solution->grader_id
+ || $tries_left <= 0
+ ) {
+ $show_solution = true;
+ $sample_solution = true;
+ }
+ }
+ } else {
+ $show_solution = true;
+ $sample_solution = $released == VipsAssignment::RELEASE_STATUS_SAMPLE_SOLUTIONS;
+
+ if (!$solution) {
+ $solution = new VipsSolution();
+ $solution->assignment = $assignment;
+ }
+ }
+
+ $template = $this->get_template_factory()->open('vips/exercises/courseware_block');
+ $template->user_id = $user_id;
+ $template->assignment = $assignment;
+ $template->exercise = $exercise;
+ $template->tries_left = $tries_left;
+ $template->solution = $solution;
+ $template->max_points = $max_points;
+ $template->sample_solution = $sample_solution;
+ $template->show_solution = $show_solution;
+
+ return $template;
+ }
+
+ public function solution_action($assignment_id, $exercise_id)
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment = VipsAssignment::find($assignment_id);
+ $block_id = Request::int('block_id');
+ $user_id = $GLOBALS['user']->id;
+
+ VipsModule::requireViewPermission($assignment, $exercise_id);
+
+ // check access to courseware block
+ if ($block_id) {
+ $block = Courseware\Block::find($block_id);
+ $payload = $block->type->getPayload();
+
+ if ($payload['assignment'] != $assignment_id) {
+ throw new AccessDeniedException(_('Sie haben keinen Zugriff auf diesen Block!'));
+ }
+ }
+
+ if ($assignment->type === 'exam') {
+ throw new AccessDeniedException(_('Sie haben keinen Zugriff auf dieses Aufgabenblatt!'));
+ }
+
+ if (!$assignment->checkAccess($user_id)) {
+ throw new AccessDeniedException(_('Das Aufgabenblatt kann zur Zeit nicht bearbeitet werden.'));
+ }
+
+ // enter user start time the moment he/she first clicks on any exercise
+ if (!$assignment->checkEditPermission()) {
+ $assignment->recordAssignmentAttempt($user_id);
+ }
+
+ if (Request::isPost()) {
+ $request = Request::getInstance();
+ $exercise = Exercise::find($exercise_id);
+ $solution = $exercise->getSolutionFromRequest($request, $_FILES);
+ $solution->user_id = $user_id;
+
+ if ($solution->isEmpty()) {
+ $this->set_status(422);
+ } else {
+ $assignment->storeSolution($solution);
+ $this->set_status(201);
+ }
+ }
+
+ if (Request::isDelete()) {
+ if ($assignment->isResetAllowed()) {
+ $assignment->deleteSolution($user_id, $exercise_id);
+ $this->set_status(204);
+ } else {
+ $this->set_status(403);
+ }
+ }
+
+ // update user progress in Courseware
+ if ($block_id) {
+ $progress = new Courseware\UserProgress([$user_id, $block_id]);
+ $progress->grade = $assignment->getUserProgress($user_id);
+ $progress->store();
+ }
+
+ $this->render_nothing();
+ }
+}
diff --git a/app/controllers/vips/config.php b/app/controllers/vips/config.php
new file mode 100644
index 0000000..d6d4e48
--- /dev/null
+++ b/app/controllers/vips/config.php
@@ -0,0 +1,95 @@
+<?php
+/**
+ * vips/config.php - global configuration controller
+ *
+ * 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 Elmar Ludwig
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ */
+
+class Vips_ConfigController extends AuthenticatedController
+{
+ /**
+ * Callback function being called before an action is executed. If this
+ * function does not return FALSE, the action will be called, otherwise
+ * an error will be generated and processing will be aborted. If this function
+ * already #rendered or #redirected, further processing of the action is
+ * withheld.
+ *
+ * @param string Name of the action to perform.
+ * @param array An array of arguments to the action.
+ *
+ * @return bool|void
+ */
+ public function before_filter(&$action, &$args)
+ {
+ parent::before_filter($action, $args);
+
+ $GLOBALS['perm']->check('root');
+
+ Navigation::activateItem('/admin/config/vips');
+ PageLayout::setHelpKeyword('Basis.VipsEinstellungen');
+ PageLayout::setTitle(_('Einstellungen für Aufgaben'));
+ }
+
+ public function index_action()
+ {
+ $this->fields = DataField::getDataFields('user');
+ $this->config = Config::get();
+
+ $widget = new ActionsWidget();
+ $widget->addLink(
+ _('Anstehende Klausuren anzeigen'),
+ $this->pending_assignmentsURL(),
+ Icon::create('doctoral_cap')
+ )->asDialog('size=big');
+ Sidebar::get()->addWidget($widget);
+ }
+
+ public function save_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $exam_mode = Request::int('exam_mode', 0);
+ $exam_terms = trim(Request::get('exam_terms'));
+ $exam_terms = Studip\Markup::purifyHtml($exam_terms);
+
+ $config = Config::get();
+ $config->store('VIPS_EXAM_RESTRICTIONS', $exam_mode);
+ $config->store('VIPS_EXAM_TERMS', $exam_terms);
+
+ $room = Request::getArray('room');
+ $ip_range = Request::getArray('ip_range');
+ $ip_ranges = [];
+
+ foreach ($room as $i => $name) {
+ $name = preg_replace('/[ ,]+/', '_', trim($name));
+
+ if ($name !== '') {
+ $ip_ranges[$name] = trim($ip_range[$i]);
+ }
+ }
+
+ if ($ip_ranges) {
+ ksort($ip_ranges);
+ $config->store('VIPS_EXAM_ROOMS', $ip_ranges);
+ }
+
+ PageLayout::postSuccess(_('Die Einstellungen wurden gespeichert.'));
+
+ $this->redirect('vips/config');
+ }
+
+ public function pending_assignments_action()
+ {
+ $this->assignments = VipsAssignment::findBySQL(
+ "range_type = 'course' AND type = 'exam' AND
+ start BETWEEN UNIX_TIMESTAMP(NOW() - INTERVAL 1 DAY) AND UNIX_TIMESTAMP(NOW() + INTERVAL 14 DAY) AND end > UNIX_TIMESTAMP()
+ ORDER BY start"
+ );
+ }
+}
diff --git a/app/controllers/vips/exam_mode.php b/app/controllers/vips/exam_mode.php
new file mode 100644
index 0000000..914a0e0
--- /dev/null
+++ b/app/controllers/vips/exam_mode.php
@@ -0,0 +1,29 @@
+<?php
+/**
+ * vips/exam_mode.php - restricted exam mode controller
+ *
+ * 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 Elmar Ludwig
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ */
+
+class Vips_ExamModeController extends AuthenticatedController
+{
+ /**
+ * Display a list of courses with currently active tests of type 'exam'.
+ * Only used when there are multiple courses with running exams.
+ */
+ public function index_action()
+ {
+ PageLayout::setTitle(_('Klausurübersicht'));
+
+ Helpbar::get()->addPlainText('',
+ _('Der normale Betrieb von Stud.IP ist für Sie zur Zeit gesperrt, da Klausuren geschrieben werden.'));
+
+ $this->courses = VipsModule::getCoursesWithRunningExams($GLOBALS['user']->id);
+ }
+}
diff --git a/app/controllers/vips/pool.php b/app/controllers/vips/pool.php
new file mode 100644
index 0000000..bcbe302
--- /dev/null
+++ b/app/controllers/vips/pool.php
@@ -0,0 +1,473 @@
+<?php
+/**
+ * vips/pool.php - assignment pool controller
+ *
+ * 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 Elmar Ludwig
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ */
+
+class Vips_PoolController extends AuthenticatedController
+{
+ /**
+ * Callback function being called before an action is executed. If this
+ * function does not return FALSE, the action will be called, otherwise
+ * an error will be generated and processing will be aborted. If this function
+ * already #rendered or #redirected, further processing of the action is
+ * withheld.
+ *
+ * @param string Name of the action to perform.
+ * @param array An array of arguments to the action.
+ *
+ * @return bool|void
+ */
+ public function before_filter(&$action, &$args)
+ {
+ parent::before_filter($action, $args);
+
+ PageLayout::setHelpKeyword('Basis.Vips');
+ }
+
+ /**
+ * Display all exercises that are available for this user.
+ * Available in this case means the exercise is in a course where the user
+ * is at least tutor.
+ * Lecturer/tutor can select which exercise to edit/assign/delete.
+ */
+ public function exercises_action()
+ {
+ Navigation::activateItem('/contents/vips/exercises');
+ PageLayout::setTitle(_('Meine Aufgaben'));
+
+ Helpbar::get()->addPlainText('',
+ _('Auf dieser Seite finden Sie eine Übersicht über alle Aufgaben, auf die Sie Zugriff haben.'));
+
+ $range_type = $_SESSION['view_context'] ?? 'user';
+ $range_type = Request::option('range_type', $range_type);
+ $_SESSION['view_context'] = $range_type;
+
+ $widget = new ViewsWidget();
+ $widget->addLink(
+ _('Persönliche Aufgabensammlung'),
+ $this->url_for('vips/pool/exercises', ['range_type' => 'user'])
+ )->setActive($range_type === 'user');
+ $widget->addLink(
+ _('Aufgaben in Veranstaltungen'),
+ $this->url_for('vips/pool/exercises', ['range_type' => 'course'])
+ )->setActive($range_type === 'course');
+ Sidebar::get()->addWidget($widget);
+
+ $sort = Request::option('sort', 'mkdate');
+ $desc = Request::int('desc', $sort === 'mkdate');
+ $page = Request::int('page', 1);
+ $size = Config::get()->ENTRIES_PER_PAGE;
+
+ $search_filter = Request::getArray('search_filter') + ['search_string' => '', 'exercise_type' => ''];
+ $search_filter['search_string'] = Request::get('pool_search_parameter', $search_filter['search_string']);
+ $search_filter['exercise_type'] = Request::get('exercise_type', $search_filter['exercise_type']);
+
+ if (Request::submitted('start_search') || Request::int('pool_search')) {
+ $search_filter = [
+ 'search_string' => Request::get('pool_search_parameter'),
+ 'exercise_type' => Request::get('exercise_type')
+ ];
+ } else if (empty($search_filter) || Request::submitted('reset_search')) {
+ $search_filter = array_fill_keys(['search_string', 'exercise_type'], '');
+ }
+
+ // get exercises of this user and where he/she has permission
+ if ($range_type === 'course') {
+ $course_ids = array_column(VipsModule::getActiveCourses($GLOBALS['user']->id), 'id');
+ } else {
+ $course_ids = [$GLOBALS['user']->id];
+ }
+
+ // set up the sql query for the quicksearch
+ $sql = "SELECT etask_tasks.id, etask_tasks.title FROM etask_tasks
+ JOIN etask_test_tasks ON etask_tasks.id = etask_test_tasks.task_id
+ JOIN etask_assignments USING (test_id)
+ WHERE etask_assignments.range_id IN ('" . implode("','", $course_ids) . "')
+ AND etask_assignments.type IN ('exam', 'practice', 'selftest')
+ AND (etask_tasks.title LIKE :input OR etask_tasks.description LIKE :input)
+ AND IF(:exercise_type = '', 1, etask_tasks.type = :exercise_type)
+ ORDER BY title";
+ $search = new SQLSearch($sql, _('Titel der Aufgabe'));
+
+ $widget = new VipsSearchWidget($this->url_for('vips/pool/exercises', ['exercise_type' => $search_filter['exercise_type']]));
+ $widget->addNeedle(_('Suche'), 'pool_search', true, $search, 'function(id, name) { this.value = name; this.form.submit(); }', $search_filter['search_string']);
+ Sidebar::get()->addWidget($widget);
+
+ $widget = new SelectWidget(_('Aufgabentyp'), $this->url_for('vips/pool/exercises', ['pool_search_parameter' => $search_filter['search_string']]), 'exercise_type');
+ $element = new SelectElement('', _('Alle Aufgabentypen'));
+ $widget->addElement($element);
+ Sidebar::get()->addWidget($widget);
+
+ foreach (Exercise::getExerciseTypes() as $type => $entry) {
+ $element = new SelectElement($type, $entry['name'], $type === $search_filter['exercise_type']);
+ $widget->addElement($element);
+ }
+
+ $result = $this->getAllExercises($course_ids, $sort, $desc, $search_filter);
+
+ $this->sort = $sort;
+ $this->desc = $desc;
+ $this->page = $page;
+ $this->count = count($result);
+ $this->exercises = array_slice($result, $size * ($page - 1), $size);
+ $this->search_filter = $search_filter;
+ }
+
+ /**
+ * Display all assignments that are available for this user.
+ * Available in this case means the assignment is in a course where the user
+ * is at least tutor.
+ * Lecturer/tutor can select which assignment to edit/delete.
+ */
+ public function assignments_action()
+ {
+ Navigation::activateItem('/contents/vips/assignments');
+ PageLayout::setTitle(_('Meine Aufgabenblätter'));
+
+ Helpbar::get()->addPlainText('',
+ _('Auf dieser Seite finden Sie eine Übersicht über alle Aufgabenblätter, auf die Sie Zugriff haben.'));
+
+ $range_type = $_SESSION['view_context'] ?? 'user';
+ $range_type = Request::option('range_type', $range_type);
+ $_SESSION['view_context'] = $range_type;
+
+ $widget = new ActionsWidget();
+ $widget->addLink(
+ _('Aufgabenblatt erstellen'),
+ $this->url_for('vips/sheets/edit_assignment'),
+ Icon::create('add')
+ );
+ $widget->addLink(
+ _('Aufgabenblatt kopieren'),
+ $this->url_for('vips/sheets/copy_assignment_dialog'),
+ Icon::create('copy')
+ )->asDialog('size=1200x800');
+ $widget->addLink(
+ _('Aufgabenblatt importieren'),
+ $this->url_for('vips/sheets/import_assignment_dialog'),
+ Icon::create('import')
+ )->asDialog('size=auto');
+ Sidebar::get()->addWidget($widget);
+
+ $widget = new ViewsWidget();
+ $widget->addLink(
+ _('Persönliche Aufgabensammlung'),
+ $this->url_for('vips/pool/assignments', ['range_type' => 'user'])
+ )->setActive($range_type === 'user');
+ $widget->addLink(
+ _('Aufgaben in Veranstaltungen'),
+ $this->url_for('vips/pool/assignments', ['range_type' => 'course'])
+ )->setActive($range_type === 'course');
+ Sidebar::get()->addWidget($widget);
+
+ $sort = Request::option('sort', 'mkdate');
+ $desc = Request::int('desc', $sort === 'mkdate');
+ $page = Request::int('page', 1);
+ $size = Config::get()->ENTRIES_PER_PAGE;
+
+ $search_filter = Request::getArray('search_filter') + ['search_string' => '', 'assignment_type' => ''];
+ $search_filter['search_string'] = Request::get('pool_search_parameter', $search_filter['search_string']);
+ $search_filter['assignment_type'] = Request::get('assignment_type', $search_filter['assignment_type']);
+
+ // get assignments of this user and where he/she has permission
+ if ($range_type === 'course') {
+ $course_ids = array_column(VipsModule::getActiveCourses($GLOBALS['user']->id), 'id');
+ } else {
+ $course_ids = [$GLOBALS['user']->id];
+ }
+
+ // set up the sql query for the quicksearch
+ $sql = "SELECT etask_assignments.id, etask_tests.title FROM etask_tests
+ JOIN etask_assignments ON etask_tests.id = etask_assignments.test_id
+ WHERE etask_assignments.range_id IN ('" . implode("','", $course_ids) . "')
+ AND etask_assignments.type IN ('exam', 'practice', 'selftest')
+ AND (etask_tests.title LIKE :input OR etask_tests.description LIKE :input)
+ AND IF(:assignment_type = '', 1, etask_assignments.type = :assignment_type)
+ ORDER BY title";
+ $search = new SQLSearch($sql, _('Titel des Aufgabenblatts'));
+
+ $widget = new VipsSearchWidget($this->url_for('vips/pool/assignments', ['assignment_type' => $search_filter['assignment_type']]));
+ $widget->addNeedle(_('Suche'), 'pool_search', true, $search, 'function(id, name) { this.value = name; this.form.submit(); }', $search_filter['search_string']);
+ Sidebar::get()->addWidget($widget);
+
+ $widget = new SelectWidget(_('Modus'), $this->url_for('vips/pool/assignments', ['pool_search_parameter' => $search_filter['search_string']]), 'assignment_type');
+ $element = new SelectElement('', _('Beliebiger Modus'));
+ $widget->addElement($element);
+ Sidebar::get()->addWidget($widget);
+
+ foreach (VipsAssignment::getAssignmentTypes() as $type => $entry) {
+ $element = new SelectElement($type, $entry['name'], $type === $search_filter['assignment_type']);
+ $widget->addElement($element);
+ }
+
+ $result = $this->getAllAssignments($course_ids, $sort, $desc, $search_filter);
+
+ $this->sort = $sort;
+ $this->desc = $desc;
+ $this->page = $page;
+ $this->count = count($result);
+ $this->assignments = array_slice($result, $size * ($page - 1), $size);
+ $this->search_filter = $search_filter;
+ }
+
+ /**
+ * Get all matching exercises from a list of courses in given order.
+ * If $search_filter is not empty, search filters are applied.
+ *
+ * @param course_ids list of courses to get exercises from
+ * @param sort sort exercise list by this property
+ * @param desc true if sort direction is descending
+ * @param search_filter the currently active search filter
+ *
+ * @return array with data of all matching exercises
+ */
+ public function getAllExercises($course_ids, $sort, $desc, $search_filter)
+ {
+ $db = DBManager::get();
+
+ // check if some filters are active
+ $search_string = $search_filter['search_string'];
+ $exercise_type = $search_filter['exercise_type'];
+
+ $sql = "SELECT etask_tasks.*,
+ auth_user_md5.Nachname,
+ auth_user_md5.Vorname,
+ etask_assignments.id AS assignment_id,
+ etask_assignments.range_id,
+ etask_assignments.range_type,
+ etask_tests.title AS test_title
+ FROM etask_tasks
+ LEFT JOIN auth_user_md5 USING(user_id)
+ JOIN etask_test_tasks ON etask_tasks.id = etask_test_tasks.task_id
+ JOIN etask_tests ON etask_tests.id = etask_test_tasks.test_id
+ JOIN etask_assignments USING (test_id)
+ WHERE etask_assignments.range_id IN (:course_ids)
+ AND etask_assignments.type IN ('exam', 'practice', 'selftest') " .
+ ($search_string ? 'AND (etask_tasks.title LIKE :input OR
+ etask_tasks.description LIKE :input) ' : '') .
+ ($exercise_type ? 'AND etask_tasks.type = :exercise_type ' : '') .
+ "ORDER BY :sort :desc, title";
+
+ $stmt = $db->prepare($sql);
+ $stmt->bindValue(':course_ids', $course_ids);
+ $stmt->bindValue(':input', '%' . $search_string . '%');
+ $stmt->bindValue(':exercise_type', $exercise_type);
+ $stmt->bindValue(':sort', $sort, StudipPDO::PARAM_COLUMN);
+ $stmt->bindValue(':desc', $desc ? 'DESC' : 'ASC', StudipPDO::PARAM_COLUMN);
+ $stmt->execute();
+
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+
+ /**
+ * Get all matching assignments from a list of courses in given order.
+ * If $search_filter is not empty, search filters are applied.
+ *
+ * @param course_ids list of courses to get assignments from
+ * @param sort sort assignment list by this property
+ * @param desc true if sort direction is descending
+ * @param search_filter the currently active search filter
+ *
+ * @return array with data of all matching assignments
+ */
+ public function getAllAssignments($course_ids, $sort, $desc, $search_filter)
+ {
+ $db = DBManager::get();
+
+ // check if some filters are active
+ $search_string = $search_filter['search_string'];
+ $assignment_type = $search_filter['assignment_type'];
+ $types = $assignment_type ? [$assignment_type] : ['exam', 'practice', 'selftest'];
+
+ $sql = "SELECT etask_assignments.*,
+ etask_tests.title AS test_title,
+ auth_user_md5.Nachname,
+ auth_user_md5.Vorname,
+ seminare.Name,
+ (SELECT MIN(beginn) FROM semester_data
+ JOIN semester_courses USING(semester_id)
+ WHERE course_id = Seminar_id) AS start_time
+ FROM etask_tests
+ LEFT JOIN auth_user_md5 USING(user_id)
+ JOIN etask_assignments ON etask_tests.id = etask_assignments.test_id
+ LEFT JOIN seminare ON etask_assignments.range_id = seminare.Seminar_id
+ WHERE etask_assignments.range_id IN (:course_ids)
+ AND etask_assignments.type IN (:types) " .
+ ($search_string ? 'AND (etask_tests.title LIKE :input OR
+ etask_tests.description LIKE :input) ' : '') .
+ "ORDER BY :sort :desc, title";
+
+ $stmt = $db->prepare($sql);
+ $stmt->bindValue(':course_ids', $course_ids);
+ $stmt->bindValue(':input', '%' . $search_string . '%');
+ $stmt->bindValue(':types', $types);
+ $stmt->bindValue(':sort', $sort, StudipPDO::PARAM_COLUMN);
+ $stmt->bindValue(':desc', $desc ? 'DESC' : 'ASC', StudipPDO::PARAM_COLUMN);
+ $stmt->execute();
+
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+
+ /**
+ * Delete a list of exercises from their respective assignments.
+ */
+ public function delete_exercises_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $exercise_ids = Request::intArray('exercise_ids');
+ $deleted = 0;
+
+ foreach ($exercise_ids as $exercise_id => $assignment_id) {
+ $assignment = VipsAssignment::find($assignment_id);
+ VipsModule::requireEditPermission($assignment, $exercise_id);
+
+ if (!$assignment->isLocked()) {
+ $assignment->test->removeExercise($exercise_id);
+ ++$deleted;
+ }
+ }
+
+ if ($deleted > 0) {
+ PageLayout::postSuccess(sprintf(ngettext('Die Aufgabe wurde gelöscht.', 'Es wurden %s Aufgaben gelöscht.', $deleted), $deleted));
+ }
+
+ if ($deleted < count($exercise_ids)) {
+ PageLayout::postError(_('Einige Aufgaben konnten nicht gelöscht werden, da die Aufgabenblätter gesperrt sind.'), [
+ _('Falls Sie diese wirklich löschen möchten, müssen Sie zuerst die Lösungen aller Teilnehmenden zurücksetzen.')
+ ]);
+ }
+
+ $this->redirect('vips/pool/exercises');
+ }
+
+ /**
+ * Dialog for copying a list of exercises into an existing assignment.
+ */
+ public function copy_exercises_dialog_action()
+ {
+ PageLayout::setTitle(_('Aufgaben in vorhandenes Aufgabenblatt kopieren'));
+
+ $this->exercise_ids = Request::intArray('exercise_ids');
+ $this->courses = VipsModule::getActiveCourses($GLOBALS['user']->id);
+ }
+
+ /**
+ * Copy the selected exercises into the selected assignment.
+ */
+ public function copy_exercises_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $exercise_ids = Request::intArray('exercise_ids');
+ $target_assignment_id = Request::int('assignment_id');
+ $target_assignment = VipsAssignment::find($target_assignment_id);
+
+ VipsModule::requireEditPermission($target_assignment);
+
+ if (!$target_assignment->isLocked()) {
+ foreach ($exercise_ids as $exercise_id => $assignment_id) {
+ $assignment = VipsAssignment::find($assignment_id);
+ VipsModule::requireEditPermission($assignment);
+
+ $exercise_ref = VipsExerciseRef::find([$assignment->test_id, $exercise_id]);
+ $exercise_ref->copyIntoTest($target_assignment->test_id);
+ }
+
+ PageLayout::postSuccess(ngettext('Die Aufgabe wurde kopiert.', 'Die Aufgaben wurden kopiert.', count($exercise_ids)));
+ }
+
+ $this->redirect('vips/pool/exercises');
+ }
+
+ /**
+ * Dialog for moving a list of exercises into an existing assignment.
+ */
+ public function move_exercises_dialog_action()
+ {
+ PageLayout::setTitle(_('Aufgaben in vorhandenes Aufgabenblatt verschieben'));
+
+ $this->exercise_ids = Request::intArray('exercise_ids');
+ $this->courses = VipsModule::getActiveCourses($GLOBALS['user']->id);
+ }
+
+ /**
+ * Move the selected exercises into the selected assignment.
+ */
+ public function move_exercises_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $exercise_ids = Request::intArray('exercise_ids');
+ $target_assignment_id = Request::int('assignment_id');
+ $target_assignment = VipsAssignment::find($target_assignment_id);
+ $moved = 0;
+
+ VipsModule::requireEditPermission($target_assignment);
+
+ if (!$target_assignment->isLocked()) {
+ foreach ($exercise_ids as $exercise_id => $assignment_id) {
+ $assignment = VipsAssignment::find($assignment_id);
+ VipsModule::requireEditPermission($assignment);
+
+ if (!$assignment->isLocked()) {
+ $exercise_ref = VipsExerciseRef::find([$assignment->test_id, $exercise_id]);
+ $exercise_ref->moveIntoTest($target_assignment->test_id);
+ ++$moved;
+ }
+ }
+ }
+
+ if ($moved > 0) {
+ PageLayout::postSuccess(sprintf(ngettext('Die Aufgabe wurde verschoben.', 'Es wurden %s Aufgaben verschoben.', $moved), $moved));
+ }
+
+ if ($moved < count($exercise_ids)) {
+ PageLayout::postError(_('Einige Aufgaben konnten nicht verschoben werden, da die Aufgabenblätter gesperrt sind.'));
+ }
+
+ $this->redirect('vips/pool/exercises');
+ }
+
+ /**
+ * Return the appropriate CSS class for sortable column (if any).
+ *
+ * @param boolean $sort sort by this column
+ * @param boolean $desc set sort direction
+ */
+ public function sort_class(bool $sort, ?bool $desc): string
+ {
+ return $sort ? ($desc ? 'sortdesc' : 'sortasc') : '';
+ }
+
+ /**
+ * Render a generic page chooser selector. The first occurence of '%d'
+ * in the URL is replaced with the selected page number.
+ *
+ * @param string $url URL for one of the pages
+ * @param string $count total number of entries
+ * @param string $page current page to display
+ * @param string|null $dialog Optional dialog attribute content
+ * @param int|null $page_size page size (defaults to system default)
+ * @return mixed
+ */
+ function page_chooser(string $url, string $count, string $page, ?string $dialog = null, ?int $page_size = null)
+ {
+ $template = $GLOBALS['template_factory']->open('shared/pagechooser');
+ $template->dialog = $dialog;
+ $template->num_postings = $count;
+ $template->page = $page;
+ $template->perPage = $page_size ?: Config::get()->ENTRIES_PER_PAGE;
+ $template->pagelink = str_replace('%%25d', '%d', str_replace('%', '%%', $url));
+
+ return $template->render();
+ }
+}
diff --git a/app/controllers/vips/sheets.php b/app/controllers/vips/sheets.php
new file mode 100644
index 0000000..036ff10
--- /dev/null
+++ b/app/controllers/vips/sheets.php
@@ -0,0 +1,2305 @@
+<?php
+/**
+ * vips/sheets.php - course assignments controller
+ *
+ * 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 Elmar Ludwig
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ */
+
+class Vips_SheetsController extends AuthenticatedController
+{
+ /**
+ * Return the default action and arguments
+ *
+ * @return an array containing the action, an array of args and the format
+ */
+ public function default_action_and_args()
+ {
+ return ['list_assignments', [], null];
+ }
+
+ /**
+ * Callback function being called before an action is executed. If this
+ * function does not return FALSE, the action will be called, otherwise
+ * an error will be generated and processing will be aborted. If this function
+ * already #rendered or #redirected, further processing of the action is
+ * withheld.
+ *
+ * @param string Name of the action to perform.
+ * @param array An array of arguments to the action.
+ *
+ * @return bool|void
+ */
+ public function before_filter(&$action, &$args)
+ {
+ parent::before_filter($action, $args);
+
+ $course_id = Context::getId();
+
+ if ($action === 'list_assignments' && !VipsModule::hasStatus('tutor', $course_id)) {
+ $action = 'list_assignments_stud';
+ }
+
+ if ($action !== 'relay') {
+ if (Context::getId()) {
+ Navigation::activateItem('/course/vips/sheets');
+ } else {
+ Navigation::activateItem('/contents/vips/assignments');
+ PageLayout::setTitle(_('Meine Aufgabenblätter'));
+ }
+ PageLayout::setHelpKeyword('Basis.Vips');
+ }
+ }
+
+ #####################################
+ # #
+ # Student Methods #
+ # #
+ #####################################
+
+ /**
+ * Restores an archived solution as the current solution.
+ */
+ public function restore_solution_action()
+ {
+ // CSRFProtection::verifyUnsafeRequest();
+
+ $exercise_id = Request::int('exercise_id');
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $solver_id = Request::option('solver_id', $GLOBALS['user']->id);
+
+ VipsModule::requireViewPermission($assignment, $exercise_id);
+
+ if (!$assignment->checkEditPermission()) {
+ $solver_id = $GLOBALS['user']->id;
+ }
+
+ $solutions = $assignment->getArchivedUserSolutions($solver_id, $exercise_id);
+
+ if ($assignment->checkAccess($solver_id) || $assignment->checkEditPermission()) {
+ if ($assignment->type === 'exam' && $solutions) {
+ $assignment->restoreSolution($solutions[0]);
+ PageLayout::postSuccess(_('Die vorherige Lösung wurde wiederhergestellt.'));
+ }
+ }
+
+ $this->redirect($this->url_for('vips/sheets/show_exercise', compact('assignment_id', 'exercise_id', 'solver_id')));
+ }
+
+ /**
+ * Only possible if test is selftest: Delete the solution of a student for
+ * a particular exercise.
+ */
+ public function delete_solution_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $exercise_id = Request::int('exercise_id');
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $solver_id = Request::option('solver_id', $GLOBALS['user']->id);
+
+ VipsModule::requireViewPermission($assignment, $exercise_id);
+
+ if (!$assignment->checkEditPermission()) {
+ $solver_id = $GLOBALS['user']->id;
+ }
+
+ if ($assignment->checkAccess($solver_id) || $assignment->checkEditPermission()) {
+ if ($assignment->isResetAllowed() || $assignment->type === 'exam') {
+ $assignment->deleteSolution($solver_id, $exercise_id);
+ $undo_link = '';
+
+ if ($assignment->type === 'exam' && !$assignment->isSelfAssessment()) {
+ $undo_link = sprintf(' <a href="%s">%s</a>',
+ $this->link_for('vips/sheets/restore_solution', compact('assignment_id', 'exercise_id', 'solver_id')),
+ _('Diese Aktion zurücknehmen.'));
+ }
+
+ PageLayout::postSuccess(_('Die Lösung wurde gelöscht.') . $undo_link);
+ }
+ }
+
+ $this->redirect($this->url_for('vips/sheets/show_exercise', compact('assignment_id', 'exercise_id', 'solver_id')));
+ }
+
+ /**
+ * Only possible if test is selftest: Deletes all the solutions of a student or
+ * the student's group to enable him/her to redo it.
+ */
+ public function delete_solutions_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $solver_id = Request::option('solver_id', $GLOBALS['user']->id);
+
+ VipsModule::requireViewPermission($assignment);
+
+ if (!$assignment->checkEditPermission()) {
+ $solver_id = $GLOBALS['user']->id;
+ }
+
+ if ($assignment->isRunning() || $assignment->checkEditPermission()) {
+ if ($assignment->isResetAllowed()) {
+ $assignment->deleteSolutions($solver_id);
+ PageLayout::postSuccess(_('Die Lösungen wurden gelöscht.'));
+ }
+ }
+
+ $this->redirect($this->url_for('vips/sheets/show_assignment', compact('assignment_id', 'solver_id')));
+ }
+
+ /**
+ * Only possible if test is exam: Begin working on the exam.
+ */
+ public function begin_assignment_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $terms_accepted = Request::int('terms_accepted');
+ $access_code = Request::get('access_code');
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $ip_address = $_SERVER['REMOTE_ADDR'];
+
+ VipsModule::requireViewPermission($assignment);
+
+ if ($assignment->type === 'exam') {
+ if (!$assignment->getAssignmentAttempt($GLOBALS['user']->id)) {
+ $exam_terms = Config::get()->VIPS_EXAM_TERMS;
+ }
+
+ if (!$assignment->isRunning() || !$assignment->active) {
+ PageLayout::postError(_('Das Aufgabenblatt kann zur Zeit nicht bearbeitet werden.'));
+ } else if (!$assignment->checkIPAccess($ip_address)) {
+ PageLayout::postError(sprintf(_('Sie haben mit Ihrer IP-Adresse &bdquo;%s&ldquo; keinen Zugriff!'), htmlReady($ip_address)));
+ } else if ($exam_terms && !$terms_accepted) {
+ PageLayout::postError(_('Ein Start der Klausur ist nur mit Bestätigung der Teilnahmebedingungen möglich.'));
+ } else if (!$assignment->checkAccessCode($access_code)) {
+ PageLayout::postError(_('Der eingegebene Zugangscode ist nicht korrekt.'));
+ } else {
+ $assignment->recordAssignmentAttempt($GLOBALS['user']->id);
+ }
+ }
+
+ $this->redirect($this->url_for('vips/sheets/show_assignment', compact('assignment_id', 'access_code')));
+ }
+
+ /**
+ * Only possible if test is exam: Immediately finish working on the exam.
+ */
+ public function finish_assignment_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+
+ VipsModule::requireViewPermission($assignment);
+
+ if ($assignment->checkAccess($GLOBALS['user']->id)) {
+ if ($assignment->finishAssignmentAttempt($GLOBALS['user']->id)) {
+ PageLayout::postSuccess(_('Das Aufgabenblatt wurde abgeschlossen, eine weitere Bearbeitung ist nicht mehr möglich.'));
+ } else {
+ PageLayout::postError(_('Eine Abgabe ist erst nach Start des Aufgabenblatts möglich.'));
+ }
+ }
+
+ $this->redirect($this->url_for('vips/sheets/show_assignment', compact('assignment_id')));
+ }
+
+ /**
+ * SHEETS/EXAMS
+ *
+ * Is called when the submit button at the bottom of an exercise is called.
+ * If there is already a solution of this exercise by the same user or same group,
+ * a dialog pops up to confirm the submission. On database-level: EVERY solution is stored
+ * (even the unconfirmed ones), with the last solution being marked as last.
+ */
+ public function submit_exercise_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $exercise_id = Request::int('exercise_id');
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+
+ VipsModule::requireViewPermission($assignment, $exercise_id);
+
+ ##################################################################
+ # in case student solution is submitted by tutor or lecturer #
+ # (can happen if the student submits his/her solution by email) #
+ ##################################################################
+
+ $solver_id = Request::option('solver_id');
+
+ if ($solver_id == '' || !$assignment->checkEditPermission()) {
+ $solver_id = $GLOBALS['user']->id;
+ }
+
+ ############################
+ # Checks before submission #
+ ############################
+
+ if (!$assignment->checkEditPermission()) {
+ $end = $assignment->getUserEndTime($solver_id);
+
+ // not yet started
+ if (!$assignment->isStarted()) {
+ PageLayout::postError(_('Das Aufgabenblatt wurde noch nicht gestartet.'));
+ $this->redirect('vips/sheets/list_assignments_stud');
+ return;
+ }
+
+ // already ended
+ if ($end && time() - $end > 120) {
+ PageLayout::postError(_('Das Aufgabenblatt wurde bereits beendet.'));
+ $this->redirect('vips/sheets/list_assignments_stud');
+ return;
+ }
+
+ if (!$assignment->checkIPAccess($_SERVER['REMOTE_ADDR']) || !$assignment->checkAccessCode()) {
+ PageLayout::postError(_('Kein Zugriff möglich!'));
+ $this->redirect('vips/sheets/list_assignments_stud');
+ return;
+ }
+
+ $assignment->recordAssignmentAttempt($solver_id);
+ }
+
+ /* if an exercise has been submitted */
+ if (Request::submitted('submit_exercise') || Request::int('forced')) {
+ $request = Request::getInstance();
+ $exercise = Exercise::find($exercise_id);
+ $solution = $exercise->getSolutionFromRequest($request, $_FILES);
+ $solution->user_id = $solver_id;
+
+ if ($solution->isEmpty()) {
+ PageLayout::postWarning(_('Ihre Lösung ist leer und wurde nicht gespeichert.'));
+ } else {
+ $assignment->storeSolution($solution);
+
+ PageLayout::postSuccess(sprintf(_('Ihre Lösung zur Aufgabe &bdquo;%s&ldquo; wurde gespeichert.'), htmlReady($exercise->title)));
+ }
+ }
+
+ $this->redirect($this->url_for('vips/sheets/show_exercise', compact('assignment_id', 'exercise_id', 'solver_id')));
+ }
+
+ /**
+ * SHEETS/EXAMS
+ *
+ * Displays an exercise (from student perspective)
+ */
+ public function show_exercise_action()
+ {
+ $exercise_id = Request::int('exercise_id');
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $solver_id = Request::option('solver_id'); // solver is handed over via address line, ie. user is a lecturer
+
+ VipsModule::requireViewPermission($assignment, $exercise_id);
+
+ if ($solver_id == '' || !$assignment->checkEditPermission()) {
+ $solver_id = $GLOBALS['user']->id;
+ }
+
+ ##############################################################
+ # check for ip_address, remaining time and interrupted #
+ ##############################################################
+
+ // restrict access for students!
+ if (!$assignment->checkEditPermission()) {
+ // the assignment is not accessible any more after it has run out
+ if (!$assignment->checkAccess()) {
+ PageLayout::postError(_('Das Aufgabenblatt kann zur Zeit nicht bearbeitet werden.'));
+ $this->redirect('vips/sheets/list_assignments_stud');
+ return;
+ }
+
+ if ($assignment->isFinished($solver_id)) {
+ PageLayout::postError(_('Die Zeit ist leider abgelaufen!'));
+ $this->redirect($this->url_for('vips/sheets/show_assignment', compact('assignment_id')));
+ return;
+ }
+
+ // enter user start time the moment he/she first clicks on any exercise
+ $assignment->recordAssignmentAttempt($solver_id);
+ }
+
+ // fetch exercise info, type, points
+ $exercise_ref = VipsExerciseRef::find([$assignment->test_id, $exercise_id]);
+ $exercise = $exercise_ref->exercise;
+
+ ###################################
+ # get user solution if applicable #
+ ###################################
+
+ $solution = $assignment->getSolution($solver_id, $exercise_id);
+ $max_tries = $assignment->getMaxTries();
+ $max_points = $exercise_ref->points;
+ $exercise_position = $exercise_ref->position;
+ $show_solution = false;
+ $tries_left = null;
+
+ // if a solution has been submitted during a selftest
+ if ($max_tries && $solution) {
+ $tries_left = $max_tries - $solution->countTries();
+
+ if ($solution->points == $max_points || !$solution->state || $solution->grader_id || $tries_left <= 0) {
+ $show_solution = true;
+ }
+ }
+
+ ##############################
+ # set template variables #
+ ##############################
+
+ $this->assignment = $assignment;
+ $this->assignment_id = $assignment_id;
+ $this->exercise = $exercise;
+ $this->exercise_id = $exercise_id;
+ $this->exercise_position = $exercise_position;
+
+ $this->solver_id = $solver_id;
+ $this->solution = $solution; // can be empty
+ $this->max_points = $max_points;
+ $this->show_solution = $show_solution;
+ $this->tries_left = $tries_left;
+ $this->user_end_time = $assignment->getUserEndTime($solver_id);
+ $this->remaining_time = $this->user_end_time - time();
+
+ $this->contentbar = $this->create_contentbar($assignment, $exercise_id, 'show');
+
+ $widget = new ActionsWidget();
+
+ if (($assignment->isResetAllowed() || $assignment->type === 'exam') && $solution) {
+ $widget->addLink(
+ _('Lösung dieser Aufgabe löschen'),
+ $this->url_for('vips/sheets/delete_solution', compact('assignment_id', 'exercise_id', 'solver_id')),
+ Icon::create('refresh'),
+ ['data-confirm' => _('Wollen Sie die Lösung dieser Aufgabe wirklich löschen?')]
+ )->asButton();
+ }
+
+ Sidebar::get()->addWidget($widget);
+
+ if ($assignment->checkEditPermission()) {
+ Helpbar::get()->addPlainText('',
+ _('Dies ist die Studierendenansicht (Vorschau) der Aufgabe. Sie können hier auch Lösungen von Teilnehmenden ansehen oder für sie abgeben.'));
+
+ $widget = new ViewsWidget();
+ $widget->addLink(
+ _('Aufgabe bearbeiten'),
+ $this->url_for('vips/sheets/edit_exercise', ['assignment_id' => $assignment_id, 'exercise_id' => $exercise_id])
+ );
+ $widget->addLink(
+ _('Studierendensicht (Vorschau)'),
+ $this->url_for('vips/sheets/show_exercise', ['assignment_id' => $assignment_id, 'exercise_id' => $exercise_id])
+ )->setActive();
+ Sidebar::get()->addWidget($widget);
+
+ if ($assignment->range_type === 'course') {
+ $widget = new SelectWidget(_('Anzeigen für'), $this->url_for('vips/sheets/show_exercise', compact('assignment_id', 'exercise_id')), 'solver_id');
+ $widget->class = 'nested-select';
+ $element = new SelectElement($GLOBALS['user']->id, ' ', $GLOBALS['user']->id == $solver_id);
+ $widget->addElement($element);
+
+ foreach ($assignment->course->members->findBy('status', 'autor')->orderBy('nachname, vorname') as $member) {
+ if ($assignment->isVisible($member->user_id)) {
+ $element = new SelectElement($member->user_id, $member->nachname . ', ' . $member->vorname, $member->user_id == $solver_id);
+ $widget->addElement($element);
+ }
+ }
+ Sidebar::get()->addWidget($widget);
+ }
+ } else {
+ Helpbar::get()->addPlainText('',
+ _('Bitte denken Sie daran, vor dem Verlassen der Seite Ihre Lösung zu speichern.'));
+ }
+
+ $widget = new ViewsWidget();
+ $widget->setTitle(_('Aufgabenblatt'));
+
+ setlocale(LC_NUMERIC, $_SESSION['_language'] . '.UTF-8');
+
+ foreach ($assignment->getExerciseRefs($solver_id) as $i => $item) {
+ $this->item = $item;
+ $this->position = $i + 1;
+ $element = new WidgetElement($this->render_template_as_string('vips/sheets/show_exercise_link'));
+ $element->active = $item->task_id === $exercise->id;
+ $widget->addElement($element, 'exercise-' . $item->task_id);
+ }
+
+ setlocale(LC_NUMERIC, 'C');
+
+ Sidebar::get()->addWidget($widget);
+ }
+
+ /**
+ * Displays all running assignments "work-on ready" for students (view of
+ * students when clicking on tab Uebungsblatt), respectively student view
+ * for lecturers and tutors.
+ */
+ public function list_assignments_stud_action()
+ {
+ $course_id = Context::getId();
+ $sort = Request::option('sort', 'start');
+ $desc = Request::int('desc');
+ VipsModule::requireStatus('autor', $course_id);
+
+ $this->sort = $sort;
+ $this->desc = $desc;
+ $this->assignments = [];
+
+ $assignments = VipsAssignment::findByRangeId($course_id);
+ $blocks = VipsBlock::findBySQL('range_id = ? ORDER BY name', [$course_id]);
+ $blocks[] = VipsBlock::build(['name' => _('Aufgabenblätter')]);
+ $ip_address = $_SERVER['REMOTE_ADDR'];
+
+ usort($assignments, function($a, $b) use ($sort) {
+ if ($sort === 'title') {
+ return strcoll($a->test->title, $b->test->title);
+ } else if ($sort === 'type') {
+ return strcmp($a->type, $b->type);
+ } else if ($sort === 'start') {
+ return strcmp($a->start, $b->start);
+ } else {
+ return strcmp($a->end ?: '~', $b->end ?: '~');
+ }
+ });
+
+ if ($desc) {
+ $assignments = array_reverse($assignments);
+ }
+
+ foreach ($blocks as $block) {
+ $this->blocks[$block->id]['title'] = $block->name;
+ }
+
+ foreach ($assignments as $assignment) {
+ if ($assignment->isRunning() && $assignment->isVisible($GLOBALS['user']->id)) {
+ if ($assignment->checkIPAccess($ip_address)) {
+ if (isset($assignment->block->group_id)) {
+ $this->blocks['']['assignments'][] = $assignment;
+ } else {
+ $this->blocks[$assignment->block_id]['assignments'][] = $assignment;
+ }
+ }
+ }
+ }
+
+ // delete empty blocks
+ foreach ($blocks as $block) {
+ if (empty($this->blocks[$block->id]['assignments'])) {
+ unset($this->blocks[$block->id]);
+ }
+ }
+
+ $this->user_id = $GLOBALS['user']->id;
+ }
+
+ /**
+ * Display one assignment to the student, including the list of exercises.
+ */
+ public function show_assignment_action()
+ {
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $solver_id = Request::option('solver_id', $GLOBALS['user']->id);
+ $ip_address = $_SERVER['REMOTE_ADDR'];
+
+ VipsModule::requireViewPermission($assignment);
+
+ if (!$assignment->checkEditPermission()) {
+ $solver_id = $GLOBALS['user']->id;
+ }
+
+ $this->solver_id = $solver_id;
+ $this->user_end_time = $assignment->getUserEndTime($solver_id);
+ $this->remaining_time = $this->user_end_time - time();
+ $this->access_code = trim(Request::get('access_code'));
+ $this->assignment = $assignment;
+ $this->needs_code = false;
+ $this->exam_terms = null;
+ $this->preview_exam_terms = null;
+
+ $this->contentbar = $this->create_contentbar($assignment, null, 'show');
+
+ if (!$assignment->checkEditPermission()) {
+ if (!$assignment->isRunning() || !$assignment->active) {
+ PageLayout::postError(_('Das Aufgabenblatt kann zur Zeit nicht bearbeitet werden.'));
+ $this->redirect('vips/sheets/list_assignments_stud');
+ return;
+ }
+
+ if (!$assignment->checkIPAccess($ip_address)) {
+ PageLayout::postError(sprintf(_('Sie haben mit Ihrer IP-Adresse &bdquo;%s&ldquo; keinen Zugriff!'), htmlReady($ip_address)));
+ $this->redirect('vips/sheets/list_assignments_stud');
+ return;
+ }
+
+ $this->assignment_attempt = $assignment->getAssignmentAttempt($solver_id);
+
+ if ($assignment->type === 'exam') {
+ if (!$assignment->checkAccessCode()) {
+ $this->needs_code = true;
+ }
+
+ if (!$this->assignment_attempt) {
+ $this->exam_terms = Config::get()->VIPS_EXAM_TERMS;
+ }
+
+ if ($this->exam_terms || $this->needs_code) {
+ $this->contentbar = $this->contentbar->withProps(['toc' => null]);
+ }
+ }
+
+ $widget = new ActionsWidget();
+
+ if ($assignment->type !== 'exam') {
+ $widget->addLink(
+ _('Aufgabenblatt drucken'),
+ $this->url_for('vips/sheets/print_assignments', ['assignment_id' => $assignment_id, 'print_files' => 1]),
+ Icon::create('print'),
+ ['target' => '_blank']
+ );
+ }
+ if ($assignment->isResetAllowed()) {
+ $widget->addLink(
+ _('Lösungen dieses Blatts löschen'),
+ $this->url_for('vips/sheets/delete_solutions', ['assignment_id' => $assignment_id]),
+ Icon::create('refresh'),
+ ['data-confirm' => _('Wollen Sie die Lösungen dieses Aufgabenblatts wirklich löschen?')]
+ )->asButton();
+ }
+ if ($assignment->type === 'exam' && $this->assignment_attempt && $this->remaining_time > 0) {
+ $widget->addLink(
+ _('Klausur vorzeitig abgeben'),
+ $this->url_for('vips/sheets/finish_assignment', ['assignment_id' => $assignment_id]),
+ Icon::create('lock-locked'),
+ ['data-confirm' => _('Achtung: Wenn Sie die Klausur abgeben, sind keine weiteren Eingaben mehr möglich!')]
+ )->asButton();
+ }
+ if ($assignment->type === 'selftest' && $this->assignment_attempt && $this->assignment_attempt->end === null) {
+ $widget->addLink(
+ _('Aufgabenblatt jetzt abgeben'),
+ $this->url_for('vips/sheets/finish_assignment', ['assignment_id' => $assignment_id]),
+ Icon::create('lock-locked'),
+ ['data-confirm' => _('Achtung: Wenn Sie das Aufgabenblatt abgeben, sind keine weiteren Eingaben mehr möglich!')]
+ )->asButton();
+ }
+ Sidebar::get()->addWidget($widget);
+ } else {
+ if ($assignment->type === 'exam') {
+ $this->preview_exam_terms = Config::get()->VIPS_EXAM_TERMS;
+ }
+
+ Helpbar::get()->addPlainText('',
+ _('Dies ist die Studierendensicht (Vorschau) des Aufgabenblatts.'));
+
+ $widget = new ActionsWidget();
+
+ if ($assignment->type !== 'exam') {
+ $widget->addLink(
+ _('Aufgabenblatt drucken'),
+ $this->url_for('vips/sheets/print_assignments', ['assignment_id' => $assignment_id, 'print_files' => 1, 'user_ids[]' => $solver_id]),
+ Icon::create('print'),
+ ['target' => '_blank']
+ );
+ }
+ if ($assignment->isResetAllowed()) {
+ $widget->addLink(
+ _('Lösungen dieses Blatts löschen'),
+ $this->url_for('vips/sheets/delete_solutions', ['assignment_id' => $assignment_id, 'solver_id' => $solver_id]),
+ Icon::create('refresh'),
+ ['data-confirm' => _('Wollen Sie die Lösungen dieses Aufgabenblatts wirklich löschen?')]
+ )->asButton();
+ }
+ Sidebar::get()->addWidget($widget);
+
+ $widget = new ViewsWidget();
+ $widget->addLink(
+ _('Aufgabenblatt bearbeiten'),
+ $this->url_for('vips/sheets/edit_assignment', ['assignment_id' => $assignment_id])
+ );
+ $widget->addLink(
+ _('Studierendensicht (Vorschau)'),
+ $this->url_for('vips/sheets/show_assignment', ['assignment_id' => $assignment_id])
+ )->setActive();
+ Sidebar::get()->addWidget($widget);
+
+ if ($assignment->range_type === 'course') {
+ $widget = new SelectWidget(_('Anzeigen für'), $this->url_for('vips/sheets/show_assignment', compact('assignment_id')), 'solver_id');
+ $widget->class = 'nested-select';
+ $element = new SelectElement($GLOBALS['user']->id, ' ', $GLOBALS['user']->id == $solver_id);
+ $widget->addElement($element);
+
+ foreach ($assignment->course->members->findBy('status', 'autor')->orderBy('nachname, vorname') as $member) {
+ if ($assignment->isVisible($member->user_id)) {
+ $element = new SelectElement($member->user_id, $member->nachname . ', ' . $member->vorname, $member->user_id == $solver_id);
+ $widget->addElement($element);
+ }
+ }
+ Sidebar::get()->addWidget($widget);
+ }
+ }
+ }
+
+ #####################################
+ # #
+ # Lecturer Methods #
+ # #
+ #####################################
+
+
+ /**
+ * Dialog for confirming the end date of a starting assignment.
+ */
+ public function start_assignment_dialog_action()
+ {
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+
+ VipsModule::requireEditPermission($assignment);
+
+ $this->assignment = $assignment;
+ }
+
+ /**
+ * EXAMS/SHEETS
+ *
+ * If an assignment hasn't started yet this function sets the start time to NOW
+ * so that it's running
+ *
+ */
+ public function start_assignment_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+
+ VipsModule::requireEditPermission($assignment);
+
+ $end_date = trim(Request::get('end_date'));
+ $end_time = trim(Request::get('end_time'));
+ $end_datetime = DateTime::createFromFormat('d.m.Y H:i', $end_date.' '.$end_time);
+
+ // unlimited selftest
+ if ($assignment->type === 'selftest' && $end_date === '' && $end_time === '') {
+ $end = null;
+ } else if ($end_datetime) {
+ $end = strtotime($end_datetime->format('Y-m-d H:i:s'));
+ } else {
+ $end = $assignment->end;
+ PageLayout::postWarning(_('Ungültiger Endzeitpunkt, der Wert wurde nicht übernommen.'));
+ }
+
+ // set new start and end time in database
+ $assignment->start = time();
+ $assignment->end = $end;
+ $assignment->active = 1;
+ $assignment->store();
+
+ // delete start time for exam from database
+ VipsAssignmentAttempt::deleteBySQL('assignment_id = ?', [$assignment_id]);
+
+ $this->redirect('vips/sheets');
+ }
+
+
+ /**
+ * EXAMS/SHEETS
+ *
+ * Stops/continues an assignment (no change of start/end time but temporary closure)
+ *
+ */
+ public function stopgo_assignment_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $db = DBManager::get();
+
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+
+ VipsModule::requireEditPermission($assignment);
+
+ if ($assignment->type === 'exam') {
+ if ($assignment->active) {
+ $assignment->options['stopdate'] = date('Y-m-d H:i:s');
+ } else if ($assignment->options['stopdate']) {
+ // extend exam duration for already active participants
+ $interval = time() - strtotime($assignment->options['stopdate']);
+ $sql = 'UPDATE etask_assignment_attempts SET end = end + ?
+ WHERE assignment_id = ? AND end > ?';
+ $stmt = $db->prepare($sql);
+ $stmt->execute([$interval, $assignment_id, $assignment->options['stopdate']]);
+
+ unset($assignment->options['stopdate']);
+ }
+ }
+
+ $assignment->active = !$assignment->active;
+ $assignment->store();
+
+ $this->redirect('vips/sheets');
+ }
+
+
+ /**
+ * EXAMS/SHEETS
+ *
+ * Deletes an assignment from the course (and block if applicable).
+ */
+ public function delete_assignment_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $test_title = $assignment->test->title;
+
+ VipsModule::requireEditPermission($assignment);
+
+ if (!$assignment->isLocked()) {
+ $assignment->delete();
+ PageLayout::postSuccess(sprintf(_('Das Aufgabenblatt „%s“ wurde gelöscht.'), htmlReady($test_title)));
+ }
+
+ $this->redirect('vips/sheets');
+ }
+
+ /**
+ * Delete a list of assignments from the course (and block if applicable).
+ */
+ public function delete_assignments_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment_ids = Request::intArray('assignment_ids');
+ $deleted = 0;
+
+ foreach ($assignment_ids as $assignment_id) {
+ $assignment = VipsAssignment::find($assignment_id);
+ VipsModule::requireEditPermission($assignment);
+
+ if (!$assignment->isLocked()) {
+ $assignment->delete();
+ ++$deleted;
+ }
+ }
+
+ if ($deleted > 0) {
+ PageLayout::postSuccess(sprintf(_('Es wurden %s Aufgabenblätter gelöscht.'), $deleted));
+ }
+
+ if ($deleted < count($assignment_ids)) {
+ PageLayout::postError(_('Einige Aufgabenblätter konnten nicht gelöscht werden, da bereits Lösungen abgegeben wurden.'), [
+ _('Falls Sie diese wirklich löschen möchten, müssen Sie zuerst die Lösungen aller Teilnehmenden zurücksetzen.')
+ ]);
+ }
+
+ $this->redirect(Context::getId() ? 'vips/sheets' : 'vips/pool/assignments');
+ }
+
+ /**
+ * Dialog for selecting a block for a list of assignments.
+ */
+ public function assign_block_dialog_action()
+ {
+ $course_id = Context::getId();
+ VipsModule::requireStatus('tutor', $course_id);
+
+ $this->assignment_ids = Request::intArray('assignment_ids');
+ $this->blocks = VipsBlock::findBySQL('range_id = ? ORDER BY name', [$course_id]);
+ }
+
+ /**
+ * Assign a list of assignments to the specified block.
+ */
+ public function assign_block_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment_ids = Request::intArray('assignment_ids');
+ $block_id = Request::int('block_id');
+
+ if ($block_id) {
+ $block = VipsBlock::find($block_id);
+ }
+
+ foreach ($assignment_ids as $assignment_id) {
+ $assignment = VipsAssignment::find($assignment_id);
+ VipsModule::requireEditPermission($assignment);
+
+ if (!$block_id || $block->range_id === $assignment->range_id) {
+ $assignment->block_id = $block_id ?: null;
+ $assignment->store();
+ }
+ }
+
+ PageLayout::postSuccess(_('Die Blockzuordnung wurde gespeichert.'));
+
+ $this->redirect('vips/sheets');
+ }
+
+ /**
+ * Dialog for copying a list of assignments into a course.
+ */
+ public function copy_assignments_dialog_action()
+ {
+ PageLayout::setTitle(_('Aufgabenblätter kopieren'));
+
+ $this->assignment_ids = Request::intArray('assignment_ids');
+ $this->courses = VipsModule::getActiveCourses($GLOBALS['user']->id);
+ $this->course_id = Context::getId();
+ }
+
+ /**
+ * Copy the selected assignments into the selected course.
+ */
+ public function copy_assignments_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment_ids = Request::intArray('assignment_ids');
+ $course_id = Request::option('course_id');
+
+ if ($course_id) {
+ VipsModule::requireStatus('tutor', $course_id);
+ }
+
+ foreach ($assignment_ids as $assignment_id) {
+ $assignment = VipsAssignment::find($assignment_id);
+ VipsModule::requireEditPermission($assignment);
+
+ if ($course_id) {
+ $assignment->copyIntoCourse($course_id);
+ } else {
+ $assignment->copyIntoCourse($GLOBALS['user']->id, 'user');
+ }
+ }
+
+ PageLayout::postSuccess(ngettext('Das Aufgabenblatt wurde kopiert.', 'Die Aufgabenblätter wurden kopiert.', count($assignment_ids)));
+
+ $this->redirect(Context::getId() ? 'vips/sheets' : 'vips/pool/assignments');
+ }
+
+ /**
+ * Dialog for moving a list of assignments to another course.
+ */
+ public function move_assignments_dialog_action()
+ {
+ PageLayout::setTitle(_('Aufgabenblätter verschieben'));
+
+ $this->assignment_ids = Request::intArray('assignment_ids');
+ $this->courses = VipsModule::getActiveCourses($GLOBALS['user']->id);
+ $this->course_id = Context::getId();
+ }
+
+ /**
+ * Move a list of assignments to the specified course.
+ */
+ public function move_assignments_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment_ids = Request::intArray('assignment_ids');
+ $course_id = Request::option('course_id');
+
+ if ($course_id) {
+ VipsModule::requireStatus('tutor', $course_id);
+ }
+
+ foreach ($assignment_ids as $assignment_id) {
+ $assignment = VipsAssignment::find($assignment_id);
+ VipsModule::requireEditPermission($assignment);
+
+ if ($course_id) {
+ $assignment->moveIntoCourse($course_id);
+ } else {
+ $assignment->moveIntoCourse($GLOBALS['user']->id, 'user');
+ }
+ }
+
+ PageLayout::postSuccess(ngettext('Das Aufgabenblatt wurde verschoben.', 'Die Aufgabenblätter wurden verschoben.', count($assignment_ids)));
+
+ $this->redirect(Context::getId() ? 'vips/sheets' : 'vips/pool/assignments');
+ }
+
+ /**
+ * Delete the solutions of all students and reset the assignment.
+ */
+ public function reset_assignment_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+
+ VipsModule::requireEditPermission($assignment);
+
+ if ($assignment->type === 'exam') {
+ $assignment->deleteAllSolutions();
+ PageLayout::postSuccess(_('Die Klausur wurde zurückgesetzt und alle abgegebenen Lösungen archiviert.'));
+ }
+
+ $this->redirect(Context::getId() ? 'vips/sheets' : 'vips/pool/assignments');
+ }
+
+
+ /**
+ * SHEETS/EXAMS
+ *
+ * Takes an exercise off an assignment and deletes it.
+ */
+ public function delete_exercise_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $exercise_id = Request::int('exercise_id');
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $exercise = Exercise::find($exercise_id);
+
+ VipsModule::requireEditPermission($assignment, $exercise_id);
+
+ if (!$assignment->isLocked()) {
+ $assignment->test->removeExercise($exercise_id);
+ PageLayout::postSuccess(sprintf(_('Die Aufgabe „%s“ wurde gelöscht.'), htmlReady($exercise->title)));
+ }
+
+ $this->redirect($this->url_for('vips/sheets/edit_assignment', compact('assignment_id')));
+ }
+
+ /**
+ * Deletes a list of exercises from a specific assignment.
+ */
+ public function delete_exercises_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $exercise_ids = Request::intArray('exercise_ids');
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+
+ if (!$assignment->isLocked()) {
+ foreach ($exercise_ids as $exercise_id) {
+ VipsModule::requireEditPermission($assignment, $exercise_id);
+ $assignment->test->removeExercise($exercise_id);
+ }
+
+ PageLayout::postSuccess(sprintf(_('Es wurden %s Aufgaben gelöscht.'), count($exercise_ids)));
+ }
+
+ $this->redirect($this->url_for('vips/sheets/edit_assignment', compact('assignment_id')));
+ }
+
+ /**
+ * Reorder exercise positions within an assignment.
+ */
+ public function move_exercise_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $list = Request::intArray('item');
+
+ VipsModule::requireEditPermission($assignment);
+
+ /* renumber all exercises in current assignment */
+ foreach ($list as $i => $exercise_id) {
+ $exercise_ref = VipsExerciseRef::find([$assignment->test_id, $exercise_id]);
+
+ if ($exercise_ref) {
+ $exercise_ref->position = $i + 1;
+ $exercise_ref->store();
+ }
+ }
+
+ $this->render_nothing();
+ }
+
+ /**
+ * SHEETS/EXAMS
+ *
+ * Displays the form for editing an exercise.
+ *
+ * Is called when editing an existing exercise or creating a new exercise.
+ */
+ public function edit_exercise_action()
+ {
+ PageLayout::setHelpKeyword('Basis.VipsAufgaben');
+
+ $exercise_id = Request::int('exercise_id'); // is not set when creating new exercise
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+
+ VipsModule::requireEditPermission($assignment, $exercise_id);
+
+ if ($exercise_id) {
+ // edit already existing exercise
+ $exercise_ref = $assignment->test->getExerciseRef($exercise_id);
+ $exercise = $exercise_ref->exercise;
+
+ $max_points = $exercise_ref->points;
+ $exercise_position = $exercise_ref->position;
+ } else {
+ // create new exercise
+ $exercise_type = Request::option('exercise_type');
+ $exercise = new $exercise_type();
+
+ $max_points = null;
+ $exercise_position = null;
+ }
+
+ $this->assignment = $assignment;
+ $this->assignment_id = $assignment_id;
+ $this->exercise = $exercise;
+ $this->exercise_position = $exercise_position;
+ $this->max_points = $max_points;
+
+ $this->contentbar = $this->create_contentbar($assignment, $exercise_id);
+
+ Helpbar::get()->addPlainText('',
+ _('Sie können hier den Aufgabentext und die Antwortoptionen dieser Aufgabe bearbeiten.'));
+
+ $widget = new ActionsWidget();
+
+ if (!$assignment->isLocked()) {
+ $widget->addLink(
+ _('Neue Aufgabe erstellen'),
+ $this->url_for('vips/sheets/add_exercise_dialog', ['assignment_id' => $assignment_id]),
+ Icon::create('add')
+ )->asDialog('size=auto');
+ }
+
+ Sidebar::get()->addWidget($widget);
+
+ if ($exercise->id) {
+ $widget = new ViewsWidget();
+ $widget->addLink(
+ _('Aufgabe bearbeiten'),
+ $this->url_for('vips/sheets/edit_exercise', ['assignment_id' => $assignment_id, 'exercise_id' => $exercise->id])
+ )->setActive();
+ $widget->addLink(
+ _('Studierendensicht (Vorschau)'),
+ $this->url_for('vips/sheets/show_exercise', ['assignment_id' => $assignment_id, 'exercise_id' => $exercise->id])
+ );
+ Sidebar::get()->addWidget($widget);
+ }
+
+ $widget = new ViewsWidget();
+ $widget->setTitle(_('Aufgabenblatt'));
+
+ foreach ($assignment->test->exercise_refs as $item) {
+ $widget->addLink(
+ sprintf(_('Aufgabe %d'), $item->position),
+ $this->url_for('vips/sheets/edit_exercise', ['assignment_id' => $assignment_id, 'exercise_id' => $item->task_id])
+ )->setActive($item->task_id === $exercise->id);
+ }
+
+ Sidebar::get()->addWidget($widget);
+ }
+
+
+ /**
+ * SHEETS/EXAMS
+ *
+ * Inserts/Updates an exercise into the database
+ */
+ public function store_exercise_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $exercise_id = Request::int('exercise_id'); // not set when storing new exercise
+ $exercise_type = Request::option('exercise_type');
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $test_id = $assignment->test_id;
+ $file_ids = Request::optionArray('file_ids');
+ $request = Request::getInstance();
+
+ VipsModule::requireEditPermission($assignment, $exercise_id);
+
+ if ($exercise_id) {
+ // update existing exercise.
+ $exercise = Exercise::find($exercise_id);
+ $item_count = $exercise->itemCount();
+ $exercise->initFromRequest($request);
+ $exercise->store();
+
+ // update maximum points
+ if ($exercise->itemCount() != $item_count) {
+ $exercise_ref = VipsExerciseRef::find([$test_id, $exercise_id]);
+ $exercise_ref->points = $exercise->itemCount();
+ $exercise_ref->store();
+ }
+ } else {
+ // store exercise in database.
+ $exercise = new $exercise_type();
+ $exercise->initFromRequest($request);
+ $exercise->user_id = $GLOBALS['user']->id;
+ $exercise->store();
+
+ // link new exercise to the assignment.
+ $assignment->test->addExercise($exercise);
+ $exercise_id = $exercise->id;
+ }
+
+ $upload = $_FILES['upload'] ?: ['name' => []];
+ $folder = Folder::findTopFolder($exercise->id, 'ExerciseFolder', 'task');
+
+ foreach ($folder->file_refs as $file_ref) {
+ if (!in_array($file_ref->id, $file_ids) || in_array($file_ref->name, $upload['name'])) {
+ $file_ref->delete();
+ }
+ }
+
+ FileManager::handleFileUpload($upload, $folder->getTypedFolder());
+
+ PageLayout::postSuccess(_('Die Aufgabe wurde eingetragen.'));
+
+ $this->redirect($this->url_for('vips/sheets/edit_exercise', compact('assignment_id', 'exercise_id')));
+ }
+
+ /**
+ * Copy the selected exercises into this assignment.
+ */
+ public function copy_exercise_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $exercise_id = Request::int('exercise_id');
+ $exercise_ids = $exercise_id ? [$exercise_id => $assignment_id] : Request::intArray('exercise_ids');
+
+ VipsModule::requireEditPermission($assignment);
+
+ if (!$assignment->isLocked()) {
+ foreach ($exercise_ids as $exercise_id => $copy_assignment_id) {
+ $copy_assignment = VipsAssignment::find($copy_assignment_id);
+ VipsModule::requireEditPermission($copy_assignment);
+
+ $exercise_ref = VipsExerciseRef::find([$copy_assignment->test_id, $exercise_id]);
+ $exercise_ref->copyIntoTest($assignment->test_id);
+ }
+
+ PageLayout::postSuccess(ngettext('Die Aufgabe wurde kopiert.', 'Die Aufgaben wurden kopiert.', count($exercise_ids)));
+ }
+
+ $this->redirect($this->url_for('vips/sheets/edit_assignment', compact('assignment_id')));
+ }
+
+ /**
+ * Dialog for copying a list of exercises to another assignment.
+ */
+ public function copy_exercises_dialog_action()
+ {
+ $this->assignment_id = Request::int('assignment_id');
+ $this->exercise_ids = Request::intArray('exercise_ids');
+ $this->courses = VipsModule::getActiveCourses($GLOBALS['user']->id);
+ }
+
+ /**
+ * Copy a list of exercises to the specified assignment.
+ */
+ public function copy_exercises_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment_id = Request::int('assignment_id');
+ $target_assignment_id = Request::int('target_assignment_id');
+ $exercise_ids = Request::intArray('exercise_ids');
+
+ $assignment = VipsAssignment::find($assignment_id);
+ $target_assignment = VipsAssignment::find($target_assignment_id);
+
+ VipsModule::requireEditPermission($assignment);
+ VipsModule::requireEditPermission($target_assignment);
+
+ if (!$target_assignment->isLocked()) {
+ foreach ($exercise_ids as $exercise_id) {
+ $exercise_ref = $assignment->test->getExerciseRef($exercise_id);
+ $exercise_ref->copyIntoTest($target_assignment->test_id);
+ }
+
+ PageLayout::postSuccess(ngettext('Die Aufgabe wurde kopiert.', 'Die Aufgaben wurden kopiert.', count($exercise_ids)));
+ }
+
+ $this->redirect($this->url_for('vips/sheets/edit_assignment', compact('assignment_id')));
+ }
+
+ /**
+ * Dialog for moving a list of exercises to another assignment.
+ */
+ public function move_exercises_dialog_action()
+ {
+ $this->assignment_id = Request::int('assignment_id');
+ $this->exercise_ids = Request::intArray('exercise_ids');
+ $this->courses = VipsModule::getActiveCourses($GLOBALS['user']->id);
+ }
+
+ /**
+ * Move a list of exercises to the specified assignment.
+ */
+ public function move_exercises_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment_id = Request::int('assignment_id');
+ $target_assignment_id = Request::int('target_assignment_id');
+ $exercise_ids = Request::intArray('exercise_ids');
+
+ $assignment = VipsAssignment::find($assignment_id);
+ $target_assignment = VipsAssignment::find($target_assignment_id);
+
+ VipsModule::requireEditPermission($assignment);
+ VipsModule::requireEditPermission($target_assignment);
+
+ if (!$assignment->isLocked() && !$target_assignment->isLocked()) {
+ foreach ($exercise_ids as $exercise_id) {
+ $exercise_ref = VipsExerciseRef::find([$assignment->test_id, $exercise_id]);
+ $exercise_ref->moveIntoTest($target_assignment->test_id);
+ }
+
+ PageLayout::postSuccess(ngettext('Die Aufgabe wurde verschoben.', 'Die Aufgaben wurden verschoben.', count($exercise_ids)));
+ }
+
+ $this->redirect($this->url_for('vips/sheets/edit_assignment', compact('assignment_id')));
+ }
+
+ /**
+ * SHEETS/EXAMS
+ *
+ * Stores the specification (Grunddaten) of an assignment
+ * OR add new exercise, edit points/Bewertung (basically everything that can be done on
+ * page edit_exercise_action())
+ */
+ public function store_assignment_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $db = DBManager::get();
+
+ $assignment_id = Request::int('assignment_id');
+
+ if ($assignment_id) {
+ $assignment = VipsAssignment::find($assignment_id);
+ } else {
+ $assignment = new VipsAssignment();
+ $assignment->range_id = Context::getId() ?: $GLOBALS['user']->id;
+ $assignment->range_type = Context::getId() ? 'course' : 'user';
+ }
+
+ VipsModule::requireEditPermission($assignment);
+
+ $assignment_name = trim(Request::get('assignment_name'));
+ $assignment_description = trim(Request::get('assignment_description'));
+ $assignment_description = Studip\Markup::purifyHtml($assignment_description);
+ $assignment_notes = trim(Request::get('assignment_notes'));
+ $assignment_type = Request::option('assignment_type', 'practice');
+ $assignment_block = Request::int('assignment_block', 0);
+ $assignment_block_name = trim(Request::get('assignment_block_name'));
+ $start_date = trim(Request::get('start_date'));
+ $start_time = trim(Request::get('start_time'));
+ $end_date = trim(Request::get('end_date'));
+ $end_time = trim(Request::get('end_time'));
+
+ $exam_length = Request::int('exam_length');
+ $access_code = trim(Request::get('access_code'));
+ $ip_range = trim(Request::get('ip_range'));
+ $use_groups = Request::int('use_groups', 0);
+ $shuffle_answers = Request::int('shuffle_answers', 0);
+ $shuffle_exercises = Request::int('shuffle_exercises', 0);
+ $self_assessment = Request::int('self_assessment', 0);
+ $max_tries = Request::int('max_tries', 0);
+ $resets = Request::int('resets', 0);
+ $evaluation_mode = Request::int('evaluation_mode', 0);
+ $exercise_points = Request::floatArray('exercise_points');
+ $selftest_threshold = Request::getArray('threshold');
+ $selftest_feedback = Request::getArray('feedback');
+
+ $start_datetime = DateTime::createFromFormat('d.m.Y H:i', $start_date.' '.$start_time);
+ $end_datetime = DateTime::createFromFormat('d.m.Y H:i', $end_date.' '.$end_time);
+
+ if ($assignment_name === '') {
+ $assignment_name = _('Aufgabenblatt');
+ }
+
+ if ($start_datetime) {
+ $start = $start_datetime->format('Y-m-d H:i:s');
+ } else {
+ $start = date('Y-m-d H:00:00');
+ PageLayout::postWarning(_('Ungültiger Startzeitpunkt, der Wert wurde nicht übernommen.'));
+ }
+
+ // unlimited selftest
+ if ($assignment_type == 'selftest' && $end_date == '' && $end_time == '') {
+ $end = null;
+ } else if ($end_datetime) {
+ $end = $end_datetime->format('Y-m-d H:i:s');
+ } else {
+ $end = date('Y-m-d H:00:00');
+ PageLayout::postWarning(_('Ungültiger Endzeitpunkt, der Wert wurde nicht übernommen.'));
+ }
+
+ if ($end && $end <= $start) { // start is *later* than end!
+ $end = $start;
+ PageLayout::postWarning(_('Bitte überprüfen Sie den Start- und den Endzeitpunkt!'));
+ }
+
+ if ($assignment_block_name != '') {
+ $block = VipsBlock::create(['name' => $assignment_block_name, 'range_id' => $assignment->range_id]);
+ } else if ($assignment_block) {
+ $block = VipsBlock::find($assignment_block);
+
+ if ($block->range_id !== $assignment->range_id) {
+ $block = null;
+ }
+ } else {
+ $block = null;
+ }
+
+ foreach ($selftest_threshold as $i => $threshold) {
+ if ($threshold !== '') {
+ $feedback[$threshold] = Studip\Markup::purifyHtml($selftest_feedback[$i]);
+ }
+ }
+
+ /*** store basic data (Grunddaten) of assignment */
+ if ($assignment_id) {
+ // check whether the exam's start time has been moved
+ if ($assignment->start != strtotime($start) && time() <= strtotime($start)) {
+ $assignment->active = 1;
+ }
+
+ // extend exam duration for already active participants
+ if ($assignment_type === 'exam' && $assignment->options['duration'] != $exam_length) {
+ $sql = 'UPDATE etask_assignment_attempts SET end = GREATEST(end + ? * 60, UNIX_TIMESTAMP())
+ WHERE assignment_id = ? AND end > UNIX_TIMESTAMP()';
+ $stmt = $db->prepare($sql);
+ $stmt->execute([$exam_length - $assignment->options['duration'], $assignment_id]);
+ }
+
+ $assignment->test->setData([
+ 'title' => $assignment_name,
+ 'description' => $assignment_description
+ ]);
+ $assignment->test->store();
+ } else {
+ $assignment->test = VipsTest::create([
+ 'title' => $assignment_name,
+ 'description' => $assignment_description,
+ 'user_id' => $GLOBALS['user']->id
+ ]);
+ }
+
+ $assignment->setData([
+ 'type' => $assignment_type,
+ 'start' => strtotime($start),
+ 'end' => $end ? strtotime($end) : null,
+ 'block_id' => $block ? $block->id : null
+ ]);
+
+ // update options array
+ $assignment->options['evaluation_mode'] = $evaluation_mode;
+ $assignment->options['notes'] = $assignment_notes;
+
+ unset($assignment->options['access_code']);
+ unset($assignment->options['ip_range']);
+ unset($assignment->options['shuffle_answers']);
+ unset($assignment->options['shuffle_exercises']);
+ unset($assignment->options['self_assessment']);
+ unset($assignment->options['use_groups']);
+ unset($assignment->options['max_tries']);
+ unset($assignment->options['resets']);
+ unset($assignment->options['feedback']);
+
+ if ($assignment_type === 'exam') {
+ $assignment->options['duration'] = $exam_length;
+
+ if ($access_code !== '') {
+ $assignment->options['access_code'] = $access_code;
+ }
+
+ if ($ip_range !== '') {
+ $assignment->options['ip_range'] = $ip_range;
+ }
+
+ $assignment->options['shuffle_answers'] = $shuffle_answers;
+
+ if ($shuffle_exercises === 1) {
+ $assignment->options['shuffle_exercises'] = $shuffle_exercises;
+ }
+
+ if ($self_assessment === 1) {
+ $assignment->options['self_assessment'] = $self_assessment;
+ }
+ }
+
+ if ($assignment_type === 'practice') {
+ $assignment->options['use_groups'] = $use_groups;
+ }
+
+ if ($assignment_type === 'selftest') {
+ $assignment->options['max_tries'] = $max_tries;
+
+ if ($resets === 0) {
+ $assignment->options['resets'] = $resets;
+ }
+
+ if (isset($feedback)) {
+ krsort($feedback);
+ $assignment->options['feedback'] = $feedback;
+ }
+ }
+
+ $assignment->store();
+ $assignment_id = $assignment->id;
+
+ foreach ($assignment->test->exercise_refs as $exercise_ref) {
+ $points = $exercise_points[$exercise_ref->task_id];
+ $exercise_ref->points = round($points * 2) / 2;
+ $exercise_ref->store();
+ }
+
+ PageLayout::postSuccess(_('Das Aufgabenblatt wurde gespeichert.'));
+ $this->redirect($this->url_for('vips/sheets/edit_assignment', compact('assignment_id')));
+ }
+
+ /**
+ * Returns the dialog content to create a new exercise.
+ */
+ public function add_exercise_dialog_action()
+ {
+ PageLayout::setHelpKeyword('Basis.VipsAufgaben');
+
+ $assignment_id = Request::int('assignment_id');
+
+ $this->assignment_id = $assignment_id;
+ $this->exercise_types = Exercise::getExerciseTypes();
+ }
+
+ /**
+ * Returns the dialog content to copy an existing exercise.
+ */
+ public function copy_exercise_dialog_action()
+ {
+ $assignment_id = Request::int('assignment_id');
+ $search_filter = Request::getArray('search_filter');
+
+ $sort = Request::option('sort', 'start_time');
+ $desc = Request::int('desc', $sort === 'start_time');
+ $page = Request::int('page', 1);
+ $size = 15;
+
+ if (empty($search_filter) || Request::submitted('reset_search')) {
+ $search_filter = array_fill_keys(['search_string', 'exercise_type'], '');
+ $search_filter['range_type'] = Context::getId() ? 'course' : 'user';
+ }
+
+ if ($search_filter['range_type'] === 'course') {
+ $course_ids = array_column(VipsModule::getActiveCourses($GLOBALS['user']->id), 'id');
+ } else {
+ $course_ids = [$GLOBALS['user']->id];
+ }
+
+ $exercises = $this->getAllExercises($course_ids, $sort, $desc, $search_filter);
+
+ $this->sort = $sort;
+ $this->desc = $desc;
+ $this->page = $page;
+ $this->size = $size;
+ $this->count = count($exercises);
+ $this->exercises = array_slice($exercises, $size * ($page - 1), $size);
+ $this->exercise_types = Exercise::getExerciseTypes();
+ $this->assignment_id = $assignment_id;
+ $this->search_filter = $search_filter;
+ }
+
+ /**
+ * Get all matching exercises from a list of courses in given order.
+ * If $search_filter is not empty, search filters are applied.
+ *
+ * @param course_ids list of courses to get exercises from
+ * @param sort sort exercise list by this property
+ * @param desc true if sort direction is descending
+ * @param search_filter the currently active search filter
+ *
+ * @return array with data of all matching exercises
+ */
+ public function getAllExercises($course_ids, $sort, $desc, $search_filter)
+ {
+ $db = DBManager::get();
+
+ // check if some filters are active
+ $search_string = $search_filter['search_string'];
+ $exercise_type = $search_filter['exercise_type'];
+
+ $sql = "SELECT etask_tasks.*,
+ etask_assignments.id AS assignment_id,
+ etask_assignments.range_id,
+ etask_assignments.range_type,
+ etask_tests.title AS test_title,
+ seminare.name AS course_name,
+ (SELECT MIN(beginn) FROM semester_data
+ JOIN semester_courses USING(semester_id)
+ WHERE course_id = Seminar_id) AS start_time
+ FROM etask_tasks
+ JOIN etask_test_tasks ON etask_tasks.id = etask_test_tasks.task_id
+ JOIN etask_tests ON etask_tests.id = etask_test_tasks.test_id
+ JOIN etask_assignments USING (test_id)
+ LEFT JOIN seminare ON etask_assignments.range_id = seminare.seminar_id
+ WHERE etask_assignments.range_id IN (:course_ids)
+ AND etask_assignments.type IN ('exam', 'practice', 'selftest') " .
+ ($search_string ? 'AND (etask_tasks.title LIKE :input OR
+ etask_tasks.description LIKE :input OR
+ etask_tests.title LIKE :input OR
+ seminare.name LIKE :input) ' : '') .
+ ($exercise_type ? 'AND etask_tasks.type = :exercise_type ' : '') .
+ "ORDER BY :sort :desc, start_time DESC, seminare.name,
+ etask_tests.mkdate DESC, etask_test_tasks.position";
+
+ $stmt = $db->prepare($sql);
+ $stmt->bindValue(':course_ids', $course_ids);
+ $stmt->bindValue(':input', '%' . $search_string . '%');
+ $stmt->bindValue(':exercise_type', $exercise_type);
+ $stmt->bindValue(':sort', $sort, StudipPDO::PARAM_COLUMN);
+ $stmt->bindValue(':desc', $desc ? 'DESC' : 'ASC', StudipPDO::PARAM_COLUMN);
+ $stmt->execute();
+
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+
+ /**
+ * SHEETS/EXAMS
+ *
+ * Displays form to edit an existing assignment
+ *
+ */
+ public function edit_assignment_action()
+ {
+ PageLayout::setHelpKeyword('Basis.VipsAufgabenblatt');
+
+ $assignment_id = Request::int('assignment_id');
+
+ if ($assignment_id) {
+ $assignment = VipsAssignment::find($assignment_id);
+ $test = $assignment->test;
+ } else {
+ $test = new VipsTest();
+ $test->title = _('Aufgabenblatt');
+
+ $assignment = new VipsAssignment();
+ $assignment->range_id = Context::getId() ?: $GLOBALS['user']->id;
+ $assignment->range_type = Context::getId() ? 'course' : 'user';
+ $assignment->type = 'practice';
+ $assignment->start = strtotime(date('Y-m-d H:00:00'));
+ $assignment->end = strtotime(date('Y-m-d H:00:00'));
+ }
+
+ VipsModule::requireEditPermission($assignment);
+
+ if (!isset($assignment->options['feedback'])) {
+ $assignment->options['feedback'] = ['' => ''];
+ }
+
+ $blocks = VipsBlock::findBySQL('range_id = ? ORDER BY name', [$assignment->range_id]);
+
+ $this->assignment = $assignment;
+ $this->assignment_id = $assignment_id;
+ $this->test = $test;
+ $this->blocks = $blocks;
+ $this->locked = $assignment_id && $assignment->isLocked();
+ $this->exercises = $test->exercises;
+ $this->assignment_types = VipsAssignment::getAssignmentTypes();
+ $this->exam_rooms = Config::get()->VIPS_EXAM_ROOMS;
+
+ $this->contentbar = $this->create_contentbar($assignment);
+
+ Helpbar::get()->addPlainText('',
+ _('Sie können hier die Grunddaten des Aufgabenblatts verwalten und Aufgaben hinzufügen, bearbeiten oder löschen.') . ' ' .
+ _('Alle Daten können später geändert oder ergänzt werden.'));
+
+ $widget = new ActionsWidget();
+
+ if ($assignment_id && !$this->locked) {
+ $widget->addLink(
+ _('Neue Aufgabe erstellen'),
+ $this->url_for('vips/sheets/add_exercise_dialog', compact('assignment_id')),
+ Icon::create('add')
+ )->asDialog('size=auto');
+ $widget->addLink(
+ _('Vorhandene Aufgabe kopieren'),
+ $this->url_for('vips/sheets/copy_exercise_dialog', compact('assignment_id')),
+ Icon::create('copy')
+ )->asDialog('size=big');
+ }
+
+ if ($assignment_id) {
+ if ($assignment->range_type === 'course') {
+ $widget->addLink(
+ _('Aufgabenblatt korrigieren'),
+ $this->url_for('vips/solutions/assignment_solutions', ['assignment_id' => $assignment_id]),
+ Icon::create('accept')
+ );
+ }
+
+ $widget->addLink(
+ _('Aufgabenblatt drucken'),
+ $this->url_for('vips/sheets/print_assignments', ['assignment_id' => $assignment_id]),
+ Icon::create('print'),
+ ['target' => '_blank']
+ );
+ Sidebar::get()->addWidget($widget);
+
+ $widget = new ViewsWidget();
+ $widget->addLink(
+ _('Aufgabenblatt bearbeiten'),
+ $this->url_for('vips/sheets/edit_assignment', ['assignment_id' => $assignment_id])
+ )->setActive();
+ $widget->addLink(
+ _('Studierendensicht (Vorschau)'),
+ $this->url_for('vips/sheets/show_assignment', ['assignment_id' => $assignment_id])
+ );
+ Sidebar::get()->addWidget($widget);
+
+ $widget = new ExportWidget();
+ $widget->addLink(
+ _('Aufgabenblatt exportieren'),
+ $this->url_for('vips/sheets/export_xml', ['assignment_id' => $assignment_id]),
+ Icon::create('export')
+ );
+ }
+
+ Sidebar::get()->addWidget($widget);
+ }
+
+ /**
+ * Show preview of an existing exercise (using print view for now).
+ */
+ public function preview_exercise_action()
+ {
+ $exercise_id = Request::int('exercise_id');
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+
+ VipsModule::requireEditPermission($assignment, $exercise_id);
+
+ // fetch exercise info
+ $exercise_ref = VipsExerciseRef::find([$assignment->test_id, $exercise_id]);
+ $exercise = $exercise_ref->exercise;
+
+ $this->assignment = $assignment;
+ $this->exercise = $exercise;
+ $this->exercise_position = $exercise_ref->position;
+ $this->max_points = $exercise_ref->points;
+ $this->solution = new VipsSolution();
+ $this->show_solution = false;
+ $this->print_correction = false;
+ $this->user_id = null;
+
+ $this->render_template('vips/exercises/print_exercise');
+ }
+
+ /**
+ * Copy the selected assignments into the current course.
+ */
+ public function copy_assignment_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $course_id = Context::getId();
+
+ if ($course_id) {
+ VipsModule::requireStatus('tutor', $course_id);
+ }
+
+ $assignment_id = Request::int('assignment_id');
+ $assignment_ids = $assignment_id ? [$assignment_id] : Request::intArray('assignment_ids');
+
+ foreach ($assignment_ids as $assignment_id) {
+ $assignment = VipsAssignment::find($assignment_id);
+ VipsModule::requireEditPermission($assignment);
+
+ if ($course_id) {
+ $assignment->copyIntoCourse($course_id);
+ } else {
+ $assignment->copyIntoCourse($GLOBALS['user']->id, 'user');
+ }
+ }
+
+ PageLayout::postSuccess(ngettext('Das Aufgabenblatt wurde kopiert.', 'Die Aufgabenblätter wurden kopiert.', count($assignment_ids)));
+
+ $this->redirect($course_id ? 'vips/sheets' : 'vips/pool/assignments');
+ }
+
+ /**
+ * Imports a test from a text file.
+ */
+ public function import_test_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $course_id = Context::getId();
+
+ if ($course_id) {
+ VipsModule::requireStatus('tutor', $course_id);
+ }
+
+ if ($_FILES['upload']['name'][0] == '') {
+ PageLayout::postError(_('Sie müssen eine Datei zum Importieren auswählen.'));
+ $this->redirect($course_id ? 'vips/sheets' : 'vips/pool/assignments');
+ return;
+ }
+
+ $num_assignments = 0;
+ $num_exercises = 0;
+
+ for ($i = 0; $i < count($_FILES['upload']['name']); ++$i) {
+ if (!is_uploaded_file($_FILES['upload']['tmp_name'][$i])) {
+ $message = sprintf(_('Es trat ein Fehler beim Hochladen der Datei „%s“ auf.'), htmlReady($_FILES['upload']['name'][$i]));
+ PageLayout::postError($message);
+ continue;
+ }
+
+ $text = file_get_contents($_FILES['upload']['tmp_name'][$i]);
+
+ if (str_contains($text, '<?xml')) {
+ $assignment = VipsAssignment::importXML($text, $GLOBALS['user']->id, $course_id);
+ } else {
+ // convert from windows-1252 if legacy text format
+ $text = mb_decode_numericentity(mb_convert_encoding($text, 'UTF-8', 'WINDOWS-1252'), [0x100, 0xffff, 0, 0xffff], 'UTF-8');
+ $test_title = trim(basename($_FILES['upload']['name'][$i], '.txt'));
+ $assignment = VipsAssignment::importText($test_title, $text, $GLOBALS['user']->id, $course_id);
+ }
+
+ $num_assignments += 1;
+ $num_exercises += count($assignment->test->exercise_refs);
+ }
+
+ if ($num_assignments == 1) {
+ $message = sprintf(ngettext('Das Aufgabenblatt „%s“ mit %d Aufgabe wurde hinzugefügt.',
+ 'Das Aufgabenblatt „%s“ mit %d Aufgaben wurde hinzugefügt.', $num_exercises),
+ htmlReady($assignment->test->title), $num_exercises);
+ PageLayout::postSuccess($message);
+ } else if ($num_assignments > 1) {
+ $message = sprintf(_('%1$d Aufgabenblätter mit insgesamt %2$d Aufgaben wurden hinzugefügt.'), $num_assignments, $num_exercises);
+ PageLayout::postSuccess($message);
+ }
+
+ $this->redirect($course_id ? 'vips/sheets' : 'vips/pool/assignments');
+ }
+
+ /**
+ * Creates html print view of a sheet/exam (new window) specified by id
+ */
+ public function print_assignments_action()
+ {
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+
+ VipsModule::requireViewPermission($assignment);
+
+ $user_ids = Request::optionArray('user_ids');
+ $print_files = Request::int('print_files');
+ $print_correction = Request::int('print_correction');
+ $print_sample_solution = Request::int('print_sample_solution');
+ $print_student_ids = false;
+ $assignment_data = [];
+
+ if (!$assignment->checkEditPermission()) {
+ $user_ids = [$GLOBALS['user']->id];
+ $released = $assignment->releaseStatus($user_ids[0]);
+ $print_correction = $released >= VipsAssignment::RELEASE_STATUS_CORRECTIONS;
+ $print_sample_solution = $released == VipsAssignment::RELEASE_STATUS_SAMPLE_SOLUTIONS;
+
+ if ($assignment->type !== 'exam' && $assignment->checkAccess($user_ids[0])) {
+ $assignment->recordAssignmentAttempt($user_ids[0]);
+ } else if ($released < VipsAssignment::RELEASE_STATUS_CORRECTIONS) {
+ PageLayout::postError(_('Kein Zugriff möglich!'));
+ $this->redirect('vips/sheets/list_assignments_stud');
+ return;
+ }
+ }
+
+ if ($assignment->range_type === 'course') {
+ foreach ($assignment->course->getMembersWithStatus('dozent') as $member) {
+ $lecturers[] = $member->getUserFullname();
+ }
+
+ $sem_class = $assignment->course->getSemClass();
+ $print_student_ids = !$sem_class['studygroup_mode'];
+ }
+
+ if ($user_ids) {
+ foreach ($user_ids as $user_id) {
+ $group = $assignment->getUserGroup($user_id);
+ $students = $stud_ids = [];
+
+ if ($group) {
+ $name = $group->name;
+ $members = $assignment->getGroupMembers($group);
+
+ usort($members, function($a, $b) {
+ return strcoll($a->user->getFullName('no_title_rev'), $b->user->getFullName('no_title_rev'));
+ });
+
+ foreach ($members as $member) {
+ $students[] = $member->user->getFullName('no_title');
+ $stud_ids[] = $member->user->matriculation_number ?: _('(keine Matrikelnummer)');
+ }
+ } else {
+ $user = User::find($user_id);
+ $name = $user->getFullName('no_title_rev');
+ $students[] = $user->getFullName('no_title');
+ $stud_ids[] = $user->matriculation_number ?: _('(keine Matrikelnummer)');
+ }
+
+ $assignment_data[] = [
+ 'user_id' => $user_id,
+ 'students' => $students,
+ 'stud_ids' => $stud_ids
+ ];
+ }
+ } else {
+ $assignment_data[] = [
+ 'user_id' => null
+ ];
+ }
+
+ if (count($user_ids) === 1) {
+ Config::get()->UNI_NAME_CLEAN = $name;
+ }
+
+ PageLayout::setTitle($assignment->test->title);
+ $this->set_layout('vips/sheets/print_layout');
+
+ $this->assignment = $assignment;
+ $this->user_ids = $user_ids;
+ $this->lecturers = $lecturers;
+ $this->print_files = $print_files;
+ $this->print_correction = $print_correction;
+ $this->print_sample_solution = $print_sample_solution;
+ $this->print_student_ids = $print_student_ids;
+ $this->assignment_data = $assignment_data;
+ }
+
+ /**
+ * SHEETS/EXAMS
+ *
+ * Main page of sheets/exams.
+ * Lists all the assignments (sheets or exams) in the course, grouped by "not yet started",
+ * "running" and "finished".
+ */
+ public function list_assignments_action()
+ {
+ $course_id = Context::getId();
+ VipsModule::requireStatus('tutor', $course_id);
+
+ $sort = Request::option('sort', 'start');
+ $desc = Request::int('desc');
+ $group = isset($_SESSION['group_assignments']) ? $_SESSION['group_assignments'] : 0;
+ $group = Request::int('group', $group);
+
+ $_SESSION['group_assignments'] = $group;
+ $running = false;
+
+ ######################################
+ # get assignments in this course #
+ ######################################
+
+ $assignments = VipsAssignment::findByRangeId($course_id);
+ $blocks = VipsBlock::findBySQL('range_id = ? ORDER BY name', [$course_id]);
+ $blocks[] = VipsBlock::build(['name' => _('Aufgabenblätter ohne Blockzuordnung')]);
+
+ usort($assignments, function($a, $b) use ($sort) {
+ if ($sort === 'title') {
+ return strcoll($a->test->title, $b->test->title);
+ } else if ($sort === 'type') {
+ return strcmp($a->type, $b->type);
+ } else if ($sort === 'start') {
+ return strcmp($a->start, $b->start);
+ } else {
+ return strcmp($a->end ?: '~', $b->end ?: '~');
+ }
+ });
+
+ if ($desc) {
+ $assignments = array_reverse($assignments);
+ }
+
+ $plugin_manager = PluginManager::getInstance();
+ $courseware = $plugin_manager->getPluginInfo('CoursewareModule');
+ $courseware_active = $courseware && $plugin_manager->isPluginActivated($courseware['id'], $course_id);
+
+ if ($group == 2 && $courseware_active) {
+ $elements = Courseware\StructuralElement::findBySQL('range_id = ?', [$course_id]);
+ $unassigned = array_column($assignments, 'id');
+
+ foreach ($elements as $element) {
+ $assigned = $this->courseware_assignments($element);
+ $unassigned = array_diff($unassigned, $assigned);
+
+ $assignment_data[] = [
+ 'title' => $element->title,
+ 'assignments' => array_filter($assignments, function($assignment) use ($assigned) {
+ return in_array($assignment->id, $assigned);
+ })
+ ];
+ }
+
+ $assignment_data[] = [
+ 'title' => _('Aufgabenblätter ohne Courseware-Einbindung'),
+ 'assignments' => array_filter($assignments, function($assignment) use ($unassigned) {
+ return in_array($assignment->id, $unassigned);
+ })
+ ];
+ } else if ($group == 1) {
+ foreach ($blocks as $block) {
+ $assignment_data[$block->id] = [
+ 'title' => $block->name,
+ 'block' => $block,
+ 'assignments' => []
+ ];
+ }
+
+ foreach ($assignments as $assignment) {
+ $assignment_data[$assignment->block_id]['assignments'][] = $assignment;
+ }
+ } else {
+ $group = 0;
+ $assignment_data = [
+ [
+ 'title' => _('Noch nicht gestartete Aufgabenblätter'),
+ 'assignments' => []
+ ], [
+ 'title' => _('Laufende Aufgabenblätter'),
+ 'assignments' => []
+ ], [
+ 'title' => _('Beendete Aufgabenblätter'),
+ 'assignments' => []
+ ]
+ ];
+
+ foreach ($assignments as $assignment) {
+ if ($assignment->isFinished()) {
+ $assignment_data[2]['assignments'][] = $assignment;
+ } else if ($assignment->isRunning()) {
+ $assignment_data[1]['assignments'][] = $assignment;
+ } else {
+ $assignment_data[0]['assignments'][] = $assignment;
+ }
+ }
+ }
+
+ foreach ($assignments as $assignment) {
+ if ($assignment->isRunning()) {
+ $running = true;
+ }
+ }
+
+ $this->assignment_data = $assignment_data;
+ $this->num_assignments = count($assignments);
+ $this->sort = $sort;
+ $this->desc = $desc;
+ $this->group = $group;
+ $this->blocks = $blocks;
+
+ Helpbar::get()->addPlainText('',
+ _('Hier können Übungen, Tests und Klausuren online vorbereitet und durchgeführt werden. Sie erhalten ' .
+ 'dabei auch eine Übersicht über die Lösungen bzw. Antworten der Studierenden.') . "\n\n" .
+ _('Auf dieser Seite können Sie Aufgabenblätter in Ihrem Kurs anlegen und verwalten.'));
+
+ $widget = new ActionsWidget();
+ $widget->addLink(
+ _('Aufgabenblatt erstellen'),
+ $this->url_for('vips/sheets/edit_assignment'),
+ Icon::create('add')
+ );
+ $widget->addLink(
+ _('Aufgabenblatt kopieren'),
+ $this->url_for('vips/sheets/copy_assignment_dialog'),
+ Icon::create('copy')
+ )->asDialog('size=1200x800');
+ $widget->addLink(
+ _('Aufgabenblatt importieren'),
+ $this->url_for('vips/sheets/import_assignment_dialog'),
+ Icon::create('import')
+ )->asDialog('size=auto');
+ $widget->addLink(
+ _('Neuen Block erstellen'),
+ $this->url_for('vips/admin/edit_block'),
+ Icon::create('add')
+ )->asDialog('size=auto');
+ Sidebar::get()->addWidget($widget);
+
+ $widget = new ViewsWidget();
+ $widget->addLink(
+ _('Gruppiert nach Status'),
+ $this->url_for('vips/sheets', ['group' => 0])
+ )->setActive($group == 0);
+ $widget->addLink(
+ _('Gruppiert nach Blöcken'),
+ $this->url_for('vips/sheets', ['group' => 1])
+ )->setActive($group == 1);
+
+ if ($courseware_active) {
+ $widget->addLink(
+ _('Verwendung in Courseware'),
+ $this->url_for('vips/sheets', ['group' => 2])
+ )->setActive($group == 2);
+ }
+
+ Sidebar::get()->addWidget($widget);
+ }
+
+ /**
+ * Collect all assignment_ids used in the given Courseware element.
+ */
+ private function courseware_assignments($element)
+ {
+ $result = [];
+
+ foreach ($element->containers as $container) {
+ foreach ($container->blocks as $block) {
+ if ($block->block_type === 'test') {
+ $payload = json_decode($block->payload, true);
+
+ if ($payload['assignment']) {
+ $result[] = $payload['assignment'];
+ }
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns the dialog content to import an assignment from text file.
+ */
+ public function import_assignment_dialog_action()
+ {
+ }
+
+ /**
+ * Returns the dialog content to copy available assignments.
+ */
+ public function copy_assignment_dialog_action()
+ {
+ $search_filter = Request::getArray('search_filter');
+
+ $sort = Request::option('sort', 'start_time');
+ $desc = Request::int('desc', $sort === 'start_time');
+ $page = Request::int('page', 1);
+ $size = 15;
+
+ if (empty($search_filter) || Request::submitted('reset_search')) {
+ $search_filter = array_fill_keys(['search_string', 'assignment_type'], '');
+ $search_filter['range_type'] = Context::getId() ? 'course' : 'user';
+ }
+
+ if ($search_filter['range_type'] === 'course') {
+ $course_ids = array_column(VipsModule::getActiveCourses($GLOBALS['user']->id), 'id');
+ } else {
+ $course_ids = [$GLOBALS['user']->id];
+ }
+
+ $assignments = $this->getAllAssignments($course_ids, $sort, $desc, $search_filter);
+
+ $this->sort = $sort;
+ $this->desc = $desc;
+ $this->page = $page;
+ $this->size = $size;
+ $this->count = count($assignments);
+ $this->assignments = array_slice($assignments, $size * ($page - 1), $size);
+ $this->assignment_types = VipsAssignment::getAssignmentTypes();
+ $this->search_filter = $search_filter;
+ }
+
+ /**
+ * Get all matching assignments from a list of courses in given order.
+ * If $search_filter is not empty, search filters are applied.
+ *
+ * @param array $course_ids list of courses to get assignments from
+ * @param string $sort sort assignment list by this property
+ * @param bool $desc true if sort direction is descending
+ * @param array $search_filter the currently active search filter
+ *
+ * @return array with data of all matching assignments
+ */
+ public function getAllAssignments(array $course_ids, string $sort, bool $desc, array $search_filter)
+ {
+ $db = DBManager::get();
+
+ // check if some filters are active
+ $search_string = $search_filter['search_string'];
+ $assignment_type = $search_filter['assignment_type'];
+ $types = $assignment_type ? [$assignment_type] : ['exam', 'practice', 'selftest'];
+
+ $sql = "SELECT etask_assignments.*,
+ etask_tests.title AS test_title,
+ seminare.name AS course_name,
+ (SELECT MIN(beginn) FROM semester_data
+ JOIN semester_courses USING(semester_id)
+ WHERE course_id = Seminar_id) AS start_time
+ FROM etask_tests
+ JOIN etask_assignments ON etask_tests.id = etask_assignments.test_id
+ LEFT JOIN seminare ON etask_assignments.range_id = seminare.seminar_id
+ WHERE etask_assignments.range_id IN (:course_ids)
+ AND etask_assignments.type IN (:types) " .
+ ($search_string ? 'AND (etask_tests.title LIKE :input OR
+ etask_tests.description LIKE :input OR
+ seminare.name LIKE :input) ' : '') .
+ "ORDER BY :sort :desc, start_time DESC, seminare.name,
+ etask_tests.mkdate DESC";
+
+ $stmt = $db->prepare($sql);
+ $stmt->bindValue(':course_ids', $course_ids);
+ $stmt->bindValue(':input', '%' . $search_string . '%');
+ $stmt->bindValue(':types', $types);
+ $stmt->bindValue(':sort', $sort, StudipPDO::PARAM_COLUMN);
+ $stmt->bindValue(':desc', $desc ? 'DESC' : 'ASC', StudipPDO::PARAM_COLUMN);
+ $stmt->execute();
+
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+
+ /**
+ * Exports all exercises in this assignment in Vips XML format.
+ */
+ public function export_xml_action()
+ {
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+
+ VipsModule::requireEditPermission($assignment);
+
+ $this->set_content_type('text/xml; charset=UTF-8');
+ header('Content-Disposition: attachment; ' . encode_header_parameter('filename', $assignment->test->title.'.xml'));
+
+ $this->render_text($assignment->exportXML());
+ }
+
+ public function relay_action($action)
+ {
+ $params = func_get_args();
+ $params[0] = $this;
+ $exercise_id = Request::int('exercise_id');
+ $exercise = Exercise::find($exercise_id);
+ $action = $action . '_action';
+
+ $this->exercise = $exercise;
+
+ if (method_exists($exercise, $action)) {
+ call_user_func_array([$exercise, $action], $params);
+ } else {
+ throw new InvalidArgumentException(get_class($exercise) . '::' . $action);
+ }
+ }
+
+ /**
+ * Create a ContentBar for this assignment (if no exercise is specified)
+ * or for the given exercise on the assignment.
+ */
+ public function create_contentbar(
+ VipsAssignment $assignment,
+ ?int $exercise_id = null,
+ string $view = 'edit',
+ ?string $solver_id = null
+ ) {
+ $toc = new TOCItem($assignment->test->title);
+ $toc->setURL($this->url_for("vips/sheets/{$view}_assignment", ['assignment_id' => $assignment->id]));
+ $toc->setActive($exercise_id === null);
+
+ if ($view === 'edit') {
+ $exercise_refs = $assignment->test->exercise_refs;
+ } else {
+ $exercise_refs = $assignment->getExerciseRefs($solver_id);
+ }
+
+ foreach ($exercise_refs as $i => $item) {
+ $child = new TOCItem(sprintf('%d. %s', $i + 1, $item->exercise->title));
+ $child->setURL($this->url_for(
+ "vips/sheets/{$view}_exercise",
+ ['assignment_id' => $assignment->id, 'exercise_id' => $item->task_id, 'solver_id' => $solver_id]
+ ));
+
+ $child->setActive($item->task_id == $exercise_id);
+ $toc->children[] = $child;
+ }
+
+ foreach ($toc->children as $i => $item) {
+ if ($item->isActive()) {
+ $icons = $this->get_template_factory()->open('vips/sheets/content_bar_icons');
+
+ if ($i > 0) {
+ $icons->prev_exercise_url = $toc->children[$i - 1]->getURL();
+ }
+
+ if ($i < count($toc->children) - 1) {
+ $icons->next_exercise_url = $toc->children[$i + 1]->getURL();
+ }
+ }
+ }
+
+ return Studip\VueApp::create('ContentBar')->withProps([
+ 'isContentBar' => true,
+ 'toc' => $toc
+ ])->withComponent(
+ 'ContentBarBreadcrumbs'
+ )->withSlot(
+ 'breadcrumb-list', sprintf("<content-bar-breadcrumbs :toc='%s'/>", json_encode($toc))
+ )->withSlot(
+ 'buttons-left', $icons ?? ''
+ );
+ }
+
+ /**
+ * Return the appropriate CSS class for sortable column (if any).
+ *
+ * @param boolean $sort sort by this column
+ * @param boolean $desc set sort direction
+ */
+ public function sort_class(bool $sort, ?bool $desc): string
+ {
+ return $sort ? ($desc ? 'sortdesc' : 'sortasc') : '';
+ }
+
+ /**
+ * Render a generic page chooser selector. The first occurence of '%d'
+ * in the URL is replaced with the selected page number.
+ *
+ * @param string $url URL for one of the pages
+ * @param string $count total number of entries
+ * @param string $page current page to display
+ * @param string|null $dialog Optional dialog attribute content
+ * @param int|null $page_size page size (defaults to system default)
+ * @return mixed
+ */
+ public function page_chooser(string $url, string $count, string $page, ?string $dialog = null, ?int $page_size = null)
+ {
+ $template = $GLOBALS['template_factory']->open('shared/pagechooser');
+ $template->dialog = $dialog;
+ $template->num_postings = $count;
+ $template->page = $page;
+ $template->perPage = $page_size ?: Config::get()->ENTRIES_PER_PAGE;
+ $template->pagelink = str_replace('%%25d', '%d', str_replace('%', '%%', $url));
+
+ return $template->render();
+ }
+}
diff --git a/app/controllers/vips/solutions.php b/app/controllers/vips/solutions.php
new file mode 100644
index 0000000..ca46922
--- /dev/null
+++ b/app/controllers/vips/solutions.php
@@ -0,0 +1,2521 @@
+<?php
+/**
+ * vips/solutions.php - assignment solutions controller
+ *
+ * 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 Elmar Ludwig
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ */
+
+class Vips_SolutionsController extends AuthenticatedController
+{
+ /**
+ * Return the default action and arguments
+ *
+ * @return array containing the action, an array of args and the format
+ */
+ public function default_action_and_args()
+ {
+ return ['assignments', [], null];
+ }
+
+ /**
+ * Callback function being called before an action is executed. If this
+ * function does not return FALSE, the action will be called, otherwise
+ * an error will be generated and processing will be aborted. If this function
+ * already #rendered or #redirected, further processing of the action is
+ * withheld.
+ *
+ * @param string Name of the action to perform.
+ * @param array An array of arguments to the action.
+ *
+ * @return bool|void
+ */
+ public function before_filter(&$action, &$args)
+ {
+ parent::before_filter($action, $args);
+
+ Navigation::activateItem('/course/vips/solutions');
+ PageLayout::setHelpKeyword('Basis.VipsErgebnisse');
+ PageLayout::setTitle(PageLayout::getTitle() . ' - ' . _('Ergebnisse'));
+ }
+
+ /**
+ * Displays all exercise sheets.
+ * Lecturer can select what sheet to correct.
+ */
+ public function assignments_action()
+ {
+ $sort = Request::option('sort', 'start');
+ $desc = Request::int('desc');
+ $course_id = Context::getId();
+ VipsModule::requireStatus('autor', $course_id);
+
+ $this->sort = $sort;
+ $this->desc = $desc;
+ $this->course_id = $course_id;
+ $this->user_id = $GLOBALS['user']->id;
+ $this->test_data = $this->get_assignments_data($course_id, $this->user_id, $sort, $desc);
+ $this->blocks = VipsBlock::findBySQL('range_id = ? ORDER BY name', [$course_id]);
+ $this->blocks[] = VipsBlock::build(['name' => _('Aufgabenblätter ohne Blockzuordnung')]);
+
+ foreach ($this->test_data['assignments'] as $assignment) {
+ $this->block_assignments[$assignment['assignment']->block_id][] = $assignment;
+ }
+
+ $this->use_weighting = false;
+
+ foreach ($this->blocks as $block) {
+ if ($block->weight !== null) {
+ if ($block->weight) {
+ $this->use_weighting = true;
+ }
+ } else if (isset($this->block_assignments[$block->id])) {
+ foreach ($this->block_assignments[$block->id] as $ass) {
+ if ($ass['assignment']->weight) {
+ $this->use_weighting = true;
+ }
+ }
+ }
+ }
+
+ $settings = CourseConfig::get($course_id);
+
+ // display course results if grades are defined for this course
+ if (!VipsModule::hasStatus('tutor', $course_id) && $settings->VIPS_COURSE_GRADES) {
+ $assignments = VipsAssignment::findBySQL("range_id = ? AND type IN ('exam', 'practice')", [$course_id]);
+ $show_overview = true;
+
+ // find unreleased or unfinished assignments
+ foreach ($assignments as $assignment) {
+ if (!$this->use_weighting || $assignment->weight || $assignment->block_id && $assignment->block->weight) {
+ if (
+ $assignment->isVisible($this->user_id)
+ && $assignment->releaseStatus($this->user_id) == VipsAssignment::RELEASE_STATUS_NONE
+ ) {
+ $show_overview = false;
+ }
+ }
+ }
+
+ // if all assignments are finished and released
+ if ($show_overview) {
+ $this->overview_data = $this->participants_overview_data($course_id, $this->user_id);
+ }
+ }
+
+ if (VipsModule::hasStatus('tutor', $course_id)) {
+ Helpbar::get()->addPlainText('',
+ _('Hier finden Sie eine Übersicht über den Korrekturstatus Ihrer Aufgabenblätter und können Aufgaben korrigieren. ' .
+ 'Außerdem können Sie hier die Einstellungen für die Notenberechnung in Ihrem Kurs vornehmen.'));
+
+ $widget = new ActionsWidget();
+ $widget->addLink(
+ _('Notenverteilung festlegen'),
+ $this->url_for('vips/admin/edit_grades'),
+ Icon::create('graph')
+ )->asDialog('size=auto');
+ Sidebar::get()->addWidget($widget);
+
+ $widget = new ViewsWidget();
+ $widget->addLink(
+ _('Ergebnisse'),
+ $this->url_for('vips/solutions')
+ )->setActive();
+ $widget->addLink(
+ _('Punkteübersicht'),
+ $this->url_for('vips/solutions/participants_overview', ['display' => 'points'])
+ );
+ $widget->addLink(
+ _('Notenübersicht'),
+ $this->url_for('vips/solutions/participants_overview', ['display' => 'weighting'])
+ );
+ $widget->addLink(
+ _('Statistik'),
+ $this->url_for('vips/solutions/statistics')
+ );
+ Sidebar::get()->addWidget($widget);
+ }
+ }
+
+ /**
+ * Changes which correction information is released to the student (either
+ * nothing or only the points or points and correction).
+ */
+ public function update_released_dialog_action()
+ {
+ PageLayout::setTitle(_('Freigabe für Studierende'));
+
+ $this->assignment_ids = Request::intArray('assignment_ids');
+
+ foreach ($this->assignment_ids as $assignment_id) {
+ $assignment = VipsAssignment::find($assignment_id);
+ VipsModule::requireEditPermission($assignment);
+
+ $released = $assignment->options['released'];
+ $default = isset($default) ? ($released === $default ? $default : -1) : $released;
+
+ if ($assignment->type === 'exam') {
+ $this->exam_options = true;
+ }
+ }
+
+ $this->default = $default;
+ }
+
+ /**
+ * Changes which correction information is released to the student (either
+ * nothing or only the points or points and correction).
+ */
+ public function update_released_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment_ids = Request::intArray('assignment_ids');
+ $released = Request::int('released');
+
+ if (isset($released)) {
+ foreach ($assignment_ids as $assignment_id) {
+ $assignment = VipsAssignment::find($assignment_id);
+ VipsModule::requireEditPermission($assignment);
+
+ $assignment->options['released'] = $released;
+ $assignment->store();
+ }
+
+ PageLayout::postSuccess(_('Die Freigabeeinstellungen wurden geändert.'));
+ }
+
+ $this->redirect('vips/solutions');
+ }
+
+ /**
+ * Changes which correction information is released to the student (either
+ * nothing or only the points or points and correction).
+ */
+ public function change_released_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+
+ VipsModule::requireEditPermission($assignment);
+
+ $assignment->options['released'] = Request::int('released');
+ $assignment->store();
+
+ $this->redirect($this->url_for('vips/solutions/assignment_solutions', compact('assignment_id')));
+ }
+
+ /**
+ * Shows solution points for each student/group with a link to view solution and correct it.
+ */
+ public function assignment_solutions_action()
+ {
+ PageLayout::setHelpKeyword('Basis.VipsKorrektur');
+
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $format = Request::option('format');
+
+ VipsModule::requireEditPermission($assignment);
+
+ $view = Request::option('view');
+ $expand = Request::option('expand');
+
+ // fetch info about assignment
+ $end = $assignment->end;
+ $duration = $assignment->options['duration'];
+ $released = $assignment->options['released'];
+
+ // fetch solvers, exercises and solutions //
+ $arrays = $this->get_solutions($assignment, $view);
+ $solvers = $arrays['solvers'];
+ $exercises = $arrays['exercises'];
+ $solutions = $arrays['solutions'];
+
+ if ($assignment->type === 'exam') {
+ $all_solvers = $solvers;
+ $solvers = [];
+ $started = [];
+
+ // find all user ids which have an entry in etask_assignment_attempts
+ foreach ($assignment->assignment_attempts as $attempt) {
+ $start = $attempt->start;
+ $user_end = $attempt->end ? $attempt->end : $start + $duration * 60;
+ $user_end = min($end, $user_end);
+ $remaining_time = ceil(($user_end - time()) / 60);
+
+ $started[$attempt->user_id] = [
+ 'start' => $start,
+ 'remaining' => $remaining_time,
+ 'ip' => $attempt->ip_address
+ ];
+ }
+
+ // remove users which are not shown
+ foreach ($all_solvers as $solver) {
+ $user_id = $solver['id'];
+
+ if (isset($started[$user_id])) {
+ $remaining = $started[$user_id]['remaining'];
+
+ if ($view === 'working' && $remaining > 0 || $view == '' && $remaining <= 0) {
+ // working or finished
+ $solvers[$user_id] = $all_solvers[$user_id];
+ $solvers[$user_id]['running_info'] = $started[$user_id];
+ }
+ } else if ($view === 'pending') {
+ if ($assignment->isVisible($user_id)) {
+ // not yet started
+ $solvers[$user_id] = $all_solvers[$user_id];
+ }
+ }
+ }
+ }
+
+ /* reached points, uncorrected solutions and unanswered exercises */
+
+ $overall_uncorrected_solutions = 0;
+ $first_uncorrected_solution = null;
+
+ foreach ($solvers as $solver_id => $solver) {
+ $extra_info = [
+ 'points' => 0,
+ 'progress' => 0,
+ 'uncorrected' => 0,
+ 'unanswered' => count($exercises),
+ 'files' => 0
+ ];
+
+ if (isset($solutions[$solver_id])) {
+ foreach ($solutions[$solver_id] as $solution) {
+ $extra_info['points'] += $solution['points'];
+ $extra_info['progress'] += $exercises[$solution['exercise_id']]['points'];
+ $extra_info['uncorrected'] += $solution['corrected'] ? 0 : 1;
+ $extra_info['unanswered'] -= 1;
+ $extra_info['files'] += $solution['uploads'];
+
+ if (!$solution['corrected']) {
+ if (!isset($first_uncorrected_solution)) {
+ $first_uncorrected_solution = [
+ 'solver_id' => $solver['user_id'],
+ 'exercise_id' => $solution['exercise_id'],
+ ];
+ }
+ }
+ }
+ }
+
+ $overall_uncorrected_solutions += $extra_info['uncorrected'];
+ $solvers[$solver_id]['extra_info'] = $extra_info;
+ }
+
+ $this->assignment = $assignment;
+ $this->assignment_id = $assignment_id;
+ $this->view = $view;
+ $this->expand = $expand;
+ $this->solutions = $solutions;
+ $this->solvers = $solvers;
+ $this->exercises = $exercises;
+ $this->overall_max_points = $assignment->test->getTotalPoints();
+ $this->overall_uncorrected_solutions = $overall_uncorrected_solutions;
+ $this->first_uncorrected_solution = $first_uncorrected_solution;
+
+ if ($format === 'csv') {
+ $columns = [_('Teilnehmende')];
+
+ foreach ($exercises as $exercise) {
+ $columns[] = $exercise['position'] . '. ' . $exercise['title'];
+ }
+
+ $columns[] = _('Summe');
+
+ $data = [$columns];
+
+ setlocale(LC_NUMERIC, $_SESSION['_language'] . '.UTF-8');
+
+ $row = [_('Maximalpunktzahl:')];
+
+ foreach ($exercises as $exercise) {
+ $row[] = sprintf('%g', $exercise['points']);
+ }
+
+ $row[] = sprintf('%g', $this->overall_max_points);
+
+ $data[] = $row;
+
+ foreach ($solvers as $solver) {
+ $row = [$solver['name']];
+
+ foreach ($exercises as $exercise) {
+ if (isset($solutions[$solver['id']][$exercise['id']])) {
+ $row[] = sprintf('%g', $solutions[$solver['id']][$exercise['id']]['points']);
+ } else {
+ $row[] = '';
+ }
+ }
+
+ $row[] = sprintf('%g', $solver['extra_info']['points']);
+
+ $data[] = $row;
+ }
+
+ setlocale(LC_NUMERIC, 'C');
+
+ $this->render_csv($data, $assignment->test->title . '.csv');
+ } else {
+ Helpbar::get()->addPlainText('',
+ _('In dieser Übersicht können Sie sich anzeigen lassen, welche Teilnehmenden Lösungen abgegeben haben, und diese Lösungen korrigieren und freigeben.'));
+
+ $widget = new ActionsWidget();
+ $widget->addLink(
+ _('Aufgabenblatt bearbeiten'),
+ $this->url_for('vips/sheets/edit_assignment', ['assignment_id' => $assignment_id]),
+ Icon::create('edit')
+ );
+ $widget->addLink(
+ _('Aufgabenblatt drucken'),
+ $this->url_for('vips/sheets/print_assignments', ['assignment_id' => $assignment_id]),
+ Icon::create('print'),
+ ['target' => '_blank']
+ );
+ $widget->addLink(
+ _('Autokorrektur starten'),
+ $this->url_for('vips/solutions/autocorrect_dialog', compact('assignment_id', 'expand', 'view')),
+ Icon::create('accept')
+ )->asDialog('size=auto');
+
+ if ($assignment->type === 'exam') {
+ $widget->addLink(
+ _('Alle Lösungen zurücksetzen'),
+ $this->url_for('vips/solutions/delete_solutions', compact('assignment_id', 'view') + ['solver_id' => 'all']),
+ Icon::create('refresh'),
+ ['data-confirm' => _('Achtung: Wenn Sie die Lösungen zurücksetzen, werden die Lösungen aller Teilnehmenden archiviert!')]
+ )->asButton();
+ }
+
+ $plugin_manager = PluginManager::getInstance();
+ $gradebook = $plugin_manager->getPluginInfo('GradebookModule');
+
+ if ($gradebook && $plugin_manager->isPluginActivated($gradebook['id'], $assignment->range_id)) {
+ if ($assignment->options['gradebook_id']) {
+ $widget->addLink(
+ _('Gradebook-Eintrag entfernen'),
+ $this->url_for('vips/solutions/gradebook_unpublish', compact('assignment_id', 'expand', 'view')),
+ Icon::create('assessment'),
+ ['data-confirm' => _('Eintrag aus dem Gradebook löschen?')]
+ )->asButton();
+ } else {
+ $widget->addLink(
+ _('Eintrag im Gradebook anlegen'),
+ $this->url_for('vips/solutions/gradebook_dialog', compact('assignment_id', 'expand', 'view')),
+ Icon::create('assessment')
+ )->asDialog('size=auto');
+ }
+ }
+
+ Sidebar::get()->addWidget($widget);
+
+ $widget = new ExportWidget();
+ $widget->addLink(
+ _('Punktetabelle exportieren'),
+ $this->url_for('vips/solutions/assignment_solutions', ['assignment_id' => $assignment_id, 'format' => 'csv']),
+ Icon::create('export')
+ );
+
+ if ($assignment->type === 'exam') {
+ $widget->addLink(
+ _('Abgabeprotokolle exportieren'),
+ $this->url_for('vips/solutions/assignment_logs', ['assignment_id' => $assignment_id]),
+ Icon::create('export')
+ );
+ }
+
+ $widget->addLink(
+ _('Lösungen der Teilnehmenden exportieren'),
+ $this->url_for('vips/solutions/download_responses', ['assignment_id' => $assignment_id]),
+ Icon::create('export')
+ );
+ $widget->addLink(
+ _('Abgegebene Dateien herunterladen'),
+ $this->url_for('vips/solutions/download_all_uploads', ['assignment_id' => $assignment_id]),
+ Icon::create('export')
+ );
+ Sidebar::get()->addWidget($widget);
+
+ $widget = new OptionsWidget();
+ $widget->setTitle(_('Freigabe für Studierende'));
+ $widget->addRadioButton(
+ _('Nichts'),
+ $this->url_for('vips/solutions/change_released', [
+ 'assignment_id' => $assignment_id,
+ 'released' => VipsAssignment::RELEASE_STATUS_NONE,
+ ]),
+ $released == VipsAssignment::RELEASE_STATUS_NONE
+ );
+ $widget->addRadioButton(
+ _('Vergebene Punkte'),
+ $this->url_for('vips/solutions/change_released', [
+ 'assignment_id' => $assignment_id,
+ 'released' => VipsAssignment::RELEASE_STATUS_POINTS,
+ ]),
+ $released == VipsAssignment::RELEASE_STATUS_POINTS
+ );
+ $widget->addRadioButton(
+ _('Punkte und Kommentare'),
+ $this->url_for('vips/solutions/change_released', [
+ 'assignment_id' => $assignment_id,
+ 'released' => VipsAssignment::RELEASE_STATUS_COMMENTS,
+ ]),
+ $released == VipsAssignment::RELEASE_STATUS_COMMENTS
+ );
+ $widget->addRadioButton(
+ _('… zusätzlich Aufgaben und Korrektur'),
+ $this->url_for('vips/solutions/change_released', [
+ 'assignment_id' => $assignment_id,
+ 'released' => VipsAssignment::RELEASE_STATUS_CORRECTIONS,
+ ]),
+ $released == VipsAssignment::RELEASE_STATUS_CORRECTIONS
+ );
+ $widget->addRadioButton(
+ _('… zusätzlich Musterlösungen'),
+ $this->url_for('vips/solutions/change_released', [
+ 'assignment_id' => $assignment_id,
+ 'released' => VipsAssignment::RELEASE_STATUS_SAMPLE_SOLUTIONS,
+ ]),
+ $released == VipsAssignment::RELEASE_STATUS_SAMPLE_SOLUTIONS
+ );
+ Sidebar::get()->addWidget($widget);
+ }
+ }
+
+ /**
+ * Download responses to all exercises for all users in an assignment.
+ */
+ public function download_responses_action()
+ {
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+
+ VipsModule::requireEditPermission($assignment);
+
+ $arrays = $this->get_solutions($assignment, null);
+ $columns = [_('Teilnehmende')];
+ $exercises = [];
+ $item_count = [];
+
+ foreach ($arrays['exercises'] as $exercise) {
+ $exercises[$exercise['id']] = Exercise::find($exercise['id']);
+ $item_count[$exercise['id']] = $exercises[$exercise['id']]->itemCount();
+
+ for ($i = 0; $i < $item_count[$exercise['id']]; ++$i) {
+ if ($i === 0) {
+ $columns[] = $exercise['position'] . '. ' . $exercise['title'];
+ } else {
+ $columns[] = '';
+ }
+ }
+
+ if ($exercises[$exercise['id']]->options['comment']) {
+ $columns[] = _('Bemerkungen');
+ }
+ }
+
+ $data = [$columns];
+
+ foreach ($arrays['solvers'] as $solver) {
+ $row = [$solver['name']];
+
+ if (isset($arrays['solutions'][$solver['id']])) {
+ $solutions = $arrays['solutions'][$solver['id']];
+
+ foreach ($arrays['exercises'] as $exercise) {
+ $vips_solution = null;
+ $export = [];
+
+ if (isset($solutions[$exercise['id']])) {
+ $vips_solution = VipsSolution::find($solutions[$exercise['id']]['id']);
+ $export = $exercises[$exercise['id']]->exportResponse($vips_solution->response);
+ }
+
+ for ($i = 0; $i < $item_count[$exercise['id']]; ++$i) {
+ $row[] = isset($export[$i]) ? $export[$i] : '';
+ }
+
+ if ($exercises[$exercise['id']]->options['comment']) {
+ $row[] = $vips_solution ? $vips_solution->student_comment : '';
+ }
+ }
+
+ $data[] = $row;
+ }
+ }
+
+ $this->render_csv($data, $assignment->test->title . '.csv');
+ }
+
+ /**
+ * Download uploads to current solutions for all users in an assignment.
+ */
+ public function download_all_uploads_action()
+ {
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+
+ VipsModule::requireEditPermission($assignment);
+
+ $sem_class = $assignment->course->getSemClass();
+ $filename = $assignment->test->title . '.zip';
+ $zipfile = tempnam($GLOBALS['TMP_PATH'], 'upload');
+ $zip = new ZipArchive();
+
+ if (!$zip->open($zipfile, ZipArchive::CREATE | ZipArchive::OVERWRITE)) {
+ throw new Exception(_('ZIP-Archiv konnte nicht erzeugt werden.'));
+ }
+
+ $arrays = $this->get_solutions($assignment, null);
+
+ foreach ($arrays['solvers'] as $solver) {
+ foreach ($arrays['exercises'] as $exercise) {
+ $solution = $arrays['solutions'][$solver['id']][$exercise['id']]; // may be null
+ $folder = $solver['name'];
+
+ if ($solver['type'] === 'single' && !$sem_class['studygroup_mode']) {
+ $folder .= sprintf(' (%s)', $solver['stud_id'] ?: $solver['username']);
+ }
+
+ if ($solution && $solution['uploads']) {
+ foreach (VipsSolution::find($solution['id'])->folder->file_refs as $file_ref) {
+ $zip->addFile($file_ref->file->getPath(), sprintf(_('%s/Aufgabe %d/'), $folder, $exercise['position']) . $file_ref->name);
+ }
+ }
+ }
+ }
+
+ $zip->close();
+
+ if (!file_exists($zipfile)) {
+ file_put_contents($zipfile, base64_decode('UEsFBgAAAAAAAAAAAAAAAAAAAAAAAA=='));
+ }
+
+ header('Content-Type: application/zip');
+ header('Content-Disposition: attachment; ' . encode_header_parameter('filename', $filename));
+ header('Content-Length: ' . filesize($zipfile));
+
+ readfile($zipfile);
+ unlink($zipfile);
+ die();
+ }
+
+ /**
+ * Download uploads to current solutions for a user in an assignment.
+ */
+ public function download_uploads_action()
+ {
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $solver_id = Request::option('solver_id');
+
+ VipsModule::requireEditPermission($assignment);
+
+ $group = $assignment->getUserGroup($solver_id);
+ $solver_name = $group ? $group->name : get_username($solver_id);
+
+ $filename = $assignment->test->title . '-' . $solver_name . '.zip';
+ $zipfile = tempnam($GLOBALS['TMP_PATH'], 'upload');
+ $zip = new ZipArchive();
+
+ if (!$zip->open($zipfile, ZipArchive::CREATE | ZipArchive::OVERWRITE)) {
+ throw new Exception(_('ZIP-Archiv konnte nicht erzeugt werden.'));
+ }
+
+ foreach ($assignment->test->exercises as $i => $exercise) {
+ $solution = $assignment->getSolution($solver_id, $exercise->id);
+
+ if ($solution) {
+ foreach ($solution->folder->file_refs as $file_ref) {
+ $zip->addFile($file_ref->file->getPath(), sprintf(_('Aufgabe %d/'), $i + 1) . $file_ref->name);
+ }
+ }
+ }
+
+ $zip->close();
+
+ header('Content-Type: application/zip');
+ header('Content-Disposition: attachment; ' . encode_header_parameter('filename', $filename));
+ header('Content-Length: ' . filesize($zipfile));
+
+ readfile($zipfile);
+ unlink($zipfile);
+ die();
+ }
+
+ /**
+ * Show dialog for publishing the assignment in the gradebook.
+ */
+ public function gradebook_dialog_action()
+ {
+ $this->assignment_id = Request::int('assignment_id');
+ $this->assignment = VipsAssignment::find($this->assignment_id);
+ $this->view = Request::option('view');
+ $this->expand = Request::option('expand');
+
+ VipsModule::requireEditPermission($this->assignment);
+
+ $definitions = Grading\Definition::findByCourse_id($this->assignment->range_id);
+ $this->weights = array_sum(array_column($definitions, 'weight'));
+ }
+
+ /**
+ * Publish this assignment in the gradebook of the course.
+ */
+ public function gradebook_publish_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $view = Request::option('view');
+ $expand = Request::option('expand');
+ $title = Request::get('title');
+ $weight = Request::float('weight');
+
+ VipsModule::requireEditPermission($assignment);
+
+ $assignment->insertIntoGradebook($title, $weight);
+ $assignment->updateGradebookEntries();
+
+ PageLayout::postSuccess(_('Das Aufgabenblatt wurde in das Gradebook eingetragen.'));
+ $this->redirect($this->url_for('vips/solutions/assignment_solutions', compact('assignment_id', 'view', 'expand')));
+ }
+
+ /**
+ * Remove this assignment from the gradebook of the course.
+ */
+ public function gradebook_unpublish_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $view = Request::option('view');
+ $expand = Request::option('expand');
+
+ VipsModule::requireEditPermission($assignment);
+
+ $assignment->removeFromGradebook();
+
+ PageLayout::postSuccess(_('Das Aufgabenblatt wurde aus dem Gradebook gelöscht.'));
+ $this->redirect($this->url_for('vips/solutions/assignment_solutions', compact('assignment_id', 'view', 'expand')));
+ }
+
+ /**
+ * Download a summary of the event logs for an assignment.
+ */
+ public function assignment_logs_action()
+ {
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+
+ VipsModule::requireEditPermission($assignment);
+
+ $columns = [_('Nachname'), _('Vorname'), _('Kennung'), _('Ereignis'),
+ _('Zeit'), _('IP-Adresse'), _('Rechnername'), _('Sitzungs-ID')];
+ $data = [];
+
+ foreach ($assignment->assignment_attempts as $assignment_attempt) {
+ foreach ($assignment_attempt->getLogEntries() as $entry) {
+ $data[] = [
+ $assignment_attempt->user->nachname,
+ $assignment_attempt->user->vorname,
+ $assignment_attempt->user->username,
+ $entry['label'],
+ $entry['time'],
+ $entry['ip_address'],
+ $entry['ip_address'] ? $this->gethostbyaddr($entry['ip_address']) : '',
+ $entry['session_id']
+ ];
+ }
+ }
+
+ usort($data, function($a, $b) {
+ return strcoll("{$a[0]},{$a[1]},{$a[2]},{$a[4]}", "{$b[0]},{$b[1]},{$b[2]},{$b[4]}");
+ });
+
+ array_unshift($data, $columns);
+
+ $this->render_csv($data, $assignment->test->title . '_log.csv');
+ }
+
+
+
+ /******************************************************************************/
+ /*
+ /* A U T O K O R R E K T U R
+ /*
+ /******************************************************************************/
+
+ /**
+ * Select options and run automatic correction of solutions.
+ */
+ public function autocorrect_dialog_action()
+ {
+ $this->assignment_id = Request::int('assignment_id');
+ $this->view = Request::option('view');
+ $this->expand = Request::option('expand');
+ }
+
+ /**
+ * Deletes all solution-points, where the solutions are automatically corrected
+ */
+ public function autocorrect_solutions_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $view = Request::option('view');
+ $expand = Request::option('expand');
+ $corrected = Request::int('corrected', 0);
+
+ VipsModule::requireEditPermission($assignment);
+
+ $corrected_solutions = 0;
+
+ // select all solutions not manually corrected
+ $solutions = $assignment->solutions->findBy('grader_id', null);
+
+ foreach ($solutions as $solution) {
+ $assignment->correctSolution($solution, $corrected);
+ $solution->store();
+
+ if ($solution->state) {
+ ++$corrected_solutions;
+ }
+ }
+
+ $message = sprintf(ngettext('Es wurde %d Lösung korrigiert.', 'Es wurden %d Lösungen korrigiert.', $corrected_solutions), $corrected_solutions);
+ PageLayout::postSuccess($message);
+ $this->redirect($this->url_for('vips/solutions/assignment_solutions', compact('assignment_id', 'view', 'expand')));
+ }
+
+ /**
+ * Display form that allows lecturer to correct the student's solution.
+ */
+ public function edit_solution_action()
+ {
+ PageLayout::setHelpKeyword('Basis.VipsKorrektur');
+
+ $exercise_id = Request::int('exercise_id');
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+
+ VipsModule::requireEditPermission($assignment, $exercise_id);
+
+ $archived_id = Request::int('solution_id');
+ $solver_id = Request::option('solver_id');
+ $view = Request::option('view');
+
+ $group = $assignment->getUserGroup($solver_id);
+ $solver_name = $group ? $group->name : get_fullname($solver_id, 'no_title_rev');
+ $solver_or_group_id = $group ? $group->id : $solver_id;
+
+ // fetch solvers, exercises and solutions //
+
+ $arrays = $this->get_solutions($assignment, $view);
+ $solvers = $arrays['solvers'];
+ $exercises = $arrays['exercises'];
+ $solutions = $arrays['solutions'];
+
+ // previous and next solver, exercise and uncorrected exercise //
+
+ $prev_solver = null;
+ $prev_exercise = null;
+ $next_solver = null;
+ $next_exercise = null;
+ $next_uncorrected_exercise = null;
+ $before_current = true; // before current solver + current exercise
+
+ foreach ($solvers as $solver) {
+ foreach ($exercises as $exercise) {
+ // current solver and current exercise
+ if ($solver['id'] == $solver_or_group_id && $exercise['id'] == $exercise_id) {
+ $before_current = false;
+ $exercise_position = $exercise['position'];
+ $max_points = $exercise['points'];
+ continue;
+ }
+
+ if (isset($solutions[$solver['id']][$exercise['id']])) {
+ // previous/next solver (same exercise)
+ if ($solver['id'] != $solver_or_group_id && $exercise['id'] == $exercise_id) {
+ if ($before_current) {
+ $prev_solver = $solver;
+ } else if (!isset($next_solver)) {
+ $next_solver = $solver;
+ }
+ }
+
+ // previous/next exercise (same solver)
+ if ($solver['id'] == $solver_or_group_id && $exercise['id'] != $exercise_id) {
+ if ($before_current) {
+ $prev_exercise = $exercise;
+ } else if (!isset($next_exercise)) {
+ $next_exercise = $exercise;
+ }
+ }
+
+ // previous/next uncorrected exercise
+ if (!$solutions[$solver['id']][$exercise['id']]['corrected']) {
+ if ($before_current) {
+ $prev_uncorrected_exercise = [
+ 'id' => $exercise['id'],
+ 'solver_id' => $solver['user_id']
+ ];
+ } else if (!isset($next_uncorrected_exercise)) {
+ $next_uncorrected_exercise = [
+ 'id' => $exercise['id'],
+ 'solver_id' => $solver['user_id']
+ ];
+ }
+ }
+
+ // break condition
+ if (isset($next_uncorrected_exercise) && isset($next_solver)) {
+ break 2;
+ }
+ }
+ }
+ }
+
+ ###################################
+ # get user solution if applicable #
+ ###################################
+
+ $exercise = Exercise::find($exercise_id);
+ $solution = $assignment->getSolution($solver_id, $exercise_id);
+ $solution_archive = $assignment->getArchivedSolutions($solver_id, $exercise_id);
+
+ if (!$solution) {
+ $solution = new VipsSolution();
+ $solution->assignment = $assignment;
+ $version = _('Nicht abgegeben');
+ } else {
+ $version = date('d.m.Y, H:i', $solution->mkdate);
+ }
+
+ if ($assignment->type !== 'selftest' && !isset($solution->feedback)) {
+ $solution->feedback = $exercise->options['feedback'];
+ }
+
+ if ($archived_id) {
+ foreach ($solution_archive as $old_solution) {
+ if ($old_solution->id == $archived_id) {
+ $solution = $old_solution;
+ break;
+ }
+ }
+ }
+
+ ##############################
+ # set template variables #
+ ##############################
+
+ $this->exercise = $exercise;
+ $this->exercise_id = $exercise_id;
+ $this->exercise_position = $exercise_position;
+ $this->assignment = $assignment;
+ $this->assignment_id = $assignment_id;
+ $this->solver_id = $solver_id;
+ $this->solver_name = $solver_name;
+ $this->solver_or_group_id = $solver_or_group_id;
+ $this->solution = $solution;
+ $this->edit_solution = !$archived_id;
+ $this->show_solution = true;
+ $this->max_points = $max_points;
+ $this->prev_solver = $prev_solver;
+ $this->prev_exercise = $prev_exercise;
+ $this->next_solver = $next_solver;
+ $this->next_exercise = $next_exercise;
+ $this->view = $view;
+
+ Helpbar::get()->addPlainText('',
+ _('Sie können hier die Ergebnisse der Autokorrektur ansehen und Aufgaben manuell korrigieren.'));
+
+ $widget = new ActionsWidget();
+ $widget->addLink(
+ _('Aufgabe bearbeiten'),
+ $this->url_for('vips/sheets/edit_exercise', compact('assignment_id', 'exercise_id')),
+ Icon::create('edit')
+ );
+ $widget->addLink(
+ _('Aufgabenblatt drucken'),
+ $this->url_for('vips/sheets/print_assignments', ['assignment_id' => $assignment_id, 'user_ids[]' => $solver_id, 'print_files' => 1, 'print_correction' => !$view]),
+ Icon::create('print'),
+ ['target' => '_blank']
+ );
+ Sidebar::get()->addWidget($widget);
+
+ $widget = new LinksWidget();
+ $widget->setTitle(_('Links'));
+ if (isset($prev_uncorrected_exercise)) {
+ $widget->addLink(
+ _('Vorige unkorrigierte Aufgabe'),
+ $this->url_for('vips/solutions/edit_solution', [
+ 'assignment_id' => $assignment_id,
+ 'exercise_id' => $prev_uncorrected_exercise['id'],
+ 'solver_id' => $prev_uncorrected_exercise['solver_id'],
+ 'view' => $view,
+ ]),
+ Icon::create('arr_1left')
+ );
+ }
+ if (isset($next_uncorrected_exercise)) {
+ $widget->addLink(
+ _('Nächste unkorrigierte Aufgabe'),
+ $this->url_for('vips/solutions/edit_solution', [
+ 'assignment_id' => $assignment_id,
+ 'exercise_id' => $next_uncorrected_exercise['id'],
+ 'solver_id' => $next_uncorrected_exercise['solver_id'],
+ 'view' => $view,
+ ]),
+ Icon::create('arr_1right')
+ );
+ }
+ Sidebar::get()->addWidget($widget);
+
+ $widget = new SelectWidget(_('Aufgabenblatt'), $this->url_for('vips/solutions/edit_solution', compact('assignment_id', 'solver_id', 'view')), 'exercise_id');
+
+ foreach ($exercises as $exercise) {
+ $element = new SelectElement($exercise['id'], sprintf(_('Aufgabe %d'), $exercise['position']), $exercise['id'] === $exercise_id);
+ $widget->addElement($element);
+ }
+ Sidebar::get()->addWidget($widget);
+
+ $widget = new SelectWidget(_('Versionen'), $this->url_for('vips/solutions/edit_solution', compact('assignment_id', 'exercise_id', 'solver_id', 'view')), 'solution_id');
+ $element = new SelectElement(0, sprintf(_('Aktuelle Version: %s'), $version), !$archived_id);
+ $widget->addElement($element);
+
+ if (count($solution_archive) === 0) {
+ $widget->attributes = ['disabled' => 'disabled'];
+ }
+
+ foreach ($solution_archive as $i => $old_solution) {
+ $element = new SelectElement($old_solution->id,
+ sprintf(_('Version %s vom %s'), count($solution_archive) - $i, date('d.m.Y, H:i', $old_solution->mkdate)),
+ $old_solution->id == $archived_id);
+ $widget->addElement($element);
+ }
+ Sidebar::get()->addWidget($widget);
+ }
+
+ /**
+ * Display a student's corrected solution for a single exercise.
+ */
+ public function view_solution_action()
+ {
+ $exercise_id = Request::int('exercise_id');
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+
+ VipsModule::requireViewPermission($assignment, $exercise_id);
+
+ $solver_id = $GLOBALS['user']->id;
+ $released = $assignment->releaseStatus($solver_id);
+
+ if ($released < VipsAssignment::RELEASE_STATUS_CORRECTIONS) {
+ // the assignment is not finished or not yet released
+ PageLayout::postError(_('Die Korrekturen des Aufgabenblatts sind nicht freigegeben.'));
+ $this->redirect($this->url_for('vips/solutions/student_assignment_solutions', compact('assignment_id')));
+ return;
+ }
+
+ $exercise = Exercise::find($exercise_id);
+ $solution = $assignment->getSolution($solver_id, $exercise_id);
+
+ if (!$solution) {
+ $solution = new VipsSolution();
+ $solution->assignment = $assignment;
+ }
+
+ // fetch previous and next exercises
+ $prev_exercise_id = null;
+ $next_exercise_id = null;
+ $before_current = true;
+
+ foreach ($assignment->getExerciseRefs($solver_id) as $i => $item) {
+ if ($item->task_id == $exercise_id) {
+ $before_current = false;
+ $exercise_position = $i + 1;
+ $max_points = $item->points;
+ } else if ($before_current) {
+ $prev_exercise_id = $item->task_id;
+ } else {
+ $next_exercise_id = $item->task_id;
+ break;
+ }
+ }
+
+ $this->exercise = $exercise;
+ $this->exercise_position = $exercise_position;
+ $this->assignment = $assignment;
+ $this->solution = $solution;
+ $this->show_solution = $released == VipsAssignment::RELEASE_STATUS_SAMPLE_SOLUTIONS;
+ $this->max_points = $max_points;
+ $this->prev_exercise_id = $prev_exercise_id;
+ $this->next_exercise_id = $next_exercise_id;
+
+ $widget = new SelectWidget(_('Aufgabenblatt'), $this->url_for('vips/solutions/view_solution', compact('assignment_id')), 'exercise_id');
+
+ foreach ($assignment->getExerciseRefs($solver_id) as $i => $item) {
+ $element = new SelectElement($item->task_id, sprintf(_('Aufgabe %d'), $i + 1), $item->task_id === $exercise->id);
+ $widget->addElement($element);
+ }
+ Sidebar::get()->addWidget($widget);
+ }
+
+ /**
+ * Restores an archived solution as the current solution.
+ */
+ public function restore_solution_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $solution_id = Request::int('solution_id');
+ $solver_id = Request::option('solver_id');
+ $view = Request::option('view');
+
+ $solution = VipsSolution::find($solution_id);
+
+ $exercise_id = $solution->task_id;
+ $assignment_id = $solution->assignment_id;
+ $assignment = $solution->assignment;
+
+ VipsModule::requireEditPermission($assignment);
+
+ $assignment->restoreSolution($solution);
+ PageLayout::postSuccess(_('Die ausgewählte Lösung wurde als aktuelle Version gespeichert.'));
+
+ $this->redirect($this->url_for('vips/solutions/edit_solution', compact('exercise_id', 'assignment_id', 'solver_id', 'view')));
+ }
+
+ /**
+ * Displays a student's event log for an assignment.
+ */
+ public function show_assignment_log_action()
+ {
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $solver_id = Request::option('solver_id');
+
+ VipsModule::requireEditPermission($assignment);
+
+ $assignment_attempt = $assignment->getAssignmentAttempt($solver_id);
+
+ $this->user = User::find($solver_id);
+ $this->logs = $assignment_attempt->getLogEntries();
+ }
+
+ /**
+ * Offer to remove users from a group for this assignment.
+ */
+ public function edit_group_dialog_action()
+ {
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $solver_id = Request::option('solver_id');
+ $view = Request::option('view');
+
+ VipsModule::requireEditPermission($assignment);
+
+ $this->group = $assignment->getUserGroup($solver_id);
+ $this->members = $assignment->getGroupMembers($this->group);
+
+ usort($this->members, function($a, $b) {
+ return strcoll($a->user->getFullName('no_title_rev'), $b->user->getFullName('no_title_rev'));
+ });
+
+ $this->assignment = $assignment;
+ $this->view = $view;
+ }
+
+ /**
+ * Update group assignment for a list of users for this assignment.
+ */
+ public function edit_group_action()
+ {
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $group_id = Request::option('group_id');
+ $group = VipsGroup::find($group_id);
+ $user_ids = Request::optionArray('user_ids');
+ $view = Request::option('view');
+
+ VipsModule::requireEditPermission($assignment);
+
+ if ($assignment->isFinished() && $user_ids) {
+ foreach ($assignment->getGroupMembers($group) as $member) {
+ if (in_array($member->user_id, $user_ids)) {
+ $clone = $member->build($member);
+ $member->end = $assignment->end;
+ $member->store();
+ $clone->start = $assignment->end;
+ $clone->store();
+ }
+ }
+
+ PageLayout::postSuccess(_('Die ausgewählten Personen wurden aus der Gruppe entfernt.'));
+ }
+
+ $this->redirect($this->url_for('vips/solutions/assignment_solutions', compact('assignment_id', 'view')));
+ }
+
+ /**
+ * Write a message to selected members for an assignment.
+ */
+ public function write_message_action()
+ {
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $user_ids = Request::optionArray('user_ids');
+
+ VipsModule::requireEditPermission($assignment);
+
+ foreach ($user_ids as $user_id) {
+ $group = $assignment->getUserGroup($user_id);
+
+ if ($group) {
+ foreach ($assignment->getGroupMembers($group) as $member) {
+ $users[] = $member->username;
+ }
+ } else {
+ $users[] = get_username($user_id);
+ }
+ }
+
+ $_SESSION['sms_data']['p_rec'] = $users;
+ $this->redirect(URLHelper::getURL('dispatch.php/messages/write'));
+ }
+
+ /**
+ * Stores the lecturer comment and the corrected points for a solution.
+ */
+ public function store_correction_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $solution_id = Request::int('solution_id');
+ $solver_id = Request::option('solver_id');
+ $view = Request::option('view');
+
+ $feedback = trim(Request::get('feedback'));
+ $feedback = Studip\Markup::purifyHtml($feedback);
+ $file_ids = Request::optionArray('file_ids');
+ $corrected = Request::int('corrected', 0);
+ $reached_points = Request::float('reached_points');
+ $max_points = Request::float('max_points');
+
+ if ($solution_id) {
+ $solution = VipsSolution::find($solution_id);
+ } else {
+ // create dummy empty solution
+ $solution = new VipsSolution();
+ $solution->task_id = Request::int('exercise_id');
+ $solution->assignment_id = Request::int('assignment_id');
+ $solution->user_id = $solver_id;
+ }
+
+ $exercise_id = $solution->task_id;
+ $assignment_id = $solution->assignment_id;
+
+ VipsModule::requireEditPermission($solution->assignment, $exercise_id);
+
+ // let exercise class handle special controls added to the form
+ $exercise = Exercise::find($exercise_id);
+ $exercise->correctSolutionAction($this, $solution);
+
+ if (Request::submitted('store_solution')) {
+ // process lecturer's input
+ $solution->state = $corrected;
+ $solution->points = round($reached_points * 2) / 2;
+ $solution->feedback = $feedback ?: null;
+
+ setlocale(LC_NUMERIC, $_SESSION['_language'] . '.UTF-8');
+
+ if ($solution->points > $max_points) {
+ PageLayout::postInfo(sprintf(_('Sie haben Bonuspunkte vergeben: %g von %g.'), $solution->points, $max_points));
+ } else if ($solution->points < 0) {
+ PageLayout::postWarning(sprintf(_('Sie haben eine negative Punktzahl eingegeben: %g von %g.'), $solution->points, $max_points));
+ } else if ($solution->points != $reached_points) {
+ PageLayout::postWarning(sprintf(_('Die eingegebene Punktzahl wurde auf halbe Punkte gerundet: %g.'), $solution->points));
+ }
+
+ setlocale(LC_NUMERIC, 'C');
+
+ $upload = $_FILES['upload'] ?: ['name' => []];
+
+ if ($solution->isDirty() || count($upload)) {
+ $solution->grader_id = $GLOBALS['user']->id;
+ $solution->store();
+
+ PageLayout::postSuccess(_('Ihre Korrektur wurde gespeichert.'));
+ }
+
+ $folder = Folder::findTopFolder($solution->id, 'FeedbackFolder', 'response');
+
+ foreach ($folder->file_refs as $file_ref) {
+ if (!in_array($file_ref->id, $file_ids) || in_array($file_ref->name, $upload['name'])) {
+ $file_ref->delete();
+ }
+ }
+
+ FileManager::handleFileUpload($upload, $folder->getTypedFolder());
+ }
+
+ // show exercise and correction form again
+ $this->redirect($this->url_for('vips/solutions/edit_solution', compact('exercise_id', 'assignment_id', 'solver_id', 'view')));
+ }
+
+ /**
+ * Edit an active assignment attempt (update end time).
+ */
+ public function edit_assignment_attempt_action()
+ {
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $solver_id = Request::option('solver_id');
+ $view = Request::option('view');
+
+ VipsModule::requireEditPermission($assignment);
+
+ $this->assignment = $assignment;
+ $this->assignment_attempt = $assignment->getAssignmentAttempt($solver_id);
+ $this->solver_id = $solver_id;
+ $this->view = $view;
+ }
+
+ /**
+ * Update an active assignment attempt (store end time).
+ */
+ public function store_assignment_attempt_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $end_time = trim(Request::get('end_time'));
+ $solver_id = Request::option('solver_id');
+ $view = Request::option('view');
+
+ VipsModule::requireEditPermission($assignment);
+
+ $assignment_attempt = $assignment->getAssignmentAttempt($solver_id);
+
+ if ($assignment_attempt) {
+ $end_day = date('Y-m-d', $assignment->getUserEndTime($solver_id));
+ $end_datetime = DateTime::createFromFormat('Y-m-d H:i:s', $end_day . ' ' . $end_time);
+
+ if ($end_datetime) {
+ $assignment_attempt->end = strtotime($end_datetime->format('Y-m-d H:i:s'));
+ $assignment_attempt->store();
+
+ if ($assignment_attempt->end > $assignment->end) {
+ PageLayout::postWarning(_('Der Abgabezeitpunkt liegt nach dem Ende der Klausur.'));
+ }
+ } else {
+ PageLayout::postError(_('Der Abgabezeitpunkt ist keine gültige Uhrzeit.'));
+ }
+ }
+
+ $this->redirect($this->url_for('vips/solutions/assignment_solutions', compact('assignment_id', 'view')));
+ }
+
+ /**
+ * Deletes the solutions of a student (or all students) and resets the
+ * assignment attempt.
+ */
+ public function delete_solutions_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+ $solver_id = Request::option('solver_id');
+ $view = Request::option('view');
+
+ VipsModule::requireEditPermission($assignment);
+
+ if ($assignment->type === 'exam') {
+ if ($solver_id === 'all') {
+ $assignment->deleteAllSolutions();
+ PageLayout::postSuccess(_('Die Klausur wurde zurückgesetzt und alle abgegebenen Lösungen archiviert.'));
+ } else if ($assignment->isRunning()) {
+ $assignment->deleteSolutions($solver_id);
+ PageLayout::postSuccess(_('Die Teilnahme wurde zurückgesetzt und ggf. abgegebene Lösungen archiviert.'));
+ }
+ }
+
+ $this->redirect($this->url_for('vips/solutions/assignment_solutions', compact('assignment_id', 'view')));
+ }
+
+
+
+ /**
+ * Shows all corrected exercises of an exercise sheet, if the status
+ * of "released" allows that, i.e. is at least 1.
+ */
+ public function student_assignment_solutions_action()
+ {
+ $assignment_id = Request::int('assignment_id');
+ $assignment = VipsAssignment::find($assignment_id);
+
+ VipsModule::requireViewPermission($assignment);
+
+ $this->assignment = $assignment;
+ $this->user_id = $GLOBALS['user']->id;
+ $this->released = $assignment->releaseStatus($this->user_id);
+ $this->feedback = $assignment->getUserFeedback($this->user_id);
+
+ // Security check -- is assignment really accessible for students?
+ if ($this->released == VipsAssignment::RELEASE_STATUS_NONE) {
+ PageLayout::postError(_('Die Korrekturen wurden noch nicht freigegeben.'));
+ $this->redirect('vips/solutions');
+ return;
+ }
+
+ Helpbar::get()->addPlainText('',
+ _('Sie können hier die Ergebnisse bzw. die Korrekturen ihrer Aufgaben ansehen.'));
+
+ if ($this->released >= VipsAssignment::RELEASE_STATUS_CORRECTIONS) {
+ $widget = new ActionsWidget();
+ $widget->addLink(
+ _('Aufgabenblatt drucken'),
+ $this->url_for('vips/sheets/print_assignments', ['assignment_id' => $assignment_id]),
+ Icon::create('print'),
+ ['target' => '_blank']
+ );
+ Sidebar::get()->addWidget($widget);
+ }
+ }
+
+
+
+ /**
+ * Displays all course participants and all their results (reached points,
+ * percent, weighted percent) for all tests, blocks and exams plus their
+ * final grade.
+ */
+ public function participants_overview_action()
+ {
+ $this->course_id = Context::getId();
+ VipsModule::requireStatus('tutor', $this->course_id);
+
+ $display = Request::option('display', 'points');
+ $sort = Request::option('sort', 'name');
+ $desc = Request::int('desc');
+ $view = Request::option('view');
+ $format = Request::option('format');
+
+ $sem_class = Context::get()->getSemClass();
+ $attributes = $this->participants_overview_data($this->course_id, null, $display, $sort, $desc, $view);
+
+ $settings = CourseConfig::get($this->course_id);
+ $this->has_grades = !empty($settings->VIPS_COURSE_GRADES);
+
+ foreach ($attributes as $key => $value) {
+ $this->$key = $value;
+ }
+
+ if ($format == 'csv') {
+ $columns = [_('Nachname'), _('Vorname'), _('Kennung'), _('Matrikelnr.')];
+
+ foreach ($this->items as $category => $list) {
+ foreach ($list as $item) {
+ $columns[] = $item['name'];
+ }
+ }
+
+ $columns[] = _('Summe');
+
+ if ($display != 'points' && $this->has_grades) {
+ $columns[] = _('Note');
+ }
+
+ $data = [$columns];
+
+ setlocale(LC_NUMERIC, $_SESSION['_language'] . '.UTF-8');
+
+ if ($display == 'points' || $this->overall['weighting']) {
+ if ($display == 'points') {
+ $row = [_('Maximalpunktzahl'), '', '', ''];
+ } else {
+ $row = [_('Gewichtung'), '', '', ''];
+ }
+
+ foreach ($this->items as $category => $list) {
+ foreach ($list as $item) {
+ if ($display == 'points') {
+ $row[] = sprintf('%.1f', $item['points']);
+ } else {
+ $row[] = sprintf('%.1f%%', $item['weighting']);
+ }
+ }
+ }
+
+ if ($display == 'points') {
+ $row[] = sprintf('%.1f', $this->overall['points']);
+ } else {
+ $row[] = '100%';
+
+ if ($this->has_grades) {
+ $row[] = '';
+ }
+ }
+
+ $data[] = $row;
+ }
+
+ foreach ($this->participants as $p) {
+ $row = [$p['surname'], $p['forename'], $p['username']];
+
+ if (!$sem_class['studygroup_mode']) {
+ $row[] = $p['stud_id'];
+ } else {
+ $row[] = '';
+ }
+
+ foreach ($this->items as $category => $list) {
+ foreach ($list as $item) {
+ if ($display == 'points') {
+ if (isset($p['items'][$category][$item['id']]['points'])) {
+ $row[] = sprintf('%.1f', $p['items'][$category][$item['id']]['points']);
+ } else {
+ $row[] = '';
+ }
+ } else {
+ if (isset($p['items'][$category][$item['id']]['percent'])) {
+ $row[] = sprintf('%.1f%%', $p['items'][$category][$item['id']]['percent']);
+ } else {
+ $row[] = '';
+ }
+ }
+ }
+ }
+
+ if ($display == 'points') {
+ if (isset($p['overall']['points'])) {
+ $row[] = sprintf('%.1f', $p['overall']['points']);
+ } else {
+ $row[] = '';
+ }
+ } else {
+ if (isset($p['overall']['weighting'])) {
+ $row[] = sprintf('%.1f%%', $p['overall']['weighting']);
+ } else {
+ $row[] = '';
+ }
+
+ if ($this->has_grades) {
+ $row[] = $p['grade'];
+ }
+ }
+
+ $data[] = $row;
+ }
+
+ setlocale(LC_NUMERIC, 'C');
+
+ $this->render_csv($data, _('Notenliste.csv'));
+ } else {
+ Helpbar::get()->addPlainText('',
+ _('Diese Seite gibt einen Überblick über die von allen Teilnehmenden erreichten Punkte und ggf. Noten.'));
+
+ $widget = new ViewsWidget();
+ $widget->addLink(
+ _('Ergebnisse'),
+ $this->url_for('vips/solutions')
+ );
+ $widget->addLink(
+ _('Punkteübersicht'),
+ $this->url_for('vips/solutions/participants_overview', ['display' => 'points'])
+ )->setActive($display == 'points');
+ $widget->addLink(
+ _('Notenübersicht'),
+ $this->url_for('vips/solutions/participants_overview', ['display' => 'weighting'])
+ )->setActive($display == 'weighting');
+ $widget->addLink(
+ _('Statistik'),
+ $this->url_for('vips/solutions/statistics')
+ );
+ Sidebar::get()->addWidget($widget);
+
+ $widget = new ExportWidget();
+ $widget->addLink(
+ _('Liste im CSV-Format exportieren'),
+ $this->url_for('vips/solutions/participants_overview', ['display' => $display, 'view' => $view, 'sort' => $sort, 'format' => 'csv']),
+ Icon::create('export')
+ );
+ Sidebar::get()->addWidget($widget);
+ }
+ }
+
+ public function statistics_action()
+ {
+ $course_id = Context::getId();
+ VipsModule::requireStatus('tutor', $course_id);
+
+ $db = DBManager::get();
+
+ $format = Request::option('format');
+ $assignments = [];
+
+ $_assignments = VipsAssignment::findBySQL("range_id = ? AND type IN ('exam', 'practice') ORDER BY start", [$course_id]);
+
+ foreach ($_assignments as $assignment) {
+ $test_points = 0;
+ $test_average = 0;
+ $exercises = [];
+
+ foreach ($assignment->test->exercise_refs as $exercise_ref) {
+ $exercise = $exercise_ref->exercise;
+ $exercise_points = (float) $exercise_ref->points;
+ $exercise_average = 0;
+ $exercise_correct = 0;
+
+ $exercise_item_count = $exercise->itemCount();
+ $exercise_items = array_fill(0, $exercise_item_count, 0);
+ $exercise_items_c = array_fill(0, $exercise_item_count, 0);
+
+ $query = "SELECT etask_responses.* FROM etask_responses
+ LEFT JOIN seminar_user USING(user_id)
+ WHERE etask_responses.assignment_id = $assignment->id
+ AND etask_responses.task_id = $exercise->id
+ AND seminar_user.Seminar_id = '$course_id'
+ AND seminar_user.status = 'autor'";
+
+ $solution_result = $db->query($query);
+ $num_solutions = $solution_result->rowCount();
+
+ foreach ($solution_result as $solution_row) {
+ $solution = VipsSolution::buildExisting($solution_row);
+ $solution_points = (float) $solution->points;
+
+ if ($exercise_item_count > 1) {
+ $items = $exercise->evaluateItems($solution);
+ $item_scale = $exercise_points / max(count($items), 1);
+
+ foreach ($items as $index => $item) {
+ $exercise_items[$index] += $item['points'] * $item_scale / $num_solutions;
+
+ if ($item['points'] == 1) {
+ $exercise_items_c[$index] += 1 / $num_solutions;
+ }
+ }
+ }
+
+ if ($solution_points >= $exercise_points) {
+ ++$exercise_correct;
+ }
+
+ $exercise_average += $solution_points / $num_solutions;
+ }
+
+ $exercises[] = [
+ 'id' => $exercise->id,
+ 'name' => $exercise->title,
+ 'position' => $exercise_ref->position,
+ 'points' => $exercise_points,
+ 'average' => $exercise_average,
+ 'correct' => $exercise_correct / max($num_solutions, 1),
+ 'items' => $exercise_items,
+ 'items_c' => $exercise_items_c
+ ];
+
+ $test_points += $exercise_points;
+ $test_average += $exercise_average;
+ }
+
+ $assignments[] = [
+ 'assignment' => $assignment,
+ 'points' => $test_points,
+ 'average' => $test_average,
+ 'exercises' => $exercises
+ ];
+ }
+
+ $this->assignments = $assignments;
+
+ if ($format == 'csv') {
+ $columns = [
+ _('Titel'),
+ _('Aufgabe'),
+ _('Item'),
+ _('Erreichbare Punkte'),
+ _('Durchschn. Punkte'),
+ _('Korrekte Lösungen')
+ ];
+
+ $data = [$columns];
+
+ setlocale(LC_NUMERIC, $_SESSION['_language'] . '.UTF-8');
+
+ foreach ($assignments as $assignment) {
+ if (count($assignment['exercises'])) {
+ $data[] = [
+ $assignment['assignment']->test->title,
+ '',
+ '',
+ sprintf('%.1f', $assignment['points']),
+ sprintf('%.1f', $assignment['average']),
+ ''
+ ];
+
+ foreach ($assignment['exercises'] as $exercise) {
+ $data[] = [
+ $assignment['assignment']->test->title,
+ $exercise['position'] . '. ' . $exercise['name'],
+ '',
+ sprintf('%.1f', $exercise['points']),
+ sprintf('%.1f', $exercise['average']),
+ sprintf('%.1f%%', $exercise['correct'] * 100)
+ ];
+
+ if (count($exercise['items']) > 1) {
+ foreach ($exercise['items'] as $index => $item) {
+ $data[] = [
+ $assignment['assignment']->test->title,
+ $exercise['position'] . '. ' . $exercise['name'],
+ sprintf(_('Item %d'), $index + 1),
+ sprintf('%.1f', $exercise['points'] / count($exercise['items'])),
+ sprintf('%.1f', $item),
+ sprintf('%.1f%%', $exercise['items_c'][$index] * 100)
+ ];
+ }
+ }
+ }
+ }
+ }
+
+ setlocale(LC_NUMERIC, 'C');
+
+ $this->render_csv($data, _('Statistik.csv'));
+ } else {
+ Helpbar::get()->addPlainText('',
+ _('Diese Seite gibt einen Überblick über die im Durchschnitt von allen Teilnehmenden erreichten Punkte ' .
+ 'sowie den Prozentsatz der vollständig korrekten Lösungen.'));
+
+ $widget = new ViewsWidget();
+ $widget->addLink(
+ _('Ergebnisse'),
+ $this->url_for('vips/solutions')
+ );
+ $widget->addLink(
+ _('Punkteübersicht'),
+ $this->url_for('vips/solutions/participants_overview', ['display' => 'points'])
+ );
+ $widget->addLink(
+ _('Notenübersicht'),
+ $this->url_for('vips/solutions/participants_overview', ['display' => 'weighting'])
+ );
+ $widget->addLink(
+ _('Statistik'),
+ $this->url_for('vips/solutions/statistics')
+ )->setActive();
+ Sidebar::get()->addWidget($widget);
+
+ $widget = new ExportWidget();
+ $widget->addLink(
+ _('Liste im CSV-Format exportieren'),
+ $this->url_for('vips/solutions/statistics', ['format' => 'csv']),
+ Icon::create('export')
+ );
+ Sidebar::get()->addWidget($widget);
+ }
+ }
+
+ /**
+ * Get the internet host name corresponding to a given IP address.
+ *
+ * @param string $ip_address host IP address
+ */
+ public function gethostbyaddr(string $ip_address): ?string
+ {
+ static $hostname = [];
+
+ if (!array_key_exists($ip_address, $hostname)) {
+ $hostname[$ip_address] = gethostbyaddr($ip_address);
+ }
+
+ if ($hostname[$ip_address] !== $ip_address) {
+ return $hostname[$ip_address];
+ }
+
+ return null;
+ }
+
+ /**
+ * Get all exercise sheets belonging to course.
+ */
+ private function get_assignments_data($course_id, $user_id, $sort, $desc)
+ {
+ $assignments_array = [];
+ $m_sum_max_points = 0; // holds the maximum points of all exercise sheets
+ $sum_reached_points = 0; // holds the reached points of all assignments
+
+ // find all assignments
+ $assignments = VipsAssignment::findByRangeId($course_id);
+
+ usort($assignments, function($a, $b) use ($sort) {
+ if ($sort === 'title') {
+ return strcoll($a->test->title, $b->test->title);
+ } else if ($sort === 'start') {
+ return strcmp($a->start, $b->start);
+ } else {
+ return strcmp($a->end ?: '~', $b->end ?: '~');
+ }
+ });
+
+ if ($desc) {
+ $assignments = array_reverse($assignments);
+ }
+
+ foreach ($assignments as $assignment) {
+ $max_points = $assignment->test->getTotalPoints();
+
+ // for students, get reached points
+ if (!VipsModule::hasStatus('tutor', $course_id)) {
+ $released = $assignment->releaseStatus($user_id);
+
+ if ($assignment->isVisible($user_id) && $released > 0) {
+ $reached_points = $assignment->getUserPoints($user_id);
+ $sum_reached_points += $reached_points;
+ $m_sum_max_points += $max_points;
+ } else {
+ continue;
+ }
+ } else {
+ $released = $assignment->options['released'];
+ $reached_points = null;
+ $m_sum_max_points += $max_points;
+ }
+
+ // count uncorrected solutions
+ $uncorrected_solutions = $this->count_uncorrected_solutions($assignment->id);
+
+ $assignments_array[] = [
+ 'assignment' => $assignment,
+ 'released' => $released,
+ 'reached_points' => $reached_points,
+ 'max_points' => $max_points,
+ 'uncorrected_solutions' => $uncorrected_solutions
+ ];
+ }
+
+ return [
+ 'assignments' => $assignments_array,
+ 'sum_reached_points' => $sum_reached_points,
+ 'sum_max_points' => $m_sum_max_points
+ ];
+ }
+
+ private function participants_overview_data($course_id, $param_user_id, $display = null, $sort = null, $desc = null, $view = null)
+ {
+ $db = DBManager::get();
+
+ // fetch all course participants //
+
+ $participants = [];
+
+ $sql = "SELECT user_id
+ FROM seminar_user
+ WHERE Seminar_id = ?
+ AND status NOT IN ('dozent', 'tutor')";
+ $result = $db->prepare($sql);
+ $result->execute([$course_id]);
+
+ foreach ($result as $row) {
+ $participants[$row['user_id']] = [];
+ }
+
+ // fetch all assignments with maximum points, assigned to blocks //
+ // (if appropriate), and with weighting (if appropriate) //
+
+ $types = $view === 'selftest' ? ['selftest'] : ['exam', 'practice'];
+
+ $sql = "SELECT etask_assignments.id,
+ etask_assignments.type,
+ etask_tests.title,
+ etask_assignments.end,
+ etask_assignments.weight,
+ etask_assignments.options,
+ etask_assignments.block_id,
+ SUM(etask_test_tasks.points) AS points,
+ etask_blocks.name AS block_name,
+ etask_blocks.weight AS block_weight
+ FROM etask_assignments
+ JOIN etask_tests ON etask_tests.id = etask_assignments.test_id
+ LEFT JOIN etask_test_tasks
+ ON etask_test_tasks.test_id = etask_tests.id
+ LEFT JOIN etask_blocks
+ ON etask_blocks.id = etask_assignments.block_id
+ WHERE etask_assignments.range_id = ?
+ AND etask_assignments.type IN (?)
+ GROUP BY etask_assignments.id
+ ORDER BY etask_assignments.type DESC, etask_blocks.name, etask_assignments.start";
+ $result = $db->prepare($sql);
+ $result->execute([$course_id, $types]);
+
+ // the result is ordered by
+ // * tests
+ // * blocks
+ // * exams
+ // with ascending start points in each category
+
+ $assignments = [];
+ $items = [
+ 'tests' => [],
+ 'blocks' => [],
+ 'exams' => []
+ ];
+ $overall_points = 0;
+ $overall_weighting = 0;
+
+ // each assignment
+ foreach ($result as $row) {
+ $assignment_id = (int) $row['id'];
+ $test_type = $row['type'];
+ $test_title = $row['title'];
+ $points = (float) $row['points'];
+ $block_id = $row['block_id'];
+ $block_name = $row['block_name'];
+ $weighting = (float) $row['weight'];
+
+ $assignment = VipsAssignment::find($assignment_id);
+
+ if (isset($block_id) && $row['block_weight'] !== null) {
+ $category = 'blocks';
+
+ // store assignment
+ $assignments[$assignment_id] = [
+ 'assignment' => $assignment,
+ 'category' => $category,
+ 'item_id' => $block_id
+ ];
+
+ // store item
+ if (!isset($items[$category][$block_id])) {
+ $weighting = (float) $row['block_weight'];
+
+ // initialise block
+ $items[$category][$block_id] = [
+ 'id' => $block_id,
+ 'item' => VipsBlock::find($block_id),
+ 'name' => $block_name,
+ 'tooltip' => $block_name.': '.$test_title,
+ 'points' => 0,
+ 'weighting' => $weighting
+ ];
+
+ // increase overall weighting (just once for each block!)
+ $overall_weighting += $weighting;
+ } else {
+ // extend tooltip for existing block
+ $items[$category][$block_id]['tooltip'] .= ', '.$test_title;
+ }
+
+ // increase block's points (for each assignment)
+ $items[$category][$block_id]['points'] += $points;
+
+ // increase overall points (for each assignment)
+ $overall_points += $points;
+ } else {
+ $category = $test_type === 'exam' ? 'exams' : 'tests';
+
+ // store assignment
+ $assignments[$assignment_id] = [
+ 'assignment' => $assignment,
+ 'category' => $category,
+ 'item_id' => $assignment_id
+ ];
+
+ // store item
+ $items[$category][$assignment_id] = [
+ 'id' => $assignment_id,
+ 'item' => $assignment,
+ 'name' => $test_title,
+ 'tooltip' => $test_title,
+ 'points' => $points,
+ 'weighting' => $weighting
+ ];
+
+ // increase overall points and weighting
+ $overall_points += $points;
+ $overall_weighting += $weighting;
+ }
+ }
+
+ // overall sum column
+ $overall = [
+ 'points' => $overall_points,
+ 'weighting' => $overall_weighting
+ ];
+
+ if ($overall['weighting'] == 0 && count($assignments) > 0) {
+ // if weighting is not used, all items weigh equally
+ $equal_weight = 100 / (count($items['tests']) + count($items['blocks']) + count($items['exams']));
+
+ foreach ($items as &$list) {
+ foreach ($list as &$item) {
+ $item['weighting'] = $equal_weight;
+ }
+ }
+ }
+
+ if (count($assignments) > 0) {
+
+ // fetch all assignments, grouped and summed up by user //
+ // (assignments that are not solved by any user won't appear) //
+
+ $sql = "SELECT etask_responses.assignment_id, etask_responses.user_id
+ FROM etask_responses
+ LEFT JOIN seminar_user
+ ON seminar_user.user_id = etask_responses.user_id
+ AND seminar_user.Seminar_id = ?
+ WHERE etask_responses.assignment_id IN (?)
+ AND (
+ seminar_user.status IS NULL OR
+ seminar_user.status NOT IN ('dozent', 'tutor')
+ )
+ GROUP BY etask_responses.assignment_id, etask_responses.user_id";
+ $result = $db->prepare($sql);
+ $result->execute([$course_id, array_keys($assignments)]);
+
+ // each assignment
+ foreach ($result as $row) {
+ $assignment_id = (int) $row['assignment_id'];
+ $assignment = $assignments[$assignment_id]['assignment'];
+ $user_id = $row['user_id'];
+ $reached_points = $assignment->getUserPoints($user_id); // points in the assignment
+
+ $category = $assignments[$assignment_id]['category'];
+ $item_id = $assignments[$assignment_id]['item_id'];
+
+ $max_points = $items[$category][$item_id]['points']; // max points for the item
+ $weighting = $items[$category][$item_id]['weighting']; // item weighting
+
+ // recalc weighting based on item visibility
+ $sum_weight = $this->participant_weight_sum($items, $user_id);
+
+ if ($sum_weight && ($assignment->isVisible($user_id) || $assignment->getAssignmentAttempt($user_id))) {
+ $weighting = 100 * $weighting / $sum_weight;
+ } else {
+ $weighting = 0;
+ }
+
+ // compute percent and weighted percent
+ if ($max_points > 0) {
+ $percent = round(100 * $reached_points / $max_points, 1);
+ $weighted_percent = round($weighting * $reached_points / $max_points, 1);
+ } else {
+ $percent = 0;
+ $weighted_percent = 0;
+ }
+
+ $group = $assignment->getUserGroup($user_id);
+
+ if (isset($group)) {
+ $members = array_column($assignment->getGroupMembers($group), 'user_id');
+ } else {
+ $members = [$user_id];
+ }
+
+ // tests //
+
+ if ($category == 'tests') {
+ foreach ($members as $member_id) {
+ if (!isset($participants[$member_id]['items']['tests'][$item_id])) {
+ // store reached points, percent and weighted percent for this item, for each group member
+ $participants[$member_id]['items'][$category][$item_id] = [
+ 'points' => $reached_points,
+ 'percent' => $percent
+ ];
+
+ if (!isset($participants[$member_id]['overall'])) {
+ $participants[$member_id]['overall'] = ['points' => 0, 'weighting' => 0];
+ }
+
+ // sum up overall points and weighted percent
+ $participants[$member_id]['overall']['points'] += $reached_points;
+ $participants[$member_id]['overall']['weighting'] += $weighted_percent;
+ }
+ }
+ }
+
+ // blocks //
+
+ if ($category == 'blocks') {
+ foreach ($members as $member_id) {
+ if (!isset($participants[$member_id]['items']['tests_seen'][$assignment_id])) {
+ $participants[$member_id]['items']['tests_seen'][$assignment_id] = true;
+
+ if (!isset($participants[$member_id]['items']['blocks'][$item_id])) {
+ $participants[$member_id]['items']['blocks'][$item_id] = ['points' => 0, 'percent' => 0];
+ }
+
+ // store reached points, percent and weighted percent for this item, for each group member
+ $participants[$member_id]['items']['blocks'][$item_id]['points'] += $reached_points;
+ $participants[$member_id]['items']['blocks'][$item_id]['percent'] += $percent;
+
+ if (!isset($participants[$member_id]['overall'])) {
+ $participants[$member_id]['overall'] = ['points' => 0, 'weighting' => 0];
+ }
+
+ // sum up overall points and weighted percent
+ $participants[$member_id]['overall']['points'] += $reached_points;
+ $participants[$member_id]['overall']['weighting'] += $weighted_percent;
+ }
+ }
+ }
+
+ // exams //
+
+ if ($category == 'exams') {
+ // store reached points, percent and weighted percent for this item
+ $participants[$user_id]['items'][$category][$item_id] = [
+ 'points' => $reached_points,
+ 'percent' => $percent
+ ];
+
+ if (!isset($participants[$user_id]['overall'])) {
+ $participants[$user_id]['overall'] = ['points' => 0, 'weighting' => 0];
+ }
+
+ // sum up overall points and weighted percent
+ $participants[$user_id]['overall']['points'] += $reached_points;
+ $participants[$user_id]['overall']['weighting'] += $weighted_percent;
+ }
+ }
+ }
+
+ // if user_id parameter has been passed, delete all participants but the
+ // requested user (this must take place AFTER all that has been done before
+ // for to catch all group solutions)
+ if (isset($param_user_id)) {
+ $participants = [$param_user_id => $participants[$param_user_id]];
+ }
+
+ // get information for each participant
+ foreach ($participants as $user_id => $rest) {
+ $user = User::find($user_id);
+
+ $participants[$user_id]['username'] = $user->username;
+ $participants[$user_id]['forename'] = $user->vorname;
+ $participants[$user_id]['surname'] = $user->nachname;
+ $participants[$user_id]['name'] = $user->nachname . ', ' . $user->vorname;
+ $participants[$user_id]['stud_id'] = $user->matriculation_number;
+ }
+
+
+ // sort participant array //
+
+ $sort_by_name = function($a, $b) { // sort by name
+ return strcoll($a['name'], $b['name']);
+ };
+
+ $sort_by_points = function($a, $b) use ($sort_by_name) { // sort by points (or name, if points are equal)
+ if ($a['overall']['points'] == $b['overall']['points']) {
+ return $sort_by_name($a, $b);
+ } else {
+ return $a['overall']['points'] < $b['overall']['points'] ? -1 : 1;
+ }
+ };
+
+ $sort_by_grade = function($a, $b) use ($sort_by_name) { // sort by grade (or name, if grade is equal)
+ if ($a['overall']['weighting'] == $b['overall']['weighting']) {
+ return $sort_by_name($a, $b);
+ } else {
+ return $a['overall']['weighting'] < $b['overall']['weighting'] ? -1 : 1;
+ }
+ };
+
+ switch ($sort) {
+ case 'sum': // sort by sum row
+ if ($display == 'points') {
+ uasort($participants, $sort_by_points);
+ } else {
+ uasort($participants, $sort_by_grade);
+ }
+ break;
+
+ case 'grade': // sort by grade (or name, if grade is equal)
+ uasort($participants, $sort_by_grade);
+ break;
+
+ case 'name': // sort by name
+ default:
+ uasort($participants, $sort_by_name);
+ }
+
+ if ($desc) {
+ $participants = array_reverse($participants, true);
+ }
+
+ // fetch grades from database
+ $settings = CourseConfig::get($course_id);
+
+ // grading is used
+ if ($settings->VIPS_COURSE_GRADES) {
+ foreach ($participants as $user_id => $participant) {
+ $participants[$user_id]['grade'] = '5,0';
+
+ if (isset($participant['overall'])) {
+ foreach ($settings->VIPS_COURSE_GRADES as $g) {
+ $grade = $g['grade'];
+ $percent = $g['percent'];
+ $comment = $g['comment'];
+
+ if ($participant['overall']['weighting'] >= $percent) {
+ $participants[$user_id]['grade'] = $grade;
+ $participants[$user_id]['grade_comment'] = $comment;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ return [
+ 'display' => $display,
+ 'sort' => $sort,
+ 'desc' => $desc,
+ 'view' => $view,
+ 'items' => $items,
+ 'overall' => $overall,
+ 'participants' => $participants
+ ];
+ }
+
+ private function participant_weight_sum($items, $user_id)
+ {
+ static $weight_sum = [];
+
+ if (!array_key_exists($user_id, $weight_sum)) {
+ $weight_sum[$user_id] = 0;
+
+ foreach ($items as $list) {
+ foreach ($list as $item) {
+ if ($item['item']->isVisible($user_id) || $item['item']->getAssignmentAttempt($user_id)) {
+ $weight_sum[$user_id] += $item['weighting'];
+ }
+ }
+ }
+ }
+
+ return $weight_sum[$user_id];
+ }
+
+ /**
+ * Get all solutions for an assignment.
+ *
+ * @param object $assignment The assignment
+ * @param string|bool $view If set to the empty string, only users with solutions are
+ * returned. If set to string <code>all</code>, virtually
+ * <i>all</i> course participants (including those who have
+ * not delivered any solution) are returned.
+ * @return Array An array consisting of <i>three</i> arrays, namely 'solvers'
+ * (containing all single solvers and groups), 'exercises'
+ * (containing all exercises in the assignment) and 'solutions'
+ * (containing all solvers and their solved exercises).
+ */
+ private function get_solutions($assignment, $view)
+ {
+ // get exercises //
+
+ $exercises = [];
+
+ foreach ($assignment->test->exercise_refs as $exercise_ref) {
+ $exercise_id = (int) $exercise_ref->task_id;
+
+ $exercises[$exercise_id] = [
+ 'id' => $exercise_id,
+ 'title' => $exercise_ref->exercise->title,
+ 'type' => $exercise_ref->exercise->type,
+ 'position' => (int) $exercise_ref->position,
+ 'points' => (float) $exercise_ref->points
+ ];
+ }
+
+ // get course participants //
+
+ $solvers = [];
+ $tutors = [];
+
+ foreach ($assignment->course->members as $member) {
+ $user_id = $member->user_id;
+ $status = $member->status;
+
+ // don't include tutors and lecturers
+ if ($status == 'tutor' || $status == 'dozent') {
+ $tutors[$user_id] = $status;
+ } else {
+ $solvers[$user_id] = [
+ 'type' => 'single',
+ 'id' => $user_id,
+ 'user_id' => $user_id
+ ];
+ }
+ }
+
+ // get assignment attempts //
+
+ foreach ($assignment->assignment_attempts as $attempt) {
+ $user_id = $attempt->user_id;
+
+ $solvers[$user_id] = [
+ 'type' => 'single',
+ 'id' => $user_id,
+ 'user_id' => $user_id
+ ];
+ }
+
+ // get solutions //
+
+ $solutions = [];
+
+ foreach ($assignment->solutions as $solution) {
+ $exercise_id = (int) $solution->task_id;
+ $user_id = $solution->user_id;
+
+ $solutions[$user_id][$exercise_id] = [
+ 'id' => (int) $solution->id,
+ 'exercise_id' => $exercise_id,
+ 'user_id' => $user_id,
+ 'time' => $solution->mkdate,
+ 'corrected' => (boolean) $solution->state,
+ 'points' => (float) $solution->points,
+ 'grader_id' => $solution->grader_id,
+ 'feedback' => $solution->feedback,
+ 'uploads' => $solution->folder && count($solution->folder->file_refs)
+ ];
+
+ // solver may be a non-participant (and must not be a tutor)
+ if (!isset($solvers[$user_id]) && !isset($tutors[$user_id])) {
+ $solvers[$user_id] = [
+ 'type' => 'single',
+ 'id' => $user_id,
+ 'user_id' => $user_id
+ ];
+ }
+ }
+
+ /// NOTE: $solvers now *additionally* contains all students which have
+ /// submitted a solution
+
+ // get groups //
+
+ $groups = [];
+
+ if ($assignment->hasGroupSolutions()) {
+ $all_groups = VipsGroup::findBySQL('range_id = ? ORDER BY name', [$assignment->range_id]);
+
+ foreach ($all_groups as $group) {
+ $members = $assignment->getGroupMembers($group);
+
+ foreach ($members as $member) {
+ $group_id = (int) $group->id;
+ $user_id = $member->user_id;
+
+ if (!isset($solvers[$user_id])) {
+ // add group member to $solvers
+ $solvers[$user_id] = [
+ 'type' => 'group_member',
+ 'id' => $user_id,
+ 'user_id' => $user_id
+ ];
+ } else {
+ // update type for existing solvers
+ $solvers[$user_id]['type'] = 'group_member';
+ }
+
+ if (!isset($groups[$group_id])) {
+ $groups[$group_id] = [
+ 'type' => 'group',
+ 'id' => $group_id,
+ 'user_id' => $user_id,
+ 'name' => $group->name,
+ 'members' => []
+ ];
+ }
+
+ // store which user is member of which group (user_id => group_id)
+ $map_user_to_group[$user_id] = $group_id;
+ }
+ }
+ }
+
+ /// NOTE: $solvers now *additionally* contains group members (if applicable)
+
+ if (count($solvers)) {
+ $result = User::findMany(array_keys($solvers));
+
+ // get user names
+ foreach ($result as $user) {
+ $solvers[$user->id]['username'] = $user->username;
+ $solvers[$user->id]['forename'] = $user->vorname;
+ $solvers[$user->id]['surname'] = $user->nachname;
+ $solvers[$user->id]['name'] = $user->nachname . ', ' . $user->vorname;
+ $solvers[$user->id]['stud_id'] = $user->matriculation_number;
+ }
+
+ uasort($solvers, function($a, $b) {
+ return strcoll($a['name'], $b['name']);
+ });
+ }
+
+ // add groups to $solvers array //
+
+ foreach ($groups as $group_id => $group) {
+ $solvers[$group_id] = $group;
+ }
+
+ // sort single solvers to groups //
+
+ foreach ($solvers as $solver_id => $solver) {
+ if ($solver['type'] == 'group_member') {
+ $group_id = $map_user_to_group[$solver_id];
+
+ $solvers[$group_id]['members'][$solver_id] = $solver; // store solver as group member
+ unset($solvers[$solver_id]); // delete him as single solver
+ }
+ }
+
+ // change solution user ids to group ids //
+
+ foreach ($solutions as $solver_id => $exercise_solutions) {
+ if (isset($map_user_to_group[$solver_id])) {
+ $group_id = $map_user_to_group[$solver_id];
+
+ foreach ($exercise_solutions as $exercise_id => $solution) {
+ // always store most recent solution
+ if (!isset($solutions[$group_id][$exercise_id]) || $solution['time'] > $solutions[$group_id][$exercise_id]['time']) {
+ $solutions[$group_id][$exercise_id] = $solution; // store solution as group solution
+ }
+ unset($solutions[$solver_id][$exercise_id]); // delete single-solver-solution
+ }
+ }
+ }
+
+ // remove hidden solver entries //
+
+ if ($assignment->type !== 'exam') {
+ foreach ($solvers as $solver_id => $solver) {
+ if (!isset($solutions[$solver_id])) { // has no solutions
+ if (!$view || $view == 'todo') {
+ unset($solvers[$solver_id]);
+ }
+ } else if ($view == 'todo') {
+ foreach ($solutions[$solver_id] as $solution) {
+ if (!$solution['corrected']) {
+ continue 2;
+ }
+ }
+
+ unset($solvers[$solver_id]);
+ }
+ }
+ }
+
+ return [
+ 'solvers' => $solvers, // ordered by name
+ 'exercises' => $exercises, // ordered by position
+ 'solutions' => $solutions // first single solvers then groups, furthermore unordered
+ ];
+ }
+
+ /**
+ * Counts uncorrected solutions for a assignment.
+ *
+ * @param $assignment_id The assignment id
+ * @return <code>null</code> if there does not exist any solution at all, else
+ * the number of uncorrected solutions
+ */
+ private function count_uncorrected_solutions($assignment_id)
+ {
+ $db = DBManager::get();
+
+ $assignment = VipsAssignment::find($assignment_id);
+ $course_id = $assignment->range_id;
+
+ // get all corrected and uncorrected solutions
+ $sql = "SELECT etask_responses.task_id,
+ etask_responses.user_id,
+ etask_responses.state
+ FROM etask_responses
+ LEFT JOIN seminar_user
+ ON seminar_user.user_id = etask_responses.user_id
+ AND seminar_user.Seminar_id = ?
+ WHERE etask_responses.assignment_id = ?
+ AND (
+ seminar_user.status IS NULL OR
+ seminar_user.status NOT IN ('dozent', 'tutor')
+ )
+ ORDER BY etask_responses.mkdate DESC";
+ $result = $db->prepare($sql);
+ $result->execute([$course_id, $assignment_id]);
+
+ // no solutions at all
+ if ($result->rowCount() == 0) {
+ return null;
+ }
+
+ // count uncorrected solutions
+ $uncorrected_solutions = 0;
+ $solution = [];
+ $group = [];
+
+ foreach ($result as $row) {
+ $exercise_id = (int) $row['task_id'];
+ $user_id = $row['user_id'];
+ $corrected = (boolean) $row['state'];
+
+ if (!array_key_exists($user_id, $group)) {
+ $group[$user_id] = $assignment->getUserGroup($user_id);
+ }
+
+ if (!array_key_exists($exercise_id . '_' . $user_id, $solution)) {
+ if (isset($group[$user_id])) {
+ $members = array_column($assignment->getGroupMembers($group[$user_id]), 'user_id');
+ } else {
+ $members = [$user_id];
+ }
+
+ foreach ($members as $user_id) {
+ $solution[$exercise_id . '_' . $user_id] = true;
+ }
+
+ if (!$corrected) {
+ $uncorrected_solutions++;
+ }
+ }
+ }
+
+ return $uncorrected_solutions;
+ }
+
+ /**
+ * Return the appropriate CSS class for sortable column (if any).
+ *
+ * @param boolean $sort sort by this column
+ * @param boolean $desc set sort direction
+ */
+ public function sort_class(bool $sort, ?bool $desc): string
+ {
+ return $sort ? ($desc ? 'sortdesc' : 'sortasc') : '';
+ }
+}