Merge branch 'MDL-47246_master' of git://github.com/dmonllao/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 8 Dec 2014 13:34:02 +0000 (13:34 +0000)
committerDan Poltawski <dan@moodle.com>
Mon, 8 Dec 2014 13:34:02 +0000 (13:34 +0000)
24 files changed:
admin/tool/health/index.php
admin/tool/health/locallib.php [new file with mode: 0644]
admin/tool/health/tests/healthlib_test.php [new file with mode: 0644]
backup/util/factories/backup_factory.class.php
enrol/self/db/access.php
enrol/self/lang/en/enrol_self.php
enrol/self/locallib.php
enrol/self/version.php
grade/report/singleview/classes/local/screen/grade.php
grade/report/singleview/classes/local/screen/screen.php
grade/report/singleview/classes/local/screen/user.php
grade/report/singleview/index.php
grade/report/singleview/lib.php
grade/report/singleview/tests/fixtures/screen.php [new file with mode: 0644]
grade/report/singleview/tests/screen_test.php [new file with mode: 0644]
grade/report/upgrade.txt
lib/navigationlib.php
message/module.js
pix/t/viewdetails.png [new file with mode: 0644]
pix/t/viewdetails.svg [new file with mode: 0644]
report/backups/index.php
report/backups/lang/en/report_backups.php
user/edit_form.php
user/view.php

index f0fe6c8..1e5b147 100644 (file)
@@ -28,6 +28,7 @@
     $extraws = ob_get_clean();
 
     require_once($CFG->libdir.'/adminlib.php');
+    require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/health/locallib.php');
 
     admin_externalpage_setup('toolhealth');
 
@@ -603,34 +604,10 @@ class problem_000017 extends problem_base {
             $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 = tool_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 = tool_health_category_find_loops($categories);
 
             $answer = array($missingparent, $loops);
         }
@@ -651,29 +628,126 @@ class problem_000017 extends problem_base {
                 ' structures by the question_categories.parent field. Sometimes ' .
                 ' this tree structure gets messed up.</p>';
 
+        $description .= tool_health_category_list_missing_parents($missingparent);
+        $description .= tool_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
+     */
+    public 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.
+ *
+ * @copyright  2013 Marko Vidberg
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class problem_000018 extends problem_base {
+    /**
+     * Generate title for this problem.
+     *
+     * @return string Title of problem.
+     */
+    public 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.
+     */
+    public 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 = tool_health_category_find_missing_parents($categories);
+
+            // Look for loops.
+            $loops = tool_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
+     */
+    public function exists() {
+        list($missingparent, $loops) = $this->find_problems();
+        return !empty($missingparent) || !empty($loops);
+    }
+
+    /**
+     * Set problem severity.
+     *
+     * @return constant Problem severity.
+     */
+    public function severity() {
+        return SEVERITY_SIGNIFICANT;
+    }
+
+    /**
+     * Generate problem description.
+     *
+     * @return string HTML containing details of the problem.
+     */
+    public 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 .= tool_health_category_list_missing_parents($missingparent);
+        $description .= tool_health_category_list_loops($loops);
+
         return $description;
     }
-    function solution() {
+
+    /**
+     * Generate solution text.
+     *
+     * @uses $CFG
+     * @return string HTML containing the suggested solution.
+     */
+    public function solution() {
         global $CFG;
         list($missingparent, $loops) = $this->find_problems();
 
@@ -681,14 +755,14 @@ class problem_000017 extends problem_base {
                 '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";
         }
 
diff --git a/admin/tool/health/locallib.php b/admin/tool/health/locallib.php
new file mode 100644 (file)
index 0000000..9eb28f9
--- /dev/null
@@ -0,0 +1,128 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Functions used by the health tool.
+ *
+ * @package    tool_health
+ * @copyright  2013 Marko Vidberg
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * 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 tool_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 tool_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 tool_health_category_find_loops($categories) {
+    $loops = array();
+
+    while (!empty($categories)) {
+
+        $current = array_pop($categories);
+        $thisloop = array($current->id => $current);
+
+        while (true) {
+            if (isset($thisloop[$current->parent])) {
+                // Loop detected.
+                $loops = $loops + $thisloop;
+                break;
+            } else if ($current->parent === 0) {
+                // Top level.
+                break;
+            } else if (isset($loops[$current->parent])) {
+                // If the parent is in a loop we should also update this category.
+                $loops = $loops + $thisloop;
+                break;
+            } else if (!isset($categories[$current->parent])) {
+                // We already checked this category and is correct.
+                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 tool_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>\n";
+            $description .= "Category $loop->id: " . s($loop->name) . " has parent $loop->parent\n";
+            $description .= "</li>\n";
+        }
+        $description .= "</ul>\n";
+    }
+
+    return $description;
+}
diff --git a/admin/tool/health/tests/healthlib_test.php b/admin/tool/health/tests/healthlib_test.php
new file mode 100644 (file)
index 0000000..0190af9
--- /dev/null
@@ -0,0 +1,218 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for tool_health.
+ *
+ * @package    tool_health
+ * @copyright  2013 Marko Vidberg
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/health/locallib.php');
+
+/**
+ * Health lib testcase.
+ *
+ * @package    tool_health
+ * @copyright  2013 Marko Vidberg
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class healthlib_testcase extends advanced_testcase {
+
+    /**
+     * Data provider for test_tool_health_category_find_loops.
+     */
+    public static function provider_loop_categories() {
+        return array(
+            // One item loop including root.
+            0 => array(
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 1)
+                ),
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 1)
+                ),
+            ),
+            // One item loop not including root.
+            1 => array(
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 0),
+                    '2' => (object) array('id' => 2, 'parent' => 2)
+                ),
+                array(
+                    '2' => (object) array('id' => 2, 'parent' => 2)
+                ),
+            ),
+            // Two item loop including root.
+            2 => array(
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 2),
+                    '2' => (object) array('id' => 2, 'parent' => 1)
+                ),
+                array(
+                    '2' => (object) array('id' => 2, 'parent' => 1),
+                    '1' => (object) array('id' => 1, 'parent' => 2),
+                )
+            ),
+            // Two item loop not including root.
+            3 => array(
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 0),
+                    '2' => (object) array('id' => 2, 'parent' => 3),
+                    '3' => (object) array('id' => 3, 'parent' => 2),
+                ),
+                array(
+                    '3' => (object) array('id' => 3, 'parent' => 2),
+                    '2' => (object) array('id' => 2, 'parent' => 3),
+                )
+            ),
+            // Three item loop including root.
+            4 => array(
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 2),
+                    '2' => (object) array('id' => 2, 'parent' => 3),
+                    '3' => (object) array('id' => 3, 'parent' => 1),
+                ),
+                array(
+                    '3' => (object) array('id' => 3, 'parent' => 1),
+                    '1' => (object) array('id' => 1, 'parent' => 2),
+                    '2' => (object) array('id' => 2, 'parent' => 3),
+                )
+            ),
+            // Three item loop not including root.
+            5 => array(
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 0),
+                    '2' => (object) array('id' => 2, 'parent' => 3),
+                    '3' => (object) array('id' => 3, 'parent' => 4),
+                    '4' => (object) array('id' => 4, 'parent' => 2)
+                ),
+                array(
+                    '4' => (object) array('id' => 4, 'parent' => 2),
+                    '2' => (object) array('id' => 2, 'parent' => 3),
+                    '3' => (object) array('id' => 3, 'parent' => 4),
+                )
+            ),
+            // Multi-loop.
+            6 => array(
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 2),
+                    '2' => (object) array('id' => 2, 'parent' => 1),
+                    '3' => (object) array('id' => 3, 'parent' => 4),
+                    '4' => (object) array('id' => 4, 'parent' => 5),
+                    '5' => (object) array('id' => 5, 'parent' => 3),
+                    '6' => (object) array('id' => 6, 'parent' => 6),
+                    '7' => (object) array('id' => 7, 'parent' => 1),
+                    '8' => (object) array('id' => 8, 'parent' => 7),
+                ),
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 2),
+                    '2' => (object) array('id' => 2, 'parent' => 1),
+                    '8' => (object) array('id' => 8, 'parent' => 7),
+                    '7' => (object) array('id' => 7, 'parent' => 1),
+                    '6' => (object) array('id' => 6, 'parent' => 6),
+                    '5' => (object) array('id' => 5, 'parent' => 3),
+                    '3' => (object) array('id' => 3, 'parent' => 4),
+                    '4' => (object) array('id' => 4, 'parent' => 5),
+                )
+            ),
+            // Double-loop
+            7 => array(
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 2),
+                    '2' => (object) array('id' => 2, 'parent' => 1),
+                    '3' => (object) array('id' => 3, 'parent' => 2),
+                    '4' => (object) array('id' => 4, 'parent' => 2),
+                ),
+                array(
+                    '4' => (object) array('id' => 4, 'parent' => 2),
+                    '3' => (object) array('id' => 3, 'parent' => 2),
+                    '2' => (object) array('id' => 2, 'parent' => 1),
+                    '1' => (object) array('id' => 1, 'parent' => 2),
+                )
+            )
+        );
+    }
+
+    /**
+     * Data provider for test_tool_health_category_find_missing_parents.
+     */
+    public static function provider_missing_parent_categories() {
+        return array(
+           // Test for two items, both with direct ancestor (parent) missing.
+            0 => array(
+                array(
+                    '1' => (object) array('id' => 1, 'parent' => 0),
+                    '2' => (object) array('id' => 2, 'parent' => 3),
+                    '4' => (object) array('id' => 4, 'parent' => 5),
+                    '6' => (object) array('id' => 6, 'parent' => 2)
+                ),
+                array(
+                    '4' => (object) array('id' => 4, 'parent' => 5),
+                    '2' => (object) array('id' => 2, 'parent' => 3)
+                ),
+            )
+        );
+    }
+
+    /**
+     * Test finding loops between two items referring to each other.
+     *
+     * @param array $categories
+     * @param array $expected
+     * @dataProvider provider_loop_categories
+     */
+    public function test_tool_health_category_find_loops($categories, $expected) {
+        $loops = tool_health_category_find_loops($categories);
+        $this->assertEquals($expected, $loops);
+    }
+
+    /**
+     * Test finding missing parent categories.
+     *
+     * @param array $categories
+     * @param array $expected
+     * @dataProvider provider_missing_parent_categories
+     */
+    public function test_tool_health_category_find_missing_parents($categories, $expected) {
+        $missingparent = tool_health_category_find_missing_parents($categories);
+        $this->assertEquals($expected, $missingparent);
+    }
+
+    /**
+     * Test listing missing parent categories.
+     */
+    public function test_tool_health_category_list_missing_parents() {
+        $missingparent = array((object) array('id' => 2, 'parent' => 3, 'name' => 'test'),
+                               (object) array('id' => 4, 'parent' => 5, 'name' => 'test2'));
+        $result = tool_health_category_list_missing_parents($missingparent);
+        $this->assertRegExp('/Category 2: test/', $result);
+        $this->assertRegExp('/Category 4: test2/', $result);
+    }
+
+    /**
+     * Test listing loop categories.
+     */
+    public function test_tool_health_category_list_loops() {
+        $loops = array((object) array('id' => 2, 'parent' => 3, 'name' => 'test'));
+        $result = tool_health_category_list_loops($loops);
+        $this->assertRegExp('/Category 2: test/', $result);
+    }
+}
index 6f8ba31..38c72a8 100644 (file)
@@ -66,7 +66,7 @@ abstract class backup_factory {
 
         // Create database_logger, observing $CFG->backup_database_logger_level and defaulting to LOG_WARNING
         // and pointing to the backup_logs table
-        $dllevel = isset($CFG->backup_database_logger_level) ? $CFG->backup_database_logger_level : backup::LOG_WARNING;
+        $dllevel = isset($CFG->backup_database_logger_level) ? $CFG->backup_database_logger_level : $dfltloglevel;
         $columns = array('backupid' => $backupid);
         $enabledloggers[] = new database_logger($dllevel, 'timecreated', 'loglevel', 'message', 'backup_logs', $columns);
 
index 9cdd3c3..d2c5972 100644 (file)
@@ -48,6 +48,15 @@ $capabilities = array(
         )
     ),
 
+    'enrol/self:holdkey' => array(
+
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_COURSE,
+        'archetypes' => array(
+            'manager' => CAP_PROHIBIT,
+        )
+    ),
+
     /* Voluntarily unenrol self from course - watch out for data loss. */
     'enrol/self:unenrolself' => array(
         'captype' => 'write',
index a964a9f..87a43f0 100644 (file)
@@ -63,6 +63,7 @@ $string['groupkey_desc'] = 'Use group enrolment keys by default.';
 $string['groupkey_help'] = 'In addition to restricting access to the course to only those who know the key, use of group enrolment keys means users are automatically added to groups when they enrol in the course.
 
 Note: An enrolment key for the course must be specified in the self enrolment settings as well as group enrolment keys in the group settings.';
+$string['keyholder'] = 'You should have received this enrolment key from:';
 $string['longtimenosee'] = 'Unenrol inactive after';
 $string['longtimenosee_help'] = 'If users haven\'t accessed a course for a long time, then they are automatically unenrolled. This parameter specifies that time limit.';
 $string['maxenrolled'] = 'Max enrolled users';
@@ -88,6 +89,7 @@ $string['requirepassword'] = 'Require enrolment key';
 $string['requirepassword_desc'] = 'Require enrolment key in new courses and prevent removing of enrolment key from existing courses.';
 $string['role'] = 'Default assigned role';
 $string['self:config'] = 'Configure self enrol instances';
+$string['self:holdkey'] = 'Appear as the self enrolment key holder';
 $string['self:manage'] = 'Manage enrolled users';
 $string['self:unenrol'] = 'Unenrol users from course';
 $string['self:unenrolself'] = 'Unenrol self from the course';
index cd010eb..226555d 100644 (file)
@@ -41,6 +41,7 @@ class enrol_self_enrol_form extends moodleform {
     }
 
     public function definition() {
+        global $USER, $OUTPUT, $CFG;
         $mform = $this->_form;
         $instance = $this->_customdata;
         $this->instance = $instance;
@@ -53,6 +54,26 @@ class enrol_self_enrol_form extends moodleform {
             // Change the id of self enrolment key input as there can be multiple self enrolment methods.
             $mform->addElement('passwordunmask', 'enrolpassword', get_string('password', 'enrol_self'),
                     array('id' => 'enrolpassword_'.$instance->id));
+            $context = context_course::instance($this->instance->courseid);
+            $keyholders = get_users_by_capability($context, 'enrol/self:holdkey', user_picture::fields('u'));
+            $keyholdercount = 0;
+            foreach ($keyholders as $keyholder) {
+                $keyholdercount++;
+                if ($keyholdercount === 1) {
+                    $mform->addElement('static', 'keyholder', '', get_string('keyholder', 'enrol_self'));
+                }
+                $keyholdercontext = context_user::instance($keyholder->id);
+                if ($USER->id == $keyholder->id || has_capability('moodle/user:viewdetails', context_system::instance()) ||
+                        has_coursecontact_role($keyholder->id)) {
+                    $profilelink = '<a href="' . $CFG->wwwroot . '/user/view.php?id=' . $keyholder->id . '&amp;course=' .
+                    $this->instance->courseid . '">' . fullname($keyholder) . '</a>';
+                } else {
+                    $profilelink = fullname($keyholder);
+                }
+                $profilepic = $OUTPUT->user_picture($keyholder, array('size' => 35, 'courseid' => $this->instance->courseid));
+                $mform->addElement('static', 'keyholder'.$keyholdercount, '', $profilepic . $profilelink);
+            }
+
         } else {
             $mform->addElement('static', 'nokey', '', get_string('nopassword', 'enrol_self'));
         }
index adc754e..59c2ca0 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014111000;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2014111001;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2014110400;        // Requires this Moodle version
 $plugin->component = 'enrol_self';      // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 600;
index 51346d1..2905a9a 100644 (file)
@@ -121,17 +121,9 @@ class grade extends tablelike implements selectable_items, filterable_items {
      * @param bool $selfitemisempty True if we have not selected a user.
      */
     public function init($selfitemisempty = false) {
-        $roleids = explode(',', get_config('moodle', 'gradebookroles'));
-
-        $this->items = array();
-        foreach ($roleids as $roleid) {
-            // Keeping the first user appearance.
-            $this->items = $this->items + get_role_users(
-                $roleid, $this->context, false, '',
-                'u.lastname, u.firstname', null, $this->groupid);
-        }
 
-        $this->totalitemcount = count_role_users($roleids, $this->context);
+        $this->items = $this->load_users();
+        $this->totalitemcount = count($this->items);
 
         if ($selfitemisempty) {
             return;
@@ -273,7 +265,7 @@ class grade extends tablelike implements selectable_items, filterable_items {
             new moodle_url('/grade/report/singleview/index.php', array(
                 'perpage' => $this->perpage,
                 'id' => $this->courseid,
-                'groupid' => $this->groupid,
+                'group' => $this->groupid,
                 'itemid' => $this->itemid,
                 'item' => 'grade'
             ))
index 0550af3..f31c1d2 100644 (file)
@@ -375,4 +375,29 @@ abstract class screen {
     public function supports_next_prev() {
         return true;
     }
+
+    /**
+     * Load a valid list of users for this gradebook as the screen "items".
+     * @return array $users A list of enroled users.
+     */
+    protected function load_users() {
+        global $CFG;
+
+        // Create a graded_users_iterator because it will properly check the groups etc.
+        $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
+        $showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol);
+        $showonlyactiveenrol = $showonlyactiveenrol || !has_capability('moodle/course:viewsuspendedusers', $this->context);
+
+        require_once($CFG->dirroot.'/grade/lib.php');
+        $gui = new \graded_users_iterator($this->course, null, $this->groupid);
+        $gui->require_active_enrolment($showonlyactiveenrol);
+        $gui->init();
+
+        // Flatten the users.
+        $users = array();
+        while ($user = $gui->next_user()) {
+            $users[$user->user->id] = $user->user;
+        }
+        return $users;
+    }
 }
index cf3fddd..6993237 100644 (file)
@@ -101,7 +101,11 @@ class user extends tablelike implements selectable_items {
         global $DB;
 
         if (!$selfitemisempty) {
-            $this->item = $DB->get_record('user', array('id' => $this->itemid));
+            $validusers = $this->load_users();
+            if (!isset($validusers[$this->itemid])) {
+                print_error('invaliduserid');
+            }
+            $this->item = $validusers[$this->itemid];
         }
 
         $params = array('courseid' => $this->courseid);
@@ -307,7 +311,7 @@ class user extends tablelike implements selectable_items {
             new moodle_url('/grade/report/singleview/index.php', array(
                 'perpage' => $this->perpage,
                 'id' => $this->courseid,
-                'groupid' => $this->groupid,
+                'group' => $this->groupid,
                 'itemid' => $this->itemid,
                 'item' => 'user'
             ))
index a355b28..42e99bf 100644 (file)
@@ -76,10 +76,7 @@ $USER->grade_last_report[$course->id] = 'singleview';
 // this must be done before constructing of the grade tree.
 grade_regrade_final_grades($courseid);
 
-$report = new gradereport_singleview(
-    $courseid, $gpr, $context,
-    $itemtype, $itemid, $groupid
-);
+$report = new gradereport_singleview($courseid, $gpr, $context, $itemtype, $itemid);
 
 $reportname = $report->screen->heading();
 
@@ -130,7 +127,7 @@ if (!empty($options)) {
 
     $relreport = new gradereport_singleview(
                 $courseid, $gpr, $context,
-                $report->screen->item_type(), $optionitemid, $groupid
+                $report->screen->item_type(), $optionitemid
     );
     $reloptions = $relreport->screen->options();
     $reloptionssorting = array_keys($relreport->screen->options());
index 5382c6c..b40711b 100644 (file)
@@ -74,18 +74,11 @@ class gradereport_singleview extends grade_report {
      * @param context_course $context
      * @param string $itemtype Should be user, select or grade
      * @param int $itemid The id of the user or grade item
-     * @param int $groupid (optional) The current groupid.
+     * @param string $unused Used to be group id but that was removed and this is now unused.
      */
-    public function __construct($courseid, $gpr, $context, $itemtype, $itemid, $groupid=null) {
+    public function __construct($courseid, $gpr, $context, $itemtype, $itemid, $unused = null) {
         parent::__construct($courseid, $gpr, $context);
 
-        $screenclass = "\\gradereport_singleview\\local\\screen\\${itemtype}";
-
-        $this->screen = new $screenclass($courseid, $itemid, $groupid);
-
-        // Load custom or predifined js.
-        $this->screen->js();
-
         $base = '/grade/report/singleview/index.php';
 
         $idparams = array('id' => $courseid);
@@ -93,11 +86,19 @@ class gradereport_singleview extends grade_report {
         $this->baseurl = new moodle_url($base, $idparams);
 
         $this->pbarurl = new moodle_url($base, $idparams + array(
-            'item' => $itemtype,
-            'itemid' => $itemid
-        ));
+                'item' => $itemtype,
+                'itemid' => $itemid
+            ));
 
+        //  The setup_group method is used to validate group mode and permissions and define the currentgroup value.
         $this->setup_groups();
+
+        $screenclass = "\\gradereport_singleview\\local\\screen\\${itemtype}";
+
+        $this->screen = new $screenclass($courseid, $itemid, $this->currentgroup);
+
+        // Load custom or predifined js.
+        $this->screen->js();
     }
 
     /**
diff --git a/grade/report/singleview/tests/fixtures/screen.php b/grade/report/singleview/tests/fixtures/screen.php
new file mode 100644 (file)
index 0000000..33ae6e3
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Fixtures for single view report screen class testing.
+ *
+ * @package    gradereport_singleview
+ * @copyright  2014 onwards Simey Lameze <simey@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+class gradereport_singleview_screen_testable extends \gradereport_singleview\local\screen\screen {
+
+    /**
+     * Wrapper to make protected method accessible during testing.
+     *
+     * @return array returns array of users.
+     */
+    public function test_load_users() {
+        return $this->load_users();
+    }
+
+    /**
+     * Return the HTML for the page.
+     */
+    public function init($selfitemisempty = false) {}
+
+    /**
+     * Get the type of items on this screen, not valid so return false.
+     */
+    public function item_type() {}
+
+    /**
+     * Return the HTML for the page.
+     */
+    public function html() {}
+}
diff --git a/grade/report/singleview/tests/screen_test.php b/grade/report/singleview/tests/screen_test.php
new file mode 100644 (file)
index 0000000..2476d08
--- /dev/null
@@ -0,0 +1,92 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for gradereport_singleview screen class.
+ *
+ * @package    gradereport_singleview
+ * @category   test
+ * @copyright  2014 onwards Simey Lameze <simey@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+require_once(__DIR__ . '/fixtures/screen.php');
+
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Tests for screen class.
+ *
+ * Class gradereport_singleview_screen_testcase.
+ */
+class gradereport_singleview_screen_testcase extends advanced_testcase {
+
+    /**
+     * Test load_users method.
+     */
+    public function test_load_users() {
+        global $DB;
+
+        $this->setAdminUser();
+        $this->resetAfterTest(true);
+
+        $roleteacher = $DB->get_record('role', array('shortname' => 'teacher'), '*', MUST_EXIST);
+
+        // Create a course, users and groups.
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+        $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        $teacher = $this->getDataGenerator()->create_user();
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $roleteacher->id);
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group->id, 'userid' => $teacher->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group->id, 'userid' => $user1->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group->id, 'userid' => $user2->id));
+
+        $screentest = new gradereport_singleview_screen_testable($course->id, 0, $group->id);
+        $groupusers = $screentest->test_load_users();
+        $this->assertCount(2, $groupusers);
+
+        // Now, let's suspend the enrolment of a user. Should return only one user.
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, $roleteacher->id, 'manual', 0, 0, ENROL_USER_SUSPENDED);
+        $users = $screentest->test_load_users();
+        $this->assertCount(1, $users);
+
+        // Change the viewsuspendedusers capabilities and set the user preference to display suspended users.
+        assign_capability('moodle/course:viewsuspendedusers', CAP_ALLOW, $roleteacher->id, $coursecontext, true);
+        set_user_preference('grade_report_showonlyactiveenrol', false, $teacher);
+        accesslib_clear_all_caches_for_unit_testing();
+        $this->setUser($teacher);
+        $screentest = new gradereport_singleview_screen_testable($course->id, 0, $group->id);
+        $users = $screentest->test_load_users();
+        $this->assertCount(2, $users);
+
+        // Change the capability again, now the user can't see the suspended enrolments.
+        assign_capability('moodle/course:viewsuspendedusers', CAP_PROHIBIT, $roleteacher->id, $coursecontext, true);
+        set_user_preference('grade_report_showonlyactiveenrol', false, $teacher);
+        accesslib_clear_all_caches_for_unit_testing();
+        $users = $screentest->test_load_users();
+        $this->assertCount(1, $users);
+
+        // Now, activate the user enrolment again. We shall get 2 users now.
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, $roleteacher->id, 'manual', 0, 0, ENROL_USER_ACTIVE);
+        $users = $screentest->test_load_users();
+        $this->assertCount(2, $users);
+    }
+}
\ No newline at end of file
index 748b210..8c10d0b 100644 (file)
@@ -1,6 +1,9 @@
 This files describes API changes in /grade/report/*,
 information provided here is intended especially for developers.
 
+=== 2.8.2 ===
+* gradereport_singleview::__construct doesn't need groupid parameter anymore, so it was renamed to $unused.
+
 === 2.6.5, 2.7.2 ===
 
 * The callback function grade_report_*_profilereport now takes one more parameter $viewasuser. This parameter
index 3a53d86..4bbdc67 100644 (file)
@@ -4150,8 +4150,8 @@ class settings_navigation extends navigation_node {
                     return false;
                 }
                 $canaccessallgroups = has_capability('moodle/site:accessallgroups', $coursecontext);
-                if (!$canaccessallgroups && groups_get_course_groupmode($course) == SEPARATEGROUPS) {
-                    // If groups are in use, make sure we can see that group (MDL-45874).
+                if (!$canaccessallgroups && groups_get_course_groupmode($course) == SEPARATEGROUPS && !$canviewuser) {
+                    // If groups are in use, make sure we can see that group (MDL-45874). That does not apply to parents.
                     if ($courseid == $this->page->course->id) {
                         $mygroups = get_fast_modinfo($this->page->course)->groups;
                     } else {
index 6fd3bf4..bc10ce0 100644 (file)
@@ -102,7 +102,7 @@ M.core_message.init_defaultoutputs = function(Y) {
                 }, this);
                 parentnode.addClass('dimmed_text');
             } else {
-                parentnode.all('select').each(function(node) {
+                parentnode.all('select[disabled]').each(function(node) {
                     node.removeAttribute('disabled');
                     node.set('value', 'permitted');
                     defaultoutputs.updateCheckboxes(node.ancestor('td'), 0, 0);
diff --git a/pix/t/viewdetails.png b/pix/t/viewdetails.png
new file mode 100644 (file)
index 0000000..cf6824b
Binary files /dev/null and b/pix/t/viewdetails.png differ
diff --git a/pix/t/viewdetails.svg b/pix/t/viewdetails.svg
new file mode 100644 (file)
index 0000000..9b3ddba
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->\r
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
+       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
+]>\r
+<svg version="1.1"\r
+        xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"\r
+        x="0px" y="0px" width="12px" height="12px" viewBox="0 0 12 12" style="overflow:visible;enable-background:new 0 0 12 12;"\r
+        xml:space="preserve" preserveAspectRatio="xMinYMid meet">\r
+<defs>\r
+</defs>\r
+<path style="fill:#999999;" d="M11.4,9.4L9.9,7.9C9.8,7.8,9.7,7.8,9.6,7.7c0.4-0.8,0.7-1.6,0.7-2.5C10.3,2.3,8,0,5.2,0\r
+       C2.3,0,0,2.3,0,5.2s2.3,5.2,5.2,5.2c0.9,0,1.7-0.2,2.4-0.6C7.7,9.8,7.7,9.9,7.8,10l1.5,1.5c0.6,0.6,1.5,0.6,2.1,0S12,10,11.4,9.4z\r
+        M5.2,7.3C4,7.3,3,6.4,3,5.2C3,4,4,3,5.2,3c1.2,0,2.2,1,2.2,2.2C7.3,6.4,6.4,7.3,5.2,7.3z"/>\r
+</svg>\r
index b695034..39dadc4 100644 (file)
  */
 
 require_once('../../config.php');
-require_once($CFG->libdir.'/adminlib.php');
+require_once($CFG->libdir . '/adminlib.php');
+
+// Required for backup::xxx constants.
+require_once($CFG->dirroot . '/backup/util/interfaces/checksumable.class.php');
+require_once($CFG->dirroot . '/backup/backup.class.php');
+
+$courseid = optional_param('courseid', 0, PARAM_INT);
+$page = optional_param('page', 0, PARAM_INT); // This represents which backup we are viewing.
 
 // Required for constants in backup_cron_automated_helper
 require_once($CFG->dirroot.'/backup/util/helper/backup_cron_helper.class.php');
 
 admin_externalpage_setup('reportbackups', '', null, '', array('pagelayout'=>'report'));
 
+$strftimedatetime = get_string('strftimerecent');
+$strerror = get_string('error');
+$strok = get_string('ok');
+$strunfinished = get_string('unfinished');
+$strskipped = get_string('skipped');
+$strwarning = get_string('warning');
+$strnotyetrun = get_string('backupnotyetrun');
+
+if ($courseid) {
+    $course = $DB->get_record('course', array('id' => $courseid), 'id, fullname', MUST_EXIST);
+
+    // Get the automated backups that have been performed for this course.
+    $params = array('operation' => backup::OPERATION_BACKUP,
+                    'type' => backup::TYPE_1COURSE,
+                    'itemid' => $course->id,
+                    'interactive' => backup::INTERACTIVE_NO);
+    if ($backups = $DB->get_records('backup_controllers', $params, 'timecreated DESC',
+        'id, backupid, status, timecreated', $page, 1)) {
+        // Get the backup we want to use.
+        $backup = reset($backups);
+
+        // Get the backup status.
+        if ($backup->status == backup::STATUS_FINISHED_OK) {
+            $status = $strok;
+            $statusclass = 'backup-ok'; // Green.
+        } else if ($backup->status == backup::STATUS_AWAITING || $backup->status == backup::STATUS_EXECUTING) {
+            $status = $strunfinished;
+            $statusclass = 'backup-unfinished'; // Red.
+        } else { // Else show error.
+            $status = $strerror;
+            $statusclass = 'backup-error'; // Red.
+        }
+
+        $table = new html_table();
+        $table->head = array('');
+        $table->data = array();
+        $statusrow = get_string('status') . ' - ' . html_writer::tag('span', $status, array('class' => $statusclass));
+        $table->data[] = array($statusrow);
+
+        // Get the individual logs for this backup.
+        if ($logs = $DB->get_records('backup_logs', array('backupid' => $backup->backupid), 'timecreated ASC',
+            'id, message, timecreated')) {
+            foreach ($logs as $log) {
+                $table->data[] = array(userdate($log->timecreated, get_string('strftimetime', 'report_backups')) .
+                    ' - ' . $log->message);
+            }
+        } else {
+            $table->data[] = array(get_string('nologsfound', 'report_backups'));
+        }
+    }
+
+    // Set the course name to display.
+    $coursename = format_string($course->fullname, true, array('context' => context_course::instance($course->id)));
+
+    echo $OUTPUT->header();
+    echo $OUTPUT->heading(get_string('backupofcourselogs', 'report_backups', $coursename));
+    if (isset($backup)) {
+        // We put this logic down here as we may be viewing a backup that was performed which there were no logs
+        // recorded for. We still want to display the pagination so the user can still navigate to other backups,
+        // and we also display a message so they are aware that the backup happened but there were no logs.
+        $baseurl = new moodle_url('/report/backups/index.php', array('courseid' => $courseid));
+        $numberofbackups = $DB->count_records('backup_controllers', $params);
+        $pagingbar = new paging_bar($numberofbackups, $page, 1, $baseurl);
+
+        echo $OUTPUT->render($pagingbar);
+        echo $OUTPUT->heading(get_string('logsofbackupexecutedon', 'report_backups', userdate($backup->timecreated)), 3);
+        echo html_writer::table($table);
+        echo $OUTPUT->render($pagingbar);
+    } else {
+        echo $OUTPUT->box(get_string('nobackupsfound', 'report_backups'), 'center');
+    }
+    echo $OUTPUT->footer();
+    exit();
+}
+
 $table = new html_table;
 $table->head = array(
     get_string("course"),
@@ -42,17 +124,9 @@ $table->headspan = array(1, 3, 1, 1);
 $table->attributes = array('class' => 'generaltable backup-report');
 $table->data = array();
 
-$strftimedatetime = get_string('strftimerecent');
-$strerror = get_string('error');
-$strok = get_string('ok');
-$strunfinished = get_string('unfinished');
-$strskipped = get_string('skipped');
-$strwarning = get_string('warning');
-$strnotyetrun = get_string('backupnotyetrun');
-
 $select = ', ' . context_helper::get_preload_record_columns_sql('ctx');
 $join = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
-$sql = "SELECT bc.*, c.fullname $select
+$sql = "SELECT bc.*, c.id as courseid, c.fullname $select
           FROM {backup_courses} bc
           JOIN {course} c ON c.id = bc.courseid
                $join";
@@ -86,8 +160,11 @@ foreach ($rs as $backuprow) {
     $status->attributes = array('class' => $statusclass);
 
     // Create the row and add it to the table
+    $backuprowname = format_string($backuprow->fullname, true, array('context' => context_course::instance($backuprow->courseid)));
+    $backuplogsurl = new moodle_url('/report/backups/index.php', array('courseid' => $backuprow->courseid));
+    $backuplogsicon = new pix_icon('t/viewdetails', get_string('viewlogs', 'report_backups'));
     $cells = array(
-        format_string($backuprow->fullname, true, array('context' => context_course::instance($backuprow->courseid))),
+        $backuprowname . ' ' . $OUTPUT->action_icon($backuplogsurl, $backuplogsicon),
         userdate($backuprow->laststarttime, $strftimedatetime),
         '-',
         userdate($backuprow->lastendtime, $strftimedatetime),
index 25bfccc..3128d3f 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['backupofcourselogs'] = 'Backup logs of {$a}';
+$string['logsofbackupexecutedon'] = 'Logs of the backup executed on {$a}';
+$string['nobackupsfound'] = 'There were no backups found.';
+$string['nologsfound'] = 'There were no logs found for this backup.';
 $string['pluginname'] = 'Backups report';
+$string['strftimetime'] = '%I:%M:%S %p';
+$string['viewlogs'] = 'View logs';
index 4c46d6b..5456c38 100644 (file)
@@ -133,17 +133,23 @@ class user_edit_form extends moodleform {
             $fields = get_user_fieldnames();
             $authplugin = get_auth_plugin($user->auth);
             foreach ($fields as $field) {
-                if (!$mform->elementExists($field)) {
+                if ($field === 'description') {
+                    // Hard coded hack for description field. See MDL-37704 for details.
+                    $formfield = 'description_editor';
+                } else {
+                    $formfield = $field;
+                }
+                if (!$mform->elementExists($formfield)) {
                     continue;
                 }
                 $configvariable = 'field_lock_' . $field;
                 if (isset($authplugin->config->{$configvariable})) {
                     if ($authplugin->config->{$configvariable} === 'locked') {
-                        $mform->hardFreeze($field);
-                        $mform->setConstant($field, $user->$field);
+                        $mform->hardFreeze($formfield);
+                        $mform->setConstant($formfield, $user->$field);
                     } else if ($authplugin->config->{$configvariable} === 'unlockedifempty' and $user->$field != '') {
-                        $mform->hardFreeze($field);
-                        $mform->setConstant($field, $user->$field);
+                        $mform->hardFreeze($formfield);
+                        $mform->setConstant($formfield, $user->$field);
                     }
                 }
             }
index 48e56c6..f5feffb 100644 (file)
@@ -142,8 +142,12 @@ if ($currentuser) {
     }
 
     // If groups are in use and enforced throughout the course, then make sure we can meet in at least one course level group.
-    if (groups_get_course_groupmode($course) == SEPARATEGROUPS and $course->groupmodeforce
-      and !has_capability('moodle/site:accessallgroups', $coursecontext) and !has_capability('moodle/site:accessallgroups', $coursecontext, $user->id)) {
+    // Except when we are a parent, in which case we would not be in any group.
+    if (groups_get_course_groupmode($course) == SEPARATEGROUPS
+            and $course->groupmodeforce
+            and !has_capability('moodle/site:accessallgroups', $coursecontext)
+            and !has_capability('moodle/site:accessallgroups', $coursecontext, $user->id)
+            and !$isparent) {
         if (!isloggedin() or isguestuser()) {
             // Do not use require_login() here because we might have already used require_login($course).
             redirect(get_login_url());