Skip to content
Snippets Groups Projects
Select Git revision
  • a5c197334f96033b6bb54aeb0f755f9acdf6198c
  • master default protected
  • ticket-959
  • vips-1.8
  • 1.8.1
  • 1.8
6 results

solutions.php

Blame
  • Forked from Uni Osnabrück / Plugins / Vips
    Source project has a limited visibility.
    Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    solutions.php 85.16 KiB
    <?php
    /*
     * vips_solutions.inc.php - Vips plugin for Stud.IP
     * Copyright (c) 2003-2005  Erik Schmitt, Philipp Hügelmeyer
     * Copyright (c) 2005-2006  Christa Deiwiks
     * Copyright (c) 2006-2009  Elmar Ludwig, Martin Schröder
     *
     * 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.
     */
    
    /**
     * Handles student solutions and corrections
     *
     * @version 2009-06-29
     */
    
    class SolutionsController extends StudipController
    {
        /**
         * Return the default action and arguments
         *
         * @return an array containing the action, an array of args and the format
         */
        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
         */
        function before_filter(&$action, &$args)
        {
            parent::before_filter($action, $args);
    
            vips_require_status('autor');
            Navigation::activateItem('/course/vips/solutions');
            PageLayout::setHelpKeyword('Vips.Lösung');
            PageLayout::setTitle(PageLayout::getTitle() . ' - ' . _vips('Ergebnisse'));
        }
    
        /**
         * Render given data as csv, data is assumed to be utf-8.
         * The first row of data may contain column titles.
         *
         * @param array $data       data as two dimensional array
         * @param string $filename  download file name (optional)
         * @param string $delimiter field delimiter char (optional)
         * @param string $enclosure field enclosure char (optional)
         */
        public function render_csv($data, $filename = null, $delimiter = ';', $enclosure = '"')
        {
            $this->set_content_type('text/csv; charset=UTF-8');
    
            $output = fopen('php://temp', 'rw');
            fputs($output, "\xEF\xBB\xBF");
    
            foreach ($data as $row) {
                fputcsv($output, $row, $delimiter, $enclosure);
            }
    
            rewind($output);
            $csv_data = stream_get_contents($output);
            fclose($output);
    
            if (isset($filename)) {
                $this->response->add_header('Content-Disposition', 'attachment; ' . vips_encode_header_parameter('filename', $filename));
            }
    
            $this->response->add_header('Content-Length', strlen($csv_data));
    
            return $this->render_text($csv_data);
        }
    
        /**
         * Displays all exercise sheets.
         * Lecturer can select what sheet to correct.
         */
        function assignments_action()
        {
            $sort = Request::option('sort', 'start');
            $desc = Request::int('desc');
            $course_id = Context::getId();
    
            $this->sort      = $sort;
            $this->desc      = $desc;
            $this->user_id   = $GLOBALS['user']->id;
            $this->test_data = get_assignments_data($course_id, $this->user_id, $sort, $desc);
            $this->blocks    = VipsBlock::findBySQL('course_id = ? ORDER BY name', [$course_id]);
            $this->blocks[]  = VipsBlock::build(['name' => _vips('Aufgabenblätter ohne Blockzuordnung')]);
    
            foreach ($this->test_data['assignments'] as $assignment) {
                $this->block_assignments[$assignment['assignment']->block_id][] = $assignment;
            }
    
            foreach ($this->blocks as $block) {
                if ($block->weight !== NULL) {
                    if ($block->weight) {
                        $this->use_weighting = true;
                    }
                } else if ($this->block_assignments[$block->id]) {
                    foreach ($this->block_assignments[$block->id] as $ass) {
                        if ($ass['assignment']->weight) {
                            $this->use_weighting = true;
                        }
                    }
                }
            }
    
            $settings = VipsSettings::find($course_id);
            $has_grades = isset($settings->grades);
    
            // display course results if grades are defined for this course
            if (!vips_has_status('tutor') && $has_grades) {
                $assignments = VipsAssignment::findBySQL('course_id = ?', [$course_id]);
                $show_overview = true;
    
                // find unreleased or unfinished assignments
                foreach ($assignments as $assignment) {
                    if ($assignment->releaseStatus($this->user_id) == 0) {
                        $show_overview = false;
                    }
                }
    
                // if all assignments are finished and released
                if ($show_overview) {
                    $this->overview_data = participants_overview_data($course_id, $this->user_id);
                }
            }
    
            if (vips_has_status('tutor')) {
                Helpbar::get()->addPlainText('',
                    _vips('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(_vips('Notenverteilung festlegen'), $this->url_for('admin/edit_grades'), Icon::create('graph'), ['data-dialog' => 'size=auto']);
                Sidebar::get()->addWidget($widget);
    
                $widget = new ViewsWidget();
                $widget->addLink(_vips('Ergebnisse'), $this->url_for('solutions'))->setActive(true);
                $widget->addLink(_vips('Punkteübersicht'), $this->url_for('solutions/participants_overview', ['display' => 'points']));
                $widget->addLink(_vips('Notenübersicht'), $this->url_for('solutions/participants_overview', ['display' => 'weighting']));
                $widget->addLink(_vips('Statistik'), $this->url_for('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(_vips('Freigabe für Studierende'));
    
            vips_require_status('tutor');
    
            $this->assignment_ids = Request::intArray('assignment_ids');
    
            foreach ($this->assignment_ids as $assignment_id) {
                $assignment = VipsAssignment::find($assignment_id);
                check_assignment_access($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();
    
            vips_require_status('tutor');
    
            $assignment_ids = Request::intArray('assignment_ids');
            $released = Request::int('released');
    
            if (isset($released)) {
                foreach ($assignment_ids as $assignment_id) {
                    $assignment = VipsAssignment::find($assignment_id);
                    check_assignment_access($assignment);
    
                    $assignment->options['released'] = $released;
                    $assignment->store();
                }
    
                PageLayout::postSuccess(_vips('Die Freigabeeinstellungen wurden geändert.'));
            }
    
            $this->redirect('solutions');
        }
    
        /**
         * Changes which correction information is released to the student (either
         * nothing or only the points or points and correction).
         */
        function change_released_action()
        {
            // CSRFProtection::verifyUnsafeRequest();
    
            vips_require_status('tutor');
    
            $assignment_id = Request::int('assignment_id');
            $assignment = VipsAssignment::find($assignment_id);
    
            check_assignment_access($assignment);
    
            $assignment->options['released'] = Request::int('released');
            $assignment->store();
    
            $this->redirect($this->url_for('solutions/assignment_solutions', compact('assignment_id')));
        }
    
        /**
         * Shows solution points for each student/group with a link to view solution and correct it.
         */
        function assignment_solutions_action()
        {
            vips_require_status('tutor');
    
            $assignment_id = Request::int('assignment_id');
            $assignment    = VipsAssignment::find($assignment_id);
            $format        = Request::option('format');
    
            check_assignment_access($assignment);
    
            $view      = Request::option('view');
            $expand    = Request::option('expand');
    
            // fetch info about assignment
            $end        = strtotime($assignment->end);
            $duration   = $assignment->options['duration'];
            $released   = $assignment->options['released'];
    
            // fetch solvers, exercises and solutions //
            $arrays    = 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 vips_assignment_attempt
                foreach ($assignment->assignment_attempts as $attempt) {
                    $start = strtotime($attempt->start);
                    $user_end = $attempt->end ? strtotime($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,
                    '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['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 = [_vips('Teilnehmer')];
    
                foreach ($exercises as $exercise) {
                    $columns[] = $exercise['position'] . '. ' . $exercise['title'];
                }
    
                $columns[] = _vips('Summe');
    
                $data = [$columns];
    
                setlocale(LC_NUMERIC, $_SESSION['_language'] . '.UTF-8');
    
                foreach ($solvers as $solver) {
                    $row = [$solver['name']];
    
                    foreach ($exercises as $exercise) {
                        $row[] = (string) $solutions[$solver['id']][$exercise['id']]['points'];
                    }
    
                    $row[] = $solver['extra_info']['points'];
    
                    $data[] = $row;
                }
    
                setlocale(LC_NUMERIC, 'C');
    
                $this->render_csv($data, $assignment->test->title . '.csv');
            } else {
                Helpbar::get()->addPlainText('',
                    _vips('In dieser Übersicht können Sie sich anzeigen lassen, welche Teilnehmer Lösungen abgegeben haben, und diese Lösungen korrigieren und freigeben.'));
    
                $widget = new ActionsWidget();
                $widget->addLink(_vips('Aufgabenblatt bearbeiten'),
                    $this->url_for('sheets/edit_assignment', ['assignment_id' => $assignment_id]),
                    Icon::create('edit'));
                $widget->addLink(_vips('Aufgabenblatt drucken'),
                    $this->url_for('sheets/print_assignments', ['assignment_id' => $assignment_id]),
                    Icon::create('print'), ['target' => '_blank']);
                $widget->addLink(_vips('Autokorrektur starten'),
                    $this->url_for('solutions/autocorrect_dialog', compact('assignment_id', 'expand', 'view')),
                    Icon::create('accept'), ['data-dialog' => 'size=auto']);
    
                if ($assignment->type === 'exam') {
                    $widget->addLink(_vips('Alle Lösungen zurücksetzen'),
                        $this->url_for('solutions/delete_solutions', compact('assignment_id', 'expand', 'view') + ['solver_id' => 'all']),
                        Icon::create('refresh'), ['data-confirm' => _vips('Achtung: Wenn Sie die Lösungen zurücksetzen, werden die Lösungen aller Teilnehmer archiviert!')]);
                }
    
                if (class_exists('Grading\\Definition')) {
                    if ($assignment->options['gradebook_id']) {
                        $widget->addLink(_vips('Gradebook-Eintrag entfernen'),
                            $this->url_for('solutions/gradebook_unpublish', compact('assignment_id', 'expand', 'view')),
                            Icon::create('assessment+remove'), ['data-confirm' => _vips('Eintrag aus dem Gradebook löschen?')]);
                    } else {
                        $widget->addLink(_vips('Eintrag im Gradebook anlegen'),
                            $this->url_for('solutions/gradebook_dialog', compact('assignment_id', 'expand', 'view')),
                            Icon::create('assessment+add'), ['data-dialog' => 'size=auto']);
                    }
                }
    
                Sidebar::get()->addWidget($widget);
    
                $widget = new ExportWidget();
                $widget->addLink(_vips('Punktetabelle exportieren'),
                    $this->url_for('solutions/assignment_solutions', ['assignment_id' => $assignment_id, 'format' => 'csv']),
                    Icon::create('download'));
    
                if ($assignment->type === 'exam') {
                    $widget->addLink(_vips('Abgabeprotokolle exportieren'),
                        $this->url_for('solutions/assignment_logs', ['assignment_id' => $assignment_id]),
                        Icon::create('download'));
                }
    
                $widget->addLink(_vips('Abgegebene Dateien herunterladen'),
                    $this->url_for('solutions/download_all_uploads', ['assignment_id' => $assignment_id]),
                    Icon::create('download'));
                Sidebar::get()->addWidget($widget);
    
                if ($assignment->type !== 'selftest') {
                    $widget = new OptionsWidget();
                    $widget->setTitle(_vips('Freigabe für Studierende'));
                    $widget->addRadioButton(_vips('nichts'),
                        $this->link_for('solutions/change_released', ['assignment_id' => $assignment_id, 'released' => 0]),
                        $released == 0);
    
                    if ($assignment->type === 'exam') {
                        $widget->addRadioButton(_vips('vergebene Punkte'),
                            $this->link_for('solutions/change_released', ['assignment_id' => $assignment_id, 'released' => 1]),
                            $released == 1);
                        $widget->addRadioButton(_vips('Punkte und Kommentare'),
                            $this->link_for('solutions/change_released', ['assignment_id' => $assignment_id, 'released' => 2]),
                            $released == 2);
                        $widget->addRadioButton(_vips('… zusätzlich Aufgaben und Korrektur'),
                            $this->link_for('solutions/change_released', ['assignment_id' => $assignment_id, 'released' => 3]),
                            $released == 3);
                    } else {
                        $widget->addRadioButton(_vips('Aufgaben, Punkte und Korrektur'),
                            $this->link_for('solutions/change_released', ['assignment_id' => $assignment_id, 'released' => 3]),
                            $released >= 1 && $released <= 3);
                    }
    
                    $widget->addRadioButton(_vips('… zusätzlich Musterlösungen'),
                        $this->link_for('solutions/change_released', ['assignment_id' => $assignment_id, 'released' => 4]),
                        $released == 4);
                    Sidebar::get()->addWidget($widget);
                }
    
                $widget = new SidebarWidget();
                $widget->setTitle(_vips('Legende'));
                $widget->addElement(new WidgetElement(
                    sprintf(_vips('%sBlau dargestellte Aufgaben%s wurden automatisch und sicher korrigiert.'), '<span class="solution-autocorrected">', '</span>') . '<br>'));
                $widget->addElement(new WidgetElement(
                    sprintf(_vips('%sGrün dargestellte Aufgaben%s wurden von Hand korrigiert.'), '<span class="solution-corrected">', '</span>') . '<br>'));
                $widget->addElement(new WidgetElement(
                    sprintf(_vips('%sRot dargestellte Aufgaben%s wurden noch nicht fertig korrigiert.'), '<span class="solution-uncorrected">', '</span>') . '<br>'));
                $widget->addElement(new WidgetElement(
                    sprintf(_vips('%sAusgegraute Aufgaben%s wurden vom Teilnehmer nicht bearbeitet.'), '<span class="solution-none">', '</span>') . '<br>'));
                Sidebar::get()->addWidget($widget);
            }
        }
    
        /**
         * Download uploads to current solutions for all users in an assignment.
         */
        function download_all_uploads_action()
        {
            vips_require_status('tutor');
    
            $assignment_id = Request::int('assignment_id');
            $assignment    = VipsAssignment::find($assignment_id);
    
            check_assignment_access($assignment);
    
            $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(_vips('ZIP-Archiv konnte nicht erzeugt werden.'));
            }
    
            $arrays = 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') {
                        $folder .= sprintf(' (%s)', $solver['stud_id'] ?: $solver['username']);
                    }
    
                    if ($solution && $solution['uploads']) {
                        foreach (VipsSolution::find($solution['id'])->files as $file) {
                            $zip->addFile($file->getFilePath(), sprintf(_vips('%s/Aufgabe %d/'), $folder, $exercise['position']) . $file->name);
                        }
                    }
                }
            }
    
            $zip->close();
    
            if (!file_exists($zipfile)) {
                file_put_contents($zipfile, base64_decode('UEsFBgAAAAAAAAAAAAAAAAAAAAAAAA=='));
            }
    
            vips_clean_output_buffer();
            header('Content-Type: application/zip');
            header('Content-Disposition: attachment; ' . vips_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.
         */
        function download_uploads_action()
        {
            vips_require_status('tutor');
    
            $assignment_id = Request::int('assignment_id');
            $assignment    = VipsAssignment::find($assignment_id);
            $solver_id     = Request::option('solver_id');
    
            check_assignment_access($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(_vips('ZIP-Archiv konnte nicht erzeugt werden.'));
            }
    
            foreach ($assignment->test->exercises as $i => $exercise) {
                $solution = $assignment->getSolution($solver_id, $exercise->id);
    
                if ($solution) {
                    foreach ($solution->files as $file) {
                        $zip->addFile($file->getFilePath(), sprintf(_vips('Aufgabe %d/'), $i + 1) . $file->name);
                    }
                }
            }
    
            $zip->close();
    
            vips_clean_output_buffer();
            header('Content-Type: application/zip');
            header('Content-Disposition: attachment; ' . vips_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()
        {
            vips_require_status('tutor');
    
            $this->assignment_id = Request::int('assignment_id');
            $this->assignment    = VipsAssignment::find($this->assignment_id);
            $this->view          = Request::option('view');
            $this->expand        = Request::option('expand');
    
            check_assignment_access($this->assignment);
    
            $definitions = Grading\Definition::findByCourse_id($this->assignment->course_id);
            $this->weights = array_sum(array_column_object($definitions, 'weight'));
        }
    
        /**
         * Publish this assignment in the gradebook of the course.
         */
        public function gradebook_publish_action()
        {
            CSRFProtection::verifyUnsafeRequest();
    
            vips_require_status('tutor');
    
            $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');
    
            check_assignment_access($assignment);
    
            $assignment->insertIntoGradebook($title, $weight);
            $assignment->updateGradebookEntries();
    
            PageLayout::postSuccess(_vips('Das Aufgabenblatt wurde in das Gradebook eingetragen.'));
            $this->redirect($this->url_for('solutions/assignment_solutions', compact('assignment_id', 'view', 'expand')));
        }
    
        /**
         * Remove this assignment from the gradebook of the course.
         */
        public function gradebook_unpublish_action()
        {
            // CSRFProtection::verifyUnsafeRequest();
    
            vips_require_status('tutor');
    
            $assignment_id = Request::int('assignment_id');
            $assignment    = VipsAssignment::find($assignment_id);
            $view          = Request::option('view');
            $expand        = Request::option('expand');
    
            check_assignment_access($assignment);
    
            $assignment->removeFromGradebook();
    
            PageLayout::postSuccess(_vips('Das Aufgabenblatt wurde aus dem Gradebook gelöscht.'));
            $this->redirect($this->url_for('solutions/assignment_solutions', compact('assignment_id', 'view', 'expand')));
        }
    
        /**
         * Download a summary of the event logs for an assignment.
         */
        public function assignment_logs_action()
        {
            vips_require_status('tutor');
    
            $assignment_id = Request::int('assignment_id');
            $assignment    = VipsAssignment::find($assignment_id);
    
            check_assignment_access($assignment);
    
            $columns = [_vips('Nachname'), _vips('Vorname'), _vips('Kennung'), _vips('Ereignis'),
                        _vips('Zeit'), _vips('IP-Adresse'), _vips('Rechnername'), _vips('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'] ? vips_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.
         */
        function autocorrect_dialog_action()
        {
            vips_require_status('tutor');
    
            $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
         */
        function autocorrect_solutions_action()
        {
            CSRFProtection::verifyUnsafeRequest();
    
            vips_require_status('tutor');
    
            $assignment_id = Request::int('assignment_id');
            $assignment    = VipsAssignment::find($assignment_id);
            $view          = Request::option('view');
            $expand        = Request::option('expand');
            $corrected     = Request::int('corrected');
    
            check_assignment_access($assignment);
    
            $corrected_solutions = 0;
    
            // select all solutions not manually corrected
            $solutions = $assignment->solutions->findBy('corrector_id', NULL);
    
            foreach ($solutions as $solution) {
                $assignment->correctSolution($solution, $corrected);
                $solution->store();
    
                if ($solution->corrected) {
                    ++$corrected_solutions;
                }
            }
    
            $message = sprintf(n_vips('Es wurde %d Lösung korrigiert.', 'Es wurden %d Lösungen korrigiert.', $corrected_solutions), $corrected_solutions);
            PageLayout::postSuccess($message);
            $this->redirect($this->url_for('solutions/assignment_solutions', compact('assignment_id', 'view', 'expand')));
        }
    
        /**
         * Display form that allows lecturer to correct the student's solution.
         */
        function edit_solution_action()
        {
            vips_require_status('tutor');
    
            $exercise_id   = Request::int('exercise_id');
            $assignment_id = Request::int('assignment_id');
            $assignment    = VipsAssignment::find($assignment_id);
    
            check_exercise_assignment($exercise_id, $assignment);
            check_assignment_access($assignment);
    
            $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 = get_solutions($assignment, $view);
            $solvers   = $arrays['solvers'];
            $exercises = $arrays['exercises'];
            $solutions = $arrays['solutions'];
    
            // previous and next solver, exercise and uncorrected exercise //
    
            $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) {
                    $solution = $solutions[$solver['id']][$exercise['id']];  // may be null
    
                    // 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;
                    }
    
                    // previous/next solver (same exercise)
                    if ($solver['id'] != $solver_or_group_id && $exercise['id'] == $exercise_id && isset($solution)) {
                        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 && isset($solution)) {
                        if ($before_current) {
                            $prev_exercise = $exercise;
                        } else if (!isset($next_exercise)) {
                            $next_exercise = $exercise;
                        }
                    }
    
                    // previous/next uncorrected exercise
                    if (isset($solution) && !$solution['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;
            }
    
            if ($assignment->type !== 'selftest' && !isset($solution->corrector_comment)) {
                $solution->corrector_comment = $exercise->options['feedback'];
            }
    
            if ($archived_id) {
                foreach ($solution_archive as $old_solution) {
                    if ($old_solution->id == $archived_id) {
                        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                  = $old_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('',
                _vips('Sie können hier die Ergebnisse der Autokorrektur ansehen und Aufgaben manuell korrigieren.'));
    
            $widget = new ActionsWidget();
            $widget->addLink(_vips('Zeichenwähler öffnen'), '#',
                Icon::create('comment'), ['class' => 'open_character_picker']);
            $widget->addLink(_vips('Aufgabe bearbeiten'),
                $this->url_for('sheets/edit_exercise', compact('assignment_id', 'exercise_id')),
                Icon::create('edit'));
            $widget->addLink(_vips('Aufgabenblatt drucken'),
                $this->url_for('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(_vips('Links'));
            if (isset($prev_uncorrected_exercise)) {
                $widget->addLink(_vips('Vorige unkorrigierte Aufgabe'),
                    $this->url_for('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(_vips('Nächste unkorrigierte Aufgabe'),
                    $this->url_for('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(_vips('Aufgabenblatt'), $this->url_for('solutions/edit_solution', compact('assignment_id', 'solver_id', 'view')), 'exercise_id');
    
            foreach ($exercises as $exercise) {
                $element = new SelectElement($exercise['id'], sprintf(_vips('Aufgabe %d'), $exercise['position']), $exercise['id'] === $exercise_id);
                $widget->addElement($element);
            }
            Sidebar::get()->addWidget($widget);
    
            $widget = new SelectWidget(_vips('Versionen'), $this->url_for('solutions/edit_solution', compact('assignment_id', 'exercise_id', 'solver_id', 'view')), 'solution_id');
            $version = $solution->id ? date('d.m.Y, H:i', strtotime($solution->time)) : _vips('nicht abgegeben');
            $element = new SelectElement(0, sprintf(_vips('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(_vips('Version %s vom %s'), count($solution_archive) - $i, date('d.m.Y, H:i', strtotime($old_solution->time))),
                    $old_solution->id == $archived_id);
                $widget->addElement($element);
            }
            Sidebar::get()->addWidget($widget);
        }
    
        /**
         * Display a student's corrected solution for a single exercise.
         */
        function view_solution_action()
        {
            $exercise_id   = Request::int('exercise_id');
            $assignment_id = Request::int('assignment_id');
            $assignment    = VipsAssignment::find($assignment_id);
    
            check_exercise_assignment($exercise_id, $assignment);
            check_assignment_access($assignment);
    
            $solver_id     = $GLOBALS['user']->id;
            $released      = $assignment->releaseStatus($solver_id);
    
            if ($released < 3) {
                // the assignment is not finished or not yet released
                PageLayout::postError(_vips('Die Korrekturen des Aufgabenblatts sind nicht freigegeben.'));
                $this->redirect($this->url_for('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->exercise_id == $exercise_id) {
                    $before_current = false;
                    $exercise_position = $i + 1;
                    $max_points = $item->points;
                } else if ($before_current) {
                    $prev_exercise_id = $item->exercise_id;
                } else {
                    $next_exercise_id = $item->exercise_id;
                    break;
                }
            }
    
            $this->exercise                  = $exercise;
            $this->exercise_position         = $exercise_position;
            $this->assignment                = $assignment;
            $this->solution                  = $solution;
            $this->show_solution             = $released == 4;
            $this->max_points                = $max_points;
            $this->prev_exercise_id          = $prev_exercise_id;
            $this->next_exercise_id          = $next_exercise_id;
    
            $widget = new SelectWidget(_vips('Aufgabenblatt'), $this->url_for('solutions/view_solution', compact('assignment_id')), 'exercise_id');
    
            foreach ($assignment->getExerciseRefs($solver_id) as $i => $item) {
                $element = new SelectElement($item->exercise_id, sprintf(_vips('Aufgabe %d'), $i + 1), $item->exercise_id === $exercise->id);
                $widget->addElement($element);
            }
            Sidebar::get()->addWidget($widget);
        }
    
        /**
         * Restores an archived solution as the current solution.
         */
        function restore_solution_action()
        {
            CSRFProtection::verifyUnsafeRequest();
    
            vips_require_status('tutor');
    
            $solution_id       = Request::int('solution_id');
            $solver_id         = Request::option('solver_id');
            $view              = Request::option('view');
    
            $solution = VipsSolutionArchive::find($solution_id);
    
            $exercise_id = $solution->exercise_id;
            $assignment_id = $solution->assignment_id;
            $assignment = $solution->assignment;
    
            check_assignment_access($assignment);
    
            $assignment->restoreSolution($solution);
            PageLayout::postSuccess(_vips('Die ausgewählte Lösung wurde als aktuelle Version gespeichert.'));
    
            $this->redirect($this->url_for('solutions/edit_solution', compact('exercise_id', 'assignment_id', 'solver_id', 'view')));
        }
    
        /**
         * Displays a student's event log for an assignment.
         */
        function show_assignment_log_action()
        {
            vips_require_status('tutor');
    
            $assignment_id = Request::int('assignment_id');
            $assignment    = VipsAssignment::find($assignment_id);
            $solver_id     = Request::option('solver_id');
    
            check_assignment_access($assignment);
    
            $assignment_attempt = $assignment->getAssignmentAttempt($solver_id);
    
            $this->user = User::find($solver_id);
            $this->logs = $assignment_attempt->getLogEntries();
        }
    
        /**
         * Stores the lecturer comment and the corrected points for a solution.
         */
        function store_correction_action()
        {
            CSRFProtection::verifyUnsafeRequest();
    
            vips_require_status('tutor');
    
            $solution_id       = Request::int('solution_id');
            $solver_id         = Request::option('solver_id');
            $view              = Request::option('view');
    
            $corrector_comment = trim(Request::get('corrector_comment'));
            $corrector_comment = Studip\Markup::purifyHtml($corrector_comment);
            $reached_points    = Request::float('reached_points');
            $max_points        = Request::float('max_points');
    
            if ($solution_id) {
                $solution = VipsSolution::find($solution_id) ?: VipsSolutionArchive::find($solution_id);
            } else {
                // create dummy empty solution
                $solution = new VipsSolution();
                $solution->exercise_id = Request::int('exercise_id');
                $solution->assignment_id = Request::int('assignment_id');
                $solution->time = date('Y-m-d H:i:s');
                $solution->user_id = $solver_id;
            }
    
            $exercise_id = $solution->exercise_id;
            $assignment_id = $solution->assignment_id;
    
            check_exercise_assignment($exercise_id, $solution->assignment);
            check_assignment_access($solution->assignment);
    
            // 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
                $reached_points = round_to_half_point($reached_points);
    
                if ($reached_points > $max_points) {
                    PageLayout::postInfo(sprintf(_vips('Sie haben Bonuspunkte vergeben (%s von %s).'), $reached_points, $max_points));
                } else if ($reached_points < 0) {
                    PageLayout::postWarning(sprintf(_vips('Sie haben eine negative Punktzahl eingegeben (%s von %s).'), $reached_points, $max_points));
                }
    
                $solution->corrected = 1;
                $solution->points = $reached_points;
                $solution->corrector_comment = $corrector_comment ?: NULL;
    
                if ($solution->isDirty()) {
                    $solution->corrector_id = $GLOBALS['user']->id;
                    $solution->correction_time = date('Y-m-d H:i:s');
                    $solution->store();
    
                    PageLayout::postSuccess(_vips('Ihre Korrektur wurde gespeichert.'));
                }
            }
    
            // show exercise and correction form again
            $this->redirect($this->url_for('solutions/edit_solution', compact('exercise_id', 'assignment_id', 'solver_id', 'view')));
        }
    
        /**
         * Edit an active assignment attempt (update end time).
         */
        function edit_assignment_attempt_action()
        {
            vips_require_status('tutor');
    
            $assignment_id = Request::int('assignment_id');
            $assignment    = VipsAssignment::find($assignment_id);
            $solver_id     = Request::option('solver_id');
            $view          = Request::option('view');
    
            check_assignment_access($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).
         */
        function store_assignment_attempt_action()
        {
            CSRFProtection::verifyUnsafeRequest();
    
            vips_require_status('tutor');
    
            $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');
    
            check_assignment_access($assignment);
    
            $assignment_attempt = $assignment->getAssignmentAttempt($solver_id);
    
            if ($assignment_attempt) {
                list($end_day) = explode(' ', $assignment->getUserEndTime($solver_id));
                $end_datetime  = DateTime::createFromFormat('Y-m-d H:i:s', $end_day . ' ' . $end_time);
    
                if ($end_datetime) {
                    $assignment_attempt->end = $end_datetime->format('Y-m-d H:i:s');
                    $assignment_attempt->store();
    
                    if ($assignment_attempt->end > $assignment->end) {
                        PageLayout::postWarning(_vips('Der Abgabezeitpunkt liegt nach dem Ende der Klausur.'));
                    }
                } else {
                    PageLayout::postError(_vips('Der Abgabezeitpunkt ist keine gültige Uhrzeit.'));
                }
            }
    
            $this->redirect($this->url_for('solutions/assignment_solutions', compact('assignment_id', 'view')));
        }
    
        /**
         * Deletes the solutions of a student (or all students) and resets the
         * assignment attempt.
         */
        function delete_solutions_action()
        {
            // CSRFProtection::verifyUnsafeRequest();
    
            vips_require_status('tutor');
    
            $assignment_id = Request::int('assignment_id');
            $assignment = VipsAssignment::find($assignment_id);
            $solver_id = Request::option('solver_id');
    
            check_assignment_access($assignment);
    
            if ($assignment->type === 'exam') {
                if ($solver_id === 'all') {
                    $assignment->deleteAllSolutions();
                    PageLayout::postSuccess(_vips('Die Klausur wurde zurückgesetzt und alle abgegebenen Lösungen archiviert.'));
                } else if ($assignment->isRunning()) {
                    $assignment->deleteSolutions($solver_id);
                    PageLayout::postSuccess(_vips('Die Teilnahme wurde zurückgesetzt und ggf. abgegebene Lösungen archiviert.'));
                }
            }
    
            $this->redirect($this->url_for('solutions/assignment_solutions', compact('assignment_id')));
        }
    
    
    
        /**
         * Shows all corrected exercises of an exercise sheet, if the status
         * of "released" allows that, i.e. is at least 1.
         */
        function student_assignment_solutions_action()
        {
            $assignment_id = Request::int('assignment_id');
            $assignment = VipsAssignment::find($assignment_id);
    
            check_assignment_access($assignment);
    
            $this->assignment = $assignment;
            $this->user_id = $GLOBALS['user']->id;
            $this->released = $assignment->releaseStatus($this->user_id);
    
            // Security check -- is assignment really accessible for students?
            if ($this->released == 0) {
                PageLayout::postError(_vips('Die Korrekturen wurden noch nicht freigegeben.'));
                $this->redirect('solutions');
                return;
            }
    
            Helpbar::get()->addPlainText('',
                _vips('Sie können hier die Ergebnisse bzw. die Korrekturen ihrer Aufgaben ansehen.'));
    
            if ($this->released >= 3) {
                $widget = new ActionsWidget();
                $widget->addLink(_vips('Aufgabenblatt drucken'),
                    $this->url_for('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.
         */
        function participants_overview_action()
        {
            vips_require_status('tutor');
    
            $display = Request::option('display', 'points');
            $sort    = Request::option('sort', 'name');
            $desc    = Request::int('desc');
            $view    = Request::option('view');
            $format  = Request::option('format');
    
            $this->course_id = Context::getId();
            $attributes = participants_overview_data($this->course_id, NULL, $display, $sort, $desc, $view);
    
            $settings = VipsSettings::find($this->course_id);
            $this->has_grades = isset($settings->grades);
    
            foreach ($attributes as $key => $value) {
                $this->$key = $value;
            }
    
            if ($format == 'csv') {
                $columns = [_vips('Nachname'), _vips('Vorname'), _vips('Matrikelnr.')];
    
                foreach ($this->items as $category => $list) {
                    foreach ($list as $item) {
                        $columns[] = $item['name'];
                    }
                }
    
                $columns[] = _vips('Summe');
    
                if ($display != 'points' && $this->has_grades) {
                    $columns[] = _vips('Note');
                }
    
                $data = [$columns];
    
                setlocale(LC_NUMERIC, $_SESSION['_language'] . '.UTF-8');
    
                if ($display == 'points' || $this->overall['weighting']) {
                    if ($display == 'points') {
                        $row = [_vips('Maximalpunktzahl:'), '', ''];
                    } else {
                        $row = [_vips('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['stud_id']];
    
                    foreach ($this->items as $category => $list) {
                        foreach ($list as $item) {
                            if ($display == 'points') {
                                $row[] = sprintf('%.1f', $p['items'][$category][$item['id']]['points']);
                            } else if (isset($p['items'][$category][$item['id']]['weighting'])) {
                                $row[] = sprintf('%.1f%%', $p['items'][$category][$item['id']]['weighting']);
                            } else {
                                $row[] = '';
                            }
                        }
                    }
    
                    if ($display == 'points') {
                        $row[] = sprintf('%.1f', $p['overall']['points']);
                    } else {
                        $row[] = sprintf('%.1f%%', $p['overall']['weighting']);
    
                        if ($this->has_grades) {
                            $row[] = $p['grade'];
                        }
                    }
    
                    $data[] = $row;
                }
    
                setlocale(LC_NUMERIC, 'C');
    
                $this->render_csv($data, _vips('Notenliste.csv'));
            } else {
                Helpbar::get()->addPlainText('',
                    _vips('Diese Seite gibt einen Überblick über die von allen Teilnehmern erreichten Punkte und ggf. Noten.'));
    
                $widget = new ViewsWidget();
                $widget->addLink(_vips('Ergebnisse'), $this->url_for('solutions'));
                $widget->addLink(_vips('Punkteübersicht'), $this->url_for('solutions/participants_overview', ['display' => 'points']))->setActive($display == 'points');
                $widget->addLink(_vips('Notenübersicht'), $this->url_for('solutions/participants_overview', ['display' => 'weighting']))->setActive($display == 'weighting');
                $widget->addLink(_vips('Statistik'), $this->url_for('solutions/statistics'));
                Sidebar::get()->addWidget($widget);
    
                $widget = new ExportWidget();
                $widget->addLink(_vips('Liste im CSV-Format exportieren'),
                    $this->url_for('solutions/participants_overview', ['display' => $display, 'view' => $view, 'sort' => $sort, 'format' => 'csv']),
                    Icon::create('download'));
                Sidebar::get()->addWidget($widget);
            }
        }
    
        function statistics_action()
        {
            vips_require_status('tutor');
    
            $db = DBManager::get();
    
            $format = Request::option('format');
            $course_id = Context::getId();
            $assignments = [];
    
            $_assignments = VipsAssignment::findBySQL("course_id = ? AND type != 'selftest' 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_items   = [];
                    $exercise_items_c = [];
    
                    $exercise_item_count = $exercise->itemCount();
    
                    $query = "SELECT vips_solution.* FROM vips_solution
                              LEFT JOIN seminar_user USING(user_id)
                              WHERE vips_solution.assignment_id = $assignment->id
                                AND vips_solution.exercise_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 = [
                    _vips('Titel'),
                    _vips('Aufgabe'),
                    _vips('Item'),
                    _vips('erreichbare Punkte'),
                    _vips('durchschn. Punkte'),
                    _vips('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(_vips('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, _vips('Statistik.csv'));
            } else {
                Helpbar::get()->addPlainText('',
                    _vips('Diese Seite gibt einen Überblick über die im Durchschnitt von allen Teilnehmern erreichten Punkte ' .
                          'sowie den Prozentsatz der vollständig korrekten Lösungen.'));
    
                $widget = new ViewsWidget();
                $widget->addLink(_vips('Ergebnisse'), $this->url_for('solutions'));
                $widget->addLink(_vips('Punkteübersicht'), $this->url_for('solutions/participants_overview', ['display' => 'points']));
                $widget->addLink(_vips('Notenübersicht'), $this->url_for('solutions/participants_overview', ['display' => 'weighting']));
                $widget->addLink(_vips('Statistik'), $this->url_for('solutions/statistics'))->setActive(true);
                Sidebar::get()->addWidget($widget);
    
                $widget = new ExportWidget();
                $widget->addLink(_vips('Liste im CSV-Format exportieren'),
                    $this->url_for('solutions/statistics', ['format' => 'csv']),
                    Icon::create('download'));
                Sidebar::get()->addWidget($widget);
            }
        }
    }
    
    /**
     * Shows all exercise sheets belonging to course.
     */
    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::findBySQL('course_id = ?', [$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 (!vips_has_status('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'];
                $m_sum_max_points += $max_points;
            }
    
            // count uncorrected solutions
            $uncorrected_solutions = vips_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
        ];
    }
    
    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 = '$course_id' AND status NOT IN ('dozent', 'tutor')";
        $result = $db->query($sql);
    
        foreach ($result as $row) {
            $participants[$row['user_id']] = [];
        }
    
        // fetch all assignments with maximum points, assigned to blocks //
        // (if appropriate), and with weighting (if appropriate)         //
    
        $op = $view === 'selftest' ? '=' : '!=';
    
        $sql = "SELECT vips_assignment.id,
                vips_assignment.type,
                vips_test.title,
                vips_assignment.end,
                vips_assignment.weight,
                vips_assignment.options,
                vips_assignment.block_id,
                SUM(vips_exercise_ref.points) AS points,
                vips_block.name AS block_name,
                vips_block.weight AS block_weight
            FROM vips_assignment
                 JOIN vips_test ON vips_test.id = vips_assignment.test_id
            LEFT JOIN vips_exercise_ref
                   ON vips_exercise_ref.test_id = vips_test.id
            LEFT JOIN vips_block
                   ON vips_block.id = vips_assignment.block_id
            WHERE vips_assignment.course_id = '$course_id'
              AND vips_assignment.type $op 'selftest'
            GROUP BY vips_assignment.id
            ORDER BY vips_assignment.type DESC,
                vips_block.name,
                vips_assignment.start";
        $result = $db->query($sql);
    
        // the result is ordered by
        //  * tests
        //  * blocks
        //  * exams
        // with ascending start points in each category
    
        $assignments    = [];
        $items          = [
            'tests'     => [],
            'blocks'    => [],
            'exams'     => []
        ];
    
        // each assignment
        foreach ($result as $row) {
            $assignment_id = (int) $row['id'];
            $test_type     = $row['type'];
            $test_title    = $row['title'];
            $points        = (float) $row['points'];   // 0 if NULL
            $block_id      = $row['block_id'];         // may be NULL
            $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 vips_solution.assignment_id, vips_solution.user_id
                    FROM vips_solution
                    LEFT JOIN seminar_user
                           ON seminar_user.user_id = vips_solution.user_id
                          AND seminar_user.Seminar_id = ?
                    WHERE vips_solution.assignment_id IN (?)
                      AND (seminar_user.status IS NULL OR
                           seminar_user.status NOT IN ('dozent', 'tutor'))
                    GROUP BY vips_solution.assignment_id, vips_solution.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 = 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_object($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,
                                'weighting' => $weighted_percent
                            ];
    
                            // sum up overall points and weighted percent
                            $participants[$member_id]['overall']['points']          += $reached_points;
                            $participants[$member_id]['overall']['points_tests']    += $reached_points;
                            $participants[$member_id]['overall']['weighting']       += $weighted_percent;
                            $participants[$member_id]['overall']['weighting_tests'] += $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;
    
                            // 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;
                            $participants[$member_id]['items']['blocks'][$item_id]['weighting'] += $weighted_percent;
    
                            // sum up overall points and weighted percent
                            $participants[$member_id]['overall']['points']           += $reached_points;
                            $participants[$member_id]['overall']['points_blocks']    += $reached_points;
                            $participants[$member_id]['overall']['weighting']        += $weighted_percent;
                            $participants[$member_id]['overall']['weighting_blocks'] += $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,
                        'weighting' => $weighted_percent
                    ];
    
                    // sum up overall points and weighted percent
                    $participants[$user_id]['overall']['points']          += $reached_points;
                    $participants[$user_id]['overall']['points_exams']    += $reached_points;
                    $participants[$user_id]['overall']['weighting']       += $weighted_percent;
                    $participants[$user_id]['overall']['weighting_exams'] += $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]['forename'] = $user->vorname;
            $participants[$user_id]['surname']  = $user->nachname;
            $participants[$user_id]['name']     = $user->nachname . ', ' . $user->vorname;
            $participants[$user_id]['stud_id']  = get_student_id($user);
        }
    
    
        // sort participant array //
    
        function sort_by_name($a, $b) {  // sort by name
            return strcoll($a['name'], $b['name']);
        }
    
        function sort_by_points($a, $b) {  // 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;
            }
        }
    
        function sort_by_grade($a, $b) {  // 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 = VipsSettings::find($course_id);
        $grades = $settings->grades;
    
        // grading is used
        if (isset($grades)) {
            foreach ($participants as $user_id => $participant) {
                $participants[$user_id]['grade'] = '5,0';
    
                foreach ($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
        ];
    }
    
    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];
    }
    
    
    ///////////////////////////////////////////////
    //   A U X I L I A R Y   F U N C T I O N S   //
    ///////////////////////////////////////////////
    
    
    
    /**
     * Get all solutions for a assignment.
     *
     * @param object $assignment The assignment
     * @param 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).
     */
    function get_solutions($assignment, $view)
    {
        // get exercises //
    
        $exercises = [];
    
        foreach ($assignment->test->exercise_refs as $exercise_ref) {
            $exercise_id = (int) $exercise_ref->exercise_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 solutions //
    
        $solutions = [];
    
        foreach ($assignment->solutions as $solution) {
            $exercise_id = (int) $solution->exercise_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->time,
                'corrected'         => (boolean) $solution->corrected,
                'points'            => (float) $solution->points,
                'corrector_id'      => $solution->corrector_id,
                'corrector_comment' => $solution->corrector_comment,
                'uploads'           => count($solution->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('course_id = ? ORDER BY name', [$assignment->course_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']  = get_student_id($user);
            }
    
            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];  // get group 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) {
            $group_id = $map_user_to_group[$solver_id];  // may be null
    
            if (isset($group_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
     */
    function vips_count_uncorrected_solutions($assignment_id)
    {
        $db = DBManager::get();
    
        $assignment = VipsAssignment::find($assignment_id);
        $course_id = $assignment->course_id;
    
        // get all corrected and uncorrected solutions
        $sql = "SELECT vips_solution.exercise_id,
                       vips_solution.user_id,
                       vips_solution.corrected
                FROM vips_solution
                LEFT JOIN seminar_user
                       ON seminar_user.user_id = vips_solution.user_id
                      AND seminar_user.Seminar_id = '$course_id'
                WHERE vips_solution.assignment_id = $assignment_id
                  AND (seminar_user.status IS NULL OR
                       seminar_user.status NOT IN ('dozent', 'tutor'))
                ORDER BY time DESC";
        $result = $db->query($sql);
    
        // 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['exercise_id'];
            $user_id     = $row['user_id'];
            $corrected   = (boolean) $row['corrected'];
    
            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_object($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;
    }