Merge branch 'MDL-28705_master' of git://github.com/dmonllao/moodle
authorDan Poltawski <dan@moodle.com>
Tue, 15 Jan 2013 04:54:28 +0000 (12:54 +0800)
committerDan Poltawski <dan@moodle.com>
Tue, 15 Jan 2013 04:54:28 +0000 (12:54 +0800)
enrol/imsenterprise/lang/en/enrol_imsenterprise.php
enrol/imsenterprise/lib.php
enrol/imsenterprise/locallib.php
enrol/imsenterprise/settings.php
enrol/imsenterprise/tests/imsenterprise_test.php [new file with mode: 0644]

index 8b91682..511ac82 100644 (file)
@@ -42,9 +42,8 @@ Users are searched for first by their "idnumber", and second by their Moodle use
 $string['cronfrequency'] = 'Frequency of processing';
 $string['deleteusers'] = 'Delete user accounts when specified in IMS data';
 $string['deleteusers_desc'] = 'If enabled, IMS Enterprise enrolment data can specify the deletion of user accounts (if the "recstatus" flag is set to 3, which represents deletion of an account). As is standard in Moodle, the user record isn\'t actually deleted from Moodle\'s database, but a flag is set to mark the account as deleted.';
-$string['pluginname_desc'] = 'This method will repeatedly check for and process a specially-formatted text file in the location that you specify.  The file must follow the IMS Enterprise specifications containing person, group, and membership XML elements.';
 $string['doitnow'] = 'perform an IMS Enterprise import right now';
-$string['pluginname'] = 'IMS Enterprise file';
+$string['emptyattribute'] = 'Leave it empty';
 $string['filelockedmail'] = 'The text file you are using for IMS-file-based enrolments ({$a}) can not be deleted by the cron process.  This usually means the permissions are wrong on it.  Please fix the permissions so that Moodle can delete the file, otherwise it might be processed repeatedly.';
 $string['filelockedmailsubject'] = 'Important error: Enrolment file';
 $string['fixcasepersonalnames'] = 'Change personal names to Title Case';
@@ -56,12 +55,20 @@ $string['mailadmins'] = 'Notify admin by email';
 $string['mailusers'] = 'Notify users by email';
 $string['messageprovider:imsenterprise_enrolment'] = 'IMS Enterprise enrolment messages';
 $string['miscsettings'] = 'Miscellaneous';
+$string['pluginname'] = 'IMS Enterprise file';
+$string['pluginname_desc'] = 'This method will repeatedly check for and process a specially-formatted text file in the location that you specify.  The file must follow the IMS Enterprise specifications containing person, group, and membership XML elements.';
 $string['processphoto'] = 'Add user photo data to profile';
 $string['processphotowarning'] = 'Warning: Image processing is likely to add a significant burden to the server. You are recommended not to activate this option if large numbers of students are expected to be processed.';
 $string['restricttarget'] = 'Only process data if the following target is specified';
 $string['restricttarget_desc'] = 'An IMS Enterprise data file could be intended for multiple "targets" - different LMSes, or different systems within a school/university. It\'s possible to specify in the Enterprise file that the data is intended for one or more named target systems, by naming them in <target> tags contained within the <properties> tag.
 
 In general you don\'t need to worry about this. Leave the setting blank and Moodle will always process the data file, no matter whether a target is specified or not. Otherwise, fill in the exact name that will be output inside the <target> tag.';
+$string['settingfullname'] = 'IMS description tag for the course full name';
+$string['settingfullnamedescription'] = 'The full name is a required course field so you have to define the selected description tag in your IMS Enterprise file';
+$string['settingshortname'] = 'IMS description tag for the course short name';
+$string['settingshortnamedescription'] = 'The short name is a required course field so you have to define the selected description tag in your IMS Enterprise file';
+$string['settingsummary'] = 'IMS description tag for the course summary';
+$string['settingsummarydescription'] = 'Is an optional field, select \'Leave it empty\' if you dont\'t want to specify a course summary';
 $string['sourcedidfallback'] = 'Use the &quot;sourcedid&quot; for a person\'s userid if the &quot;userid&quot; field is not found';
 $string['sourcedidfallback_desc'] = 'In IMS data, the <sourcedid> field represents the persistent ID code for a person as used in the source system. The <userid> field is a separate field which should contain the ID code used by the user when logging in. In many cases these two codes may be the same - but not always.
 
index 9d11e00..5a2070e 100644 (file)
@@ -97,6 +97,8 @@ function cron() {
 
         // Make sure we understand how to map the IMS-E roles to Moodle roles
         $this->load_role_mappings();
+        // Make sure we understand how to map the IMS-E course names to Moodle course names.
+        $this->load_course_mappings();
 
         $md5 = md5_file($filename); // NB We'll write this value back to the database at the end of the cron
         $filemtime = filemtime($filename);
@@ -315,15 +317,17 @@ function process_group_tag($tagcontents) {
     if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)) {
         $group->coursecode = trim($matches[1]);
     }
-    if (preg_match('{<description>.*?<long>(.*?)</long>.*?</description>}is', $tagcontents, $matches)){
-        $group->description = trim($matches[1]);
+
+    if (preg_match('{<description>.*?<long>(.*?)</long>.*?</description>}is', $tagcontents, $matches)) {
+        $group->long = trim($matches[1]);
     }
     if (preg_match('{<description>.*?<short>(.*?)</short>.*?</description>}is', $tagcontents, $matches)) {
-        $group->shortName = trim($matches[1]);
+        $group->short = trim($matches[1]);
     }
     if (preg_match('{<description>.*?<full>(.*?)</full>.*?</description>}is', $tagcontents, $matches)) {
-        $group->fulldescription = trim($matches[1]);
+        $group->full = trim($matches[1]);
     }
+
     if (preg_match('{<org>.*?<orgunit>(.*?)</orgunit>.*?</org>}is', $tagcontents, $matches)) {
         $group->category = trim($matches[1]);
     }
@@ -361,28 +365,36 @@ function process_group_tag($tagcontents) {
                 if (!$createnewcourses) {
                     $this->log_line("Course $coursecode not found in Moodle's course idnumbers.");
                 } else {
-                    // Set shortname to description or description to shortname if one is set but not the other.
-                    $nodescription = !isset($group->description);
-                    $noshortname = !isset($group->shortName);
-                    if ( $nodescription && $noshortname) {
-                        // If neither short nor long description are set let if fail
-                        $this->log_line("Neither long nor short name are set for $coursecode");
-                    } else if ($nodescription) {
-                        // If short and ID exist, then give the long short's value, then give short the ID's value
-                        $group->description = $group->shortName;
-                        $group->shortName = $coursecode;
-                    } else if ($noshortname) {
-                        // If long and ID exist, then map long to long, then give short the ID's value.
-                        $group->shortName = $coursecode;
-                    }
+
                     // Create the (hidden) course(s) if not found
                     $courseconfig = get_config('moodlecourse'); // Load Moodle Course shell defaults
+
+                    // New course.
                     $course = new stdClass();
-                    $course->fullname = $group->description;
-                    $course->shortname = $group->shortName;
-                    if (!empty($group->fulldescription)) {
-                        $course->summary = format_text($group->fulldescription, FORMAT_HTML);
+                    foreach ($this->coursemappings as $courseattr => $imsname) {
+
+                        if ($imsname == 'ignore') {
+                            continue;
+                        }
+
+                        // Check if the IMS file contains the mapped tag, otherwise fallback on coursecode.
+                        if ($imsname == 'coursecode') {
+                            $course->{$courseattr} = $coursecode;
+                        } else if (!empty($group->{$imsname})) {
+                            $course->{$courseattr} = $group->{$imsname};
+                        } else {
+                            $this->log_line('No ' . $imsname . ' description tag found for ' . $coursecode . ' coursecode, using ' . $coursecode . ' instead');
+                            $course->{$courseattr} = $coursecode;
+                        }
+
+                        if ($courseattr == 'summary') {
+                            $format = FORMAT_HTML;
+                        } else {
+                            $format = FORMAT_PLAIN;
+                        }
+                        $course->{$courseattr} = format_text($course->$courseattr, $format);
                     }
+
                     $course->idnumber = $coursecode;
                     $course->format = $courseconfig->format;
                     $course->visible = $courseconfig->visible;
@@ -421,7 +433,6 @@ function process_group_tag($tagcontents) {
                     $course->startdate = time();
                     // Choose a sort order that puts us at the start of the list!
                     $course->sortorder = 0;
-
                     $courseid = $DB->insert_record('course', $course);
 
                     // Setup default enrolment plugins
@@ -748,7 +759,10 @@ function process_properties_tag($tagcontents){
 * @param string $string Text to write (newline will be added automatically)
 */
 function log_line($string){
-    mtrace($string);
+
+    if (!PHPUNIT_TEST) {
+        mtrace($string);
+    }
     if($this->logfp) {
         fwrite($this->logfp, $string . "\n");
     }
@@ -789,6 +803,22 @@ function load_role_mappings() {
     }
 }
 
+    /**
+     * Load the name mappings (from the config), so we can easily refer to
+     * how an IMS-E course properties corresponds to a Moodle course properties
+     */
+    function load_course_mappings() {
+        require_once('locallib.php');
+
+        $imsnames = new imsenterprise_courses();
+        $courseattrs = $imsnames->get_courseattrs();
+
+        $this->coursemappings = array();
+        foreach($courseattrs as $courseattr) {
+            $this->coursemappings[$courseattr] = $this->get_config('imscoursemap' . $courseattr);
+        }
+    }
+
     /**
      * Called whenever anybody tries (from the normal interface) to remove a group
      * member which is registered as being created by this component. (Not called
@@ -802,6 +832,7 @@ function load_role_mappings() {
         return false;
     }
 
+
 } // end of class
 
 
index c85f71b..13c6114 100644 (file)
@@ -82,3 +82,74 @@ class imsenterprise_roles {
 
 
 }  // class
+
+
+/**
+ * Mapping between Moodle course attributes and IMS enterprise group description tags
+ *
+ * @package   enrol_imsenterprise
+ * @copyright 2011 Aaron C Spike
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class imsenterprise_courses {
+
+    private $imsnames;
+    private $courseattrs;
+
+    /**
+     * Loads default
+     */
+    function __construct() {
+        $this->imsnames = array(
+            'short' => 'short',
+            'long' => 'long',
+            'full' => 'full',
+            'coursecode' => 'coursecode');
+        $this->courseattrs = array('shortname', 'fullname', 'summary');
+    }
+
+    /**
+     * Returns the assignable values for the course attribute
+     * @param string $courseattr The course attribute (shortname, fullname...)
+     * @return array Array of assignable values
+     */
+    function get_imsnames($courseattr) {
+\r
+        $values = $this->imsnames;\r
+        if ($courseattr == 'summary') {\r
+            $values = array_merge(array('ignore' => get_string('emptyattribute', 'enrol_imsenterprise')), $values);
+        }
+        return $values;
+    }
+
+    /**
+     * courseattrs getter
+     * @return array
+     */
+    function get_courseattrs() {
+        return $this->courseattrs;
+    }
+
+    /**
+     * This function is only used when first setting up the plugin, to
+     * decide which name assignments to recommend by default.
+     *
+     * @param string $coursename
+     * @return string
+     */
+    function determine_default_coursemapping($courseattr) {
+        switch($courseattr) {
+            case 'fullname':
+                $imsname = 'short';
+                break;
+            case 'shortname':
+                $imsname = 'coursecode';
+                break;
+            default:
+                $imsname = 'ignore';
+        }
+
+        return $imsname;
+    }
+
+}  // class
index 1c7f205..b644f49 100644 (file)
@@ -76,6 +76,19 @@ if ($ADMIN->fulltree) {
 
     $settings->add(new admin_setting_configcheckbox('enrol_imsenterprise/imsunenrol', get_string('allowunenrol', 'enrol_imsenterprise'), get_string('allowunenrol_desc', 'enrol_imsenterprise'), 0));
 
+    if (!during_initial_install()) {
+        $imscourses = new imsenterprise_courses();
+        foreach ($imscourses->get_courseattrs() as $courseattr) {
+
+            // The assignable values of this course attribute
+            $assignablevalues = $imscourses->get_imsnames($courseattr);
+            $name = get_string('setting' . $courseattr, 'enrol_imsenterprise');
+            $description = get_string('setting' . $courseattr . 'description', 'enrol_imsenterprise');
+            $defaultvalue = (string) $imscourses->determine_default_coursemapping($courseattr);
+            $settings->add(new admin_setting_configselect('enrol_imsenterprise/imscoursemap' . $courseattr, $name, $description, $defaultvalue, $assignablevalues));
+        }
+    }
+
     //--- miscellaneous -------------------------------------------------------------------------------------
     $settings->add(new admin_setting_heading('enrol_imsenterprise_miscsettings', get_string('miscsettings', 'enrol_imsenterprise'), ''));
 
diff --git a/enrol/imsenterprise/tests/imsenterprise_test.php b/enrol/imsenterprise/tests/imsenterprise_test.php
new file mode 100644 (file)
index 0000000..90dac3d
--- /dev/null
@@ -0,0 +1,332 @@
+<?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/>.
+
+/**
+ * IMS Enterprise enrolment tests.
+ *
+ * @package    enrol_imsenterprise
+ * @category   phpunit
+ * @copyright  2012 David MonllaĆ³
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/enrol/imsenterprise/locallib.php');
+require_once($CFG->dirroot . '/enrol/imsenterprise/lib.php');
+
+/**
+ * IMS Enterprise test case
+ *
+ * @package    enrol_imsenterprise
+ * @category   phpunit
+ * @copyright  2012 David MonllaĆ³
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class enrol_imsenterprise_testcase extends advanced_testcase {
+
+    protected $imsplugin;
+
+    protected function setUp() {
+        $this->resetAfterTest(true);
+        $this->imsplugin = enrol_get_plugin('imsenterprise');
+        $this->set_test_config();
+    }
+
+    /**
+     * With an empty IMS enterprise file
+     */
+    public function test_emptyfile() {
+        global $DB;
+
+        $prevncourses = $DB->count_records('course');
+        $prevnusers = $DB->count_records('user');
+
+        $this->set_xml_file(false, false);
+        $this->imsplugin->cron();
+
+        $this->assertEquals($prevncourses, $DB->count_records('course'));
+        $this->assertEquals($prevnusers, $DB->count_records('user'));
+    }
+
+
+    /**
+     * Existing users are not created again
+     */
+    public function test_users_existing() {
+        global $DB;
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+
+        $prevnusers = $DB->count_records('user');
+
+        $users = array($user1, $user2);
+        $this->set_xml_file($users);
+        $this->imsplugin->cron();
+
+        $this->assertEquals($prevnusers, $DB->count_records('user'));
+    }
+
+
+    /**
+     * Add new users
+     */
+    public function test_users_add() {
+        global $DB;
+
+        $prevnusers = $DB->count_records('user');
+
+        $user1 = new StdClass();
+        $user1->username = 'u1';
+        $user1->email = 'u1@u1.org';
+        $user1->firstname = 'U';
+        $user1->lastname = '1';
+
+        $users = array($user1);
+        $this->set_xml_file($users);
+        $this->imsplugin->cron();
+
+        $this->assertEquals(($prevnusers + 1), $DB->count_records('user'));
+    }
+
+
+    /**
+     * Existing courses are not created again
+     */
+    public function test_courses_existing() {
+        global $DB;
+
+        $course1 = $this->getDataGenerator()->create_course(array('idnumber' => 'id1'));
+        $course2 = $this->getDataGenerator()->create_course(array('idnumber' => 'id2'));
+
+        // Default mapping according to default course attributes - IMS description tags mapping.
+        $course1->imsshort = $course1->fullname;
+        $course2->imsshort = $course2->fullname;
+
+        $prevncourses = $DB->count_records('course');
+
+        $courses = array($course1, $course2);
+        $this->set_xml_file(false, $courses);
+        $this->imsplugin->cron();
+
+        $this->assertEquals($prevncourses, $DB->count_records('course'));
+    }
+
+
+    /**
+     * Add new courses
+     */
+    public function test_courses_add() {
+        global $DB;
+
+        $prevncourses = $DB->count_records('course');
+
+        $course1 = new StdClass();
+        $course1->idnumber = 'id1';
+        $course1->imsshort = 'id1';
+        $course1->category = 'DEFAULT CATNAME';
+
+        $course2 = new StdClass();
+        $course2->idnumber = 'id2';
+        $course2->imsshort = 'id2';
+        $course2->category = 'DEFAULT CATNAME';
+
+        $courses = array($course1, $course2);
+        $this->set_xml_file(false, $courses);
+        $this->imsplugin->cron();
+
+        $this->assertEquals(($prevncourses + 2), $DB->count_records('course'));
+    }
+
+
+    /**
+     * Course attributes mapping to IMS enterprise group description tags
+     */
+    public function test_courses_attrmapping() {
+        global $DB;
+
+        // Setting a all = coursecode (idnumber) mapping.
+        $this->imsplugin->set_config('imscoursemapshortname', 'coursecode');
+        $this->imsplugin->set_config('imscoursemapfullname', 'coursecode');
+        $this->imsplugin->set_config('imscoursemapsummary', 'coursecode');
+
+        $course1 = new StdClass();
+        $course1->idnumber = 'id1';
+        $course1->imsshort = 'description_short1';
+        $course1->imslong = 'description_long';
+        $course1->imsfull = 'description_full';
+        $course1->category = 'DEFAULT CATNAME';
+
+        $this->set_xml_file(false, array($course1));
+        $this->imsplugin->cron();
+
+        $dbcourse = $DB->get_record('course', array('idnumber' => $course1->idnumber));
+        $this->assertFalse(!$dbcourse);
+        $this->assertEquals($dbcourse->shortname, $course1->idnumber);
+        $this->assertEquals($dbcourse->fullname, $course1->idnumber);
+        $this->assertEquals($dbcourse->summary, $course1->idnumber);
+
+
+        // Setting a mapping using all the description tags.
+        $this->imsplugin->set_config('imscoursemapshortname', 'short');
+        $this->imsplugin->set_config('imscoursemapfullname', 'long');
+        $this->imsplugin->set_config('imscoursemapsummary', 'full');
+
+        $course2 = new StdClass();
+        $course2->idnumber = 'id2';
+        $course2->imsshort = 'description_short2';
+        $course2->imslong = 'description_long';
+        $course2->imsfull = 'description_full';
+        $course2->category = 'DEFAULT CATNAME';
+
+        $this->set_xml_file(false, array($course2));
+        $this->imsplugin->cron();
+
+        $dbcourse = $DB->get_record('course', array('idnumber' => $course2->idnumber));
+        $this->assertFalse(!$dbcourse);
+        $this->assertEquals($dbcourse->shortname, $course2->imsshort);
+        $this->assertEquals($dbcourse->fullname, $course2->imslong);
+        $this->assertEquals($dbcourse->summary, $course2->imsfull);
+
+
+        // Setting a mapping where the specified description tags doesn't exist in the XML file (must delegate into idnumber).
+        $this->imsplugin->set_config('imscoursemapshortname', 'short');
+        $this->imsplugin->set_config('imscoursemapfullname', 'long');
+        $this->imsplugin->set_config('imscoursemapsummary', 'full');
+
+        $course3 = new StdClass();
+        $course3->idnumber = 'id3';
+        $course3->imsshort = 'description_short3';
+        $course3->category = 'DEFAULT CATNAME';
+
+        $this->set_xml_file(false, array($course3));
+        $this->imsplugin->cron();
+
+        $dbcourse = $DB->get_record('course', array('idnumber' => $course3->idnumber));
+        $this->assertFalse(!$dbcourse);
+        $this->assertEquals($dbcourse->shortname, $course3->imsshort);
+        $this->assertEquals($dbcourse->fullname, $course3->idnumber);
+        $this->assertEquals($dbcourse->summary, $course3->idnumber);
+
+    }
+
+
+    /**
+     * Sets the plugin configuration for testing
+     */
+    protected function set_test_config() {
+        $this->imsplugin->set_config('mailadmins', false);
+        $this->imsplugin->set_config('prev_path', '');
+        $this->imsplugin->set_config('createnewusers', true);
+        $this->imsplugin->set_config('createnewcourses', true);
+        $this->imsplugin->set_config('createnewcategories', true);
+    }
+
+
+    /**
+     * Creates an IMS enterprise XML file and adds it's path to config settings
+     *
+     * @param array Array of users StdClass
+     * @param array Array of courses StdClass
+     */
+    protected function set_xml_file($users = false, $courses = false) {
+        global $DB;
+
+        $xmlcontent = '<enterprise>';
+
+        // Users.
+        if (!empty($users)) {
+            foreach ($users as $user) {
+                $xmlcontent .= '
+  <person>
+    <sourcedid>
+      <source>TestSource</source>
+      <id>'.$user->username.'</id>
+    </sourcedid>
+    <userid>'.$user->username.'</userid>
+    <name>
+      <fn>'.$user->firstname.' '.$user->lastname.'</fn>
+      <n>
+        <family>'.$user->lastname.'</family>
+        <given>'.$user->firstname.'</given>
+      </n>
+    </name>
+    <email>'.$user->email.'</email>
+  </person>';
+            }
+        }
+
+        // Courses.
+        // Mapping based on default course attributes - IMS group tags mapping.
+        if (!empty($courses)) {
+            foreach ($courses as $course) {
+
+                // orgunit tag value is used by moodle as category name.
+                // If the category does not exists the id is used as name.
+                if ($categoryname = $DB->get_field('course_categories', 'name', array('id' => $course->category))) {
+                    $categoryname = $course->category;
+                }
+
+                $xmlcontent .= '
+  <group>
+    <sourcedid>
+      <source>TestSource</source>
+      <id>'.$course->idnumber.'</id>
+    </sourcedid>
+    <description>';
+
+                // Optional to test course attributes mappings.
+                if (!empty($course->imsshort)) {
+                    $xmlcontent .= '
+      <short>'.$course->imsshort.'</short>';
+                }
+
+                // Optional to test course attributes mappings.
+                if (!empty($course->imslong)) {
+                    $xmlcontent .= '
+      <long>'.$course->imslong.'</long>';
+                }
+
+                // Optional to test course attributes mappings.
+                if (!empty($course->imsfull)) {
+                    $xmlcontent .= '
+      <full>'.$course->imsfull.'</full>';
+                }
+
+                $xmlcontent .= '
+    </description>
+    <org>
+      <orgunit>'.$categoryname.'</orgunit>
+    </org>
+  </group>';
+            }
+        }
+
+        $xmlcontent .= '
+</enterprise>';
+
+        // Creating the XML file.
+        $filename = 'ims_' . rand(1000, 9999) . '.xml';
+        $tmpdir = make_temp_directory('enrol_imsenterprise');
+        $xmlfilepath = $tmpdir . '/' . $filename;
+        file_put_contents($xmlfilepath, $xmlcontent);
+
+        // Setting the file path in CFG.
+        $this->imsplugin->set_config('imsfilelocation', $xmlfilepath);
+    }
+}