Skip to content
This repository has been archived by the owner on Apr 8, 2022. It is now read-only.

Commit

Permalink
MDL-34684 tool_health: Added health check for course category structure
Browse files Browse the repository at this point in the history
And corresponding unit test. Work by Marko Vidberg.
  • Loading branch information
Logan Reynolds authored and David Monllao committed Dec 4, 2014
1 parent d87bcfb commit 8db188c
Show file tree
Hide file tree
Showing 3 changed files with 432 additions and 44 deletions.
158 changes: 114 additions & 44 deletions admin/tool/health/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -603,34 +603,10 @@ function find_problems() {
$categories = $DB->get_records('question_categories', array(), 'id');

// Look for missing parents.
$missingparent = array();
foreach ($categories as $category) {
if ($category->parent != 0 && !array_key_exists($category->parent, $categories)) {
$missingparent[$category->id] = $category;
}
}
$missingparent = health_category_find_missing_parents($categories);

// Look for loops.
$loops = array();
while (!empty($categories)) {
$current = array_pop($categories);
$thisloop = array($current->id => $current);
while (true) {
if (isset($thisloop[$current->parent])) {
// Loop detected
$loops[$current->id] = $thisloop;
break;
} else if (!isset($categories[$current->parent])) {
// Got to the top level, or a category we already know is OK.
break;
} else {
// Continue following the path.
$current = $categories[$current->parent];
$thisloop[$current->id] = $current;
unset($categories[$current->id]);
}
}
}
$loops = health_category_find_loops($categories);

$answer = array($missingparent, $loops);
}
Expand All @@ -651,28 +627,122 @@ function description() {
' structures by the question_categories.parent field. Sometimes ' .
' this tree structure gets messed up.</p>';

$description .= health_category_list_missing_parents($missingparent);
$description .= health_category_list_loops($loops);

return $description;
}

/**
* Outputs resolutions to problems outlined in MDL-34684 with items having themselves as parent
*
* @link https://tracker.moodle.org/browse/MDL-34684
* @return string Formatted html to be output to the browser with instructions and sql statements to run
*/
function solution() {
global $CFG;
list($missingparent, $loops) = $this->find_problems();

$solution = '<p>Consider executing the following SQL queries. These fix ' .
'the problem by moving some categories to the top level.</p>';

if (!empty($missingparent)) {
$description .= '<p>The following categories are missing their parents:</p><ul>';
foreach ($missingparent as $cat) {
$description .= "<li>Category $cat->id: " . s($cat->name) . "</li>\n";
}
$description .= "</ul>\n";
$solution .= "<pre>UPDATE " . $CFG->prefix . "question_categories\n" .
" SET parent = 0\n" .
" WHERE id IN (" . implode(',', array_keys($missingparent)) . ");</pre>\n";
}

if (!empty($loops)) {
$description .= '<p>The following categories form a loop of parents:</p><ul>';
foreach ($loops as $loop) {
$description .= "<li><ul>\n";
foreach ($loop as $cat) {
$description .= "<li>Category $cat->id: " . s($cat->name) . " has parent $cat->parent</li>\n";
}
$description .= "</ul></li>\n";
}
$description .= "</ul>\n";
$solution .= "<pre>UPDATE " . $CFG->prefix . "question_categories\n" .
" SET parent = 0\n" .
" WHERE id IN (" . implode(',', array_keys($loops)) . ");</pre>\n";
}

return $solution;
}
}

/**
* Check course categories tree structure for problems.
*/
class problem_000018 extends problem_base {
/**
* Generate title for this problem.
*
* @return string Title of problem.
*/
function title() {
return 'Course categories tree structure';
}

/**
* Search for problems in the course categories.
*
* @uses $DB
* @return array List of categories that contain missing parents or loops.
*/
function find_problems() {
global $DB;
static $answer = null;

if (is_null($answer)) {
$categories = $DB->get_records('course_categories', array(), 'id');

// Look for missing parents.
$missingparent = health_category_find_missing_parents($categories);

// Look for loops.
$loops = health_category_find_loops($categories);

$answer = array($missingparent, $loops);
}

return $answer;
}

/**
* Check if the problem exists.
*
* @return boolean True if either missing parents or loops found
*/
function exists() {
list($missingparent, $loops) = $this->find_problems();
return !empty($missingparent) || !empty($loops);
}

/**
* Set problem severity.
*
* @return constant Problem severity.
*/
function severity() {
return SEVERITY_ANNOYANCE;
}

/**
* Generate problem description.
*
* @return string HTML containing details of the problem.
*/
function description() {
list($missingparent, $loops) = $this->find_problems();

$description = '<p>The course categories should be arranged into tree ' .
' structures by the course_categories.parent field. Sometimes ' .
' this tree structure gets messed up.</p>';

$description .= health_category_list_missing_parents($missingparent);
$description .= health_category_list_loops($loops);

return $description;
}

/**
* Generate solution text.
*
* @uses $CFG
* @return string HTML containing the suggested solution.
*/
function solution() {
global $CFG;
list($missingparent, $loops) = $this->find_problems();
Expand All @@ -681,14 +751,14 @@ function solution() {
'the problem by moving some categories to the top level.</p>';

if (!empty($missingparent)) {
$solution .= "<pre>UPDATE " . $CFG->prefix . "question_categories\n" .
" SET parent = 0\n" .
$solution .= "<pre>UPDATE " . $CFG->prefix . "course_categories\n" .
" SET parent = 0, depth = 1, path = CONCAT('/', id)\n" .
" WHERE id IN (" . implode(',', array_keys($missingparent)) . ");</pre>\n";
}

if (!empty($loops)) {
$solution .= "<pre>UPDATE " . $CFG->prefix . "question_categories\n" .
" SET parent = 0\n" .
$solution .= "<pre>UPDATE " . $CFG->prefix . "course_categories\n" .
" SET parent = 0, depth = 1, path = CONCAT('/', id)\n" .
" WHERE id IN (" . implode(',', array_keys($loops)) . ");</pre>\n";
}

Expand Down
97 changes: 97 additions & 0 deletions lib/adminlib.php
Original file line number Diff line number Diff line change
Expand Up @@ -8920,3 +8920,100 @@ public function output_html($data, $query='') {
return $o;
}
}

/**
* Given a list of categories, this function searches for ones
* that have a missing parent category.
*
* @param array $categories List of categories.
* @return array List of categories with missing parents.
*/
function health_category_find_missing_parents($categories) {
$missingparent = array();

foreach ($categories as $category) {
if ($category->parent != 0 && !array_key_exists($category->parent, $categories)) {
$missingparent[$category->id] = $category;
}
}

return $missingparent;
}

/**
* Generates a list of categories with missing parents.
*
* @param array $missingparent List of categories with missing parents.
* @return string Bullet point list of categories with missing parents.
*/
function health_category_list_missing_parents($missingparent) {
$description = '';

if (!empty($missingparent)) {
$description .= '<p>The following categories are missing their parents:</p><ul>';
foreach ($missingparent as $cat) {
$description .= "<li>Category $cat->id: " . s($cat->name) . "</li>\n";
}
$description .= "</ul>\n";
}

return $description;
}

/**
* Given a list of categories, this function searches for ones
* that have loops to previous parent categories.
*
* @param array $categories List of categories.
* @return array List of categories with loops.
*/
function health_category_find_loops($categories) {
$loops = array();

// TODO: Improve this code so that if the root node is included in a loop, only children in the actual loop are reported
while (!empty($categories)) {
$current = array_pop($categories);
$thisloop = array($current->id => $current);
while (true) {
if (isset($thisloop[$current->parent])) {
// Loop detected
$loops[$current->id] = $thisloop;
break;
} else if (!isset($categories[$current->parent])) {
// Got to the top level, or a category we already know is OK.
break;
} else {
// Continue following the path.
$current = $categories[$current->parent];
$thisloop[$current->id] = $current;
unset($categories[$current->id]);
}
}
}

return $loops;
}

/**
* Generates a list of categories with loops.
*
* @param array $loops List of categories with loops.
* @return string Bullet point list of categories with loops.
*/
function health_category_list_loops($loops) {
$description = '';

if (!empty($loops)) {
$description .= '<p>The following categories form a loop of parents:</p><ul>';
foreach ($loops as $loop) {
$description .= "<li><ul>\n";
foreach ($loop as $cat) {
$description .= "<li>Category $cat->id: " . s($cat->name) . " has parent $cat->parent</li>\n";
}
$description .= "</ul></li>\n";
}
$description .= "</ul>\n";
}

return $description;
}
Loading

0 comments on commit 8db188c

Please sign in to comment.