Select Git revision
_bootstrap.php
Forked from
Stud.IP / Stud.IP
Source project has a limited visibility.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
VipsAssignment.php 37.48 KiB
<?php
/*
* VipsAssignment.php - Vips test class for Stud.IP
* Copyright (c) 2014 Elmar Ludwig
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*/
class VipsAssignment extends SimpleORMap
{
/**
* Configure the database mapping.
*/
protected static function configure($config = [])
{
$config['db_table'] = 'vips_assignment';
$config['serialized_fields']['options'] = 'JSONArrayObject';
$config['has_many']['assignment_attempts'] = [
'class_name' => 'VipsAssignmentAttempt',
'assoc_foreign_key' => 'assignment_id'
];
$config['has_many']['solutions'] = [
'class_name' => 'VipsSolution',
'assoc_foreign_key' => 'assignment_id'
];
$config['has_many']['archived_solutions'] = [
'class_name' => 'VipsSolutionArchive',
'assoc_foreign_key' => 'assignment_id'
];
$config['belongs_to']['course'] = [
'class_name' => 'Course',
'foreign_key' => 'course_id'
];
$config['belongs_to']['block'] = [
'class_name' => 'VipsBlock',
'foreign_key' => 'block_id'
];
$config['belongs_to']['test'] = [
'class_name' => 'VipsTest',
'foreign_key' => 'test_id'
];
parent::configure($config);
}
/**
* Initialize a new instance of this class.
*/
public function __construct($id = null)
{
parent::__construct($id);
if (is_null($this->options)) {
$this->options = [];
}
}
/**
* Delete entry from the database.
*/
public function delete()
{
$gradebook_id = $this->options['gradebook_id'];
if ($gradebook_id) {
Grading\Definition::deleteBySQL('id = ?', [$gradebook_id]);
}
VipsAssignmentAttempt::deleteBySQL('assignment_id = ?', [$this->id]);
$ref_count = self::countBySql('test_id = ?', [$this->test_id]);
if ($ref_count == 1) {
$this->test->delete();
}
return parent::delete();
}
public static function importText($title, $string, $user_id, $course_id)
{
$duration = 7 * 24 * 60 * 60; // one week
$data_test = [
'title' => $title !== '' ? $title : _vips('Aufgabenblatt'),
'description' => '',
'user_id' => $user_id,
'created' => date('Y-m-d H:i:s'),
];
$data = [
'type' => 'practice',
'course_id' => $course_id ?: $user_id,
'context' => $course_id ? 'course' : 'user',
'start' => date('Y-m-d H:00:00'),
'end' => date('Y-m-d H:00:00', time() + $duration)
];
// remove comments
$string = preg_replace('/^#.*/m', '', $string);
// split into exercises
$segments = preg_split('/^Name:/m', $string);
array_shift($segments);
$test_obj = VipsTest::create($data_test);
$result = new VipsAssignment();
$result->setData($data);
$result->test = $test_obj;
$result->store();
foreach ($segments as $segment) {
try {
$new_exercise = Exercise::importText($segment);
$new_exercise->user_id = $user_id;
$new_exercise->created = date('Y-m-d H:i:s');
$new_exercise->store();
$test_obj->addExercise($new_exercise);
} catch (Exception $e) {
$errors[] = $e->getMessage();
}
}
if (isset($errors)) {
PageLayout::postError(_vips('Während des Imports sind folgende Fehler aufgetreten:'), $errors);
}
return $result;
}
public static function importXML($string, $user_id, $course_id)
{
// default options
$options = [
'evaluation_mode' => 0,
'released' => 0
];
$duration = 7 * 24 * 60 * 60; // one week
$data_test = [
'title' => _vips('Aufgabenblatt'),
'description' => '',
'user_id' => $user_id,
'created' => date('Y-m-d H:i:s'),
];
$data = [
'type' => 'practice',
'course_id' => $course_id ?: $user_id,
'context' => $course_id ? 'course' : 'user',
'start' => date('Y-m-d H:00:00'),
'end' => date('Y-m-d H:00:00', time() + $duration),
'options' => $options
];
$test = new SimpleXMLElement($string, LIBXML_COMPACT | LIBXML_NOCDATA);
$data['type'] = (string) $test['type'];
if (trim($test->title) !== '') {
$data_test['title'] = trim($test->title);
}
if ($test->description) {
$data_test['description'] = Studip\Markup::purifyHtml(trim($test->description));
}
if ($test->notes) {
$data['options']['notes'] = trim($test->notes);
}
if ($test->limit['access-code']) {
$data['options']['access_code'] = (string) $test->limit['access-code'];
}
if ($test->limit['ip-ranges']) {
$data['options']['ip_range'] = (string) $test->limit['ip-ranges'];
}
if ($test->limit['resets']) {
$data['options']['resets'] = (int) $test->limit['resets'];
}
if ($test->limit['tries']) {
$data['options']['max_tries'] = (int) $test->limit['tries'];
}
if ($test->option['scoring-mode'] == 'negative_points') {
$data['options']['evaluation_mode'] = 1;
} else if ($test->option['scoring-mode'] == 'all_or_nothing') {
$data['options']['evaluation_mode'] = 2;
}
if ($test->option['shuffle-answers'] == 'true') {
$data['options']['shuffle_answers'] = 1;
}
if ($test->option['shuffle-exercises'] == 'true') {
$data['options']['shuffle_exercises'] = 1;
}
if ($test['start']) {
$data['start'] = date('Y-m-d H:i:s', strtotime($test['start']));
}
if ($test['end']) {
$data['end'] = date('Y-m-d H:i:s', strtotime($test['end']));
} else if ($data['type'] === 'selftest') {
$data['end'] = VIPS_DATE_INFINITY;
}
if ($test['duration']) {
$data['options']['duration'] = (int) $test['duration'];
}
if ($test['block'] && $course_id) {
$block = VipsBlock::findOneBySQL('name = ? AND course_id = ?', [$test['block'], $course_id]);
if (!$block) {
$block = VipsBlock::create(['name' => $test['block'], 'course_id' => $course_id]);
}
$data['block_id'] = $block->id;
}
$test_obj = VipsTest::create($data_test);
$result = new VipsAssignment();
$result->setData($data);
$result->test = $test_obj;
$result->store();
if ($test->files) {
foreach ($test->files->file as $file) {
$file_id = (string) $file['id'];
$content = base64_decode((string) $file);
$test->registerXPathNamespace('vips', 'urn:vips:test:v1.0');
$file_refs = $test->xpath('vips:exercises/*/vips:file-refs/*[@ref="' . $file_id . '"]');
if ($file_refs && $content !== false) {
if (strlen($file_id) > 5 && strpos($file_id, 'file-') === 0) {
$vips_file = VipsFile::find(substr($file_id, 5));
// try to avoid reupload of identical files
if ($vips_file && sha1_file($vips_file->getFilePath()) === sha1($content)) {
$files[$file_id] = $vips_file;
continue;
}
}
$files[$file_id] = VipsFile::createWithString($file['name'], $content, $user_id);
}
}
if ($files) {
$mapped = preg_replace_callback('/\burn:vips:file-ref:([A-Za-z_][\w.-]*)/',
function($match) use ($files) {
$file = $files[$match[1]];
if ($file) {
return vips_xml_encode($file->getDownloadURL());
} else {
return $match[0];
}
}, $string);
$test = new SimpleXMLElement($mapped, LIBXML_COMPACT | LIBXML_NOCDATA);
}
}
foreach ($test->exercises->exercise as $exercise) {
try {
$new_exercise = Exercise::importXML($exercise);
$new_exercise->user_id = $user_id;
$new_exercise->created = date('Y-m-d H:i:s');
$new_exercise->store();
$exercise_ref = $test_obj->addExercise($new_exercise);
if ($exercise['points']) {
$exercise_ref->points = (float) $exercise['points'];
$exercise_ref->store();
}
if ($exercise->{'file-refs'}) {
foreach ($exercise->{'file-refs'}->{'file-ref'} as $file_ref) {
$file = $files[(string) $file_ref['ref']];
if ($file) {
VipsFileRef::create([
'file_id' => $file->id,
'object_id' => $new_exercise->id,
'type' => 'exercise'
]);
}
}
}
} catch (Exception $e) {
$errors[] = $e->getMessage();
}
}
if (isset($errors)) {
PageLayout::postError(_vips('Während des Imports sind folgende Fehler aufgetreten:'), $errors);
}
return $result;
}
/**
* Get the name of this assignment type.
*/
public function getTypeName()
{
$assignment_types = self::getAssignmentTypes();
return $assignment_types[$this->type]['name'];
}
/**
* Get the icon of this assignment type.
*/
public function getTypeIcon($role = Icon::DEFAULT_ROLE)
{
$assignment_types = self::getAssignmentTypes();
return Icon::create($assignment_types[$this->type]['icon'], $role,
['title' => $assignment_types[$this->type]['name']]);
}
/**
* Get the list of supported assignment types.
*/
public static function getAssignmentTypes()
{
return [
'practice' => ['name' => _vips('Übung'), 'icon' => 'file'],
'selftest' => ['name' => _vips('Selbsttest'), 'icon' => 'check-circle'],
'exam' => ['name' => _vips('Klausur'), 'icon' => 'doctoral_cap']
];
}
/**
* Check if this assignment is locked for editing.
*/
public function isLocked()
{
return $this->type === 'exam' && $this->countAssignmentAttempts() > 0;
}
/**
* Check if this assignment is visible to this user.
*/
public function isVisible($user_id)
{
return $this->block_id ? $this->block->isVisible($user_id) : true;
}
/**
* Check if this assignment has been started.
*/
public function isStarted()
{
$now = date('Y-m-d H:i:s');
return $now >= $this->start;
}
/**
* Check if this assignment is currently running.
*/
public function isRunning()
{
$now = date('Y-m-d H:i:s');
return $now >= $this->start && $now <= $this->end;
}
/**
* Check if this assignment is already finished.
*/
public function isFinished()
{
$now = date('Y-m-d H:i:s');
return $now > $this->end;
}
/**
* Check if this assignment has no end date.
*/
public function isUnlimited()
{
return $this->type === 'selftest' && $this->end === VIPS_DATE_INFINITY;
}
/**
* Check if this assignment may use self assessment features.
*/
public function isSelfAssessment()
{
return $this->type === 'selftest' || $this->options['self_assessment'];
}
/**
* Check if a user may reset and restart this assignment.
*/
public function isResetAllowed()
{
return $this->isSelfAssessment() && $this->options['resets'] !== 0;
}
/**
* Check if this assignment presents shuffled exercises.
*/
public function isExerciseShuffled()
{
return $this->type === 'exam' && $this->options['shuffle_exercises'];
}
/**
* Check if this assignment presents shuffled answers.
*/
public function isShuffled()
{
return $this->type === 'exam' && $this->options['shuffle_answers'] !== 0;
}
/**
* Check if this assignment is using group solutions.
*/
public function hasGroupSolutions()
{
return $this->type === 'practice' && $this->options['use_groups'] !== 0;
}
/**
* Check whether the given exercise is part of this assignment.
*
* @param int $exercise_id exercise id
*/
public function hasExercise($exercise_id)
{
return VipsExerciseRef::exists([$exercise_id, $this->test_id]);
}
/**
* Return array of exercise refs in the test of this assignment.
*/
public function getExerciseRefs($user_id)
{
$result = $this->test->exercise_refs->getArrayCopy();
if ($this->isExerciseShuffled() && $user_id) {
srand(crc32($this->id . ':' . $user_id));
shuffle($result);
srand();
}
return $result;
}
/**
* Export this assignment to XML format. Returns the XML string.
*/
public function exportXML()
{
foreach ($this->test->exercise_refs as $exercise_ref) {
$exercise = $exercise_ref->exercise;
$exercise->includeFilesForExport();
foreach ($exercise->files as $file) {
$files[$file->id] = $file;
}
}
$template = VipsPlugin::$template_factory->open('sheets/export_assignment');
$template->assignment = $this;
$template->files = $files;
return $template->render();
}
/**
* Check whether this assignment is editable by the given user.
*
* @param string $user_id user to check (defaults to current user)
*/
public function checkEditPermission($user_id = null)
{
if ($this->context === 'user') {
return $this->course_id === ($user_id ?: $GLOBALS['user']->id);
}
return $GLOBALS['perm']->have_studip_perm('tutor', $this->course_id, $user_id);
}
/**
* Check whether this assignment is viewable by the given user.
*
* @param string $user_id user to check (defaults to current user)
*/
public function checkViewPermission($user_id = null)
{
if ($this->context === 'user') {
return $this->course_id === ($user_id ?: $GLOBALS['user']->id);
}
return $GLOBALS['perm']->have_studip_perm('autor', $this->course_id, $user_id);
}
/**
* Check whether this assignment is accessible to a student. This is just
* a shortcut for checking: running, active, ip address and access code.
*/
public function checkAccess()
{
return $this->isRunning() && $this->active && $this->checkAccessCode() &&
$this->checkIPAccess($_SERVER['REMOTE_ADDR']);
}
/**
* Check whether the access code provided for this assignment is valid.
* If $access_code is null, the code stored in the user session is used.
*
* @param string $access_code access code (optional)
*/
public function checkAccessCode($access_code = null)
{
if (isset($access_code)) {
$_SESSION['vips_access_' . $this->id] = $access_code;
} else {
$access_code = $_SESSION['vips_access_' . $this->id];
}
return in_array($this->options['access_code'], [null, $access_code], true);
}
/**
* Check whether the given IP address listed among the IP addresses given
* by the lecturer for this exam (if applicable).
*
* @param string $ip_addr IPv4 address (IPv6 is not yet supported)
*/
public function checkIPAccess($ip_addr)
{
// not an exam: user has access.
if ($this->type !== 'exam') {
return true;
}
$ip_ranges = $this->options['ip_range'];
$exam_rooms = Config::get()->VIPS_EXAM_ROOMS;
// expand exam room names
if ($exam_rooms) {
$ip_ranges = preg_replace_callback('/#([^ ,]+)/',
function($match) use ($exam_rooms) {
return $exam_rooms[$match[1]];
}, $ip_ranges);
}
// Explode space separated list into an array and check the resulting single IPs
$ip_ranges = preg_split('/[ ,]+/', $ip_ranges, -1, PREG_SPLIT_NO_EMPTY);
// No IP given: user has access.
if (count($ip_ranges) == 0) {
return true;
}
// One or more IPs are given and user IP matches at least one: user has access.
foreach ($ip_ranges as $ip_range) {
if (strpos($ip_range, '/') !== false) {
list($ip_range, $bits) = explode('/', $ip_range);
$mask = (1 << 32 - $bits) - 1;
$ip_start = long2ip(ip2long($ip_range) & ~$mask);
$ip_end = long2ip(ip2long($ip_range) | $mask);
} else if (strpos($ip_range, '-') !== false) {
list($ip_start, $ip_end) = explode('-', $ip_range);
} else {
$ip_start = $ip_end = $ip_range;
}
$ip_addr_part = explode('.', $ip_addr);
$ip_start_part = explode('.', $ip_start) + array_fill(0, 4, 0);
$ip_end_part = explode('.', $ip_end) + array_fill(0, 4, 255);
if ($ip_start_part[0] <= $ip_addr_part[0] && $ip_addr_part[0] <= $ip_end_part[0] &&
$ip_start_part[1] <= $ip_addr_part[1] && $ip_addr_part[1] <= $ip_end_part[1] &&
$ip_start_part[2] <= $ip_addr_part[2] && $ip_addr_part[2] <= $ip_end_part[2] &&
$ip_start_part[3] <= $ip_addr_part[3] && $ip_addr_part[3] <= $ip_end_part[3]) {
return true;
}
}
return false;
}
/**
* Get the release status of this assignment for the given user.
* Valid values are: 0 = not released, 1 = points, 2 = comments,
* 3 = corrections, 4 = sample solutions
*
* @param string $user_id user id
*/
public function releaseStatus($user_id)
{
if ($this->isStarted()) {
$now = date('Y-m-d H:i:s');
if ($this->type === 'exam') {
if ($this->isFinished() && $this->getAssignmentAttempt($user_id) ||
$this->isSelfAssessment() && $this->getUserEndTime($user_id) < $now) {
return $this->options['released'];
}
} else {
if ($this->isFinished() && $this->options['released'] >= 3) {
return $this->options['released'];
}
}
}
return 0;
}
/**
* Count the number of assignment attempts for this assignment.
*/
public function countAssignmentAttempts()
{
return VipsAssignmentAttempt::countBySql('assignment_id = ?', [$this->id]);
}
/**
* Get the assignment attempt of the given user for this assignment.
* Returns null if there is no assignment attempt for this user.
*
* @param string $user_id user id
*/
public function getAssignmentAttempt($user_id)
{
return VipsAssignmentAttempt::findOneBySQL('assignment_id = ? AND user_id = ?', [$this->id, $user_id]);
}
/**
* Record an assignment attempt for the given user for this assignment.
*
* @param string $user_id user id
*/
public function recordAssignmentAttempt($user_id)
{
if (!$this->getAssignmentAttempt($user_id)) {
if ($this->type === 'exam') {
$end = date('Y-m-d H:i:s', time() + $this->options['duration'] * 60);
$ip_address = $_SERVER['REMOTE_ADDR'];
$options = ['session_id' => session_id()];
} else {
$end = null;
$ip_address = '';
$options = null;
}
VipsAssignmentAttempt::create([
'assignment_id' => $this->id,
'user_id' => $user_id,
'start' => date('Y-m-d H:i:s'),
'end' => $end,
'ip_address' => $ip_address,
'options' => $options
]);
}
}
/**
* Finish an assignment attempt for the given user for this assignment.
*
* @param string $user_id user id
*/
public function finishAssignmentAttempt($user_id)
{
$assignment_attempt = $this->getAssignmentAttempt($user_id);
$now = date('Y-m-d H:i:s');
if ($assignment_attempt) {
if ($assignment_attempt->end === null || $assignment_attempt->end > $now) {
$assignment_attempt->end = $now;
$assignment_attempt->store();
}
}
return $assignment_attempt;
}
/**
* Get the individual end time of the given user for this assignment.
*
* @param string $user_id user id
*/
public function getUserEndTime($user_id)
{
$assignment_attempt = $this->getAssignmentAttempt($user_id);
if ($assignment_attempt) {
$start = strtotime($assignment_attempt->start);
} else {
$start = time();
}
if ($assignment_attempt->end) {
return min($assignment_attempt->end, $this->end);
} else if ($this->type === 'exam') {
return min(date('Y-m-d H:i:s', $start + $this->options['duration'] * 60), $this->end);
} else {
return $this->end;
}
}
/**
* Get all members that were assigned to a particular group for
* this assignment.
*
* @param VipsGroup $group The group object
*/
public function getGroupMembers($group)
{
return VipsGroupMember::findBySQL(
'group_id = ? AND start <= ? AND (end > ? OR end IS NULL)',
[$group->id, $this->end, $this->end]
);
}
/**
* Get the group the user was assigned to for this assignment.
* Returns null if there is no group assignment for this user.
*
* @param string $user_id user id
*/
public function getUserGroup($user_id)
{
if (!$this->hasGroupSolutions()) {
return null;
}
return VipsGroup::findOneBySQL(
'JOIN vips_group_member ON group_id = id
WHERE course_id = ?
AND user_id = ?
AND start <= ?
AND (end > ? OR end IS NULL)',
[$this->course_id, $user_id, $this->end, $this->end]
);
}
/**
* Store a solution related to this assignment into the database.
*
* @param VipsSolution $solution The solution object
*/
public function storeSolution($solution)
{
$exercise = $solution->exercise;
$user_id = $solution->user_id;
$solution->assignment = $this;
$solution->time = date('Y-m-d H:i:s');
// store some client info for exams
if ($this->type === 'exam') {
$solution->ip_address = $_SERVER['REMOTE_ADDR'];
$solution->options['session_id'] = session_id();
}
// in selftests, autocorrect solution
if ($this->isSelfAssessment()) {
$this->correctSolution($solution);
}
// move old solutions into vips_solution_archive
VipsSolution::archiveBySQL(
'exercise_id = ? AND assignment_id = ? AND user_id = ?',
[$exercise->id, $this->id, $user_id]
);
// insert new solution into vips_solution
return $solution->store();
}
/**
* Correct a solution and store the points for the solution in the object.
*
* @param VipsSolution $solution The solution object
* @param bool $corrected mark solution as corrected
*/
public function correctSolution($solution, $corrected = false)
{
$exercise = $solution->exercise;
$exercise_ref = $this->test->getExerciseRef($exercise->id);
$max_points = (float) $exercise_ref->points;
// always set corrected to true for selftest exercises
$selftest = $this->type === 'selftest';
$evaluation = $exercise->evaluate($solution);
$feedback = $exercise->options['feedback'];
$eval_safe = $selftest ? $evaluation['safe'] !== null : $evaluation['safe'];
$reached_points = round_to_half_point($evaluation['percent'] * $max_points);
$corrected = (int) ($corrected || $eval_safe);
// insert solution points
$solution->corrected = $corrected;
$solution->points = $reached_points;
$solution->correction_time = date('Y-m-d H:i:s');
if ($selftest && $evaluation['percent'] != 1 && $feedback != '') {
$solution->corrector_comment = $feedback;
}
}
/**
* Restores an archived solution as the current solution.
*
* @param VipsSolutionArchive $solution The solution object
*/
public function restoreSolution($solution)
{
if ($solution->isArchived() && $solution->assignment_id == $this->id) {
$new_solution = VipsSolution::build($solution);
$new_solution->id = 0;
$new_solution->files = $solution->files;
$this->storeSolution($new_solution);
}
}
/**
* Fetch archived solutions related to this assignment from the database.
* Returns empty list if there are no archived solutions for this exercise.
*
* @param string $group_id group id
* @param int $exercise_id exercise id
*/
public function getArchivedGroupSolutions($group_id, $exercise_id)
{
return VipsSolutionArchive::findBySQL(
'JOIN vips_group_member USING(user_id)
WHERE exercise_id = ?
AND assignment_id = ?
AND group_id = ?
AND start <= ?
AND (end > ? OR end IS NULL)
ORDER BY time DESC',
[$exercise_id, $this->id, $group_id, $this->end, $this->end]
);
}
/**
* Fetch archived solutions related to this assignment from the database.
* NOTE: This method will NOT check the group solutions, if applicable.
* Returns empty list if there are no archived solutions for this exercise.
*
* @param string $user_id user id
* @param int $exercise_id exercise id
*/
public function getArchivedUserSolutions($user_id, $exercise_id)
{
return VipsSolutionArchive::findBySQL(
'exercise_id = ? AND assignment_id = ? AND user_id = ? ORDER BY time DESC',
[$exercise_id, $this->id, $user_id]
);
}
/**
* Fetch archived solutions related to this assignment from the database.
* Returns empty list if there are no archived solutions for this exercise.
*
* @param string $user_id user id
* @param int $exercise_id exercise id
*/
public function getArchivedSolutions($user_id, $exercise_id)
{
$group = $this->getUserGroup($user_id);
if ($group) {
return $this->getArchivedGroupSolutions($group->id, $exercise_id);
}
return $this->getArchivedUserSolutions($user_id, $exercise_id);
}
/**
* Fetch a solution related to this assignment from the database.
* Returns null if there is no solution for this exercise yet.
*
* @param string $group_id group id
* @param int $exercise_id exercise id
*/
public function getGroupSolution($group_id, $exercise_id)
{
return VipsSolution::findOneBySQL(
'JOIN vips_group_member USING(user_id)
WHERE exercise_id = ?
AND assignment_id = ?
AND group_id = ?
AND start <= ?
AND (end > ? OR end IS NULL)
ORDER BY time DESC',
[$exercise_id, $this->id, $group_id, $this->end, $this->end]
);
}
/**
* Fetch a solution related to this assignment from the database.
* NOTE: This method will NOT check the group solution, if applicable.
* Returns null if there is no solution for this exercise yet.
*
* @param string $user_id user id
* @param int $exercise_id exercise id
*/
public function getUserSolution($user_id, $exercise_id)
{
return VipsSolution::findOneBySQL(
'exercise_id = ? AND assignment_id = ? AND user_id = ?',
[$exercise_id, $this->id, $user_id]
);
}
/**
* Fetch a solution related to this assignment from the database.
* Returns null if there is no solution for this exercise yet.
*
* @param string $user_id user id
* @param int $exercise_id exercise id
*/
public function getSolution($user_id, $exercise_id)
{
$group = $this->getUserGroup($user_id);
if ($group) {
return $this->getGroupSolution($group->id, $exercise_id);
}
return $this->getUserSolution($user_id, $exercise_id);
}
/**
* Delete all solutions of the given user for a single exercise of
* this test from the DB.
*
* @param string $user_id user id
* @param int $exercise_id exercise id
*/
public function deleteSolution($user_id, $exercise_id)
{
$sql = 'exercise_id = ? AND assignment_id = ? AND user_id = ?';
if ($this->isSelfAssessment()) {
// delete in vips_solution and vips_solution_archive
VipsSolution::deleteBySQL($sql, [$exercise_id, $this->id, $user_id]);
VipsSolutionArchive::deleteBySQL($sql, [$exercise_id, $this->id, $user_id]);
} else {
// move solutions into vips_solution_archive
VipsSolution::archiveBySQL($sql, [$exercise_id, $this->id, $user_id]);
}
// update gradebook if necessary
$this->updateGradebookEntries($user_id);
}
/**
* Delete all solutions of the given user for this test from the DB.
*
* @param string $user_id user id
*/
public function deleteSolutions($user_id)
{
$sql = 'assignment_id = ? AND user_id = ?';
if ($this->isSelfAssessment()) {
// delete in vips_solution and vips_solution_archive
VipsSolution::deleteBySQL($sql, [$this->id, $user_id]);
VipsSolutionArchive::deleteBySQL($sql, [$this->id, $user_id]);
} else {
// move solutions into vips_solution_archive
VipsSolution::archiveBySQL($sql, [$this->id, $user_id]);
}
// delete start times
VipsAssignmentAttempt::deleteBySQL($sql, [$this->id, $user_id]);
// update gradebook if necessary
$this->updateGradebookEntries($user_id);
}
/**
* Delete all solutions of all users for this test from the DB.
*/
public function deleteAllSolutions()
{
$sql = 'assignment_id = ?';
if ($this->isSelfAssessment()) {
// delete in vips_solution and vips_solution_archive
VipsSolution::deleteBySQL($sql, [$this->id]);
VipsSolutionArchive::deleteBySQL($sql, [$this->id]);
} else {
// move solutions into vips_solution_archive
VipsSolution::archiveBySQL($sql, [$this->id]);
}
// delete start times
VipsAssignmentAttempt::deleteBySQL($sql, [$this->id]);
// update gradebook if necessary
$this->updateGradebookEntries();
}
/**
* Count the number of solutions of the given user for this test.
*
* @param string $user_id user id
*/
public function countSolutions($user_id)
{
$solutions = 0;
foreach ($this->test->exercise_refs as $exercise_ref) {
if ($this->getSolution($user_id, $exercise_ref->exercise_id)) {
++$solutions;
}
}
return $solutions;
}
/**
* Return the points a user has reached in all exercises in this assignment.
*
* @param string $user_id user id
*/
public function getUserPoints($user_id)
{
$group = $this->getUserGroup($user_id);
if ($group) {
$user_ids = array_column_object($this->getGroupMembers($group), 'user_id');
} else {
$user_ids = [$user_id];
}
$solutions = $this->solutions->findBy('user_id', $user_ids)->orderBy('time');
$points = [];
foreach ($solutions as $solution) {
$points[$solution->exercise_id] = (float) $solution->points;
}
return max(array_sum($points), 0);
}
/**
* Copy this assignment into the given course. Returns the new assignment.
*
* @param string $course_id course id
*/
public function copyIntoCourse($course_id, $context = 'course')
{
// determine title of new assignment
if ($this->course_id === $course_id) {
$title = sprintf(_vips('Kopie von %s'), $this->test->title);
} else {
$title = $this->test->title;
}
// reset released option for new assignment
$options = $this->options;
unset($options['released']);
unset($options['stopdate']);
unset($options['gradebook_id']);
$new_test = VipsTest::create([
'title' => $title,
'description' => $this->test->description,
'user_id' => $GLOBALS['user']->id,
'created' => date('Y-m-d H:i:s')
]);
$new_assignment = VipsAssignment::create([
'test_id' => $new_test->id,
'course_id' => $course_id,
'context' => $context,
'type' => $this->type,
'start' => $this->start,
'end' => $this->end,
'options' => $options
]);
foreach ($this->test->exercise_refs as $exercise_ref) {
$exercise_ref->copyIntoTest($new_test->id, $exercise_ref->position);
}
return $new_assignment;
}
/**
* Move this assignment into the given course.
*
* @param string $course_id course id
*/
public function moveIntoCourse($course_id, $context = 'course')
{
if ($this->course_id !== $course_id) {
$this->course_id = $course_id;
$this->context = $context;
$this->block_id = null;
$this->removeFromGradebook();
$this->store();
}
}
/**
* Insert this assignment into the gradebook of its course.
*
* @param string $title gradebook title
* @param string $weight gradebook weight
*/
public function insertIntoGradebook($title, $weight = 1)
{
$gradebook_id = $this->options['gradebook_id'];
if (!$gradebook_id) {
$definition = Grading\Definition::create([
'course_id' => $this->course_id,
'item' => $this->id,
'name' => $title,
'tool' => VipsPlugin::$instance->getPluginName(),
'category' => $this->getTypeName(),
'position' => strtotime($this->start),
'weight' => $weight
]);
$this->options['gradebook_id'] = $definition->id;
$this->store();
}
}
/**
* Remove this assignment from the gradebook of its course.
*/
public function removeFromGradebook()
{
$gradebook_id = $this->options['gradebook_id'];
if ($gradebook_id) {
Grading\Definition::find($gradebook_id)->delete();
unset($this->options['gradebook_id']);
$this->store();
}
}
/**
* Update some or all gradebook entries of this assignment. If the
* user_id is specified, only update entries related to this user.
*
* @param string $user_id user id
*/
public function updateGradebookEntries($user_id = null)
{
$gradebook_id = $this->options['gradebook_id'];
if ($gradebook_id) {
$max_points = $this->test->getTotalPoints() ?: 1;
if ($user_id) {
$group = $this->getUserGroup($user_id);
}
if ($group) {
$members = $this->getGroupMembers($group);
} else if ($user_id) {
$members = [(object) compact('user_id')];
} else {
$members = $this->course->members->findBy('status', 'autor');
}
foreach ($members as $member) {
$reached_points = $this->getUserPoints($member->user_id);
$entry = new Grading\Instance([$gradebook_id, $member->user_id]);
if ($reached_points) {
$entry->rawgrade = $reached_points / $max_points;
$entry->store();
} else {
$entry->delete();
}
}
}
}
}