Skip to content

Commit 9e6c3e0

Browse files
committed
Addition of management.php script for top-level view of available support scripts. Various other tweaks.
1 parent 8e6f6b0 commit 9e6c3e0

File tree

7 files changed

+336
-264
lines changed

7 files changed

+336
-264
lines changed

changehistory.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# CHANGE HISTORY
22

3+
### 4 November 2025, 5.8.1
4+
5+
* New scripts for question bank clean up (deletion of old unused versions) and integrity checking.
6+
* Addition of a top-level management.php script that links to other CodeRunner management scripts.
7+
* Fix bug in CodeRunner upgrade lib that resulted in the built-in prototypes being invisibly orphaned
8+
rather than properly deleted prior to installing the latest versions.
9+
310
### 28 October 2025, 5.8.0
411

512
* New question browser script moodlehome/question/type/coderunner/questionbrowserindex.php allows easy browsing

db/upgradelib.php

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -120,17 +120,17 @@ function update_question_types_internal() {
120120
*/
121121
function update_question_types_with_qbank() {
122122
global $DB;
123-
mtrace("CodeRunner prototypes set up using qbank");
123+
mtrace("CodeRunner prototypes set up using qbank.");
124124
$topcategory = moodle5_top_category();
125125
$topcategorycontextid = $topcategory->contextid;
126126
$oldprototypecategory = get_old_prototype_category();
127127
if ($oldprototypecategory) {
128-
mtrace("CodeRunner is moving old CR_PROTOTYPES category to new qbank category");
128+
mtrace("CodeRunner is moving old CR_PROTOTYPES category to new qbank category.");
129129
$sourcecategoryid = $oldprototypecategory->id;
130130
question_move_category_to_context($sourcecategoryid, $sourcecategoryid, $topcategorycontextid);
131131
$DB->set_field('question_categories', 'parent', $topcategory->id, ['parent' => $sourcecategoryid]);
132132
}
133-
mtrace("CodeRunner: replacing existing prototypes with new ones");
133+
mtrace("CodeRunner: replacing existing prototypes with new ones.");
134134
delete_existing_prototypes($topcategorycontextid);
135135
$prototypescategory = find_or_make_prototype_category($topcategorycontextid, $topcategory->id);
136136
load_new_prototypes($topcategorycontextid, $prototypescategory);
@@ -142,10 +142,10 @@ function update_question_types_with_qbank() {
142142
* @return bool true if successful
143143
*/
144144
function update_question_types_legacy() {
145-
mtrace("CodeRunner prototypes set up in system context");
145+
mtrace("CodeRunner prototypes set up in system context.");
146146
$systemcontext = context_system::instance();
147147
$systemcontextid = $systemcontext->id;
148-
mtrace("CodeRunner: replacing existing prototypes with new ones");
148+
mtrace("CodeRunner: replacing existing prototypes with new ones.");
149149
delete_existing_prototypes($systemcontextid);
150150
if (function_exists('question_get_top_category')) { // Moodle version >= 3.5.
151151
$parentid = get_top_id($systemcontextid);
@@ -185,6 +185,55 @@ function get_old_prototype_category() {
185185
}
186186
}
187187

188+
/**
189+
* Get the existing system question bank instance for Moodle 4.6+.
190+
* This is a helper function to avoid duplicating the qbank lookup logic.
191+
*
192+
* @return object|null The question bank module instance, or null if not found/applicable
193+
*/
194+
function get_system_question_bank() {
195+
if (!qtype_coderunner_util::using_mod_qbank()) {
196+
return null;
197+
}
198+
try {
199+
$course = get_site();
200+
return question_bank_helper::get_default_open_instance_system_type($course);
201+
} catch (Exception $e) {
202+
return null;
203+
}
204+
}
205+
206+
/**
207+
* Get the context ID where CodeRunner prototypes are stored.
208+
* For Moodle 4.6+: Returns the front-page question bank context ID (if it exists)
209+
* For Moodle <4.6: Returns the system context ID (if CR_PROTOTYPES category exists)
210+
*
211+
* @return int|null The context ID, or null if prototypes context doesn't exist yet
212+
*/
213+
function get_prototype_contextid() {
214+
global $DB;
215+
216+
if (qtype_coderunner_util::using_mod_qbank()) {
217+
// Moodle 4.6+ - prototypes are in front-page question bank.
218+
$qbank = get_system_question_bank();
219+
if ($qbank && $qbank->context) {
220+
return $qbank->context->id;
221+
}
222+
} else {
223+
// Moodle <4.6 - prototypes are in system context.
224+
$systemcontext = context_system::instance();
225+
// Verify CR_PROTOTYPES category exists in system context.
226+
$prototypecat = $DB->get_record('question_categories', [
227+
'contextid' => $systemcontext->id,
228+
'name' => 'CR_PROTOTYPES',
229+
]);
230+
if ($prototypecat) {
231+
return $systemcontext->id;
232+
}
233+
}
234+
return null;
235+
}
236+
188237
/**
189238
* If we're on Moodle 4.6 or later we can't use the
190239
* old system of storing prototypes in a category in the system context. Instead
@@ -198,9 +247,9 @@ function moodle5_top_category() {
198247
$course = get_site();
199248
$bankname = get_string('systembank', 'question');
200249
try {
201-
$newmod = question_bank_helper::get_default_open_instance_system_type($course);
250+
$newmod = get_system_question_bank();
202251
if (!$newmod) {
203-
mtrace('CodeRunner: creating new system question bank');
252+
mtrace('CodeRunner: creating new system question bank.');
204253
$newmod = question_bank_helper::create_default_open_instance($course, $bankname, question_bank_helper::TYPE_SYSTEM);
205254
}
206255
} catch (Exception $e) {
@@ -233,8 +282,10 @@ function delete_existing_prototypes($contextid) {
233282
}
234283

235284
// Clean up any orphaned version records from previous upgrades that used
236-
// the old direct deletion method. Only clean up records that were originally
237-
// BUILTIN_PROTOTYPE questions by checking the naming pattern.
285+
// the old direct deletion method. These orphaned versions are guaranteed to be
286+
// from BUILTIN prototypes because the old buggy code only deleted questions with
287+
// names matching 'BUILTIN_PROTOTYPE_%', so user-defined prototypes were never
288+
// deleted and thus have no orphaned version records.
238289
$orphanedversions = $DB->get_records_sql(
239290
"SELECT DISTINCT qv.id
240291
FROM {question_versions} qv
@@ -243,18 +294,12 @@ function delete_existing_prototypes($contextid) {
243294
LEFT JOIN {question} q ON q.id = qv.questionid
244295
WHERE qc.contextid = ?
245296
AND qc.name = 'CR_PROTOTYPES'
246-
AND q.id IS NULL
247-
AND EXISTS (
248-
SELECT 1 FROM {question_versions} qv2
249-
JOIN {question} q2 ON q2.id = qv2.questionid
250-
WHERE qv2.questionbankentryid = qbe.id
251-
AND q2.name LIKE 'BUILT%IN_PROTOTYPE_%'
252-
)",
297+
AND q.id IS NULL",
253298
[$contextid]
254299
);
255300

256301
if (!empty($orphanedversions)) {
257-
mtrace(" Cleaning up " . count($orphanedversions) . " orphaned prototype version records");
302+
mtrace(" Cleaning up " . count($orphanedversions) . " orphaned prototype version records.");
258303
foreach ($orphanedversions as $versionrecord) {
259304
$DB->delete_records('question_versions', ['id' => $versionrecord->id]);
260305
}
@@ -274,7 +319,7 @@ function delete_existing_prototypes($contextid) {
274319
);
275320

276321
if (!empty($orphanedentries)) {
277-
mtrace(" Cleaning up " . count($orphanedentries) . " orphaned prototype bank entries");
322+
mtrace(" Cleaning up " . count($orphanedentries) . " orphaned prototype bank entries.");
278323
foreach ($orphanedentries as $entryrecord) {
279324
$DB->delete_records('question_bank_entries', ['id' => $entryrecord->id]);
280325
}

deleteoldquestionversionsindex.php

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
require_once(__DIR__ . '/../../../config.php');
3434
require_once($CFG->libdir . '/questionlib.php');
3535
require_once(__DIR__ . '/classes/bulk_tester.php');
36+
require_once(__DIR__ . '/db/upgradelib.php');
3637

3738
// We are Moodle 4 or less if don't have mod_qbank.
3839
$oldskool = !(qtype_coderunner_util::using_mod_qbank());
@@ -168,7 +169,6 @@ function display_context_with_buttons($contextid, $name, $numcoderunnerquestions
168169
// Display.
169170
echo $OUTPUT->header();
170171

171-
echo html_writer::tag('h3', 'Delete Old Question Versions');
172172
echo html_writer::tag(
173173
'p',
174174
'This tool deletes all old versions of questions, keeping only the most recent version of each question. ' .
@@ -187,26 +187,37 @@ function display_context_with_buttons($contextid, $name, $numcoderunnerquestions
187187
echo 'Use the integrity checker links below (next to each context) to find and fix any issues.';
188188
echo html_writer::end_tag('div');
189189

190-
echo html_writer::start_tag('div', ['class' => 'alert alert-warning']);
191-
echo html_writer::tag('strong', '⚠ Important: ');
192-
$verifyurl = new moodle_url('/question/type/coderunner/verifyprototypes.php');
193-
echo 'Before using the integrity checker, ';
194-
echo html_writer::link($verifyurl, 'verify your CodeRunner prototypes', ['style' => 'font-weight: bold;']);
195-
echo ' to ensure they were not affected by any interrupted operations.';
196-
echo html_writer::end_tag('div');
197-
198190
echo html_writer::tag(
199191
'p',
200192
'Select a context below. Use <strong>Dry Run</strong> to preview what would be deleted, ' .
201193
'or <strong>Delete Old Versions</strong> to perform the actual deletion.'
202194
);
203195

196+
// Check if system prototypes context exists and user has permission.
197+
$prototypecontextid = get_prototype_contextid();
198+
$showprototypes = false;
199+
if ($prototypecontextid && has_capability('moodle/question:editall', \context::instance_by_id($prototypecontextid))) {
200+
$showprototypes = true;
201+
}
202+
203+
// Display system prototypes context first if it exists.
204+
if ($showprototypes) {
205+
echo html_writer::tag('h4', 'System Prototypes');
206+
echo html_writer::start_tag('ul');
207+
$prototypelabel = $oldskool
208+
? 'System Context - CR_PROTOTYPES (Built-in CodeRunner Prototypes)'
209+
: 'Front Page Question Bank - CR_PROTOTYPES (Built-in CodeRunner Prototypes)';
210+
display_context_with_buttons($prototypecontextid, $prototypelabel, 0);
211+
echo html_writer::end_tag('ul');
212+
echo html_writer::tag('br', '');
213+
}
214+
204215
// Find questions from contexts which the user can edit questions in.
205216
$availablequestionsbycontext = bulk_tester::get_num_available_coderunner_questions_by_context();
206217

207-
if (count($availablequestionsbycontext) == 0) {
218+
if (count($availablequestionsbycontext) == 0 && !$showprototypes) {
208219
echo html_writer::tag('p', 'You do not have permission to edit questions in any contexts.');
209-
} else {
220+
} else if (count($availablequestionsbycontext) > 0) {
210221
if ($oldskool) {
211222
// Moodle 4 style.
212223
echo html_writer::tag('h4', 'Available Contexts (' . count($availablequestionsbycontext) . ')');
@@ -216,6 +227,7 @@ function display_context_with_buttons($contextid, $name, $numcoderunnerquestions
216227
);
217228
} else {
218229
// Deal with funky question bank madness in Moodle 5.0.
230+
echo html_writer::tag('h4', 'Course Contexts');
219231
echo html_writer::tag('p', "Moodle >= 5.0 detected. Listing by course then question bank.");
220232
qtype_coderunner_util::display_course_grouped_contexts(
221233
$availablequestionsbycontext,
@@ -238,6 +250,10 @@ function display_context_with_buttons($contextid, $name, $numcoderunnerquestions
238250
border-left-color: #004085;
239251
background-color: #cce5ff;
240252
}
253+
.deleteversions.context.normal {
254+
border-left-color: #856404;
255+
background-color: #fff3cd;
256+
}
241257
.deleteversions.context:hover {
242258
background-color: #ffeaa7;
243259
}

downloadquizattemptsanon.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@
1919
* differs only in that the user name and email are replaced by a single
2020
* hashed_email field.
2121
*
22+
* This is a wrapper script that sets the ANONYMISE flag and then includes
23+
* downloadquizattempts.php, which handles all Moodle initialization and login checks.
24+
*
2225
* @package qtype_coderunner
2326
* @copyright 2017 Richard Lobb, The University of Canterbury
2427
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2528
*/
26-
defined('MOODLE_INTERNAL') || die();
2729

30+
// @codingStandardsIgnoreStart
2831
define('ANONYMISE', 1);
2932
require(__DIR__ . '/downloadquizattempts.php');
33+
// @codingStandardsIgnoreEnd

0 commit comments

Comments
 (0)