Skip to content
Snippets Groups Projects
Commit 4aaecc9d authored by Chris Sangwin's avatar Chris Sangwin
Browse files

WIP for issue #896.

parent 35447867
No related branches found
No related tags found
No related merge requests found
...@@ -38,9 +38,10 @@ raise_memory_limit(MEMORY_HUGE); ...@@ -38,9 +38,10 @@ raise_memory_limit(MEMORY_HUGE);
// Get the parameters from the URL. // Get the parameters from the URL.
$contextid = required_param('contextid', PARAM_INT); $contextid = required_param('contextid', PARAM_INT);
$context = context::instance_by_id($contextid); $context = context::instance_by_id($contextid);
$categoryid = optional_param('categoryid', null, PARAM_INT);
$skippreviouspasses = optional_param('skippreviouspasses', false, PARAM_BOOL); $skippreviouspasses = optional_param('skippreviouspasses', false, PARAM_BOOL);
$urlparams = ['contextid' => $context->id]; $urlparams = ['contextid' => $context->id, 'categoryid' => $categoryid];
if ($skippreviouspasses) { if ($skippreviouspasses) {
$urlparams['skippreviouspasses'] = 1; $urlparams['skippreviouspasses'] = 1;
} }
...@@ -72,7 +73,7 @@ echo $OUTPUT->heading($title); ...@@ -72,7 +73,7 @@ echo $OUTPUT->heading($title);
// Run the tests. // Run the tests.
list($allpassed, $failing) = $bulktester->run_all_tests_for_context( list($allpassed, $failing) = $bulktester->run_all_tests_for_context(
$context, 'web', false, $skippreviouspasses); $context, $categoryid, 'web', false, $skippreviouspasses);
// Display the final summary. // Display the final summary.
$bulktester->print_overall_result($allpassed, $failing); $bulktester->print_overall_result($allpassed, $failing);
......
...@@ -33,6 +33,8 @@ require_once(__DIR__ . '/../locallib.php'); ...@@ -33,6 +33,8 @@ require_once(__DIR__ . '/../locallib.php');
require_once(__DIR__ . '/../stack/utils.class.php'); require_once(__DIR__ . '/../stack/utils.class.php');
require_once(__DIR__ . '/../stack/bulktester.class.php'); require_once(__DIR__ . '/../stack/bulktester.class.php');
// Increase memory limit: some users with very large numbers of questions/variants have needed this.
raise_memory_limit(MEMORY_HUGE);
// Get the parameters from the URL. This is an option to restart the process // Get the parameters from the URL. This is an option to restart the process
// in the middle. Useful if it crashes. // in the middle. Useful if it crashes.
...@@ -72,19 +74,19 @@ echo $OUTPUT->header(); ...@@ -72,19 +74,19 @@ echo $OUTPUT->header();
echo $OUTPUT->heading($title, 1); echo $OUTPUT->heading($title, 1);
// Run the tests. // Run the tests.
foreach ($bulktester->get_stack_questions_by_context() as $contextid => $numstackquestions) { foreach ($bulktester->get_num_stack_questions_by_context() as $contextid => $numstackquestions) {
if ($skipping && $contextid != $startfromcontextid) { if ($skipping && $contextid != $startfromcontextid) {
continue; continue;
} }
$skipping = false; $skipping = false;
$testcontext = context::instance_by_id($contextid);
echo $OUTPUT->heading(stack_string('bulktesttitle', $testcontext->get_context_name())); $testcontext = context::instance_by_id($contextid);
if (has_capability('moodle/question:editall', $testcontext)) {
echo $OUTPUT->heading(get_string('bulktesttitle', 'qtype_stack', $testcontext->get_context_name()));
echo html_writer::tag('p', html_writer::link( echo html_writer::tag('p', html_writer::link(
new moodle_url('/question/type/stack/bulktestall.php', $urlparams), new moodle_url('/question/type/stack/adminui/bulktestall.php', $urlparams),
stack_string('bulktestcontinuefromhere'))); stack_string('bulktestcontinuefromhere')));
list($passed, $failing) = $bulktester->run_all_tests_for_context($testcontext, null, 'web', false, $skippreviouspasses);
list($passed, $failing) = $bulktester->run_all_tests_for_context($testcontext, 'web', false, $skippreviouspasses);
$allpassed = $allpassed && $passed; $allpassed = $allpassed && $passed;
foreach ($failing as $key => $arrvals) { foreach ($failing as $key => $arrvals) {
// Guard clause here to future proof any new fields from the bulk tester. // Guard clause here to future proof any new fields from the bulk tester.
...@@ -94,6 +96,7 @@ foreach ($bulktester->get_stack_questions_by_context() as $contextid => $numstac ...@@ -94,6 +96,7 @@ foreach ($bulktester->get_stack_questions_by_context() as $contextid => $numstac
$allfailing[$key] = array_merge($allfailing[$key], $arrvals); $allfailing[$key] = array_merge($allfailing[$key], $arrvals);
} }
} }
}
// Display the final summary. // Display the final summary.
$bulktester->print_overall_result($allpassed, $allfailing); $bulktester->print_overall_result($allpassed, $allfailing);
......
...@@ -54,19 +54,67 @@ $bulktester = new stack_bulk_tester(); ...@@ -54,19 +54,67 @@ $bulktester = new stack_bulk_tester();
echo $OUTPUT->header(); echo $OUTPUT->header();
echo $OUTPUT->heading(stack_string('replacedollarsindex')); echo $OUTPUT->heading(stack_string('replacedollarsindex'));
// Find in which contexts the user can edit questions.
$questionsbycontext = $bulktester->get_num_stack_questions_by_context();
$availablequestionsbycontext = array();
foreach ($questionsbycontext as $contextid => $numquestions) {
$context = context::instance_by_id($contextid);
if (has_capability('moodle/question:editall', $context)) {
$name = $context->get_context_name(true, true);
if (strpos($name, 'Quiz:') === 0) { // Quiz-specific question category.
$course = $context->get_course_context(false);
if ($course === false) {
$name = 'UnknownCourse: ' . $name;
} else {
$name = $course->get_context_name(true, true) . ': ' . $name;
}
}
$availablequestionsbycontext[$name] = array(
'contextid' => $contextid,
'numquestions' => $numquestions);
}
}
ksort($availablequestionsbycontext);
// List all contexts available to the user.
if (count($availablequestionsbycontext) == 0) {
echo html_writer::tag('p', get_string('unauthorisedbulktest', 'qtype_stack'));
} else {
echo html_writer::start_tag('ul'); echo html_writer::start_tag('ul');
foreach ($bulktester->get_stack_questions_by_context() as $contextid => $numstackquestions) { foreach ($availablequestionsbycontext as $name => $info) {
echo html_writer::tag('li', html_writer::link( $contextid = $info['contextid'];
new moodle_url('/question/type/stack/adminui/bulktest.php', $numquestions = $info['numquestions'];
$urlparams + ['contextid' => $contextid]),
context::instance_by_id($contextid)->get_context_name(true, true) . ' (' . $numstackquestions . ')')); $testallurl = new moodle_url('/question/type/stack/adminui/bulktest.php', array('contextid' => $contextid));
$testalllink = html_writer::link($testallurl,
get_string('bulktestallincontext', 'qtype_stack'), array('title' => get_string('testalltitle', 'qtype_stack')));
$litext = $name . ' (' . $numquestions . ') ' . $testalllink;
echo html_writer::start_tag('details');
echo html_writer::tag('summary', $litext);
$categories = $bulktester->get_categories_for_context($contextid);
echo html_writer::start_tag('ul', array('class' => 'expandable'));
foreach ($categories as $cat) {
if ($cat->count > 0) {
$url = new moodle_url('/question/type/stack/adminui/bulktest.php',
array('contextid' => $contextid, 'categoryid' => $cat->id));
$linktext = $cat->name . ' (' . $cat->count . ')';
$link = html_writer::link($url, $linktext);
echo html_writer::tag('li', $link,
array('title' => get_string('testallincategory', 'qtype_stack')));
}
}
echo html_writer::end_tag('ul');
echo html_writer::end_tag('details');
} }
echo html_writer::end_tag('ul'); echo html_writer::end_tag('ul');
if (has_capability('moodle/site:config', context_system::instance())) { if (has_capability('moodle/site:config', context_system::instance())) {
echo html_writer::tag('p', html_writer::link( echo html_writer::tag('p', html_writer::link(
new moodle_url('/question/type/stack/adminui/bulktestall.php', $urlparams), new moodle_url('/question/type/stack/adminui/bulktestall.php'), get_string('bulktestrun', 'qtype_stack')));
stack_string('bulktestrun'))); }
} }
echo $OUTPUT->footer(); echo $OUTPUT->footer();
...@@ -105,9 +105,9 @@ foreach ($contexts as $contextid => $numstackquestions) { ...@@ -105,9 +105,9 @@ foreach ($contexts as $contextid => $numstackquestions) {
echo "\n\n# " . $contextid . ": " . stack_string('bulktesttitle', $testcontext->get_context_name()); echo "\n\n# " . $contextid . ": " . stack_string('bulktesttitle', $testcontext->get_context_name());
if ($partialcontext === $contextid) { if ($partialcontext === $contextid) {
list($passed, $failing) = $bulktester->run_all_tests_for_context($testcontext, 'cli', (int) $options['id']); list($passed, $failing) = $bulktester->run_all_tests_for_context($testcontext, null, 'cli', (int) $options['id']);
} else { } else {
list($passed, $failing) = $bulktester->run_all_tests_for_context($testcontext, 'cli', false); list($passed, $failing) = $bulktester->run_all_tests_for_context($testcontext, null, 'cli', false);
} }
$allpassed = $allpassed && $passed; $allpassed = $allpassed && $passed;
......
...@@ -464,6 +464,9 @@ $string['replacedollarstitle'] = 'Replace $s in question texts in {$a}'; ...@@ -464,6 +464,9 @@ $string['replacedollarstitle'] = 'Replace $s in question texts in {$a}';
$string['replacedollarserrors'] = 'The following questions generated errors.'; $string['replacedollarserrors'] = 'The following questions generated errors.';
// Strings used by the bulk run question tests script. // Strings used by the bulk run question tests script.
$string['expand'] = 'Expand';
$string['expandtitle'] = 'Show question categories';
$string['unauthorisedbulktest'] = 'You do not have suitable access to any STACK questions';
$string['bulktestcontinuefromhere'] = 'Run again or resume, starting from here'; $string['bulktestcontinuefromhere'] = 'Run again or resume, starting from here';
$string['bulktestindexintro'] = 'Clicking on any of the links will run all the question tests in all the STACK questions in that context'; $string['bulktestindexintro'] = 'Clicking on any of the links will run all the question tests in all the STACK questions in that context';
$string['bulktestindextitle'] = 'Run the question tests in bulk'; $string['bulktestindextitle'] = 'Run the question tests in bulk';
...@@ -472,6 +475,9 @@ $string['bulktestnogeneralfeedback'] = 'This question does not have any general ...@@ -472,6 +475,9 @@ $string['bulktestnogeneralfeedback'] = 'This question does not have any general
$string['bulktestnodeployedseeds'] = 'This question does have random variants, but has no deployed seeds.'; $string['bulktestnodeployedseeds'] = 'This question does have random variants, but has no deployed seeds.';
$string['bulktestrun'] = 'Run all the question tests for all the questions in the system (slow, admin only)'; $string['bulktestrun'] = 'Run all the question tests for all the questions in the system (slow, admin only)';
$string['bulktesttitle'] = 'Running all the question tests in {$a}'; $string['bulktesttitle'] = 'Running all the question tests in {$a}';
$string['bulktestallincontext'] = 'Test all';
$string['testalltitle'] = 'Test all questions in this context';
$string['testallincategory'] = 'Test all questions in this category';
$string['overallresult'] = 'Overall result'; $string['overallresult'] = 'Overall result';
$string['seedx'] = 'Seed {$a}'; $string['seedx'] = 'Seed {$a}';
$string['testpassesandfails'] = '{$a->passes} passes and {$a->fails} failures.'; $string['testpassesandfails'] = '{$a->passes} passes and {$a->fails} failures.';
......
...@@ -27,12 +27,30 @@ require_once(__DIR__ . '/../../../engine/bank.php'); ...@@ -27,12 +27,30 @@ require_once(__DIR__ . '/../../../engine/bank.php');
class stack_bulk_tester { class stack_bulk_tester {
/** /**
* Get all the contexts that contain at least one STACK question, with a * Get all the courses and their contexts from the database.
* count of the number of those questions.
* *
* @return array context id => number of STACK questions. * @return array of course objects with id, contextid and name (short),
* indexed by id
*/ */
public function get_stack_questions_by_context() { public function get_all_courses() {
global $DB;
return $DB->get_records_sql("
SELECT crs.id, ctx.id as contextid, crs.shortname as name
FROM {course} crs
JOIN {context} ctx ON ctx.instanceid = crs.id
WHERE ctx.contextlevel = 50
ORDER BY name");
}
/**
* Get all the contexts that contain at least one stack question, with a
* count of the number of those questions. Only the latest version of each
* question is counted.
*
* @return array context id => number of stack questions.
*/
public function get_num_stack_questions_by_context() {
global $DB; global $DB;
// Earlier than Moodle 4.0. // Earlier than Moodle 4.0.
...@@ -51,25 +69,28 @@ class stack_bulk_tester { ...@@ -51,25 +69,28 @@ class stack_bulk_tester {
SELECT ctx.id, COUNT(q.id) AS numstackquestions SELECT ctx.id, COUNT(q.id) AS numstackquestions
FROM {context} ctx FROM {context} ctx
JOIN {question_categories} qc ON qc.contextid = ctx.id JOIN {question_categories} qc ON qc.contextid = ctx.id
JOIN {question_bank_entries} qb ON qb.questioncategoryid = qc.id JOIN {question_bank_entries} qbe ON qbe.questioncategoryid = qc.id
JOIN {question_versions} qv ON qv.questionbankentryid = qb.id JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id
JOIN {question} q ON q.id = qv.questionid JOIN {question} q ON qv.questionid = q.id
WHERE q.qtype = 'stack' WHERE q.qtype = 'stack'
AND qv.version = (SELECT MAX(v.version) AND (qv.version = (SELECT MAX(v.version)
FROM {question_versions} v FROM {question_versions} v
JOIN {question_bank_entries} be JOIN {question_bank_entries} be ON be.id = v.questionbankentryid
ON be.id = v.questionbankentryid WHERE be.id = qbe.id)
WHERE be.id = qb.id) )
GROUP BY ctx.id, ctx.path GROUP BY ctx.id, ctx.path
ORDER BY ctx.path"); ORDER BY ctx.path
");
} }
/** /**
* Get all the STACK questions in a particular context. * Find all stack questions in a given category, returning only
* * the latest version of each question.
* @return array id of STACK questions. * @param type $categoryid the id of a question category of interest
* @return all stack question ids in any state and any version in the given
* category. Each row in the returned list of rows has an id, name and version number.
*/ */
public function get_stack_questions($categoryid) { public function stack_questions_in_category($categoryid) {
global $DB; global $DB;
// Earlier than Moodle 4.0. // Earlier than Moodle 4.0.
...@@ -96,6 +117,108 @@ class stack_bulk_tester { ...@@ -96,6 +117,108 @@ class stack_bulk_tester {
WHERE be.id = qbe.id)", $qcparams); WHERE be.id = qbe.id)", $qcparams);
} }
/**
* Get a list of all the categories within the supplied contextid that
* contain stack questions in any state and any version.
* @return an associative array mapping from category id to an object
* with name and count fields for all question categories in the given context
* that contain one or more stack questions.
* The 'count' field is the number of stack questions in the given
* category.
*/
public function get_categories_for_context($contextid) {
global $DB;
// Earlier than Moodle 4.0.
if (stack_determine_moodle_version() < 400) {
return $DB->get_records_sql("
SELECT qc.id, qc.parent, qc.name as name,
(SELECT count(1)
FROM {question} q
WHERE qc.id = q.category and q.qtype='stack') AS count
FROM {question_categories} qc
WHERE qc.contextid = :contextid
ORDER BY qc.name",
array('contextid' => $contextid));
}
return $DB->get_records_sql("
SELECT qc.id, qc.parent, qc.name as name,
(SELECT count(1)
FROM {question} q
JOIN {question_versions} qv ON qv.questionid = q.id
JOIN {question_bank_entries} qbe ON qv.questionbankentryid = qbe.id
WHERE qc.id = qbe.questioncategoryid and q.qtype='stack') AS count
FROM {question_categories} qc
WHERE qc.contextid = :contextid
ORDER BY qc.name",
array('contextid' => $contextid));
}
/**
* Get all the stack questions in the given context.
*
* @param courseid The id of the course of interest.
* @param includeprototypes true to include prototypes in the returned list.
* @return array qid => question
*/
public function get_all_stack_questions_in_context($contextid) {
global $DB;
return $DB->get_records_sql("
SELECT q.id, ctx.id as contextid, qc.id as category, qc.name as categoryname, q.*, opts.*
FROM {context} ctx
JOIN {question_categories} qc ON qc.contextid = ctx.id
JOIN {question_bank_entries} qbe ON qbe.questioncategoryid = qc.id
JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id
JOIN {question} q ON q.id = qv.questionid
WHERE (qv.version = (SELECT MAX(v.version)
FROM {question_versions} v
JOIN {question_bank_entries} be ON be.id = v.questionbankentryid
WHERE be.id = qbe.id and q.qtype='stack')
)
AND ctx.id = :contextid
ORDER BY name", array('contextid' => $contextid));
}
/**
* Get all the contexts that contain at least one STACK question, with a
* count of the number of those questions.
*
* @return array context id => number of STACK questions.
*/
public function get_stack_questions_by_context() {
global $DB;
// Earlier than Moodle 4.0.
if (stack_determine_moodle_version() < 400) {
return $DB->get_records_sql_menu("
SELECT ctx.id, COUNT(q.id) AS numstackquestions
FROM {context} ctx
JOIN {question_categories} qc ON qc.contextid = ctx.id
JOIN {question} q ON q.category = qc.id
WHERE q.qtype = 'stack'
GROUP BY ctx.id, ctx.path
ORDER BY ctx.path");
}
return $DB->get_records_sql_menu("
SELECT ctx.id, COUNT(q.id) AS numstackquestions
FROM {context} ctx
JOIN {question_categories} qc ON qc.contextid = ctx.id
JOIN {question_bank_entries} qb ON qb.questioncategoryid = qc.id
JOIN {question_versions} qv ON qv.questionbankentryid = qb.id
JOIN {question} q ON q.id = qv.questionid
WHERE q.qtype = 'stack'
AND qv.version = (SELECT MAX(v.version)
FROM {question_versions} v
JOIN {question_bank_entries} be
ON be.id = v.questionbankentryid
WHERE be.id = qb.id)
GROUP BY ctx.id, ctx.path
ORDER BY ctx.path");
}
/** /**
* Run all the question tests for all variants of all questions belonging to * Run all the question tests for all variants of all questions belonging to
* a given context. * a given context.
...@@ -110,58 +233,48 @@ class stack_bulk_tester { ...@@ -110,58 +233,48 @@ class stack_bulk_tester {
* bool true if all the tests passed, else false. * bool true if all the tests passed, else false.
* array of messages relating to the questions with failures. * array of messages relating to the questions with failures.
*/ */
public function run_all_tests_for_context(context $context, $outputmode = 'web', $qidstart = null, public function run_all_tests_for_context(context $context, $categoryid = null, $outputmode = 'web', $qidstart = null,
$skippreviouspasses = false) { $skippreviouspasses = false) {
global $DB, $OUTPUT; global $DB, $OUTPUT;
if (stack_determine_moodle_version() < 400) { // Load the necessary data.
$categories = question_category_options(array($context)); $categories = $this->get_categories_for_context($context->id);
} else {
$categories = qbank_managecategories\helper::question_category_options(array($context));
}
$categories = reset($categories);
$questiontestsurl = new moodle_url('/question/type/stack/questiontestrun.php'); $questiontestsurl = new moodle_url('/question/type/stack/questiontestrun.php');
if ($context->contextlevel == CONTEXT_COURSE) { if ($context->contextlevel == CONTEXT_COURSE) {
$questiontestsurl->param('courseid', $context->instanceid); $questiontestsurl->param('courseid', $context->instanceid);
} else if ($context->contextlevel == CONTEXT_MODULE) { } else if ($context->contextlevel == CONTEXT_MODULE) {
$questiontestsurl->param('cmid', $context->instanceid); $questiontestsurl->param('cmid', $context->instanceid);
} else {
$questiontestsurl->param('courseid', SITEID);
} }
$numpasses = 0;
$allpassed = true; $allpassed = true;
$failingtests = array(); $failingtests = array();
$missinganswers = array();
$notests = array(); $notests = array();
$nogeneralfeedback = array(); $nogeneralfeedback = array();
$nodeployedseeds = array(); $nodeployedseeds = array();
$failingupgrade = array(); $failingupgrade = array();
foreach ($categories as $currentcategoryid => $nameandcount) {
if ($categoryid !== null && $currentcategoryid != $categoryid) {
continue;
}
$questions = $this->stack_questions_in_category($currentcategoryid);
if (!$questions) {
continue;
}
$readytostart = true; $readytostart = true;
if ($qidstart) { if ($qidstart) {
$readytostart = false; $readytostart = false;
} }
foreach ($categories as $key => $category) {
$qdotoutput = 0; $qdotoutput = 0;
list($categoryid) = explode(',', $key);
if ($outputmode == 'web') { if ($outputmode == 'web') {
echo $OUTPUT->heading($category, 3); echo $OUTPUT->heading($nameandcount->name . ' (' . $nameandcount->count . ')', 3);
}
if ($skippreviouspasses) {
// I think this is not strictly right. It will miss questions where
// you have run tests for some deployed seeds where all tests passed,
// but not yet run tests for some failing seeds. However, this
// is good enough for my needs now.
$questionids = $DB->get_records_sql_menu("
SELECT q.id, q.name
FROM {question} q
LEFT JOIN {qtype_stack_qtest_results} res ON res.questionid = q.id
WHERE q.category = ? AND q.qtype = ?
GROUP BY q.id, q.name
HAVING SUM(res.result) < COUNT(res.result) OR SUM(res.result) IS NULL
", [$categoryid, 'stack']);
} else {
$questionids = $this->get_stack_questions($categoryid);
} }
$questionids = $this->stack_questions_in_category($currentcategoryid);
if (!$questionids) { if (!$questionids) {
continue; continue;
} }
...@@ -177,13 +290,6 @@ class stack_bulk_tester { ...@@ -177,13 +290,6 @@ class stack_bulk_tester {
continue; continue;
} }
if ($outputmode == 'web') {
echo html_writer::tag('p', stack_string('replacedollarscount', count($questionids)));
} else {
echo "\n\n## " . $category . "\n## ";
echo stack_string('replacedollarscount', count($questionids)) . ' ' . "\n";
}
foreach ($questionids as $questionid => $name) { foreach ($questionids as $questionid => $name) {
try { try {
$question = question_bank::load_question($questionid); $question = question_bank::load_question($questionid);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment