id);
VipsModule::requireViewPermission($assignment, $exercise_id);
if (!$assignment->checkEditPermission()) {
$solver_id = $GLOBALS['user']->id;
}
$solutions = $assignment->getArchivedUserSolutions($solver_id, $exercise_id);
if ($assignment->checkAccess($solver_id) || $assignment->checkEditPermission()) {
if ($assignment->type === 'exam' && $solutions) {
$assignment->restoreSolution($solutions[0]);
PageLayout::postSuccess(_('Die vorherige Lösung wurde wiederhergestellt.'));
}
}
$this->redirect($this->url_for('vips/sheets/show_exercise', compact('assignment_id', 'exercise_id', 'solver_id')));
}
/**
* Only possible if test is selftest: Delete the solution of a student for
* a particular exercise.
*/
public function delete_solution_action()
{
CSRFProtection::verifyUnsafeRequest();
$exercise_id = Request::int('exercise_id');
$assignment_id = Request::int('assignment_id');
$assignment = VipsAssignment::find($assignment_id);
$solver_id = Request::option('solver_id', $GLOBALS['user']->id);
VipsModule::requireViewPermission($assignment, $exercise_id);
if (!$assignment->checkEditPermission()) {
$solver_id = $GLOBALS['user']->id;
}
if ($assignment->checkAccess($solver_id) || $assignment->checkEditPermission()) {
if ($assignment->isResetAllowed() || $assignment->type === 'exam') {
$assignment->deleteSolution($solver_id, $exercise_id);
$undo_link = '';
if ($assignment->type === 'exam' && !$assignment->isSelfAssessment()) {
$undo_link = sprintf(' %s',
$this->link_for('vips/sheets/restore_solution', compact('assignment_id', 'exercise_id', 'solver_id')),
_('Diese Aktion zurücknehmen.'));
}
PageLayout::postSuccess(_('Die Lösung wurde gelöscht.') . $undo_link);
}
}
$this->redirect($this->url_for('vips/sheets/show_exercise', compact('assignment_id', 'exercise_id', 'solver_id')));
}
/**
* Only possible if test is selftest: Deletes all the solutions of a student or
* the student's group to enable him/her to redo it.
*/
public function delete_solutions_action()
{
CSRFProtection::verifyUnsafeRequest();
$assignment_id = Request::int('assignment_id');
$assignment = VipsAssignment::find($assignment_id);
$solver_id = Request::option('solver_id', $GLOBALS['user']->id);
VipsModule::requireViewPermission($assignment);
if (!$assignment->checkEditPermission()) {
$solver_id = $GLOBALS['user']->id;
}
if ($assignment->isRunning() || $assignment->checkEditPermission()) {
if ($assignment->isResetAllowed()) {
$assignment->deleteSolutions($solver_id);
PageLayout::postSuccess(_('Die Lösungen wurden gelöscht.'));
}
}
$this->redirect($this->url_for('vips/sheets/show_assignment', compact('assignment_id', 'solver_id')));
}
/**
* Only possible if test is exam: Begin working on the exam.
*/
public function begin_assignment_action()
{
CSRFProtection::verifyUnsafeRequest();
$terms_accepted = Request::int('terms_accepted');
$access_code = Request::get('access_code');
$assignment_id = Request::int('assignment_id');
$assignment = VipsAssignment::find($assignment_id);
$ip_address = $_SERVER['REMOTE_ADDR'];
VipsModule::requireViewPermission($assignment);
if ($assignment->type === 'exam') {
if (!$assignment->getAssignmentAttempt($GLOBALS['user']->id)) {
$exam_terms = Config::get()->VIPS_EXAM_TERMS;
}
if (!$assignment->isRunning() || !$assignment->active) {
PageLayout::postError(_('Das Aufgabenblatt kann zur Zeit nicht bearbeitet werden.'));
} else if (!$assignment->checkIPAccess($ip_address)) {
PageLayout::postError(sprintf(_('Sie haben mit Ihrer IP-Adresse „%s“ keinen Zugriff!'), htmlReady($ip_address)));
} else if ($exam_terms && !$terms_accepted) {
PageLayout::postError(_('Ein Start der Klausur ist nur mit Bestätigung der Teilnahmebedingungen möglich.'));
} else if (!$assignment->checkAccessCode($access_code)) {
PageLayout::postError(_('Der eingegebene Zugangscode ist nicht korrekt.'));
} else {
$assignment->recordAssignmentAttempt($GLOBALS['user']->id);
}
}
$this->redirect($this->url_for('vips/sheets/show_assignment', compact('assignment_id', 'access_code')));
}
/**
* Only possible if test is exam: Immediately finish working on the exam.
*/
public function finish_assignment_action()
{
CSRFProtection::verifyUnsafeRequest();
$assignment_id = Request::int('assignment_id');
$assignment = VipsAssignment::find($assignment_id);
VipsModule::requireViewPermission($assignment);
if ($assignment->checkAccess($GLOBALS['user']->id)) {
if ($assignment->finishAssignmentAttempt($GLOBALS['user']->id)) {
PageLayout::postSuccess(_('Das Aufgabenblatt wurde abgeschlossen, eine weitere Bearbeitung ist nicht mehr möglich.'));
} else {
PageLayout::postError(_('Eine Abgabe ist erst nach Start des Aufgabenblatts möglich.'));
}
}
$this->redirect($this->url_for('vips/sheets/show_assignment', compact('assignment_id')));
}
/**
* SHEETS/EXAMS
*
* Is called when the submit button at the bottom of an exercise is called.
* If there is already a solution of this exercise by the same user or same group,
* a dialog pops up to confirm the submission. On database-level: EVERY solution is stored
* (even the unconfirmed ones), with the last solution being marked as last.
*/
public function submit_exercise_action()
{
CSRFProtection::verifyUnsafeRequest();
$exercise_id = Request::int('exercise_id');
$assignment_id = Request::int('assignment_id');
$assignment = VipsAssignment::find($assignment_id);
VipsModule::requireViewPermission($assignment, $exercise_id);
##################################################################
# in case student solution is submitted by tutor or lecturer #
# (can happen if the student submits his/her solution by email) #
##################################################################
$solver_id = Request::option('solver_id');
if ($solver_id == '' || !$assignment->checkEditPermission()) {
$solver_id = $GLOBALS['user']->id;
}
############################
# Checks before submission #
############################
if (!$assignment->checkEditPermission()) {
$end = $assignment->getUserEndTime($solver_id);
// not yet started
if (!$assignment->isStarted()) {
PageLayout::postError(_('Das Aufgabenblatt wurde noch nicht gestartet.'));
$this->redirect('vips/sheets/list_assignments_stud');
return;
}
// already ended
if ($end && time() - $end > 120) {
PageLayout::postError(_('Das Aufgabenblatt wurde bereits beendet.'));
$this->redirect('vips/sheets/list_assignments_stud');
return;
}
if (!$assignment->checkIPAccess($_SERVER['REMOTE_ADDR']) || !$assignment->checkAccessCode()) {
PageLayout::postError(_('Kein Zugriff möglich!'));
$this->redirect('vips/sheets/list_assignments_stud');
return;
}
$assignment->recordAssignmentAttempt($solver_id);
}
/* if an exercise has been submitted */
if (Request::submitted('submit_exercise') || Request::int('forced')) {
$request = Request::getInstance();
$exercise = Exercise::find($exercise_id);
$solution = $exercise->getSolutionFromRequest($request, $_FILES);
$solution->user_id = $solver_id;
if ($solution->isEmpty()) {
PageLayout::postWarning(_('Ihre Lösung ist leer und wurde nicht gespeichert.'));
} else {
$assignment->storeSolution($solution);
PageLayout::postSuccess(sprintf(_('Ihre Lösung zur Aufgabe „%s“ wurde gespeichert.'), htmlReady($exercise->title)));
}
}
$this->redirect($this->url_for('vips/sheets/show_exercise', compact('assignment_id', 'exercise_id', 'solver_id')));
}
/**
* SHEETS/EXAMS
*
* Displays an exercise (from student perspective)
*/
public function show_exercise_action()
{
$exercise_id = Request::int('exercise_id');
$assignment_id = Request::int('assignment_id');
$assignment = VipsAssignment::find($assignment_id);
$solver_id = Request::option('solver_id'); // solver is handed over via address line, ie. user is a lecturer
VipsModule::requireViewPermission($assignment, $exercise_id);
if ($solver_id == '' || !$assignment->checkEditPermission()) {
$solver_id = $GLOBALS['user']->id;
}
##############################################################
# check for ip_address, remaining time and interrupted #
##############################################################
// restrict access for students!
if (!$assignment->checkEditPermission()) {
// the assignment is not accessible any more after it has run out
if (!$assignment->checkAccess()) {
PageLayout::postError(_('Das Aufgabenblatt kann zur Zeit nicht bearbeitet werden.'));
$this->redirect('vips/sheets/list_assignments_stud');
return;
}
if ($assignment->isFinished($solver_id)) {
PageLayout::postError(_('Die Zeit ist leider abgelaufen!'));
$this->redirect($this->url_for('vips/sheets/show_assignment', compact('assignment_id')));
return;
}
// enter user start time the moment he/she first clicks on any exercise
$assignment->recordAssignmentAttempt($solver_id);
}
// fetch exercise info, type, points
$exercise_ref = VipsExerciseRef::find([$assignment->test_id, $exercise_id]);
$exercise = $exercise_ref->exercise;
###################################
# get user solution if applicable #
###################################
$solution = $assignment->getSolution($solver_id, $exercise_id);
$max_tries = $assignment->getMaxTries();
$max_points = $exercise_ref->points;
$exercise_position = $exercise_ref->position;
$show_solution = false;
$tries_left = null;
// if a solution has been submitted during a selftest
if ($max_tries && $solution) {
$tries_left = $max_tries - $solution->countTries();
if ($solution->points == $max_points || !$solution->state || $solution->grader_id || $tries_left <= 0) {
$show_solution = true;
}
}
##############################
# set template variables #
##############################
$this->assignment = $assignment;
$this->assignment_id = $assignment_id;
$this->exercise = $exercise;
$this->exercise_id = $exercise_id;
$this->exercise_position = $exercise_position;
$this->solver_id = $solver_id;
$this->solution = $solution; // can be empty
$this->max_points = $max_points;
$this->show_solution = $show_solution;
$this->tries_left = $tries_left;
$this->user_end_time = $assignment->getUserEndTime($solver_id);
$this->remaining_time = $this->user_end_time - time();
$this->contentbar = $this->create_contentbar($assignment, $exercise_id, 'show', $solver_id);
$widget = new ActionsWidget();
if (($assignment->isResetAllowed() || $assignment->type === 'exam') && $solution) {
$widget->addLink(
_('Lösung dieser Aufgabe löschen'),
$this->url_for('vips/sheets/delete_solution', compact('assignment_id', 'exercise_id', 'solver_id')),
Icon::create('refresh'),
['data-confirm' => _('Wollen Sie die Lösung dieser Aufgabe wirklich löschen?')]
)->asButton();
}
Sidebar::get()->addWidget($widget);
if ($assignment->checkEditPermission()) {
Helpbar::get()->addPlainText('',
_('Dies ist die Studierendenansicht (Vorschau) der Aufgabe. Sie können hier auch Lösungen von Teilnehmenden ansehen oder für sie abgeben.'));
$widget = new ViewsWidget();
$widget->addLink(
_('Aufgabe bearbeiten'),
$this->url_for('vips/sheets/edit_exercise', ['assignment_id' => $assignment_id, 'exercise_id' => $exercise_id])
);
$widget->addLink(
_('Studierendensicht (Vorschau)'),
$this->url_for('vips/sheets/show_exercise', ['assignment_id' => $assignment_id, 'exercise_id' => $exercise_id])
)->setActive();
Sidebar::get()->addWidget($widget);
if ($assignment->range_type === 'course') {
$widget = new SelectWidget(_('Anzeigen für'), $this->url_for('vips/sheets/show_exercise', compact('assignment_id', 'exercise_id')), 'solver_id');
$widget->class = 'nested-select';
$element = new SelectElement($GLOBALS['user']->id, ' ', $GLOBALS['user']->id == $solver_id);
$widget->addElement($element);
foreach ($assignment->course->members->findBy('status', 'autor')->orderBy('nachname, vorname') as $member) {
if ($assignment->isVisible($member->user_id)) {
$element = new SelectElement($member->user_id, $member->nachname . ', ' . $member->vorname, $member->user_id == $solver_id);
$widget->addElement($element);
}
}
Sidebar::get()->addWidget($widget);
}
} else {
Helpbar::get()->addPlainText('',
_('Bitte denken Sie daran, vor dem Verlassen der Seite Ihre Lösung zu speichern.'));
}
$widget = new ViewsWidget();
$widget->setTitle(_('Aufgabenblatt'));
foreach ($assignment->getExerciseRefs($solver_id) as $i => $item) {
$this->item = $item;
$this->position = $i + 1;
$element = new WidgetElement($this->render_template_as_string('vips/sheets/show_exercise_link'));
$element->active = $item->task_id === $exercise->id;
$widget->addElement($element, 'exercise-' . $item->task_id);
}
Sidebar::get()->addWidget($widget);
}
/**
* Displays all running assignments "work-on ready" for students (view of
* students when clicking on tab Uebungsblatt), respectively student view
* for lecturers and tutors.
*/
public function list_assignments_stud_action()
{
$course_id = Context::getId();
$sort = Request::option('sort', 'start');
$desc = Request::int('desc');
VipsModule::requireStatus('autor', $course_id);
$this->sort = $sort;
$this->desc = $desc;
$this->assignments = [];
$assignments = VipsAssignment::findByRangeId($course_id);
$blocks = VipsBlock::findBySQL('range_id = ? ORDER BY name', [$course_id]);
$blocks[] = VipsBlock::build(['name' => _('Aufgabenblätter')]);
$ip_address = $_SERVER['REMOTE_ADDR'];
usort($assignments, function($a, $b) use ($sort) {
if ($sort === 'title') {
return strcoll($a->test->title, $b->test->title);
} else if ($sort === 'type') {
return strcmp($a->type, $b->type);
} else if ($sort === 'start') {
return strcmp($a->start, $b->start);
} else {
return strcmp($a->end ?: '~', $b->end ?: '~');
}
});
if ($desc) {
$assignments = array_reverse($assignments);
}
foreach ($blocks as $block) {
$this->blocks[$block->id]['title'] = $block->name;
}
foreach ($assignments as $assignment) {
if ($assignment->isRunning() && $assignment->isVisible($GLOBALS['user']->id)) {
if ($assignment->checkIPAccess($ip_address)) {
if (isset($assignment->block->group_id)) {
$this->blocks['']['assignments'][] = $assignment;
} else {
$this->blocks[$assignment->block_id]['assignments'][] = $assignment;
}
}
}
}
// delete empty blocks
foreach ($blocks as $block) {
if (empty($this->blocks[$block->id]['assignments'])) {
unset($this->blocks[$block->id]);
}
}
$this->user_id = $GLOBALS['user']->id;
}
/**
* Display one assignment to the student, including the list of exercises.
*/
public function show_assignment_action()
{
$assignment_id = Request::int('assignment_id');
$assignment = VipsAssignment::find($assignment_id);
$solver_id = Request::option('solver_id', $GLOBALS['user']->id);
$ip_address = $_SERVER['REMOTE_ADDR'];
VipsModule::requireViewPermission($assignment);
if (!$assignment->checkEditPermission()) {
$solver_id = $GLOBALS['user']->id;
}
$this->solver_id = $solver_id;
$this->user_end_time = $assignment->getUserEndTime($solver_id);
$this->remaining_time = $this->user_end_time - time();
$this->access_code = trim(Request::get('access_code'));
$this->assignment = $assignment;
$this->needs_code = false;
$this->exam_terms = null;
$this->preview_exam_terms = null;
$this->contentbar = $this->create_contentbar($assignment, null, 'show', $solver_id);
if (!$assignment->checkEditPermission()) {
if (!$assignment->isRunning() || !$assignment->active) {
PageLayout::postError(_('Das Aufgabenblatt kann zur Zeit nicht bearbeitet werden.'));
$this->redirect('vips/sheets/list_assignments_stud');
return;
}
if (!$assignment->checkIPAccess($ip_address)) {
PageLayout::postError(sprintf(_('Sie haben mit Ihrer IP-Adresse „%s“ keinen Zugriff!'), htmlReady($ip_address)));
$this->redirect('vips/sheets/list_assignments_stud');
return;
}
$this->assignment_attempt = $assignment->getAssignmentAttempt($solver_id);
if ($assignment->type === 'exam') {
if (!$assignment->checkAccessCode()) {
$this->needs_code = true;
}
if (!$this->assignment_attempt) {
$this->exam_terms = Config::get()->VIPS_EXAM_TERMS;
}
if ($this->exam_terms || $this->needs_code) {
$this->contentbar = $this->contentbar->withProps(['toc' => null]);
}
}
$widget = new ActionsWidget();
if ($assignment->type !== 'exam') {
$widget->addLink(
_('Aufgabenblatt drucken'),
$this->url_for('vips/sheets/print_assignments', ['assignment_id' => $assignment_id, 'print_files' => 1]),
Icon::create('print'),
['target' => '_blank']
);
}
if ($assignment->isResetAllowed()) {
$widget->addLink(
_('Lösungen dieses Blatts löschen'),
$this->url_for('vips/sheets/delete_solutions', ['assignment_id' => $assignment_id]),
Icon::create('refresh'),
['data-confirm' => _('Wollen Sie die Lösungen dieses Aufgabenblatts wirklich löschen?')]
)->asButton();
}
if ($assignment->type === 'exam' && $this->assignment_attempt && $this->remaining_time > 0) {
$widget->addLink(
_('Klausur vorzeitig abgeben'),
$this->url_for('vips/sheets/finish_assignment', ['assignment_id' => $assignment_id]),
Icon::create('lock-locked'),
['data-confirm' => _('Achtung: Wenn Sie die Klausur abgeben, sind keine weiteren Eingaben mehr möglich!')]
)->asButton();
}
if ($assignment->type === 'selftest' && $this->assignment_attempt && $this->assignment_attempt->end === null) {
$widget->addLink(
_('Aufgabenblatt jetzt abgeben'),
$this->url_for('vips/sheets/finish_assignment', ['assignment_id' => $assignment_id]),
Icon::create('lock-locked'),
['data-confirm' => _('Achtung: Wenn Sie das Aufgabenblatt abgeben, sind keine weiteren Eingaben mehr möglich!')]
)->asButton();
}
Sidebar::get()->addWidget($widget);
} else {
if ($assignment->type === 'exam') {
$this->preview_exam_terms = Config::get()->VIPS_EXAM_TERMS;
}
Helpbar::get()->addPlainText('',
_('Dies ist die Studierendensicht (Vorschau) des Aufgabenblatts.'));
$widget = new ActionsWidget();
if ($assignment->type !== 'exam') {
$widget->addLink(
_('Aufgabenblatt drucken'),
$this->url_for('vips/sheets/print_assignments', ['assignment_id' => $assignment_id, 'print_files' => 1, 'user_ids[]' => $solver_id]),
Icon::create('print'),
['target' => '_blank']
);
}
if ($assignment->isResetAllowed()) {
$widget->addLink(
_('Lösungen dieses Blatts löschen'),
$this->url_for('vips/sheets/delete_solutions', ['assignment_id' => $assignment_id, 'solver_id' => $solver_id]),
Icon::create('refresh'),
['data-confirm' => _('Wollen Sie die Lösungen dieses Aufgabenblatts wirklich löschen?')]
)->asButton();
}
Sidebar::get()->addWidget($widget);
$widget = new ViewsWidget();
$widget->addLink(
_('Aufgabenblatt bearbeiten'),
$this->url_for('vips/sheets/edit_assignment', ['assignment_id' => $assignment_id])
);
$widget->addLink(
_('Studierendensicht (Vorschau)'),
$this->url_for('vips/sheets/show_assignment', ['assignment_id' => $assignment_id])
)->setActive();
Sidebar::get()->addWidget($widget);
if ($assignment->range_type === 'course') {
$widget = new SelectWidget(_('Anzeigen für'), $this->url_for('vips/sheets/show_assignment', compact('assignment_id')), 'solver_id');
$widget->class = 'nested-select';
$element = new SelectElement($GLOBALS['user']->id, ' ', $GLOBALS['user']->id == $solver_id);
$widget->addElement($element);
foreach ($assignment->course->members->findBy('status', 'autor')->orderBy('nachname, vorname') as $member) {
if ($assignment->isVisible($member->user_id)) {
$element = new SelectElement($member->user_id, $member->nachname . ', ' . $member->vorname, $member->user_id == $solver_id);
$widget->addElement($element);
}
}
Sidebar::get()->addWidget($widget);
}
}
}
#####################################
# #
# Lecturer Methods #
# #
#####################################
/**
* Dialog for confirming the end date of a starting assignment.
*/
public function start_assignment_dialog_action()
{
$assignment_id = Request::int('assignment_id');
$assignment = VipsAssignment::find($assignment_id);
VipsModule::requireEditPermission($assignment);
$this->assignment = $assignment;
}
/**
* EXAMS/SHEETS
*
* If an assignment hasn't started yet this function sets the start time to NOW
* so that it's running
*
*/
public function start_assignment_action()
{
CSRFProtection::verifyUnsafeRequest();
$assignment_id = Request::int('assignment_id');
$assignment = VipsAssignment::find($assignment_id);
VipsModule::requireEditPermission($assignment);
$end_date = trim(Request::get('end_date'));
$end_time = trim(Request::get('end_time'));
$end_datetime = DateTime::createFromFormat('d.m.Y H:i', $end_date.' '.$end_time);
// unlimited selftest
if ($assignment->type === 'selftest' && $end_date === '' && $end_time === '') {
$end = null;
} else if ($end_datetime) {
$end = strtotime($end_datetime->format('Y-m-d H:i:s'));
} else {
$end = $assignment->end;
PageLayout::postWarning(_('Ungültiger Endzeitpunkt, der Wert wurde nicht übernommen.'));
}
// set new start and end time in database
$assignment->start = time();
$assignment->end = $end;
$assignment->active = 1;
$assignment->store();
// delete start time for exam from database
VipsAssignmentAttempt::deleteBySQL('assignment_id = ?', [$assignment_id]);
$this->redirect('vips/sheets');
}
/**
* EXAMS/SHEETS
*
* Stops/continues an assignment (no change of start/end time but temporary closure)
*
*/
public function stopgo_assignment_action()
{
CSRFProtection::verifyUnsafeRequest();
$db = DBManager::get();
$assignment_id = Request::int('assignment_id');
$assignment = VipsAssignment::find($assignment_id);
VipsModule::requireEditPermission($assignment);
if ($assignment->type === 'exam') {
if ($assignment->active) {
$assignment->options['stopdate'] = date('Y-m-d H:i:s');
} else if ($assignment->options['stopdate']) {
// extend exam duration for already active participants
$interval = time() - strtotime($assignment->options['stopdate']);
$sql = 'UPDATE etask_assignment_attempts SET end = end + ?
WHERE assignment_id = ? AND end > ?';
$stmt = $db->prepare($sql);
$stmt->execute([$interval, $assignment_id, $assignment->options['stopdate']]);
unset($assignment->options['stopdate']);
}
}
$assignment->active = !$assignment->active;
$assignment->store();
$this->redirect('vips/sheets');
}
/**
* EXAMS/SHEETS
*
* Deletes an assignment from the course (and block if applicable).
*/
public function delete_assignment_action()
{
CSRFProtection::verifyUnsafeRequest();
$assignment_id = Request::int('assignment_id');
$assignment = VipsAssignment::find($assignment_id);
$test_title = $assignment->test->title;
VipsModule::requireEditPermission($assignment);
if (!$assignment->isLocked()) {
$assignment->delete();
PageLayout::postSuccess(sprintf(_('Das Aufgabenblatt „%s“ wurde gelöscht.'), htmlReady($test_title)));
}
$this->redirect('vips/sheets');
}
/**
* Delete a list of assignments from the course (and block if applicable).
*/
public function delete_assignments_action()
{
CSRFProtection::verifyUnsafeRequest();
$assignment_ids = Request::intArray('assignment_ids');
$deleted = 0;
foreach ($assignment_ids as $assignment_id) {
$assignment = VipsAssignment::find($assignment_id);
VipsModule::requireEditPermission($assignment);
if (!$assignment->isLocked()) {
$assignment->delete();
++$deleted;
}
}
if ($deleted > 0) {
PageLayout::postSuccess(sprintf(_('Es wurden %s Aufgabenblätter gelöscht.'), $deleted));
}
if ($deleted < count($assignment_ids)) {
PageLayout::postError(_('Einige Aufgabenblätter konnten nicht gelöscht werden, da bereits Lösungen abgegeben wurden.'), [
_('Falls Sie diese wirklich löschen möchten, müssen Sie zuerst die Lösungen aller Teilnehmenden zurücksetzen.')
]);
}
$this->redirect(Context::getId() ? 'vips/sheets' : 'vips/pool/assignments');
}
/**
* Dialog for selecting a block for a list of assignments.
*/
public function assign_block_dialog_action()
{
$course_id = Context::getId();
VipsModule::requireStatus('tutor', $course_id);
$this->assignment_ids = Request::intArray('assignment_ids');
$this->blocks = VipsBlock::findBySQL('range_id = ? ORDER BY name', [$course_id]);
}
/**
* Assign a list of assignments to the specified block.
*/
public function assign_block_action()
{
CSRFProtection::verifyUnsafeRequest();
$assignment_ids = Request::intArray('assignment_ids');
$block_id = Request::int('block_id');
if ($block_id) {
$block = VipsBlock::find($block_id);
}
foreach ($assignment_ids as $assignment_id) {
$assignment = VipsAssignment::find($assignment_id);
VipsModule::requireEditPermission($assignment);
if (!$block_id || $block->range_id === $assignment->range_id) {
$assignment->block_id = $block_id ?: null;
$assignment->store();
}
}
PageLayout::postSuccess(_('Die Blockzuordnung wurde gespeichert.'));
$this->redirect('vips/sheets');
}
/**
* Dialog for copying a list of assignments into a course.
*/
public function copy_assignments_dialog_action()
{
PageLayout::setTitle(_('Aufgabenblätter kopieren'));
$this->assignment_ids = Request::intArray('assignment_ids');
$this->courses = VipsModule::getActiveCourses($GLOBALS['user']->id);
$this->course_id = Context::getId();
}
/**
* Copy the selected assignments into the selected course.
*/
public function copy_assignments_action()
{
CSRFProtection::verifyUnsafeRequest();
$assignment_ids = Request::intArray('assignment_ids');
$course_id = Request::option('course_id');
if ($course_id) {
VipsModule::requireStatus('tutor', $course_id);
}
foreach ($assignment_ids as $assignment_id) {
$assignment = VipsAssignment::find($assignment_id);
VipsModule::requireEditPermission($assignment);
if ($course_id) {
$assignment->copyIntoCourse($course_id);
} else {
$assignment->copyIntoCourse($GLOBALS['user']->id, 'user');
}
}
PageLayout::postSuccess(ngettext('Das Aufgabenblatt wurde kopiert.', 'Die Aufgabenblätter wurden kopiert.', count($assignment_ids)));
$this->redirect(Context::getId() ? 'vips/sheets' : 'vips/pool/assignments');
}
/**
* Dialog for moving a list of assignments to another course.
*/
public function move_assignments_dialog_action()
{
PageLayout::setTitle(_('Aufgabenblätter verschieben'));
$this->assignment_ids = Request::intArray('assignment_ids');
$this->courses = VipsModule::getActiveCourses($GLOBALS['user']->id);
$this->course_id = Context::getId();
}
/**
* Move a list of assignments to the specified course.
*/
public function move_assignments_action()
{
CSRFProtection::verifyUnsafeRequest();
$assignment_ids = Request::intArray('assignment_ids');
$course_id = Request::option('course_id');
if ($course_id) {
VipsModule::requireStatus('tutor', $course_id);
}
foreach ($assignment_ids as $assignment_id) {
$assignment = VipsAssignment::find($assignment_id);
VipsModule::requireEditPermission($assignment);
if ($course_id) {
$assignment->moveIntoCourse($course_id);
} else {
$assignment->moveIntoCourse($GLOBALS['user']->id, 'user');
}
}
PageLayout::postSuccess(ngettext('Das Aufgabenblatt wurde verschoben.', 'Die Aufgabenblätter wurden verschoben.', count($assignment_ids)));
$this->redirect(Context::getId() ? 'vips/sheets' : 'vips/pool/assignments');
}
/**
* Delete the solutions of all students and reset the assignment.
*/
public function reset_assignment_action()
{
CSRFProtection::verifyUnsafeRequest();
$assignment_id = Request::int('assignment_id');
$assignment = VipsAssignment::find($assignment_id);
VipsModule::requireEditPermission($assignment);
if ($assignment->type === 'exam') {
$assignment->deleteAllSolutions();
PageLayout::postSuccess(_('Die Klausur wurde zurückgesetzt und alle abgegebenen Lösungen archiviert.'));
}
$this->redirect(Context::getId() ? 'vips/sheets' : 'vips/pool/assignments');
}
/**
* SHEETS/EXAMS
*
* Takes an exercise off an assignment and deletes it.
*/
public function delete_exercise_action()
{
CSRFProtection::verifyUnsafeRequest();
$exercise_id = Request::int('exercise_id');
$assignment_id = Request::int('assignment_id');
$assignment = VipsAssignment::find($assignment_id);
$exercise = Exercise::find($exercise_id);
VipsModule::requireEditPermission($assignment, $exercise_id);
if (!$assignment->isLocked()) {
$assignment->test->removeExercise($exercise_id);
PageLayout::postSuccess(sprintf(_('Die Aufgabe „%s“ wurde gelöscht.'), htmlReady($exercise->title)));
}
$this->redirect($this->url_for('vips/sheets/edit_assignment', compact('assignment_id')));
}
/**
* Deletes a list of exercises from a specific assignment.
*/
public function delete_exercises_action()
{
CSRFProtection::verifyUnsafeRequest();
$exercise_ids = Request::intArray('exercise_ids');
$assignment_id = Request::int('assignment_id');
$assignment = VipsAssignment::find($assignment_id);
if (!$assignment->isLocked()) {
foreach ($exercise_ids as $exercise_id) {
VipsModule::requireEditPermission($assignment, $exercise_id);
$assignment->test->removeExercise($exercise_id);
}
PageLayout::postSuccess(sprintf(_('Es wurden %s Aufgaben gelöscht.'), count($exercise_ids)));
}
$this->redirect($this->url_for('vips/sheets/edit_assignment', compact('assignment_id')));
}
/**
* Reorder exercise positions within an assignment.
*/
public function move_exercise_action()
{
CSRFProtection::verifyUnsafeRequest();
$assignment_id = Request::int('assignment_id');
$assignment = VipsAssignment::find($assignment_id);
$list = Request::intArray('item');
VipsModule::requireEditPermission($assignment);
/* renumber all exercises in current assignment */
foreach ($list as $i => $exercise_id) {
$exercise_ref = VipsExerciseRef::find([$assignment->test_id, $exercise_id]);
if ($exercise_ref) {
$exercise_ref->position = $i + 1;
$exercise_ref->store();
}
}
$this->render_nothing();
}
/**
* SHEETS/EXAMS
*
* Displays the form for editing an exercise.
*
* Is called when editing an existing exercise or creating a new exercise.
*/
public function edit_exercise_action()
{
PageLayout::setHelpKeyword('Basis.VipsAufgaben');
$exercise_id = Request::int('exercise_id'); // is not set when creating new exercise
$assignment_id = Request::int('assignment_id');
$assignment = VipsAssignment::find($assignment_id);
VipsModule::requireEditPermission($assignment, $exercise_id);
if ($exercise_id) {
// edit already existing exercise
$exercise_ref = $assignment->test->getExerciseRef($exercise_id);
$exercise = $exercise_ref->exercise;
$max_points = $exercise_ref->points;
$exercise_position = $exercise_ref->position;
} else {
// create new exercise
$exercise_type = Request::option('exercise_type');
$exercise = new $exercise_type();
$max_points = null;
$exercise_position = null;
}
$this->assignment = $assignment;
$this->assignment_id = $assignment_id;
$this->exercise = $exercise;
$this->exercise_position = $exercise_position;
$this->max_points = $max_points;
$this->contentbar = $this->create_contentbar($assignment, $exercise_id);
Helpbar::get()->addPlainText('',
_('Sie können hier den Aufgabentext und die Antwortoptionen dieser Aufgabe bearbeiten.'));
$widget = new ActionsWidget();
if (!$assignment->isLocked()) {
$widget->addLink(
_('Neue Aufgabe erstellen'),
$this->url_for('vips/sheets/add_exercise_dialog', ['assignment_id' => $assignment_id]),
Icon::create('add')
)->asDialog('size=auto');
}
Sidebar::get()->addWidget($widget);
if ($exercise->id) {
$widget = new ViewsWidget();
$widget->addLink(
_('Aufgabe bearbeiten'),
$this->url_for('vips/sheets/edit_exercise', ['assignment_id' => $assignment_id, 'exercise_id' => $exercise->id])
)->setActive();
$widget->addLink(
_('Studierendensicht (Vorschau)'),
$this->url_for('vips/sheets/show_exercise', ['assignment_id' => $assignment_id, 'exercise_id' => $exercise->id])
);
Sidebar::get()->addWidget($widget);
}
$widget = new ViewsWidget();
$widget->setTitle(_('Aufgabenblatt'));
foreach ($assignment->test->exercise_refs as $item) {
$widget->addLink(
sprintf(_('Aufgabe %d'), $item->position),
$this->url_for('vips/sheets/edit_exercise', ['assignment_id' => $assignment_id, 'exercise_id' => $item->task_id])
)->setActive($item->task_id === $exercise->id);
}
Sidebar::get()->addWidget($widget);
}
/**
* SHEETS/EXAMS
*
* Inserts/Updates an exercise into the database
*/
public function store_exercise_action()
{
CSRFProtection::verifyUnsafeRequest();
$exercise_id = Request::int('exercise_id'); // not set when storing new exercise
$exercise_type = Request::option('exercise_type');
$assignment_id = Request::int('assignment_id');
$assignment = VipsAssignment::find($assignment_id);
$test_id = $assignment->test_id;
$file_ids = Request::optionArray('file_ids');
$request = Request::getInstance();
VipsModule::requireEditPermission($assignment, $exercise_id);
if ($exercise_id) {
// update existing exercise.
$exercise = Exercise::find($exercise_id);
$item_count = $exercise->itemCount();
$exercise->initFromRequest($request);
$exercise->store();
// update maximum points
if ($exercise->itemCount() != $item_count) {
$exercise_ref = VipsExerciseRef::find([$test_id, $exercise_id]);
$exercise_ref->points = $exercise->itemCount();
$exercise_ref->store();
}
} else {
// store exercise in database.
$exercise = new $exercise_type();
$exercise->initFromRequest($request);
$exercise->user_id = $GLOBALS['user']->id;
$exercise->store();
// link new exercise to the assignment.
$assignment->test->addExercise($exercise);
$exercise_id = $exercise->id;
}
$upload = $_FILES['upload'] ?: ['name' => []];
$folder = Folder::findTopFolder($exercise->id, 'ExerciseFolder', 'task');
foreach ($folder->file_refs as $file_ref) {
if (!in_array($file_ref->id, $file_ids) || in_array($file_ref->name, $upload['name'])) {
$file_ref->delete();
}
}
FileManager::handleFileUpload($upload, $folder->getTypedFolder());
PageLayout::postSuccess(_('Die Aufgabe wurde eingetragen.'));
$this->redirect($this->url_for('vips/sheets/edit_exercise', compact('assignment_id', 'exercise_id')));
}
/**
* Copy the selected exercises into this assignment.
*/
public function copy_exercise_action()
{
CSRFProtection::verifyUnsafeRequest();
$assignment_id = Request::int('assignment_id');
$assignment = VipsAssignment::find($assignment_id);
$exercise_id = Request::int('exercise_id');
$exercise_ids = $exercise_id ? [$exercise_id => $assignment_id] : Request::intArray('exercise_ids');
VipsModule::requireEditPermission($assignment);
if (!$assignment->isLocked()) {
foreach ($exercise_ids as $exercise_id => $copy_assignment_id) {
$copy_assignment = VipsAssignment::find($copy_assignment_id);
VipsModule::requireEditPermission($copy_assignment);
$exercise_ref = VipsExerciseRef::find([$copy_assignment->test_id, $exercise_id]);
$exercise_ref->copyIntoTest($assignment->test_id);
}
PageLayout::postSuccess(ngettext('Die Aufgabe wurde kopiert.', 'Die Aufgaben wurden kopiert.', count($exercise_ids)));
}
$this->redirect($this->url_for('vips/sheets/edit_assignment', compact('assignment_id')));
}
/**
* Dialog for copying a list of exercises to another assignment.
*/
public function copy_exercises_dialog_action()
{
$this->assignment_id = Request::int('assignment_id');
$this->exercise_ids = Request::intArray('exercise_ids');
$this->courses = VipsModule::getActiveCourses($GLOBALS['user']->id);
}
/**
* Copy a list of exercises to the specified assignment.
*/
public function copy_exercises_action()
{
CSRFProtection::verifyUnsafeRequest();
$assignment_id = Request::int('assignment_id');
$target_assignment_id = Request::int('target_assignment_id');
$exercise_ids = Request::intArray('exercise_ids');
$assignment = VipsAssignment::find($assignment_id);
$target_assignment = VipsAssignment::find($target_assignment_id);
VipsModule::requireEditPermission($assignment);
VipsModule::requireEditPermission($target_assignment);
if (!$target_assignment->isLocked()) {
foreach ($exercise_ids as $exercise_id) {
$exercise_ref = $assignment->test->getExerciseRef($exercise_id);
$exercise_ref->copyIntoTest($target_assignment->test_id);
}
PageLayout::postSuccess(ngettext('Die Aufgabe wurde kopiert.', 'Die Aufgaben wurden kopiert.', count($exercise_ids)));
}
$this->redirect($this->url_for('vips/sheets/edit_assignment', compact('assignment_id')));
}
/**
* Dialog for moving a list of exercises to another assignment.
*/
public function move_exercises_dialog_action()
{
$this->assignment_id = Request::int('assignment_id');
$this->exercise_ids = Request::intArray('exercise_ids');
$this->courses = VipsModule::getActiveCourses($GLOBALS['user']->id);
}
/**
* Move a list of exercises to the specified assignment.
*/
public function move_exercises_action()
{
CSRFProtection::verifyUnsafeRequest();
$assignment_id = Request::int('assignment_id');
$target_assignment_id = Request::int('target_assignment_id');
$exercise_ids = Request::intArray('exercise_ids');
$assignment = VipsAssignment::find($assignment_id);
$target_assignment = VipsAssignment::find($target_assignment_id);
VipsModule::requireEditPermission($assignment);
VipsModule::requireEditPermission($target_assignment);
if (!$assignment->isLocked() && !$target_assignment->isLocked()) {
foreach ($exercise_ids as $exercise_id) {
$exercise_ref = VipsExerciseRef::find([$assignment->test_id, $exercise_id]);
$exercise_ref->moveIntoTest($target_assignment->test_id);
}
PageLayout::postSuccess(ngettext('Die Aufgabe wurde verschoben.', 'Die Aufgaben wurden verschoben.', count($exercise_ids)));
}
$this->redirect($this->url_for('vips/sheets/edit_assignment', compact('assignment_id')));
}
/**
* SHEETS/EXAMS
*
* Stores the specification (Grunddaten) of an assignment
* OR add new exercise, edit points/Bewertung (basically everything that can be done on
* page edit_exercise_action())
*/
public function store_assignment_action()
{
CSRFProtection::verifyUnsafeRequest();
$db = DBManager::get();
$assignment_id = Request::int('assignment_id');
if ($assignment_id) {
$assignment = VipsAssignment::find($assignment_id);
} else {
$assignment = new VipsAssignment();
$assignment->range_id = Context::getId() ?: $GLOBALS['user']->id;
$assignment->range_type = Context::getId() ? 'course' : 'user';
}
VipsModule::requireEditPermission($assignment);
$assignment_name = trim(Request::get('assignment_name'));
$assignment_description = trim(Request::get('assignment_description'));
$assignment_description = Studip\Markup::purifyHtml($assignment_description);
$assignment_notes = trim(Request::get('assignment_notes'));
$assignment_type = Request::option('assignment_type', 'practice');
$assignment_block = Request::int('assignment_block', 0);
$assignment_block_name = trim(Request::get('assignment_block_name'));
$start_date = trim(Request::get('start_date'));
$start_time = trim(Request::get('start_time'));
$end_date = trim(Request::get('end_date'));
$end_time = trim(Request::get('end_time'));
$exam_length = Request::int('exam_length');
$access_code = trim(Request::get('access_code'));
$ip_range = trim(Request::get('ip_range'));
$use_groups = Request::int('use_groups', 0);
$shuffle_answers = Request::int('shuffle_answers', 0);
$shuffle_exercises = Request::int('shuffle_exercises', 0);
$self_assessment = Request::int('self_assessment', 0);
$max_tries = Request::int('max_tries', 0);
$resets = Request::int('resets', 0);
$evaluation_mode = Request::int('evaluation_mode', 0);
$exercise_points = Request::floatArray('exercise_points');
$selftest_threshold = Request::getArray('threshold');
$selftest_feedback = Request::getArray('feedback');
$start_datetime = DateTime::createFromFormat('d.m.Y H:i', $start_date.' '.$start_time);
$end_datetime = DateTime::createFromFormat('d.m.Y H:i', $end_date.' '.$end_time);
if ($assignment_name === '') {
$assignment_name = _('Aufgabenblatt');
}
if ($start_datetime) {
$start = $start_datetime->format('Y-m-d H:i:s');
} else {
$start = date('Y-m-d H:00:00');
PageLayout::postWarning(_('Ungültiger Startzeitpunkt, der Wert wurde nicht übernommen.'));
}
// unlimited selftest
if ($assignment_type == 'selftest' && $end_date == '' && $end_time == '') {
$end = null;
} else if ($end_datetime) {
$end = $end_datetime->format('Y-m-d H:i:s');
} else {
$end = date('Y-m-d H:00:00');
PageLayout::postWarning(_('Ungültiger Endzeitpunkt, der Wert wurde nicht übernommen.'));
}
if ($end && $end <= $start) { // start is *later* than end!
$end = $start;
PageLayout::postWarning(_('Bitte überprüfen Sie den Start- und den Endzeitpunkt!'));
}
if ($assignment_block_name != '') {
$block = VipsBlock::create(['name' => $assignment_block_name, 'range_id' => $assignment->range_id]);
} else if ($assignment_block) {
$block = VipsBlock::find($assignment_block);
if ($block->range_id !== $assignment->range_id) {
$block = null;
}
} else {
$block = null;
}
foreach ($selftest_threshold as $i => $threshold) {
if ($threshold !== '') {
$feedback[$threshold] = Studip\Markup::purifyHtml($selftest_feedback[$i]);
}
}
/*** store basic data (Grunddaten) of assignment */
if ($assignment_id) {
// check whether the exam's start time has been moved
if ($assignment->start != strtotime($start) && time() <= strtotime($start)) {
$assignment->active = 1;
}
// extend exam duration for already active participants
if ($assignment_type === 'exam' && $assignment->options['duration'] != $exam_length) {
$sql = 'UPDATE etask_assignment_attempts SET end = GREATEST(end + ? * 60, UNIX_TIMESTAMP())
WHERE assignment_id = ? AND end > UNIX_TIMESTAMP()';
$stmt = $db->prepare($sql);
$stmt->execute([$exam_length - $assignment->options['duration'], $assignment_id]);
}
$assignment->test->setData([
'title' => $assignment_name,
'description' => $assignment_description
]);
$assignment->test->store();
} else {
$assignment->test = VipsTest::create([
'title' => $assignment_name,
'description' => $assignment_description,
'user_id' => $GLOBALS['user']->id
]);
}
$assignment->setData([
'type' => $assignment_type,
'start' => strtotime($start),
'end' => $end ? strtotime($end) : null,
'block_id' => $block ? $block->id : null
]);
// update options array
$assignment->options['evaluation_mode'] = $evaluation_mode;
$assignment->options['notes'] = $assignment_notes;
unset($assignment->options['access_code']);
unset($assignment->options['ip_range']);
unset($assignment->options['shuffle_answers']);
unset($assignment->options['shuffle_exercises']);
unset($assignment->options['self_assessment']);
unset($assignment->options['use_groups']);
unset($assignment->options['max_tries']);
unset($assignment->options['resets']);
unset($assignment->options['feedback']);
if ($assignment_type === 'exam') {
$assignment->options['duration'] = $exam_length;
if ($access_code !== '') {
$assignment->options['access_code'] = $access_code;
}
if ($ip_range !== '') {
$assignment->options['ip_range'] = $ip_range;
}
$assignment->options['shuffle_answers'] = $shuffle_answers;
if ($shuffle_exercises === 1) {
$assignment->options['shuffle_exercises'] = $shuffle_exercises;
}
if ($self_assessment === 1) {
$assignment->options['self_assessment'] = $self_assessment;
}
}
if ($assignment_type === 'practice') {
$assignment->options['use_groups'] = $use_groups;
}
if ($assignment_type === 'selftest') {
$assignment->options['max_tries'] = $max_tries;
if ($resets === 0) {
$assignment->options['resets'] = $resets;
}
if (isset($feedback)) {
krsort($feedback);
$assignment->options['feedback'] = $feedback;
}
}
$assignment->store();
$assignment_id = $assignment->id;
foreach ($assignment->test->exercise_refs as $exercise_ref) {
$points = $exercise_points[$exercise_ref->task_id];
$exercise_ref->points = round($points * 2) / 2;
$exercise_ref->store();
}
PageLayout::postSuccess(_('Das Aufgabenblatt wurde gespeichert.'));
$this->redirect($this->url_for('vips/sheets/edit_assignment', compact('assignment_id')));
}
/**
* Returns the dialog content to create a new exercise.
*/
public function add_exercise_dialog_action()
{
PageLayout::setHelpKeyword('Basis.VipsAufgaben');
$assignment_id = Request::int('assignment_id');
$this->assignment_id = $assignment_id;
$this->exercise_types = Exercise::getExerciseTypes();
}
/**
* Returns the dialog content to copy an existing exercise.
*/
public function copy_exercise_dialog_action()
{
$assignment_id = Request::int('assignment_id');
$search_filter = Request::getArray('search_filter');
$sort = Request::option('sort', 'start_time');
$desc = Request::int('desc', $sort === 'start_time');
$page = Request::int('page', 1);
$size = 15;
if (empty($search_filter) || Request::submitted('reset_search')) {
$search_filter = array_fill_keys(['search_string', 'exercise_type'], '');
$search_filter['range_type'] = Context::getId() ? 'course' : 'user';
}
if ($search_filter['range_type'] === 'course') {
$course_ids = array_column(VipsModule::getActiveCourses($GLOBALS['user']->id), 'id');
} else {
$course_ids = [$GLOBALS['user']->id];
}
$exercises = $this->getAllExercises($course_ids, $sort, $desc, $search_filter);
$this->sort = $sort;
$this->desc = $desc;
$this->page = $page;
$this->size = $size;
$this->count = count($exercises);
$this->exercises = array_slice($exercises, $size * ($page - 1), $size);
$this->exercise_types = Exercise::getExerciseTypes();
$this->assignment_id = $assignment_id;
$this->search_filter = $search_filter;
}
/**
* Get all matching exercises from a list of courses in given order.
* If $search_filter is not empty, search filters are applied.
*
* @param course_ids list of courses to get exercises from
* @param sort sort exercise list by this property
* @param desc true if sort direction is descending
* @param search_filter the currently active search filter
*
* @return array with data of all matching exercises
*/
public function getAllExercises($course_ids, $sort, $desc, $search_filter)
{
$db = DBManager::get();
// check if some filters are active
$search_string = $search_filter['search_string'];
$exercise_type = $search_filter['exercise_type'];
$sql = "SELECT etask_tasks.*,
etask_assignments.id AS assignment_id,
etask_assignments.range_id,
etask_assignments.range_type,
etask_tests.title AS test_title,
seminare.name AS course_name,
(SELECT MIN(beginn) FROM semester_data
JOIN semester_courses USING(semester_id)
WHERE course_id = Seminar_id) AS start_time
FROM etask_tasks
JOIN etask_test_tasks ON etask_tasks.id = etask_test_tasks.task_id
JOIN etask_tests ON etask_tests.id = etask_test_tasks.test_id
JOIN etask_assignments USING (test_id)
LEFT JOIN seminare ON etask_assignments.range_id = seminare.seminar_id
WHERE etask_assignments.range_id IN (:course_ids)
AND etask_assignments.type IN ('exam', 'practice', 'selftest') " .
($search_string ? 'AND (etask_tasks.title LIKE :input OR
etask_tasks.description LIKE :input OR
etask_tests.title LIKE :input OR
seminare.name LIKE :input) ' : '') .
($exercise_type ? 'AND etask_tasks.type = :exercise_type ' : '') .
"ORDER BY :sort :desc, start_time DESC, seminare.name,
etask_tests.mkdate DESC, etask_test_tasks.position";
$stmt = $db->prepare($sql);
$stmt->bindValue(':course_ids', $course_ids);
$stmt->bindValue(':input', '%' . $search_string . '%');
$stmt->bindValue(':exercise_type', $exercise_type);
$stmt->bindValue(':sort', $sort, StudipPDO::PARAM_COLUMN);
$stmt->bindValue(':desc', $desc ? 'DESC' : 'ASC', StudipPDO::PARAM_COLUMN);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* SHEETS/EXAMS
*
* Displays form to edit an existing assignment
*
*/
public function edit_assignment_action()
{
PageLayout::setHelpKeyword('Basis.VipsAufgabenblatt');
$assignment_id = Request::int('assignment_id');
if ($assignment_id) {
$assignment = VipsAssignment::find($assignment_id);
$test = $assignment->test;
} else {
$test = new VipsTest();
$test->title = _('Aufgabenblatt');
$assignment = new VipsAssignment();
$assignment->range_id = Context::getId() ?: $GLOBALS['user']->id;
$assignment->range_type = Context::getId() ? 'course' : 'user';
$assignment->type = 'practice';
$assignment->start = strtotime(date('Y-m-d H:00:00'));
$assignment->end = strtotime(date('Y-m-d H:00:00'));
}
VipsModule::requireEditPermission($assignment);
if (!isset($assignment->options['feedback'])) {
$assignment->options['feedback'] = ['' => ''];
}
$blocks = VipsBlock::findBySQL('range_id = ? ORDER BY name', [$assignment->range_id]);
$this->assignment = $assignment;
$this->assignment_id = $assignment_id;
$this->test = $test;
$this->blocks = $blocks;
$this->locked = $assignment_id && $assignment->isLocked();
$this->exercises = $test->exercises;
$this->assignment_types = VipsAssignment::getAssignmentTypes();
$this->exam_rooms = Config::get()->VIPS_EXAM_ROOMS;
$this->contentbar = $this->create_contentbar($assignment);
Helpbar::get()->addPlainText('',
_('Sie können hier die Grunddaten des Aufgabenblatts verwalten und Aufgaben hinzufügen, bearbeiten oder löschen.') . ' ' .
_('Alle Daten können später geändert oder ergänzt werden.'));
$widget = new ActionsWidget();
if ($assignment_id && !$this->locked) {
$widget->addLink(
_('Neue Aufgabe erstellen'),
$this->url_for('vips/sheets/add_exercise_dialog', compact('assignment_id')),
Icon::create('add')
)->asDialog('size=auto');
$widget->addLink(
_('Vorhandene Aufgabe kopieren'),
$this->url_for('vips/sheets/copy_exercise_dialog', compact('assignment_id')),
Icon::create('copy')
)->asDialog('size=big');
}
if ($assignment_id) {
if ($assignment->range_type === 'course') {
$widget->addLink(
_('Aufgabenblatt korrigieren'),
$this->url_for('vips/solutions/assignment_solutions', ['assignment_id' => $assignment_id]),
Icon::create('accept')
);
}
$widget->addLink(
_('Aufgabenblatt drucken'),
$this->url_for('vips/sheets/print_assignments', ['assignment_id' => $assignment_id]),
Icon::create('print'),
['target' => '_blank']
);
Sidebar::get()->addWidget($widget);
$widget = new ViewsWidget();
$widget->addLink(
_('Aufgabenblatt bearbeiten'),
$this->url_for('vips/sheets/edit_assignment', ['assignment_id' => $assignment_id])
)->setActive();
$widget->addLink(
_('Studierendensicht (Vorschau)'),
$this->url_for('vips/sheets/show_assignment', ['assignment_id' => $assignment_id])
);
Sidebar::get()->addWidget($widget);
$widget = new ExportWidget();
$widget->addLink(
_('Aufgabenblatt exportieren'),
$this->url_for('vips/sheets/export_xml', ['assignment_id' => $assignment_id]),
Icon::create('export')
);
}
Sidebar::get()->addWidget($widget);
}
/**
* Show preview of an existing exercise (using print view for now).
*/
public function preview_exercise_action()
{
$exercise_id = Request::int('exercise_id');
$assignment_id = Request::int('assignment_id');
$assignment = VipsAssignment::find($assignment_id);
VipsModule::requireEditPermission($assignment, $exercise_id);
// fetch exercise info
$exercise_ref = VipsExerciseRef::find([$assignment->test_id, $exercise_id]);
$exercise = $exercise_ref->exercise;
$this->assignment = $assignment;
$this->exercise = $exercise;
$this->exercise_position = $exercise_ref->position;
$this->max_points = $exercise_ref->points;
$this->solution = new VipsSolution();
$this->show_solution = false;
$this->print_correction = false;
$this->user_id = null;
$this->render_template('vips/exercises/print_exercise');
}
/**
* Copy the selected assignments into the current course.
*/
public function copy_assignment_action()
{
CSRFProtection::verifyUnsafeRequest();
$course_id = Context::getId();
if ($course_id) {
VipsModule::requireStatus('tutor', $course_id);
}
$assignment_id = Request::int('assignment_id');
$assignment_ids = $assignment_id ? [$assignment_id] : Request::intArray('assignment_ids');
foreach ($assignment_ids as $assignment_id) {
$assignment = VipsAssignment::find($assignment_id);
VipsModule::requireEditPermission($assignment);
if ($course_id) {
$assignment->copyIntoCourse($course_id);
} else {
$assignment->copyIntoCourse($GLOBALS['user']->id, 'user');
}
}
PageLayout::postSuccess(ngettext('Das Aufgabenblatt wurde kopiert.', 'Die Aufgabenblätter wurden kopiert.', count($assignment_ids)));
$this->redirect($course_id ? 'vips/sheets' : 'vips/pool/assignments');
}
/**
* Imports a test from a text file.
*/
public function import_test_action()
{
CSRFProtection::verifyUnsafeRequest();
$course_id = Context::getId();
$user_id = $GLOBALS['user']->id;
$range_id = $course_id ?: $user_id;
$range_type = $course_id ? 'course' : 'user';
if ($course_id) {
VipsModule::requireStatus('tutor', $course_id);
}
if ($_FILES['upload']['name'][0] == '') {
PageLayout::postError(_('Sie müssen eine Datei zum Importieren auswählen.'));
$this->redirect($course_id ? 'vips/sheets' : 'vips/pool/assignments');
return;
}
$num_assignments = 0;
$num_exercises = 0;
for ($i = 0; $i < count($_FILES['upload']['name']); ++$i) {
if (!is_uploaded_file($_FILES['upload']['tmp_name'][$i])) {
$message = sprintf(_('Es trat ein Fehler beim Hochladen der Datei „%s“ auf.'), htmlReady($_FILES['upload']['name'][$i]));
PageLayout::postError($message);
continue;
}
$text = file_get_contents($_FILES['upload']['tmp_name'][$i]);
if (str_contains($text, 'test->exercise_refs);
}
if ($num_assignments == 1) {
$message = sprintf(ngettext('Das Aufgabenblatt „%s“ mit %d Aufgabe wurde hinzugefügt.',
'Das Aufgabenblatt „%s“ mit %d Aufgaben wurde hinzugefügt.', $num_exercises),
htmlReady($assignment->test->title), $num_exercises);
PageLayout::postSuccess($message);
} else if ($num_assignments > 1) {
$message = sprintf(_('%1$d Aufgabenblätter mit insgesamt %2$d Aufgaben wurden hinzugefügt.'), $num_assignments, $num_exercises);
PageLayout::postSuccess($message);
}
$this->redirect($course_id ? 'vips/sheets' : 'vips/pool/assignments');
}
/**
* Creates html print view of a sheet/exam (new window) specified by id
*/
public function print_assignments_action()
{
$assignment_id = Request::int('assignment_id');
$assignment = VipsAssignment::find($assignment_id);
VipsModule::requireViewPermission($assignment);
$user_ids = Request::optionArray('user_ids');
$print_files = Request::int('print_files');
$print_correction = Request::int('print_correction');
$print_sample_solution = Request::int('print_sample_solution');
$print_student_ids = false;
$assignment_data = [];
if (!$assignment->checkEditPermission()) {
$user_ids = [$GLOBALS['user']->id];
$released = $assignment->releaseStatus($user_ids[0]);
$print_correction = $released >= VipsAssignment::RELEASE_STATUS_CORRECTIONS;
$print_sample_solution = $released == VipsAssignment::RELEASE_STATUS_SAMPLE_SOLUTIONS;
if ($assignment->type !== 'exam' && $assignment->checkAccess($user_ids[0])) {
$assignment->recordAssignmentAttempt($user_ids[0]);
} else if ($released < VipsAssignment::RELEASE_STATUS_CORRECTIONS) {
PageLayout::postError(_('Kein Zugriff möglich!'));
$this->redirect('vips/sheets/list_assignments_stud');
return;
}
}
if ($assignment->range_type === 'course') {
foreach ($assignment->course->getMembersWithStatus('dozent') as $member) {
$lecturers[] = $member->getUserFullname();
}
$sem_class = $assignment->course->getSemClass();
$print_student_ids = !$sem_class['studygroup_mode'];
}
if ($user_ids) {
foreach ($user_ids as $user_id) {
$group = $assignment->getUserGroup($user_id);
$students = $stud_ids = [];
if ($group) {
$name = $group->name;
$members = $assignment->getGroupMembers($group);
usort($members, function($a, $b) {
return strcoll($a->user->getFullName('no_title_rev'), $b->user->getFullName('no_title_rev'));
});
foreach ($members as $member) {
$students[] = $member->user->getFullName('no_title');
$stud_ids[] = $member->user->matriculation_number ?: _('(keine Matrikelnummer)');
}
} else {
$user = User::find($user_id);
$name = $user->getFullName('no_title_rev');
$students[] = $user->getFullName('no_title');
$stud_ids[] = $user->matriculation_number ?: _('(keine Matrikelnummer)');
}
$assignment_data[] = [
'user_id' => $user_id,
'students' => $students,
'stud_ids' => $stud_ids
];
}
} else {
$assignment_data[] = [
'user_id' => null
];
}
if (count($user_ids) === 1) {
Config::get()->UNI_NAME_CLEAN = $name;
}
PageLayout::setTitle($assignment->test->title);
$this->set_layout('vips/sheets/print_layout');
$this->assignment = $assignment;
$this->user_ids = $user_ids;
$this->lecturers = $lecturers;
$this->print_files = $print_files;
$this->print_correction = $print_correction;
$this->print_sample_solution = $print_sample_solution;
$this->print_student_ids = $print_student_ids;
$this->assignment_data = $assignment_data;
}
/**
* SHEETS/EXAMS
*
* Main page of sheets/exams.
* Lists all the assignments (sheets or exams) in the course, grouped by "not yet started",
* "running" and "finished".
*/
public function list_assignments_action()
{
$course_id = Context::getId();
VipsModule::requireStatus('tutor', $course_id);
$sort = Request::option('sort', 'start');
$desc = Request::int('desc');
$group = isset($_SESSION['group_assignments']) ? $_SESSION['group_assignments'] : 0;
$group = Request::int('group', $group);
$_SESSION['group_assignments'] = $group;
$running = false;
######################################
# get assignments in this course #
######################################
$assignments = VipsAssignment::findByRangeId($course_id);
$blocks = VipsBlock::findBySQL('range_id = ? ORDER BY name', [$course_id]);
$blocks[] = VipsBlock::build(['name' => _('Aufgabenblätter ohne Blockzuordnung')]);
usort($assignments, function($a, $b) use ($sort) {
if ($sort === 'title') {
return strcoll($a->test->title, $b->test->title);
} else if ($sort === 'type') {
return strcmp($a->type, $b->type);
} else if ($sort === 'start') {
return strcmp($a->start, $b->start);
} else {
return strcmp($a->end ?: '~', $b->end ?: '~');
}
});
if ($desc) {
$assignments = array_reverse($assignments);
}
$plugin_manager = PluginManager::getInstance();
$courseware = $plugin_manager->getPluginInfo('CoursewareModule');
$courseware_active = $courseware && $plugin_manager->isPluginActivated($courseware['id'], $course_id);
if ($group == 2 && $courseware_active) {
$elements = Courseware\StructuralElement::findBySQL('range_id = ?', [$course_id]);
$unassigned = array_column($assignments, 'id');
foreach ($elements as $element) {
$assigned = $this->courseware_assignments($element);
$unassigned = array_diff($unassigned, $assigned);
$assignment_data[] = [
'title' => $element->title,
'assignments' => array_filter($assignments, function($assignment) use ($assigned) {
return in_array($assignment->id, $assigned);
})
];
}
$assignment_data[] = [
'title' => _('Aufgabenblätter ohne Courseware-Einbindung'),
'assignments' => array_filter($assignments, function($assignment) use ($unassigned) {
return in_array($assignment->id, $unassigned);
})
];
} else if ($group == 1) {
foreach ($blocks as $block) {
$assignment_data[$block->id] = [
'title' => $block->name,
'block' => $block,
'assignments' => []
];
}
foreach ($assignments as $assignment) {
$assignment_data[$assignment->block_id]['assignments'][] = $assignment;
}
} else {
$group = 0;
$assignment_data = [
[
'title' => _('Noch nicht gestartete Aufgabenblätter'),
'assignments' => []
], [
'title' => _('Laufende Aufgabenblätter'),
'assignments' => []
], [
'title' => _('Beendete Aufgabenblätter'),
'assignments' => []
]
];
foreach ($assignments as $assignment) {
if ($assignment->isFinished()) {
$assignment_data[2]['assignments'][] = $assignment;
} else if ($assignment->isRunning()) {
$assignment_data[1]['assignments'][] = $assignment;
} else {
$assignment_data[0]['assignments'][] = $assignment;
}
}
}
foreach ($assignments as $assignment) {
if ($assignment->isRunning()) {
$running = true;
}
}
$this->assignment_data = $assignment_data;
$this->num_assignments = count($assignments);
$this->sort = $sort;
$this->desc = $desc;
$this->group = $group;
$this->blocks = $blocks;
Helpbar::get()->addPlainText('',
_('Hier können Übungen, Tests und Klausuren online vorbereitet und durchgeführt werden. Sie erhalten ' .
'dabei auch eine Übersicht über die Lösungen bzw. Antworten der Studierenden.') . "\n\n" .
_('Auf dieser Seite können Sie Aufgabenblätter in Ihrem Kurs anlegen und verwalten.'));
$widget = new ActionsWidget();
$widget->addLink(
_('Aufgabenblatt erstellen'),
$this->url_for('vips/sheets/edit_assignment'),
Icon::create('add')
);
$widget->addLink(
_('Aufgabenblatt kopieren'),
$this->url_for('vips/sheets/copy_assignment_dialog'),
Icon::create('copy')
)->asDialog('size=1200x800');
$widget->addLink(
_('Aufgabenblatt importieren'),
$this->url_for('vips/sheets/import_assignment_dialog'),
Icon::create('import')
)->asDialog('size=auto');
$widget->addLink(
_('Neuen Block erstellen'),
$this->url_for('vips/admin/edit_block'),
Icon::create('add')
)->asDialog('size=auto');
Sidebar::get()->addWidget($widget);
$widget = new ViewsWidget();
$widget->addLink(
_('Gruppiert nach Status'),
$this->url_for('vips/sheets', ['group' => 0])
)->setActive($group == 0);
$widget->addLink(
_('Gruppiert nach Blöcken'),
$this->url_for('vips/sheets', ['group' => 1])
)->setActive($group == 1);
if ($courseware_active) {
$widget->addLink(
_('Verwendung in Courseware'),
$this->url_for('vips/sheets', ['group' => 2])
)->setActive($group == 2);
}
Sidebar::get()->addWidget($widget);
}
/**
* Collect all assignment_ids used in the given Courseware element.
*/
private function courseware_assignments($element)
{
$result = [];
foreach ($element->containers as $container) {
foreach ($container->blocks as $block) {
if ($block->block_type === 'test') {
$payload = json_decode($block->payload, true);
if ($payload['assignment']) {
$result[] = $payload['assignment'];
}
}
}
}
return $result;
}
/**
* Returns the dialog content to import an assignment from text file.
*/
public function import_assignment_dialog_action()
{
}
/**
* Returns the dialog content to copy available assignments.
*/
public function copy_assignment_dialog_action()
{
$search_filter = Request::getArray('search_filter');
$sort = Request::option('sort', 'start_time');
$desc = Request::int('desc', $sort === 'start_time');
$page = Request::int('page', 1);
$size = 15;
if (empty($search_filter) || Request::submitted('reset_search')) {
$search_filter = array_fill_keys(['search_string', 'assignment_type'], '');
$search_filter['range_type'] = Context::getId() ? 'course' : 'user';
}
if ($search_filter['range_type'] === 'course') {
$course_ids = array_column(VipsModule::getActiveCourses($GLOBALS['user']->id), 'id');
} else {
$course_ids = [$GLOBALS['user']->id];
}
$assignments = $this->getAllAssignments($course_ids, $sort, $desc, $search_filter);
$this->sort = $sort;
$this->desc = $desc;
$this->page = $page;
$this->size = $size;
$this->count = count($assignments);
$this->assignments = array_slice($assignments, $size * ($page - 1), $size);
$this->assignment_types = VipsAssignment::getAssignmentTypes();
$this->search_filter = $search_filter;
}
/**
* Get all matching assignments from a list of courses in given order.
* If $search_filter is not empty, search filters are applied.
*
* @param array $course_ids list of courses to get assignments from
* @param string $sort sort assignment list by this property
* @param bool $desc true if sort direction is descending
* @param array $search_filter the currently active search filter
*
* @return array with data of all matching assignments
*/
public function getAllAssignments(array $course_ids, string $sort, bool $desc, array $search_filter)
{
$db = DBManager::get();
// check if some filters are active
$search_string = $search_filter['search_string'];
$assignment_type = $search_filter['assignment_type'];
$types = $assignment_type ? [$assignment_type] : ['exam', 'practice', 'selftest'];
$sql = "SELECT etask_assignments.*,
etask_tests.title AS test_title,
seminare.name AS course_name,
(SELECT MIN(beginn) FROM semester_data
JOIN semester_courses USING(semester_id)
WHERE course_id = Seminar_id) AS start_time
FROM etask_tests
JOIN etask_assignments ON etask_tests.id = etask_assignments.test_id
LEFT JOIN seminare ON etask_assignments.range_id = seminare.seminar_id
WHERE etask_assignments.range_id IN (:course_ids)
AND etask_assignments.type IN (:types) " .
($search_string ? 'AND (etask_tests.title LIKE :input OR
etask_tests.description LIKE :input OR
seminare.name LIKE :input) ' : '') .
"ORDER BY :sort :desc, start_time DESC, seminare.name,
etask_tests.mkdate DESC";
$stmt = $db->prepare($sql);
$stmt->bindValue(':course_ids', $course_ids);
$stmt->bindValue(':input', '%' . $search_string . '%');
$stmt->bindValue(':types', $types);
$stmt->bindValue(':sort', $sort, StudipPDO::PARAM_COLUMN);
$stmt->bindValue(':desc', $desc ? 'DESC' : 'ASC', StudipPDO::PARAM_COLUMN);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Exports all exercises in this assignment in Vips XML format.
*/
public function export_xml_action()
{
$assignment_id = Request::int('assignment_id');
$assignment = VipsAssignment::find($assignment_id);
VipsModule::requireEditPermission($assignment);
$this->set_content_type('text/xml; charset=UTF-8');
header('Content-Disposition: attachment; ' . encode_header_parameter('filename', $assignment->test->title.'.xml'));
$this->render_text($assignment->exportXML());
}
public function relay_action($action)
{
$params = func_get_args();
$params[0] = $this;
$exercise_id = Request::int('exercise_id');
$exercise = Exercise::find($exercise_id);
$action = $action . '_action';
$this->exercise = $exercise;
if (method_exists($exercise, $action)) {
call_user_func_array([$exercise, $action], $params);
} else {
throw new InvalidArgumentException(get_class($exercise) . '::' . $action);
}
}
/**
* Create a ContentBar for this assignment (if no exercise is specified)
* or for the given exercise on the assignment.
*/
public function create_contentbar(
VipsAssignment $assignment,
?int $exercise_id = null,
string $view = 'edit',
?string $solver_id = null
) {
$toc = new TOCItem($assignment->test->title);
$toc->setURL($this->url_for("vips/sheets/{$view}_assignment", ['assignment_id' => $assignment->id]));
$toc->setActive($exercise_id === null);
if (!empty($assignment->test->exercise_refs)) {
if ($view === 'edit') {
$exercise_refs = $assignment->test->exercise_refs;
} else {
$exercise_refs = $assignment->getExerciseRefs($solver_id);
}
foreach ($exercise_refs as $i => $item) {
$child = new TOCItem(sprintf('%d. %s', $i + 1, $item->exercise->title));
$child->setURL($this->url_for(
"vips/sheets/{$view}_exercise",
['assignment_id' => $assignment->id, 'exercise_id' => $item->task_id, 'solver_id' => $solver_id]
));
$child->setActive($item->task_id == $exercise_id);
$toc->children[] = $child;
}
}
foreach ($toc->children as $i => $item) {
if ($item->isActive()) {
$icons = $this->get_template_factory()->open('vips/sheets/content_bar_icons');
if ($i > 0) {
$icons->prev_exercise_url = $toc->children[$i - 1]->getURL();
}
if ($i < count($toc->children) - 1) {
$icons->next_exercise_url = $toc->children[$i + 1]->getURL();
}
}
}
return Studip\VueApp::create('ContentBar')->withProps([
'isContentBar' => true,
'toc' => $toc
])->withComponent(
'ContentBarBreadcrumbs'
)->withSlot(
'breadcrumb-list', sprintf("", json_encode($toc))
)->withSlot(
'buttons-left', $icons ?? ''
);
}
/**
* Return the appropriate CSS class for sortable column (if any).
*
* @param boolean $sort sort by this column
* @param boolean $desc set sort direction
*/
public function sort_class(bool $sort, ?bool $desc): string
{
return $sort ? ($desc ? 'sortdesc' : 'sortasc') : '';
}
/**
* Render a generic page chooser selector. The first occurence of '%d'
* in the URL is replaced with the selected page number.
*
* @param string $url URL for one of the pages
* @param string $count total number of entries
* @param string $page current page to display
* @param string|null $dialog Optional dialog attribute content
* @param int|null $page_size page size (defaults to system default)
* @return mixed
*/
public function page_chooser(string $url, string $count, string $page, ?string $dialog = null, ?int $page_size = null)
{
$template = $GLOBALS['template_factory']->open('shared/pagechooser');
$template->dialog = $dialog;
$template->num_postings = $count;
$template->page = $page;
$template->perPage = $page_size ?: Config::get()->ENTRIES_PER_PAGE;
$template->pagelink = str_replace('%%25d', '%d', str_replace('%', '%%', $url));
return $template->render();
}
}