-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathlib.php
More file actions
executable file
·452 lines (415 loc) · 15.6 KB
/
lib.php
File metadata and controls
executable file
·452 lines (415 loc) · 15.6 KB
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
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle 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 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file contains functions used by the capquiz interface
*
* @package mod_capquiz
* @author Sebastian Gundersen <sebastian@sgundersen.com>
* @author Aleksander Skrede <aleksander.l.skrede@ntnu.no>
* @author André Storhaug <andr3.storhaug@gmail.com>
* @copyright 2025 Norwegian University of Science and Technology (NTNU)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// phpcs:disable moodle.Commenting.ValidTags.Invalid
use core_question\local\bank\filter_condition_manager;
use core_question\local\bank\question_edit_contexts;
use mod_capquiz\capquiz;
use mod_capquiz\capquiz_attempt;
use mod_capquiz\capquiz_user;
use mod_capquiz\local\helpers\questions;
use mod_capquiz\local\helpers\stars;
use mod_capquiz\question\bank\question_bank_view;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/capquiz/adminlib.php');
/**
* Create a new CAPQuiz instance.
*
* @used-by add_moduleinfo()
* @param stdClass $moduleinfo The data submitted from the form
*/
function capquiz_add_instance(stdClass $moduleinfo): int {
$moduleinfo->timecreated = \core\di::get(\core\clock::class)->time();
$moduleinfo->timemodified = $moduleinfo->timecreated;
$capquiz = new capquiz(record: $moduleinfo);
$capquiz->create();
return $capquiz->get('id');
}
/**
* Update an existing CAPQuiz instance.
*
* @used-by update_moduleinfo()
* @param stdClass $moduleinfo
*/
function capquiz_update_instance(stdClass $moduleinfo): bool {
$moduleinfo->id = (int)$moduleinfo->instance;
$capquiz = new capquiz(record: $moduleinfo);
return $capquiz->update();
}
/**
* Delete a CAPQuiz instance.
*
* @author Sebastian Gundersen <sebastian@sgundersen.com>
* @author Sumaiya Javed <sumaiya.javed@catalyst.net.nz>
* @used-by course_delete_module()
* @param int $capquizid CAPQuiz ID
* @return bool
*/
function capquiz_delete_instance(int $capquizid): bool {
$capquiz = new capquiz($capquizid);
return $capquiz->delete();
}
/**
* Standard callback used by questions_in_use.
*
* @used-by questions_in_use()
* @see quiz_questions_in_use()
* @param array $questionids of question ids.
* @return bool whether any of these questions are used by any instance of this module.
*/
function capquiz_questions_in_use(array $questionids): bool {
$qubaidjoin = new qubaid_join(
from: '{' . capquiz_user::TABLE . '} cu',
usageidcolumn: 'cu.questionusageid',
);
return question_engine::questions_in_use($questionids, $qubaidjoin);
}
/**
* Implementation of the reset course functionality, delete all the assignment submissions for course $data->courseid.
*
* @param stdClass $data
* @return array containing the statusreport from execution
*/
function capquiz_reset_userdata(stdClass $data): array {
global $DB;
$status = [];
capquiz_reset_gradebook($data->courseid);
$status[] = [
'component' => get_string('modulenameplural', 'capquiz'),
'item' => get_string('deleted_grades', 'capquiz'),
'error' => false,
];
foreach (capquiz::get_records(['course' => $data->courseid]) as $capquiz) {
foreach (capquiz_user::get_records(['capquizid' => $capquiz->get('id')]) as $user) {
question_engine::delete_questions_usage_by_activity($user->get('questionusageid'));
}
$DB->delete_records(capquiz_attempt::TABLE, ['capquizid' => $capquiz->get('id')]);
$DB->delete_records(capquiz_user::TABLE, ['capquizid' => $capquiz->get('id')]);
}
$status[] = [
'component' => get_string('modulenameplural', 'capquiz'),
'item' => get_string('deleted_attempts', 'capquiz'),
'error' => false,
];
return $status;
}
/**
* Generates and returns list of available CAPQuiz report sub-plugins
*
* @return array list of valid reports present
*/
function capquiz_report_list(): array {
static $reportlist;
if (!empty($reportlist)) {
return $reportlist;
}
$reportlist = [];
$pluginmanager = new capquiz_plugin_manager('capquizreport');
$enabledplugins = core_plugin_manager::instance()->get_enabled_plugins('capquizreport');
foreach ($pluginmanager->get_sorted_plugins_list() as $reportname) {
if (isset($enabledplugins[$reportname])) {
$reportlist[] = $reportname;
}
}
return $reportlist;
}
/**
* This function extends the settings navigation block for the site.
*
* @param settings_navigation $settings
* @param navigation_node $capquiznode
*/
function capquiz_extend_settings_navigation(settings_navigation $settings, navigation_node $capquiznode): void {
global $PAGE, $CFG;
$cm = $settings->get_page()->cm;
if (!has_capability('mod/capquiz:instructor', $cm->context)) {
return;
}
$capquiznode->add_node(navigation_node::create(
text: get_string('questions', 'capquiz'),
action: new \core\url('/mod/capquiz/edit.php', ['id' => $cm->id]),
type: navigation_node::TYPE_SETTING,
key: 'capquiz_edit',
));
$reportsnode = $capquiznode->add_node(navigation_node::create(
text: get_string('results', 'quiz'),
action: new \core\url('/mod/capquiz/report.php', ['id' => $cm->id]),
type: navigation_node::TYPE_SETTING,
key: 'capquiz_viewreports',
));
// We could use showchildreninsubmenu = true to show report types in a submenu,
// but this seems to mess with the styling when a tab in the show more submenu is active.
// Maybe this changes in a future version of Moodle?
foreach (capquiz_report_list() as $reporttype) {
$reportsnode->add_node(navigation_node::create(
text: get_string('pluginname', "capquizreport_$reporttype"),
action: new \core\url('/mod/capquiz/report.php', ['id' => $cm->id, 'reporttype' => $reporttype]),
type: navigation_node::TYPE_SETTING,
key: "capquiz_viewreport_$reporttype",
icon: new pix_icon('i/report', ''),
));
}
require_once($CFG->libdir . '/questionlib.php');
question_extend_settings_navigation($capquiznode, $PAGE->cm->context)->trim_if_empty();
}
/**
* Return grade for a given user, or all users.
* TODO: This function seems to have been implemented incorrectly for a long time.
* Need to fix dategraded/datesubmitted. Raw grade has been fixed from 'higheststars' to 'starsgraded'.
*
* @param stdClass $capquiz database record
* @param int $userid int or 0 for all users
* @see quiz_get_user_grades
*/
function capquiz_get_user_grades(stdClass $capquiz, int $userid = 0): array {
$params = ['capquizid' => $capquiz->id];
if ($userid > 0) {
$params['userid'] = $userid;
}
$grades = [];
foreach (capquiz_user::get_records($params) as $user) {
$grades[$user->get('userid')] = (object) [
'userid' => $user->get('userid'),
'rawgrade' => $user->get('starsgraded'),
'dategraded' => \core\di::get(\core\clock::class)->time(),
'datesubmitted' => $user->get('timemodified'),
];
}
return $grades;
}
/**
* Update grades in gradebook.
*
* @param stdClass $capquiz database record
* @param int $userid specific user only, 0 means all
* @param bool $nullifnone
* @see quiz_update_grades
*/
function capquiz_update_grades(stdClass $capquiz, int $userid = 0, $nullifnone = true): void {
global $CFG;
require_once($CFG->libdir . '/gradelib.php');
$grades = capquiz_get_user_grades($capquiz, $userid);
if ($grades) {
capquiz_grade_item_update($capquiz, $grades);
} else if ($userid && $nullifnone) {
$grade = new stdClass();
$grade->userid = $userid;
$grade->rawgrade = null;
capquiz_grade_item_update($capquiz, $grade);
} else {
capquiz_grade_item_update($capquiz);
}
}
/**
* Create or update the grade item for a given CAPQuiz.
*
* @param stdClass $capquiz record with extra cmidnumber
* @param array|string|null $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
* @return int 0 if ok, error code otherwise
* @see quiz_grade_item_update
*/
function capquiz_grade_item_update(stdClass $capquiz, $grades = null): int {
global $CFG;
require_once($CFG->libdir . '/gradelib.php');
if (!isset($capquiz->cmidnumber)) {
$capquiz->cmidnumber = get_coursemodule_from_instance('capquiz', $capquiz->id, $capquiz->course)->id;
}
$itemdetails = [
'itemname' => $capquiz->name,
'idnumber' => $capquiz->cmidnumber,
'gradetype' => GRADE_TYPE_VALUE,
'grademax' => stars::get_max_stars($capquiz->starratings),
'grademin' => 0,
'gradepass' => $capquiz->starstopass,
];
if ($grades === 'reset') {
$itemdetails['reset'] = true;
$grades = null;
}
return grade_update('mod/capquiz', $capquiz->course, 'mod', 'capquiz', $capquiz->id, 0, $grades, $itemdetails);
}
/**
* Remove all grades from gradebook.
*
* @param int $courseid id of the course to be reset
* @param string $type Optional type of assignment to limit the reset to a particular assignment type
* @see quiz_reset_gradebook
*/
function capquiz_reset_gradebook($courseid, $type = ''): void {
foreach (capquiz::get_records(['course' => $courseid]) as $capquiz) {
capquiz_grade_item_update($capquiz->to_record(), 'reset');
}
}
/**
* Generates the question bank in a fragment output. This allows
* the question bank to be displayed in a modal.
*
* The only expected argument provided in the $args array is
* 'querystring'. The value should be the list of parameters
* URL encoded and used to build the question bank page.
*
* The individual list of parameters expected can be found in
* question_build_edit_resources.
*
* @see mod_quiz_output_fragment_quiz_question_bank()
* @param array $args The fragment arguments.
* @return string The rendered mform fragment.
*/
function capquiz_output_fragment_capquiz_qbank(array $args): string {
global $PAGE;
require_capability('mod/capquiz:instructor', $PAGE->context);
$querystring = parse_url($args['querystring'], PHP_URL_QUERY);
$params = [];
parse_str($querystring, $params);
$params['cmid'] = clean_param($args['bankcmid'], PARAM_INT);
$params['bankcmid'] = clean_param($args['bankcmid'], PARAM_INT);
$params['quizcmid'] = clean_param($args['quizcmid'], PARAM_INT);
$extraparams = [
'quizcmid' => clean_param($args['quizcmid'], PARAM_INT),
];
[
$url,
$contexts,
$cmid,
$cm,
$module,
$pagevars,
] = question_build_edit_resources('editq', '/mod/capquiz/edit.php', array_merge($params, $extraparams));
if (!has_capability('moodle/question:useall', $contexts->lowest())) {
require_capability('moodle/question:usemine', $contexts->lowest());
}
$extraparams['cmid'] = $cmid;
$course = get_course($cm->course);
ob_start();
$qbank = new question_bank_view($contexts, $url, $course, $cm, $pagevars, $extraparams);
$qbank->display();
$qbankhtml = ob_get_clean();
return html_writer::div(html_writer::div($qbankhtml, 'bd'), 'questionbankformforpopup');
}
/**
* Build and return the output for the question bank and category chooser.
*
* @see mod_quiz_output_fragment_switch_question_bank()
* @param array $args provided by the AJAX request.
* @return string html to render to the modal.
*/
function capquiz_output_fragment_switch_question_bank(array $args): string {
global $USER, $COURSE, $OUTPUT;
$quizcmid = clean_param($args['quizcmid'], PARAM_INT);
$switchbankwidget = new \core_question\output\switch_question_bank($quizcmid, $COURSE->id, $USER->id);
return $OUTPUT->render($switchbankwidget);
}
/**
* Question data fragment to get the question html via ajax call.
*
* @see mod_quiz_output_fragment_question_data()
* @param array $args
* @return string
*/
function capquiz_output_fragment_question_data(array $args): string {
if (empty($args)) {
return '';
}
// Retrieve params from query string.
[$params, $extraparams] = filter_condition_manager::extract_parameters_from_fragment_args($args);
// Build required parameters.
$cmid = clean_param($args['cmid'], PARAM_INT);
$thispageurl = new \core\url('/mod/capquiz/edit.php', ['cmid' => $cmid]);
$thiscontext = \core\context\module::instance($cmid);
$contexts = new question_edit_contexts($thiscontext);
$defaultcategory = question_get_default_category($contexts->lowest()->id, true);
$params['cat'] = implode(',', [$defaultcategory->id, $defaultcategory->contextid]);
$course = get_course($params['courseid']);
// The viewing bank mod id.
[, $cm] = get_module_from_cmid(clean_param($args['cmid'], PARAM_INT));
$params['tabname'] = 'questions';
$extraparams['quizcmid'] = clean_param($args['quizcmid'], PARAM_INT);
// Custom question bank view.
$viewclass = clean_param($args['view'], PARAM_NOTAGS);
$questionbank = new $viewclass($contexts, $thispageurl, $course, $cm, $params, $extraparams);
// Question table.
$questionbank->add_standard_search_conditions();
ob_start();
$questionbank->display_question_list();
return ob_get_clean();
}
/**
* Serve question files.
*
* @param stdClass $course
* @param stdClass $context
* @param string $component
* @param string $filearea
* @param int $questionusageid
* @param int $slot
* @param array $args
* @param bool $forcedownload
* @param array $options
* @see quiz_question_pluginfile
*/
function capquiz_question_pluginfile(
stdClass $course,
stdClass $context,
string $component,
string $filearea,
int $questionusageid,
int $slot,
array $args,
bool $forcedownload,
array $options = [],
): void {
$user = capquiz_user::get_record(['questionusageid' => $questionusageid], MUST_EXIST);
$cm = get_coursemodule_from_instance('capquiz', $user->get('capquizid'), $course->id, false, MUST_EXIST);
require_login($course, false, $cm);
$quba = question_engine::load_questions_usage_by_activity($questionusageid);
$displayoptions = questions::get_question_display_options(new capquiz($user->get('capquizid')));
if (!$quba->check_file_access($slot, $displayoptions, $component, $filearea, $args, $forcedownload)) {
send_file_not_found();
}
$fs = get_file_storage();
$relativepath = implode('/', $args);
$fullpath = "/$context->id/$component/$filearea/$relativepath";
$file = $fs->get_file_by_hash(sha1($fullpath));
if (!$file || $file->is_directory()) {
send_file_not_found();
}
send_stored_file($file, 0, 0, $forcedownload, $options);
}
/**
* Checks if $feature is supported.
*
* @param string $feature
*/
function capquiz_supports(string $feature): bool|string|null {
return match ($feature) {
FEATURE_MOD_INTRO,
FEATURE_BACKUP_MOODLE2,
FEATURE_SHOW_DESCRIPTION,
FEATURE_USES_QUESTIONS,
FEATURE_GRADE_HAS_GRADE => true,
FEATURE_MOD_PURPOSE => MOD_PURPOSE_ASSESSMENT,
default => null,
};
}