MDL-46706 Library: API to get cm_info more easily
authorsam marshall <s.marshall@open.ac.uk>
Wed, 6 Aug 2014 16:41:16 +0000 (17:41 +0100)
committersam marshall <s.marshall@open.ac.uk>
Fri, 15 Aug 2014 17:24:11 +0000 (18:24 +0100)
Creates two new functions that make it easier to get cm_info
objects (both in modinfolib.php):

get_course_and_cm($cmid) - intended for use e.g. in modules;
efficiently loads $course and $cm (normally saves 1 query).

cm_info::from_mixed($cm) - for use in back-end API where we
are not sure whether a $cm is a real cm_info or a stdClass.

lib/modinfolib.php
lib/tests/modinfolib_test.php
lib/upgrade.txt

index 0195d77..d1ea1dd 100644 (file)
@@ -1747,6 +1747,31 @@ class cm_info implements IteratorAggregate {
                 : null;
     }
 
+    /**
+     * Creates a cm_info object from a database record (also accepts cm_info
+     * in which case it is just returned unchanged).
+     *
+     * @param stdClass|cm_info|null $cm Stdclass or cm_info (or null)
+     * @param int $userid Optional userid (default to current)
+     * @return cm_info|null Object as cm_info, or null if input was null
+     */
+    public static function create($cm, $userid = 0) {
+        // Nulls get passed through.
+        if (is_null($cm)) {
+            return null;
+        }
+        // If it is already a cm_info object, just return it.
+        if ($cm instanceof cm_info) {
+            return $cm;
+        }
+        // Otherwise load modinfo.
+        if (empty($cm->id) || empty($cm->course)) {
+            throw new coding_exception('$cm must contain ->id and ->course');
+        }
+        $modinfo = get_fast_modinfo($cm->course, $userid);
+        return $modinfo->get_cm($cm->id);
+    }
+
     /**
      * If dynamic data for this course-module is not yet available, gets it.
      *
@@ -2062,6 +2087,165 @@ function get_fast_modinfo($courseorid, $userid = 0, $resetonly = false) {
     return course_modinfo::instance($courseorid, $userid);
 }
 
+/**
+ * Efficiently retrieves the $course (stdclass) and $cm (cm_info) objects, given
+ * a cmid. If module name is also provided, it will ensure the cm is of that type.
+ *
+ * Usage:
+ * list($course, $cm) = get_course_and_cm_from_cmid($cmid, 'forum');
+ *
+ * Using this method has a performance advantage because it works by loading
+ * modinfo for the course - which will then be cached and it is needed later
+ * in most requests. It also guarantees that the $cm object is a cm_info and
+ * not a stdclass.
+ *
+ * The $course object can be supplied if already known and will speed
+ * up this function - although it is more efficient to use this function to
+ * get the course if you are starting from a cmid.
+ *
+ * To avoid security problems and obscure bugs, you should always specify
+ * $modulename if the cmid value came from user input.
+ *
+ * By default this obtains information (for example, whether user can access
+ * the activity) for current user, but you can specify a userid if required.
+ *
+ * @param stdClass|int $cmorid Id of course-module, or database object
+ * @param string $modulename Optional modulename (improves security)
+ * @param stdClass|int $courseorid Optional course object if already loaded
+ * @param int $userid Optional userid (default = current)
+ * @return array Array with 2 elements $course and $cm
+ * @throws moodle_exception If the item doesn't exist or is of wrong module name
+ */
+function get_course_and_cm_from_cmid($cmorid, $modulename = '', $courseorid = 0, $userid = 0) {
+    global $DB;
+    if (is_object($cmorid)) {
+        $cmid = $cmorid->id;
+        if (isset($cmorid->course)) {
+            $courseid = (int)$cmorid->course;
+        } else {
+            $courseid = 0;
+        }
+    } else {
+        $cmid = (int)$cmorid;
+        $courseid = 0;
+    }
+
+    // Validate module name if supplied.
+    if ($modulename && !core_component::is_valid_plugin_name('mod', $modulename)) {
+        throw new coding_exception('Invalid modulename parameter');
+    }
+
+    // Get course from last parameter if supplied.
+    $course = null;
+    if (is_object($courseorid)) {
+        $course = $courseorid;
+    } else if ($courseorid) {
+        $courseid = (int)$courseorid;
+    }
+
+    if (!$course) {
+        if ($courseid) {
+            // If course ID is known, get it using normal function.
+            $course = get_course($courseid);
+        } else {
+            // Get course record in a single query based on cmid.
+            $course = $DB->get_record_sql("
+                    SELECT c.*
+                      FROM {course_modules} cm
+                      JOIN {course} c ON c.id = cm.course
+                     WHERE cm.id = ?", array($cmid), MUST_EXIST);
+        }
+    }
+
+    // Get cm from get_fast_modinfo.
+    $modinfo = get_fast_modinfo($course, $userid);
+    $cm = $modinfo->get_cm($cmid);
+    if ($modulename && $cm->modname !== $modulename) {
+        throw new moodle_exception('invalidcoursemodule', 'error');
+    }
+    return array($course, $cm);
+}
+
+/**
+ * Efficiently retrieves the $course (stdclass) and $cm (cm_info) objects, given
+ * an instance id or record and module name.
+ *
+ * Usage:
+ * list($course, $cm) = get_course_and_cm_from_instance($forum, 'forum');
+ *
+ * Using this method has a performance advantage because it works by loading
+ * modinfo for the course - which will then be cached and it is needed later
+ * in most requests. It also guarantees that the $cm object is a cm_info and
+ * not a stdclass.
+ *
+ * The $course object can be supplied if already known and will speed
+ * up this function - although it is more efficient to use this function to
+ * get the course if you are starting from an instance id.
+ *
+ * By default this obtains information (for example, whether user can access
+ * the activity) for current user, but you can specify a userid if required.
+ *
+ * @param stdclass|int $instanceorid Id of module instance, or database object
+ * @param string $modulename Modulename (required)
+ * @param stdClass|int $courseorid Optional course object if already loaded
+ * @param int $userid Optional userid (default = current)
+ * @return array Array with 2 elements $course and $cm
+ * @throws moodle_exception If the item doesn't exist or is of wrong module name
+ */
+function get_course_and_cm_from_instance($instanceorid, $modulename, $courseorid = 0, $userid = 0) {
+    global $DB;
+
+    // Get data from parameter.
+    if (is_object($instanceorid)) {
+        $instanceid = $instanceorid->id;
+        if (isset($instanceorid->course)) {
+            $courseid = (int)$instanceorid->course;
+        } else {
+            $courseid = 0;
+        }
+    } else {
+        $instanceid = (int)$instanceorid;
+        $courseid = 0;
+    }
+
+    // Get course from last parameter if supplied.
+    $course = null;
+    if (is_object($courseorid)) {
+        $course = $courseorid;
+    } else if ($courseorid) {
+        $courseid = (int)$courseorid;
+    }
+
+    // Validate module name if supplied.
+    if (!core_component::is_valid_plugin_name('mod', $modulename)) {
+        throw new coding_exception('Invalid modulename parameter');
+    }
+
+    if (!$course) {
+        if ($courseid) {
+            // If course ID is known, get it using normal function.
+            $course = get_course($courseid);
+        } else {
+            // Get course record in a single query based on instance id.
+            $pagetable = '{' . $modulename . '}';
+            $course = $DB->get_record_sql("
+                    SELECT c.*
+                      FROM $pagetable instance
+                      JOIN {course} c ON c.id = instance.course
+                     WHERE instance.id = ?", array($instanceid), MUST_EXIST);
+        }
+    }
+
+    // Get cm from get_fast_modinfo.
+    $modinfo = get_fast_modinfo($course, $userid);
+    $instances = $modinfo->get_instances_of($modulename);
+    if (!array_key_exists($instanceid, $instances)) {
+        throw new moodle_exception('invalidmoduleid', 'error', $instanceid);
+    }
+    return array($course, $instances[$instanceid]);
+}
+
+
 /**
  * Rebuilds or resets the cached list of course activities stored in MUC.
  *
index a670545..7de4539 100644 (file)
@@ -902,4 +902,236 @@ class core_modinfolib_testcase extends advanced_testcase {
         $this->assertEquals(0, $section1->groupingid);
         $this->assertDebuggingCalled(null, DEBUG_DEVELOPER);
     }
+
+    /**
+     * Tests the function for constructing a cm_info from mixed data.
+     */
+    public function test_create() {
+        global $CFG, $DB;
+        $this->resetAfterTest();
+
+        // Create a course and an activity.
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $page = $generator->create_module('page', array('course' => $course->id,
+                'name' => 'Annie'));
+
+        // Null is passed through.
+        $this->assertNull(cm_info::create(null));
+
+        // Stdclass object turns into cm_info.
+        $cm = cm_info::create(
+                (object)array('id' => $page->cmid, 'course' => $course->id));
+        $this->assertInstanceOf('cm_info', $cm);
+        $this->assertEquals('Annie', $cm->name);
+
+        // A cm_info object stays as cm_info.
+        $this->assertSame($cm, cm_info::create($cm));
+
+        // Invalid object (missing fields) causes error.
+        try {
+            cm_info::create((object)array('id' => $page->cmid));
+            $this->fail();
+        } catch (Exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
+        // Create a second hidden activity.
+        $hiddenpage = $generator->create_module('page', array('course' => $course->id,
+                'name' => 'Annie', 'visible' => 0));
+
+        // Create 2 user accounts, one is a manager who can see everything.
+        $user = $generator->create_user();
+        $generator->enrol_user($user->id, $course->id);
+        $manager = $generator->create_user();
+        $generator->enrol_user($manager->id, $course->id,
+                $DB->get_field('role', 'id', array('shortname' => 'manager'), MUST_EXIST));
+
+        // User can see the normal page but not the hidden one.
+        $cm = cm_info::create((object)array('id' => $page->cmid, 'course' => $course->id),
+                $user->id);
+        $this->assertTrue($cm->uservisible);
+        $cm = cm_info::create((object)array('id' => $hiddenpage->cmid, 'course' => $course->id),
+                $user->id);
+        $this->assertFalse($cm->uservisible);
+
+        // Manager can see the hidden one too.
+        $cm = cm_info::create((object)array('id' => $hiddenpage->cmid, 'course' => $course->id),
+                $manager->id);
+        $this->assertTrue($cm->uservisible);
+    }
+
+    /**
+     * Tests function for getting $course and $cm at once quickly from modinfo
+     * based on cmid or cm record.
+     */
+    public function test_get_course_and_cm_from_cmid() {
+        global $CFG, $DB;
+        $this->resetAfterTest();
+
+        // Create a course and an activity.
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course(array('shortname' => 'Halls'));
+        $page = $generator->create_module('page', array('course' => $course->id,
+                'name' => 'Annie'));
+
+        // Successful usage.
+        list($course, $cm) = get_course_and_cm_from_cmid($page->cmid);
+        $this->assertEquals('Halls', $course->shortname);
+        $this->assertInstanceOf('cm_info', $cm);
+        $this->assertEquals('Annie', $cm->name);
+
+        // Specified module type.
+        list($course, $cm) = get_course_and_cm_from_cmid($page->cmid, 'page');
+        $this->assertEquals('Annie', $cm->name);
+
+        // With id in object.
+        $fakecm = (object)array('id' => $page->cmid);
+        list($course, $cm) = get_course_and_cm_from_cmid($fakecm);
+        $this->assertEquals('Halls', $course->shortname);
+        $this->assertEquals('Annie', $cm->name);
+
+        // With both id and course in object.
+        $fakecm->course = $course->id;
+        list($course, $cm) = get_course_and_cm_from_cmid($fakecm);
+        $this->assertEquals('Halls', $course->shortname);
+        $this->assertEquals('Annie', $cm->name);
+
+        // With supplied course id.
+        list($course, $cm) = get_course_and_cm_from_cmid($page->cmid, 'page', $course->id);
+        $this->assertEquals('Annie', $cm->name);
+
+        // With supplied course object (modified just so we can check it is
+        // indeed reusing the supplied object).
+        $course->silly = true;
+        list($course, $cm) = get_course_and_cm_from_cmid($page->cmid, 'page', $course);
+        $this->assertEquals('Annie', $cm->name);
+        $this->assertTrue($course->silly);
+
+        // Incorrect module type.
+        try {
+            get_course_and_cm_from_cmid($page->cmid, 'forum');
+            $this->fail();
+        } catch (moodle_exception $e) {
+            $this->assertEquals('invalidcoursemodule', $e->errorcode);
+        }
+
+        // Invalid module name.
+        try {
+            get_course_and_cm_from_cmid($page->cmid, 'pigs can fly');
+            $this->fail();
+        } catch (coding_exception $e) {
+            $this->assertContains('Invalid modulename parameter', $e->getMessage());
+        }
+
+        // Doesn't exist.
+        try {
+            get_course_and_cm_from_cmid($page->cmid + 1);
+            $this->fail();
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('dml_exception', $e);
+        }
+
+        // Create a second hidden activity.
+        $hiddenpage = $generator->create_module('page', array('course' => $course->id,
+                'name' => 'Annie', 'visible' => 0));
+
+        // Create 2 user accounts, one is a manager who can see everything.
+        $user = $generator->create_user();
+        $generator->enrol_user($user->id, $course->id);
+        $manager = $generator->create_user();
+        $generator->enrol_user($manager->id, $course->id,
+                $DB->get_field('role', 'id', array('shortname' => 'manager'), MUST_EXIST));
+
+        // User can see the normal page but not the hidden one.
+        list($course, $cm) = get_course_and_cm_from_cmid($page->cmid, 'page', 0, $user->id);
+        $this->assertTrue($cm->uservisible);
+        list($course, $cm) = get_course_and_cm_from_cmid($hiddenpage->cmid, 'page', 0, $user->id);
+        $this->assertFalse($cm->uservisible);
+
+        // Manager can see the hidden one too.
+        list($course, $cm) = get_course_and_cm_from_cmid($hiddenpage->cmid, 'page', 0, $manager->id);
+        $this->assertTrue($cm->uservisible);
+    }
+
+    /**
+     * Tests function for getting $course and $cm at once quickly from modinfo
+     * based on instance id or record.
+     */
+    public function test_get_course_and_cm_from_instance() {
+        global $CFG, $DB;
+        $this->resetAfterTest();
+
+        // Create a course and an activity.
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course(array('shortname' => 'Halls'));
+        $page = $generator->create_module('page', array('course' => $course->id,
+                'name' => 'Annie'));
+
+        // Successful usage.
+        list($course, $cm) = get_course_and_cm_from_instance($page->id, 'page');
+        $this->assertEquals('Halls', $course->shortname);
+        $this->assertInstanceOf('cm_info', $cm);
+        $this->assertEquals('Annie', $cm->name);
+
+        // With id in object.
+        $fakeinstance = (object)array('id' => $page->id);
+        list($course, $cm) = get_course_and_cm_from_instance($fakeinstance, 'page');
+        $this->assertEquals('Halls', $course->shortname);
+        $this->assertEquals('Annie', $cm->name);
+
+        // With both id and course in object.
+        $fakeinstance->course = $course->id;
+        list($course, $cm) = get_course_and_cm_from_instance($fakeinstance, 'page');
+        $this->assertEquals('Halls', $course->shortname);
+        $this->assertEquals('Annie', $cm->name);
+
+        // With supplied course id.
+        list($course, $cm) = get_course_and_cm_from_instance($page->id, 'page', $course->id);
+        $this->assertEquals('Annie', $cm->name);
+
+        // With supplied course object (modified just so we can check it is
+        // indeed reusing the supplied object).
+        $course->silly = true;
+        list($course, $cm) = get_course_and_cm_from_instance($page->id, 'page', $course);
+        $this->assertEquals('Annie', $cm->name);
+        $this->assertTrue($course->silly);
+
+        // Doesn't exist (or is wrong type).
+        try {
+            get_course_and_cm_from_instance($page->id, 'forum');
+            $this->fail();
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('dml_exception', $e);
+        }
+
+        // Invalid module name.
+        try {
+            get_course_and_cm_from_cmid($page->cmid, '1337 h4x0ring');
+            $this->fail();
+        } catch (coding_exception $e) {
+            $this->assertContains('Invalid modulename parameter', $e->getMessage());
+        }
+
+        // Create a second hidden activity.
+        $hiddenpage = $generator->create_module('page', array('course' => $course->id,
+                'name' => 'Annie', 'visible' => 0));
+
+        // Create 2 user accounts, one is a manager who can see everything.
+        $user = $generator->create_user();
+        $generator->enrol_user($user->id, $course->id);
+        $manager = $generator->create_user();
+        $generator->enrol_user($manager->id, $course->id,
+                $DB->get_field('role', 'id', array('shortname' => 'manager'), MUST_EXIST));
+
+        // User can see the normal page but not the hidden one.
+        list($course, $cm) = get_course_and_cm_from_cmid($page->cmid, 'page', 0, $user->id);
+        $this->assertTrue($cm->uservisible);
+        list($course, $cm) = get_course_and_cm_from_cmid($hiddenpage->cmid, 'page', 0, $user->id);
+        $this->assertFalse($cm->uservisible);
+
+        // Manager can see the hidden one too.
+        list($course, $cm) = get_course_and_cm_from_cmid($hiddenpage->cmid, 'page', 0, $manager->id);
+        $this->assertTrue($cm->uservisible);
+    }
 }
index 9e067cf..7efdb4c 100644 (file)
@@ -7,6 +7,11 @@ information provided here is intended especially for developers.
 * renderers: We now remove the suffix _renderable when looking for a render method for a renderable.
   If you have a renderable class named like "blah_renderable" and have a method on a renderer named "render_blah_renderable"
   you will need to change the name of your render method to "render_blah" instead, as renderable at the end is no longer accepted.
+* New functions get_course_and_cm_from_cmid($cmorid, $modulename) and
+  get_course_and_cm_from_instance($instanceorid, $modulename) can be used to
+  more efficiently load these basic data objects at the start of a script.
+* New function cm_info::create($cm) can be used when you need a cm_info
+  object, but have a $cm which might only be a standard database record.
 
 DEPRECATIONS:
 * completion_info->get_incomplete_criteria() is deprecated and will be removed in Moodle 3.0.