diff --git a/question/format.php b/question/format.php index 1a53ca1bbf9..fdbcc1f3a6b 100644 --- a/question/format.php +++ b/question/format.php @@ -307,7 +307,7 @@ class qformat_default { if ($this->catfromfile) { // find/create category object $catpath = $question->category; - $newcategory = $this->create_category_path( $catpath, '/'); + $newcategory = $this->create_category_path($catpath); if (!empty($newcategory)) { $this->category = $newcategory; } @@ -379,14 +379,12 @@ class qformat_default { * but if $getcontext is set then ignore the context and use selected category context. * * @param string catpath delimited category path - * @param string delimiter path delimiting character * @param int courseid course to search for categories * @return mixed category object or null if fails */ - function create_category_path($catpath, $delimiter='/') { + function create_category_path($catpath) { global $DB; - $catpath = clean_param($catpath, PARAM_PATH); - $catnames = explode($delimiter, $catpath); + $catnames = $this->split_category_path($catpath); $parent = 0; $category = null; @@ -396,14 +394,17 @@ class qformat_default { $contextid = $this->translator->string_to_context($matches[1]); array_shift($catnames); } else { - $contextid = FALSE; + $contextid = false; } - if ($this->contextfromfile && ($contextid !== FALSE)){ + + if ($this->contextfromfile && $contextid !== false) { $context = get_context_instance_by_id($contextid); require_capability('moodle/question:add', $context); } else { $context = get_context_instance_by_id($this->category->contextid); } + + // Now create any categories that need to be created. foreach ($catnames as $catname) { if ($category = $DB->get_record( 'question_categories', array('name' => $catname, 'contextid' => $context->id, 'parent' => $parent))) { $parent = $category->id; @@ -698,16 +699,16 @@ class qformat_default { if ($this->cattofile) { if ($question->category != $trackcategory) { $trackcategory = $question->category; - $categoryname = $this->get_category_path($trackcategory, '/', $this->contexttofile); + $categoryname = $this->get_category_path($trackcategory, $this->contexttofile); // create 'dummy' question for category export $dummyquestion = new object; $dummyquestion->qtype = 'category'; $dummyquestion->category = $categoryname; - $dummyquestion->name = "switch category to $categoryname"; + $dummyquestion->name = 'Switch category to ' . $categoryname; $dummyquestion->id = 0; $dummyquestion->questiontextformat = ''; - $expout .= $this->writequestion( $dummyquestion ) . "\n"; + $expout .= $this->writequestion($dummyquestion) . "\n"; } } @@ -746,34 +747,79 @@ class qformat_default { /** * get the category as a path (e.g., tom/dick/harry) * @param int id the id of the most nested catgory - * @param string delimiter the delimiter you want * @return string the path */ - function get_category_path($id, $delimiter='/', $includecontext = true) { + function get_category_path($id, $includecontext = true) { global $DB; - $path = ''; - if (!$firstcategory = $DB->get_record('question_categories',array('id' =>$id))) { + + if (!$category = $DB->get_record('question_categories',array('id' =>$id))) { print_error('cannotfindcategory', 'error', '', $id); } - $category = $firstcategory; $contextstring = $this->translator->context_to_string($category->contextid); + + $pathsections = array(); do { - $name = $category->name; + $pathsections[] = $category->name; $id = $category->parent; - if (!empty($path)) { - $path = "{$name}{$delimiter}{$path}"; - } - else { - $path = $name; - } - } while ($category = $DB->get_record( 'question_categories',array('id' =>$id ))); + } while ($category = $DB->get_record( 'question_categories', array('id' => $id ))); if ($includecontext){ - $path = '$'.$contextstring.'$'."{$delimiter}{$path}"; + $pathsections[] = '$' . $contextstring . '$'; } + + $path = $this->assemble_category_path(array_reverse($pathsections)); + return $path; } + /** + * Convert a list of category names, possibly preceeded by one of the + * context tokens like $course$, into a string representation of the + * category path. + * + * Names are separated by / delimiters. And /s in the name are replaced by //. + * + * To reverse the process and split the paths into names, use + * {@link split_category_path()}. + * + * @param array $names + * @return string + */ + protected function assemble_category_path($names) { + $escapednames = array(); + foreach ($names as $name) { + $escapedname = str_replace('/', '//', $name); + if (substr($escapedname, 0, 1) == '/') { + $escapedname = ' ' . $escapedname; + } + if (substr($escapedname, -1) == '/') { + $escapedname = $escapedname . ' '; + } + $escapednames[] = $escapedname; + } + return implode('/', $escapednames); + } + + /** + * Convert a string, as returned by {@link assemble_category_path()}, + * back into an array of category names. + * + * Each category name is cleaned by a call to clean_param(, PARAM_MULTILANG), + * which matches the cleaning in question/category_form.php. Not that this + * addslashes the names, ready for insertion into the database. + * + * @param string $path + * @return array of category names. + */ + protected function split_category_path($path) { + $rawnames = preg_split('~(?. + + +/** + * Unit tests for the question import and export system. + * + * @package core + * @subpackage questionbank + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->dirroot . '/question/format.php'); + + +/** + * Subclass to make it easier to test qformat_default. + * + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class testable_qformat extends qformat_default { + public function assemble_category_path($names) { + return parent::assemble_category_path($names); + } + + public function split_category_path($names) { + return parent::split_category_path($names); + } +} + + +/** + * Unit tests for the matching question definition class. + * + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qformat_default_test extends UnitTestCase { + public function test_assemble_category_path() { + $format = new testable_qformat(); + $pathsections = array( + '$course$', + "Tim's questions", + "Tricky things like / // and so on", + 'Category name ending in /', + '/ and one that starts with one', + 'Matematically Matematiskt (svenska)' + ); + $this->assertEqual('$course$/Tim\'s questions/Tricky things like // //// and so on/Category name ending in // / // and one that starts with one/Matematically Matematiskt (svenska)', + $format->assemble_category_path($pathsections)); + } + + public function test_split_category_path() { + $format = new testable_qformat(); + $path = '$course$/Tim\'s questions/Tricky things like // //// and so on/Category name ending in // / // and one that starts with one/Matematically Matematiskt (svenska)'; + $this->assertEqual(array( + '$course$', + "Tim's questions", + "Tricky things like / // and so on", + 'Category name ending in /', + '/ and one that starts with one', + 'Matematically Matematiskt (svenska)' + ), $format->split_category_path($path)); + } + + public function test_split_category_path_cleans() { + $format = new testable_qformat(); + $path = 'Nasty thing'; + $this->assertEqual(array('Nasty thing'), $format->split_category_path($path)); + } +}