Commit 5d548d3e authored by Tim Hunt's avatar Tim Hunt
Browse files

question bank MDL-24995 clean up a lot of deprecated code to do with moving questions around.

This should also fix MDL-25172, MDL-23021 and MDL-23073. In other words, moving questions between categories should now work.
parent 333fde76
......@@ -120,16 +120,6 @@ define('QUESTION_PREVIEW_POPUP_OPTIONS', 'scrollbars=yes&resizable=yes&width=700
define('QUESTION_ADAPTIVE', 1);
/**#@-*/
/**#@+
* Options used in forms that move files.
*/
define('QUESTION_FILENOTHINGSELECTED', 0);
define('QUESTION_FILEDONOTHING', 1);
define('QUESTION_FILECOPY', 2);
define('QUESTION_FILEMOVE', 3);
define('QUESTION_FILEMOVELINKSONLY', 4);
/**#@-*/
/**#@+
* Options for whether flags are shown/editable when rendering questions.
*/
......@@ -718,7 +708,7 @@ function question_delete_course_category($category, $newcategory, $feedback=true
// added to a course, and then that course is moved to another category (MDL-14802).
$questionids = $DB->get_records_menu('question', array('category'=>$category->id), '', 'id,1');
if (!empty($questionids)) {
if (!$rescueqcategory = question_save_from_deletion(implode(',', array_keys($questionids)),
if (!$rescueqcategory = question_save_from_deletion(array_keys($questionids),
get_parent_contextid($context), print_context_name($context), $rescueqcategory)) {
return false;
}
......@@ -854,32 +844,57 @@ function question_delete_activity($cm, $feedback=true) {
function question_move_questions_to_category($questionids, $newcategoryid) {
global $DB, $QTYPES;
$ids = explode(',', $questionids);
foreach ($ids as $questionid) {
$questionid = (int)$questionid;
$params = array();
$params[] = $questionid;
$sql = 'SELECT q.*, c.id AS contextid, c.contextlevel, c.instanceid, c.path, c.depth
FROM {question} q, {question_categories} qc, {context} c
WHERE q.category=qc.id AND q.id=? AND qc.contextid=c.id';
$question = $DB->get_record_sql($sql, $params);
$category = $DB->get_record('question_categories', array('id'=>$newcategoryid));
// process files
$QTYPES[$question->qtype]->move_files($question, $category);
$newcontextid = $DB->get_field('question_categories', 'contextid',
array('id' => $newcategoryid));
list($questionidcondition, $params) = $DB->get_in_or_equal($questionids);
$questions = $DB->get_records_sql("
SELECT q.id, q.qtype, qc.contextid
FROM {question} q
JOIN {question_categories} qc ON q.category = qc.id
WHERE q.id $questionidcondition", $params);
foreach ($questions as $question) {
if ($newcontextid != $question->contextid) {
$QTYPES[$question->qtype]->move_files($question->id,
$question->contextid, $newcontextid);
}
}
// Move the questions themselves.
$DB->set_field_select('question', 'category', $newcategoryid, "id IN ($questionids)");
$DB->set_field_select('question', 'category', $newcategoryid, "id $questionidcondition", $params);
// Move any subquestions belonging to them.
$DB->set_field_select('question', 'category', $newcategoryid, "parent IN ($questionids)");
$DB->set_field_select('question', 'category', $newcategoryid, "parent $questionidcondition", $params);
// TODO Deal with datasets.
return true;
}
/**
* This function helps move a question cateogry to a new context by moving all
* the files belonging to all the questions to the new context.
* Also moves subcategories.
* @param integer $categoryid the id of the category being moved.
* @param integer $oldcontextid the old context id.
* @param integer $newcontextid the new context id.
*/
function question_move_category_to_context($categoryid, $oldcontextid, $newcontextid) {
global $DB, $QTYPES;
$questionids = $DB->get_records_menu('question',
array('category' => $categoryid), '', 'id,qtype');
foreach ($questionids as $questionid => $qtype) {
$QTYPES[$qtype]->move_files($questionid, $oldcontextid, $newcontextid);
}
$subcatids = $DB->get_records_menu('question_categories',
array('parent' => $categoryid), '', 'id,1');
foreach ($subcatids as $subcatid => $notused) {
$DB->set_field('question_categories', 'contextid', $newcontextid, array('id' => $subcatid));
question_move_category_to_context($subcatid, $oldcontextid, $newcontextid);
}
}
/**
* Given a list of ids, load the basic information about a set of questions from the questions table.
* The $join and $extrafields arguments can be used together to pull in extra data.
......@@ -1850,37 +1865,11 @@ function print_question_icon($question, $return = false) {
}
/**
* Returns a html link to the question image if there is one
*
* @global object
* @global object
* @return string The html image tag or the empy string if there is no image.
* @param object $question The question object
*/
function get_question_image($question) {
global $CFG, $DB;
$img = '';
if (!$category = $DB->get_record('question_categories', array('id'=>$question->category))) {
print_error('invalidcategory');
}
$coursefilesdir = get_filesdir_from_context(get_context_instance_by_id($category->contextid));
if ($question->image) {
if (substr(strtolower($question->image), 0, 7) == 'http://') {
$img .= $question->image;
} else {
require_once($CFG->libdir .'/filelib.php');
$img = get_file_url("$coursefilesdir/{$question->image}");
}
}
return $img;
}
/**
* @global array
* @param $question
* @param $state
* @param $prefix
* @param $cmoptions
* @param $caption
*/
function question_print_comment_fields($question, $state, $prefix, $cmoptions, $caption = '') {
global $QTYPES;
......@@ -2826,117 +2815,6 @@ function question_require_capability_on($question, $cap){
return true;
}
/**
* @global object
*/
function question_file_links_base_url($courseid){
global $CFG;
$baseurl = preg_quote("$CFG->wwwroot/file.php", '!');
$baseurl .= '('.preg_quote('?file=', '!').')?';//may or may not
//be using slasharguments, accept either
$baseurl .= "/$courseid/";//course directory
return $baseurl;
}
/**
* Find all course / site files linked to in a piece of html.
* @global object
* @param string html the html to search
* @param int course search for files for courseid course or set to siteid for
* finding site files.
* @return array files with keys being files.
*/
function question_find_file_links_from_html($html, $courseid){
global $CFG;
$baseurl = question_file_links_base_url($courseid);
$searchfor = '!'.
'(<\s*(a|img)\s[^>]*(href|src)\s*=\s*")'.$baseurl.'([^"]*)"'.
'|'.
'(<\s*(a|img)\s[^>]*(href|src)\s*=\s*\')'.$baseurl.'([^\']*)\''.
'!i';
$matches = array();
$no = preg_match_all($searchfor, $html, $matches);
if ($no){
$rawurls = array_filter(array_merge($matches[5], $matches[10]));//array_filter removes empty elements
//remove any links that point somewhere they shouldn't
foreach (array_keys($rawurls) as $rawurlkey){
if (!$cleanedurl = question_url_check($rawurls[$rawurlkey])){
unset($rawurls[$rawurlkey]);
} else {
$rawurls[$rawurlkey] = $cleanedurl;
}
}
$urls = array_flip($rawurls);// array_flip removes duplicate files
// and when we merge arrays will continue to automatically remove duplicates
} else {
$urls = array();
}
return $urls;
}
/**
* Check that url doesn't point anywhere it shouldn't
*
* @global object
* @param $url string relative url within course files directory
* @return mixed boolean false if not OK or cleaned URL as string if OK
*/
function question_url_check($url){
global $CFG;
if ((substr(strtolower($url), 0, strlen($CFG->moddata)) == strtolower($CFG->moddata)) ||
(substr(strtolower($url), 0, 10) == 'backupdata')){
return false;
} else {
return clean_param($url, PARAM_PATH);
}
}
/**
* Find all course / site files linked to in a piece of html.
*
* @global object
* @param string html the html to search
* @param int course search for files for courseid course or set to siteid for
* finding site files.
* @return array files with keys being files.
*/
function question_replace_file_links_in_html($html, $fromcourseid, $tocourseid, $url, $destination, &$changed){
global $CFG;
require_once($CFG->libdir .'/filelib.php');
$tourl = get_file_url("$tocourseid/$destination");
$fromurl = question_file_links_base_url($fromcourseid).preg_quote($url, '!');
$searchfor = array('!(<\s*(a|img)\s[^>]*(href|src)\s*=\s*")'.$fromurl.'(")!i',
'!(<\s*(a|img)\s[^>]*(href|src)\s*=\s*\')'.$fromurl.'(\')!i');
$newhtml = preg_replace($searchfor, '\\1'.$tourl.'\\5', $html);
if ($newhtml != $html){
$changed = true;
}
return $newhtml;
}
/**
* @global object
*/
function get_filesdir_from_context($context){
global $DB;
switch ($context->contextlevel){
case CONTEXT_COURSE :
$courseid = $context->instanceid;
break;
case CONTEXT_MODULE :
$courseid = $DB->get_field('course_modules', 'course', array('id'=>$context->instanceid));
break;
case CONTEXT_COURSECAT :
case CONTEXT_SYSTEM :
$courseid = SITEID;
break;
default :
print_error('invalidcontext');
}
return $courseid;
}
/**
* Get the real state - the correct question id and answer - for a random
* question.
......@@ -2944,7 +2822,7 @@ function get_filesdir_from_context($context){
* @return mixed return integer real question id or false if there was an
* error..
*/
function question_get_real_state($state){
function question_get_real_state($state) {
global $OUTPUT;
$realstate = clone($state);
$matches = array();
......
......@@ -38,24 +38,6 @@ class question_category_list extends moodle_list {
public function get_records() {
$this->records = get_categories_for_contexts($this->context->id, $this->sortby);
}
public function process_actions($left, $right, $moveup, $movedown, $moveupcontext, $movedowncontext, $tocontext){
global $CFG;
//parent::procces_actions redirects after any action
parent::process_actions($left, $right, $moveup, $movedown);
if ($tocontext == $this->context->id){
//only called on toplevel list
if ($moveupcontext){
$cattomove = $moveupcontext;
$totop = 0;
} elseif ($movedowncontext){
$cattomove = $movedowncontext;
$totop = 1;
}
$toparent = "0,{$this->context->id}";
$url = new moodle_url('/question/contextmove.php?', $this->pageurl->params() + compact('cattomove', 'totop', 'toparent'));
redirect($url);
}
}
}
class question_category_list_item extends list_item {
......@@ -87,12 +69,13 @@ class question_category_list_item extends list_item {
/// Each section adds html to be displayed as part of this list item
$questionbankurl = new moodle_url("/question/edit.php", ($this->parentlist->pageurl->params() + array('category'=>"$category->id,$category->contextid")));
$catediturl = $this->parentlist->pageurl->out(true, array('edit'=>$this->id));
$catediturl = $this->parentlist->pageurl->out(true, array('edit' => $this->id));
$item = "<b><a title=\"{$str->edit}\" href=\"$catediturl\">".$category->name ."</a></b> <a title=\"$editqestions\" href=\"$questionbankurl\">".'('.$category->questioncount.')</a>';
$item .= '&nbsp;'. $category->info;
if (count($this->parentlist->records)!=1){ // don't allow delete if this is the last category in this context.
// don't allow delete if this is the last category in this context.
if (count($this->parentlist->records) != 1) {
$item .= '<a title="' . $str->delete . '" href="'.$this->parentlist->pageurl->out(true, array('delete'=>$this->id, 'sesskey'=>sesskey())).'">
<img src="' . $OUTPUT->pix_url('t/delete') . '" class="iconsmall" alt="' .$str->delete. '" /></a>';
}
......@@ -374,11 +357,9 @@ class question_category_object {
public function move_questions($oldcat, $newcat){
global $DB;
$questionids = $DB->get_records_select_menu('question', "category = ? AND (parent = 0 OR parent = id)", array($oldcat), '', 'id,1');
$ids = implode(',', array_keys($questionids));
if (!question_move_questions_to_category($ids, $newcat)) {
print_error('errormovingquestions', 'question', $this->pageurl->out(), $ids);
}
$questionids = $DB->get_records_select_menu('question',
'category = ? AND (parent = 0 OR parent = id)', array($oldcat), '', 'id,1');
question_move_questions_to_category($questionids, $newcat);
}
/**
......@@ -439,7 +420,7 @@ class question_category_object {
require_capability('moodle/question:managecategory', $fromcontext);
// If moving to another context, check permissions some more.
if ($oldcat->contextid != $tocontextid){
if ($oldcat->contextid != $tocontextid) {
$tocontext = get_context_instance_by_id($tocontextid);
require_capability('moodle/question:managecategory', $tocontext);
}
......@@ -450,7 +431,7 @@ class question_category_object {
$cat->name = $newname;
$cat->info = $newinfo;
$cat->parent = $parentid;
// We don't change $cat->contextid here, if necessary we redirect to contextmove.php later.
$cat->contextid = $tocontextid;
$DB->update_record('question_categories', $cat);
// If the category name has changed, rename any random questions in that category.
......@@ -464,41 +445,11 @@ class question_category_object {
$DB->set_field_select('question', 'name', $randomqname, $where, array($cat->id, '1'));
}
// Then redirect to an appropriate place.
if ($oldcat->contextid == $tocontextid) { // not moving contexts
redirect($this->pageurl);
} else {
$url = new moodle_url('/question/contextmove.php', ($this->pageurl->params() + array('cattomove' => $updateid, 'toparent'=>$newparent)));
redirect($url);
if ($oldcat->contextid != $tocontextid) {
// Moving to a new context. Must move files belonging to questions.
question_move_category_to_context($cat->id, $oldcat->contextid, $tocontextid);
}
}
public function move_question_from_cat_confirm($fromcat, $fromcourse, $tocat=null, $question=null){
global $QTYPES, $DB;
if (!$question){
$questions[] = $question;
} else {
$questions = $DB->get_records('question', array('category' => $tocat->id));
}
$urls = array();
foreach ($questions as $question){
$urls = array_merge($urls, $QTYPES[$question->qtype]->find_file_links_in_question($question));
}
if ($fromcourse){
$append = 'tocourse';
} else {
$append = 'tosite';
}
if ($tocat){
echo '<p>'.get_string('needtomovethesefilesincat','question').'</p>';
} else {
echo '<p>'.get_string('needtomovethesefilesinquestion','question').'</p>';
}
redirect($this->pageurl);
}
}
<?php
/**
* Allows someone with appropriate permissions to move a category and associated
* files to another context.
*
* @author Jamie Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package questionbank
*/
require_once("../config.php");
require_once($CFG->dirroot."/question/editlib.php");
require_once($CFG->dirroot."/question/contextmove_form.php");
//TODO: MDL-16094
throw new coding_exception('contextmove.php was not converted to new file api yet, sorry - see MDL-16094');
list($thispageurl, $contexts, $cmid, $cm, $module, $pagevars) =
question_edit_setup('categories', '/question/contextmove.php');
// get values from form for actions on this page
$toparent = required_param('toparent', PARAM_SEQUENCE);
$cattomove = required_param('cattomove', PARAM_INT);
$totop = optional_param('totop', 0, PARAM_INT); // optional param moves category to top of peers. Default is
//to add it to the bottom.
$onerrorurl = new moodle_url('/question/category.php', $thispageurl->params());
list($toparent, $contextto) = explode(',', $toparent);
if (!empty($toparent)){//not top level category, make it a child of $toparent
if (!$toparent = $DB->get_record('question_categories', array('id' => $toparent))){
print_error('invalidcategoryidforparent', 'question', $onerrorurl);
}
$contextto = $toparent->contextid;
} else {
$toparent = new stdClass();
$toparent->id = 0;
$toparent->contextid = $contextto;
}
if (!$cattomove = $DB->get_record('question_categories', array('id' => $cattomove))){
print_error('invalidcategoryidtomove', 'question', $onerrorurl);
}
if ($cattomove->contextid == $contextto){
print_error('contexterror', '', $onerrorurl);
}
$cattomove->categorylist = question_categorylist($cattomove->id);
$thispageurl->params(array('cattomove'=>$cattomove->id,
'toparent'=>"{$toparent->id},{$toparent->contextid}",
'totop'=>$totop));
$contextfrom = get_context_instance_by_id($cattomove->contextid);
$contextto = get_context_instance_by_id($contextto);
$contexttostring = print_context_name($contextto);
require_capability('moodle/question:managecategory', $contextfrom);
require_capability('moodle/question:managecategory', $contextto);
$fromcoursefilesid = get_filesdir_from_context($contextfrom);//siteid or courseid
$tocoursefilesid = get_filesdir_from_context($contextto);//siteid or courseid
if ($fromcoursefilesid != $tocoursefilesid){
list($usql, $params) = $DB->get_in_or_equal(explode(',', $cattomove->categorylist));
$questions = $DB->get_records_select('question', "category $usql", $params);
$urls = array();
if ($questions){
foreach ($questions as $id => $question){
$QTYPES[$questions[$id]->qtype]->get_question_options($questions[$id]);
$urls = array_merge_recursive($urls, $QTYPES[$questions[$id]->qtype]->find_file_links($questions[$id], $fromcoursefilesid));
}
}
ksort($urls);
} else {
$urls = array();
}
$brokenurls = array();
foreach (array_keys($urls) as $url){
if (!file_exists($CFG->dataroot."/$fromcoursefilesid/".$url)){
$brokenurls[] = $url;
}
}
if ($fromcoursefilesid == SITEID){
$fromareaname = get_string('filesareasite', 'question');
} else {
$fromareaname = get_string('filesareacourse', 'question');
}
if ($tocoursefilesid == SITEID){
$toareaname = get_string('filesareasite', 'question');
} else {
$toareaname = get_string('filesareacourse', 'question');
}
$contextmoveform = new question_context_move_form($thispageurl,
compact('urls', 'fromareaname', 'toareaname', 'brokenurls',
'fromcoursefilesid', 'tocoursefilesid'));
if ($contextmoveform->is_cancelled()){
$thispageurl->remove_params('cattomove', 'toparent', 'totop');
redirect(new moodle_url("/question/category.php".$thispageurl->params()));
}elseif ($moveformdata = $contextmoveform->get_data()) {
if (isset($moveformdata->urls) && is_array($moveformdata->urls)){
check_dir_exists($CFG->dataroot."/$tocoursefilesid/", true);
$flipurls = array_keys($urls);
foreach ($moveformdata->urls as $key => $urlaction){
$source = $CFG->dataroot."/$fromcoursefilesid/".$flipurls[$key];
$destination = $flipurls[$key];
if (($urlaction != QUESTION_FILEDONOTHING) && ($urlaction != QUESTION_FILEMOVELINKSONLY)){
// Ensure the target folder exists.
check_dir_exists(dirname($CFG->dataroot."/$tocoursefilesid/".$destination), true);
// Then make sure the destination file name does not exist. If it does, change the name to be unique.
while (file_exists($CFG->dataroot."/$tocoursefilesid/".$destination)){
$matches = array();
//check for '_'. copyno after filename, before extension.
if (preg_match('!\_([0-9]+)(\.[^\.\\/]+)?$!', $destination, $matches)){
$copyno = $matches[1]+1;
} else {
$copyno = 1;
}
//replace old copy no with incremented one.
$destination = preg_replace('!(\_[0-9]+)?(\.[^\.\\/]+)?$!', '_'.$copyno.'\\2', $destination, 1);
}
}
switch ($urlaction){
case QUESTION_FILECOPY :
if (!copy($source, $CFG->dataroot."/$tocoursefilesid/".$destination)){
print_error('errorfilecannotbecopied', 'question', $onerrorurl, $source);
}
break;
case QUESTION_FILEMOVE :
if (!rename($source, $CFG->dataroot."/$tocoursefilesid/".$destination)){
print_error('errorfilecannotbemoved', 'question', $onerrorurl, $source);
}
break;
case QUESTION_FILEDONOTHING :
case QUESTION_FILEMOVELINKSONLY :
break;
default :
print_error('invalidaction', '', $onerrorurl);
}
switch ($urlaction){
//now search and replace urls in questions.
case QUESTION_FILECOPY :
case QUESTION_FILEMOVE :
case QUESTION_FILEMOVELINKSONLY :
$url = $flipurls[$key];
$questionids = array_unique($urls[$url]);
foreach ($questionids as $questionid){
$question = $questions[$questionid];
$QTYPES[$question->qtype]->replace_file_links($question, $fromcoursefilesid, $tocoursefilesid, $url, $destination);
}
break;
case QUESTION_FILEDONOTHING :
default :
break;
}
}
}
//adjust sortorder before we make the cat a peer of it's new peers
$peers = $DB->get_records_select_menu('question_categories',
'contextid = ? AND parent = ?', array($toparent->contextid, $toparent->id),
'sortorder ASC', 'id, 1');
$peers = array_keys($peers);
if ($totop){
array_unshift($peers, $cattomove->id);
} else {
$peers[] = $cattomove->id;
}
$sortorder = 0;
foreach ($peers as $peer) {
$DB->set_field('question_categories', "sortorder", $sortorder, array("id" => $peer));
$sortorder++;
}
//now move category
$cat = new stdClass();
$cat->id = $cattomove->id;
$cat->parent = $toparent->id;
//set context of category we are moving and all children also!
list($usql, $params) = $DB->get_in_or_equal(explode(',', $cattomove->categorylist));
$params = array_merge(array($contextto->id), $params);
$DB->execute("UPDATE {question_categories} SET contextid = ? WHERE id $usql", $params);
//finally set the new parent id
$DB->update_record("question_categories", $cat);
$thispageurl->remove_params('cattomove', 'toparent', 'totop');
$url = new moodle_url('/question/category.php', ($thispageurl->params() + array('cat'=>"{$cattomove->id},{$contextto->id}")));
redirect($url);
}
$streditingcategories = get_string('editcategories', 'quiz');
$PAGE->set_url($thispageurl->out());
$PAGE->navbar->add($streditingcategories, $thispageurl->out());
$PAGE->navbar->add(get_string('movingcategory', 'question'));
$PAGE->set_heading($COURSE->fullname);
echo $OUTPUT->header();
//parameter for get_string
$cattomove->contextto = $contexttostring;
if (count($urls)){
$defaults = array();
for ($default_key = 0; $default_key < count($urls); $default_key++){
$defaults['urls'][$default_key] = QUESTION_FILECOPY;
}
$contextmoveform->set_data($defaults);
//some parameters for get_string
$cattomove->urlcount = count($urls);
$cattomove->toareaname = $toareaname;
$cattomove->fromareaname = $fromareaname;
echo $OUTPUT->box(get_string('movingcategoryandfiles', 'question', $cattomove), 'boxwidthnarrow boxaligncenter generalbox');
} else {
echo $OUTPUT->box(get_string('movingcategorynofiles', 'question', $cattomove), 'boxwidthnarrow boxaligncenter generalbox');
}
$contextmoveform->display();
echo $OUTPUT->footer();