Select Git revision
solutions.php
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;
}