1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
|
<?php
/*
* VipsModule.php - Vips plugin class for Stud.IP
* Copyright (c) 2007-2021 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.
*/
use Courseware\CoursewarePlugin;
/**
* Vips plugin class for Stud.IP
*/
class VipsModule extends CorePlugin implements StudipModule, SystemPlugin, PrivacyPlugin, CoursewarePlugin
{
public static ?bool $exam_mode = null;
public static ?VipsModule $instance = null;
public static ?Flexi\Factory $template_factory = null;
public function __construct()
{
global $perm, $user;
parent::__construct();
self::$instance = $this;
self::$template_factory = new Flexi\Factory($GLOBALS['STUDIP_BASE_PATH'] . '/app/views/vips');
NotificationCenter::addObserver($this, 'userDidDelete', 'UserDidDelete');
NotificationCenter::addObserver($this, 'courseDidDelete', 'CourseDidDelete');
NotificationCenter::addObserver($this, 'userDidLeaveCourse', 'UserDidLeaveCourse');
NotificationCenter::addObserver($this, 'userDidMigrate', 'UserDidMigrate');
NotificationCenter::addObserver($this, 'statusgruppeUserDidCreate', 'StatusgruppeUserDidCreate');
NotificationCenter::addObserver($this, 'statusgruppeUserDidDelete', 'StatusgruppeUserDidDelete');
Exercise::addExerciseType(_('Single Choice'), SingleChoiceTask::class, ['choice-single', '']);
Exercise::addExerciseType(_('Multiple Choice'), MultipleChoiceTask::class, 'choice-multiple');
Exercise::addExerciseType(_('Multiple Choice Matrix'), MatrixChoiceTask::class, 'choice-matrix');
Exercise::addExerciseType(_('Freie Antwort'), TextLineTask::class, 'text-line');
Exercise::addExerciseType(_('Textaufgabe'), TextTask::class, 'text-area');
Exercise::addExerciseType(_('Lückentext'), ClozeTask::class, ['cloze-input', 'cloze-select', 'cloze-drag']);
Exercise::addExerciseType(_('Zuordnung'), MatchingTask::class, ['matching', 'matching-multiple']);
Exercise::addExerciseType(_('Reihenfolge'), SequenceTask::class, 'sequence');
if ($perm->have_perm('root')) {
$nav_item = new Navigation(_('Klausuren'), 'dispatch.php/vips/config');
Navigation::addItem('/admin/config/vips', $nav_item);
}
if (Navigation::hasItem('/contents')) {
$nav_item = new Navigation(_('Aufgaben'));
$nav_item->setImage(Icon::create('vips'));
$nav_item->setDescription(_('Erstellen und Verwalten von Aufgabenblättern'));
Navigation::addItem('/contents/vips', $nav_item);
$sub_item = new Navigation(_('Aufgabenblätter'), 'dispatch.php/vips/pool/assignments');
$nav_item->addSubNavigation('assignments', $sub_item);
$sub_item = new Navigation(_('Aufgaben'), 'dispatch.php/vips/pool/exercises');
$nav_item->addSubNavigation('exercises', $sub_item);
}
// check for running exams
if (Config::get()->VIPS_EXAM_RESTRICTIONS && !isset(self::$exam_mode)) {
$courses = self::getCoursesWithRunningExams($user->id);
self::$exam_mode = count($courses) > 0;
if (self::$exam_mode) {
$page = basename($_SERVER['PHP_SELF']);
$path_info = Request::pathInfo();
$course_id = Context::getId();
// redirect page calls if necessary
if (match_route('dispatch.php/jsupdater/get')) {
// always allow jsupdater calls
UpdateInformation::setInformation('vips', ['exam_mode' => true]);
} else if (isset($course_id, $courses[$course_id])) {
// course with running exam is selected, allow all exam actions
if (!match_route('dispatch.php/vips/sheets')) {
header('Location: ' . URLHelper::getURL('dispatch.php/vips/sheets'));
sess()->save();
die();
}
} else if (count($courses) === 1) {
// only one course with running exam, redirect there
header('Location: ' . URLHelper::getURL('dispatch.php/vips/sheets', ['cid' => key($courses)]));
sess()->save();
die();
} else if (!match_route('dispatch.php/vips/exam_mode')) {
// forward to overview of all running courses with exams
header('Location: ' . URLHelper::getURL('dispatch.php/vips/exam_mode'));
sess()->save();
die();
}
} else {
PageLayout::addHeadElement(
'script',
[],
'STUDIP.JSUpdater.register("vips", () => location.reload());'
);
}
}
}
/**
* Return whether or not the current user has the given status in a course.
*
* @param string $status status name: 'autor', 'tutor' or 'dozent'
* @param string $course_id course to check
*/
public static function hasStatus(string $status, string $course_id): bool
{
return $course_id && $GLOBALS['perm']->have_studip_perm($status, $course_id);
}
/**
* Check whether or not the current user has the required status in a course.
*
* @param string $status required status: 'autor', 'tutor' or 'dozent'
* @param string $course_id course to check
* @throws AccessDeniedException if the requirement is not met, an exception is thrown
*/
public static function requireStatus(string $status, string $course_id): void
{
if (!VipsModule::hasStatus($status, $course_id)) {
throw new AccessDeniedException(_('Sie verfügen nicht über die notwendigen Rechte für diese Aktion.'));
}
}
/**
* Checks whether or not the current user may view an assignment.
*
* @param VipsAssignment|null $assignment assignment to check
* @param int|null $exercise_id check that this exercise is on the assignment (optional)
* @throws AccessDeniedException If the current user doesn't have access, an exception is thrown
*/
public static function requireViewPermission(?VipsAssignment $assignment, ?int $exercise_id = null): void
{
if (!$assignment || !$assignment->checkViewPermission()) {
throw new AccessDeniedException(_('Sie haben keinen Zugriff auf dieses Aufgabenblatt!'));
}
if ($exercise_id && !$assignment->hasExercise($exercise_id)) {
throw new AccessDeniedException(_('Sie haben keinen Zugriff auf diese Aufgabe!'));
}
}
/**
* Checks whether or not the current user may edit an assignment.
*
* @param VipsAssignment|null $assignment assignment to check
* @param int|null $exercise_id check that this exercise is on the assignment (optional)
* @throws AccessDeniedException If the current user doesn't have access, an exception is thrown
*/
public static function requireEditPermission(?VipsAssignment $assignment, ?int $exercise_id = null): void
{
if (!$assignment || !$assignment->checkEditPermission()) {
throw new AccessDeniedException(_('Sie haben keinen Zugriff auf dieses Aufgabenblatt!'));
}
if ($exercise_id && !$assignment->hasExercise($exercise_id)) {
throw new AccessDeniedException(_('Sie haben keinen Zugriff auf diese Aufgabe!'));
}
}
/**
* Get all courses where the user is at least tutor and Vips is activated.
*
* @return array with all course ids, null if no courses
*/
public static function getActiveCourses(string $user_id): array
{
$plugin_manager = PluginManager::getInstance();
$vips_plugin_id = VipsModule::$instance->getPluginId();
$sql = "JOIN seminar_user USING(Seminar_id)
WHERE user_id = ? AND seminar_user.status IN ('dozent', 'tutor')
ORDER BY (SELECT MIN(beginn) FROM semester_data
JOIN semester_courses USING(semester_id)
WHERE course_id = Seminar_id) DESC, Name";
$courses = Course::findBySQL($sql, [$user_id]);
// remove courses where Vips is not active
foreach ($courses as $key => $course) {
if (!$plugin_manager->isPluginActivated($vips_plugin_id, $course->id)) {
unset($courses[$key]);
}
}
return $courses;
}
/**
* Get all courses with currently running exams for the given user.
*
* @param string $user_id The user id
*
* @return array associative array of course ids and course names
*/
public static function getCoursesWithRunningExams(string $user_id): array
{
$db = DBManager::get();
$courses = [];
$sql = "SELECT DISTINCT seminare.Seminar_id, seminare.Name, etask_assignments.id
FROM etask_assignments
JOIN seminar_user ON seminar_user.Seminar_id = etask_assignments.range_id
JOIN seminare USING(Seminar_id)
WHERE etask_assignments.type = 'exam'
AND etask_assignments.start <= UNIX_TIMESTAMP()
AND etask_assignments.end > UNIX_TIMESTAMP()
AND seminar_user.user_id = ?
AND seminar_user.status = 'autor'
ORDER BY seminare.Name";
$stmt = $db->prepare($sql);
$stmt->execute([$user_id]);
foreach ($stmt as $row) {
$assignment = VipsAssignment::find($row['id']);
$ip_range = $assignment->options['ip_range'];
if ($assignment->isVisible($user_id)) {
if (strlen($ip_range) > 0 && $assignment->checkIPAccess($_SERVER['REMOTE_ADDR'])) {
$courses[$row['Seminar_id']] = $row['Name'];
}
}
}
return $courses;
}
public function setupExamNavigation()
{
$navigation = new Navigation('');
$start = Navigation::getItem('/start');
$start->setURL('dispatch.php/vips/exam_mode');
$navigation->addSubNavigation('start', $start);
$course = new Navigation(_('Veranstaltung'));
$navigation->addSubNavigation('course', $course);
$vips = new Navigation($this->getPluginName());
$vips->setImage(Icon::create('vips'));
$course->addSubNavigation('vips', $vips);
$nav_item = new Navigation(_('Aufgabenblätter'), 'dispatch.php/vips/sheets');
$vips->addSubNavigation('sheets', $nav_item);
$links = new Navigation('Links');
$links->addSubNavigation('logout', new Navigation(_('Logout'), 'logout.php'));
$navigation->addSubNavigation('links', $links);
Config::get()->PERSONAL_NOTIFICATIONS_ACTIVATED = 0;
PageLayout::addStyle('#navigation-level-1, #navigation-level-2, #context-title { display: none; }');
PageLayout::addCustomQuicksearch('<div style="width: 64px;"></div>');
Navigation::setRootNavigation($navigation);
}
public function getIconNavigation($course_id, $last_visit, $user_id)
{
if (VipsModule::hasStatus('tutor', $course_id)) {
// find all uncorrected exercises in finished assignments in this course
// Added JOIN with seminar_user to filter out lecturer/tutor solutions.
$new_items = VipsSolution::countBySql(
"JOIN etask_assignments ON etask_responses.assignment_id = etask_assignments.id
LEFT JOIN seminar_user
ON seminar_user.Seminar_id = etask_assignments.range_id
AND seminar_user.user_id = etask_responses.user_id
WHERE etask_assignments.range_id = ?
AND etask_assignments.type IN ('exam', 'practice', 'selftest')
AND etask_assignments.end <= UNIX_TIMESTAMP()
AND etask_responses.state = 0
AND IFNULL(seminar_user.status, 'autor') = 'autor'",
[$course_id]
);
$message = ngettext('%d unkorrigierte Lösung', '%d unkorrigierte Lösungen', $new_items);
} else {
// find all active assignments not yet seen by the student
$assignments = VipsAssignment::findBySQL(
"LEFT JOIN etask_assignment_attempts
ON etask_assignment_attempts.assignment_id = etask_assignments.id
AND etask_assignment_attempts.user_id = ?
WHERE etask_assignments.range_id = ?
AND etask_assignments.type IN ('exam', 'practice', 'selftest')
AND etask_assignments.start <= UNIX_TIMESTAMP()
AND (etask_assignments.end IS NULL OR etask_assignments.end > UNIX_TIMESTAMP())
AND etask_assignment_attempts.user_id IS NULL",
[$user_id, $course_id]
);
$new_items = 0;
foreach ($assignments as $assignment) {
if ($assignment->isVisible($user_id)) {
++$new_items;
}
}
$message = ngettext('%d neues Aufgabenblatt', '%d neue Aufgabenblätter', $new_items);
}
$overview_message = $this->getPluginName();
$icon = Icon::create('vips');
if ($new_items > 0) {
$overview_message = sprintf($message, $new_items);
$icon = Icon::create('vips', Icon::ROLE_NEW);
}
$icon_navigation = new Navigation($this->getPluginName(), 'dispatch.php/vips/sheets');
$icon_navigation->setImage($icon->copyWithAttributes(['title' => $overview_message]));
return $icon_navigation;
}
public function getInfoTemplate($course_id)
{
return null;
}
public function getTabNavigation($course_id)
{
$navigation = new Navigation($this->getPluginName());
$navigation->setImage(Icon::create('vips'));
$nav_item = new Navigation(_('Aufgabenblätter'), 'dispatch.php/vips/sheets');
$navigation->addSubNavigation('sheets', $nav_item);
$nav_item = new Navigation(_('Ergebnisse'), 'dispatch.php/vips/solutions');
$navigation->addSubNavigation('solutions', $nav_item);
return ['vips' => $navigation];
}
public function getMetadata()
{
$metadata['category'] = _('Inhalte und Aufgabenstellungen');
$metadata['displayname'] = _('Aufgaben und Prüfungen');
$metadata['summary'] =
_('Erstellung und Durchführung von Übungen, Tests und Klausuren');
$metadata['description'] =
_('Mit diesem Werkzeug können Übungen, Tests und Klausuren online vorbereitet und durchgeführt werden. ' .
'Die Lehrenden erhalten eine Übersicht darüber, welche Teilnehmenden eine Übung oder einen ' .
'Test mit welchem Ergebnis abgeschlossen haben. Im Gegensatz zu herkömmlichen Übungszetteln ' .
'oder Klausurbögen sind in Stud.IP alle Texte gut lesbar und sortiert abgelegt. Lehrende ' .
'erhalten sofort einen Überblick darüber, was noch zu korrigieren ist. Neben allgemein ' .
'üblichen Fragetypen wie Multiple Choice und Freitextantwort verfügt das Werkzeug auch über ' .
'ungewöhnlichere, aber didaktisch durchaus sinnvolle Fragetypen wie Lückentext und Zuordnung.');
$metadata['keywords'] =
_('Einsatz bei Hausaufgaben und Präsenzprüfungen; Reduzierter Arbeitsaufwand bei der Auswertung; ' .
'Sortierte Übersicht der eingereichten Ergebnisse; Single-, Multiple-Choice- und Textaufgaben, ' .
'Lückentexte und Zuordnungen; Notwendige Korrekturen und erzielte Punktzahlen auf einen Blick');
$metadata['icon'] = Icon::create('vips');
return $metadata;
}
public function userDidDelete($event, $user)
{
// delete all personal assignments
VipsAssignment::deleteBySQL('range_id = ?', [$user->id]);
// delete in etask_responses
VipsSolution::deleteBySQL('user_id = ?', [$user->id]);
// delete start times and group memberships
VipsAssignmentAttempt::deleteBySQL('user_id = ?', [$user->id]);
VipsGroupMember::deleteBySQL('user_id = ?', [$user->id]);
}
public function courseDidDelete($event, $course)
{
// delete all assignments in course
VipsAssignment::deleteBySQL('range_id = ?', [$course->id]);
// delete other course related info
VipsBlock::deleteBySQL('range_id = ?', [$course->id]);
}
public function userDidLeaveCourse($event, $course_id, $user_id)
{
// terminate group membership when leaving a course
$group_member = VipsGroupMember::findOneBySQL(
'JOIN statusgruppen ON statusgruppe_id = group_id WHERE range_id = ? AND user_id = ? AND end IS NULL',
[$course_id, $user_id]
);
if ($group_member) {
$group_member->end = time();
$group_member->store();
}
}
public function userDidMigrate($event, $user_id, $new_id)
{
$db = DBManager::get();
$db->execute('UPDATE IGNORE etask_assignment_attempts SET user_id = ? WHERE user_id = ?', [$new_id, $user_id]);
$db->execute('UPDATE etask_tasks SET user_id = ? WHERE user_id = ?', [$new_id, $user_id]);
$db->execute('UPDATE IGNORE etask_responses SET user_id = ? WHERE user_id = ?', [$new_id, $user_id]);
$db->execute('UPDATE etask_tests SET user_id = ? WHERE user_id = ?', [$new_id, $user_id]);
}
public function statusgruppeUserDidCreate($event, $statusgruppe_user)
{
VipsGroupMember::create([
'group_id' => $statusgruppe_user->statusgruppe_id,
'user_id' => $statusgruppe_user->user_id,
'start' => time()
]);
}
public function statusgruppeUserDidDelete($event, $statusgruppe_user)
{
$member = VipsGroupMember::findOneBySQL(
'group_id = ? AND user_id = ? AND end IS NULL',
[$statusgruppe_user->statusgruppe_id, $statusgruppe_user->user_id]
);
if ($member) {
$member->end = time();
$member->store();
}
}
/**
* Export available data of a given user into a storage object
* (an instance of the StoredUserData class) for that user.
*
* @param StoredUserData $store object to store data into
*/
public function exportUserData(StoredUserData $store)
{
$db = DBManager::get();
$data = $db->fetchAll('SELECT * FROM etask_group_members WHERE user_id = ?', [$store->user_id]);
$store->addTabularData(_('Aufgaben-Gruppenzuordnung'), 'etask_group_members', $data);
}
/**
* Implement this method to register more block types.
*
* You get the current list of block types and return an updated list
* containing your own block types.
*/
public function registerBlockTypes(array $otherBlockTypes): array
{
$otherBlockTypes[] = Courseware\BlockTypes\TestBlock::class;
return $otherBlockTypes;
}
/**
* Implement this method to register more container types.
*
* You get the current list of container types and return an updated list
* containing your own container types.
*/
public function registerContainerTypes(array $otherContainerTypes): array
{
return $otherContainerTypes;
}
}
|