diff --git a/enrol/imsenterprise/lang/en/enrol_imsenterprise.php b/enrol/imsenterprise/lang/en/enrol_imsenterprise.php index 515a1f17a0ca8..fc38894202e76 100644 --- a/enrol/imsenterprise/lang/en/enrol_imsenterprise.php +++ b/enrol/imsenterprise/lang/en/enrol_imsenterprise.php @@ -26,6 +26,8 @@ $string['allowunenrol'] = 'Allow the IMS data to unenrol students/teachers'; $string['allowunenrol_desc'] = 'If enabled, course enrolments will be removed when specified in the Enterprise data.'; $string['basicsettings'] = 'Basic settings'; +$string['categoryseparator'] = 'Category Separator Character'; +$string['categoryseparator_desc'] = 'You can create nested categories (if you allow category creation). '; $string['coursesettings'] = 'Course data options'; $string['createnewcategories'] = 'Create new (hidden) course categories if not found in Moodle'; $string['createnewcategories_desc'] = 'If the element is present in a course\'s incoming data, its content will be used to specify a category if the course is to be created from scratch. The plugin will NOT re-categorise existing courses. @@ -75,6 +77,10 @@ Some student information systems fail to output the field. If this is the case, you should enable this setting to allow for using the as the Moodle user ID. Otherwise, leave this setting disabled.'; $string['truncatecoursecodes'] = 'Truncate course codes to this length'; $string['truncatecoursecodes_desc'] = 'In some situations you may have course codes which you wish to truncate to a specified length before processing. If so, enter the number of characters in this box. Otherwise, leave the box blank and no truncation will occur.'; +$string['updatecourses'] = 'Update course full names'; +$string['updatecourses_desc'] = 'If enabled, the IMS Enterprise enrolment plugin can update course full names (if the "recstatus" flag is set to 2, which represents an update).'; +$string['updateusers'] = 'Update user accounts when specified in IMS data'; +$string['updateusers_desc'] = 'If enabled, IMS Enterprise enrolment data can specify changes to user accounts (if the "recstatus" flag is set to 2, which represents an update).'; $string['usecapitafix'] = 'Tick this box if using "Capita" (their XML format is slightly wrong)'; $string['usecapitafix_desc'] = 'The student data system produced by Capita has been found to have one slight error in its XML output. If you are using Capita you should enable this setting - otherwise leave it un-ticked.'; $string['usersettings'] = 'User data options'; diff --git a/enrol/imsenterprise/lib.php b/enrol/imsenterprise/lib.php index 56c4d06ae7edc..a91f42cc31b55 100644 --- a/enrol/imsenterprise/lib.php +++ b/enrol/imsenterprise/lib.php @@ -39,6 +39,21 @@ */ class enrol_imsenterprise_plugin extends enrol_plugin { + /** + * @var IMSENTERPRISE_ADD imsenterprise add action. + */ + const IMSENTERPRISE_ADD = 1; + + /** + * @var IMSENTERPRISE_UPDATE imsenterprise update action. + */ + const IMSENTERPRISE_UPDATE = 2; + + /** + * @var IMSENTERPRISE_DELETE imsenterprise delete action. + */ + const IMSENTERPRISE_DELETE = 3; + /** * @var $logfp resource file pointer for writing log data to. */ @@ -276,7 +291,14 @@ protected function process_group_tag($tagcontents) { // Get configs. $truncatecoursecodes = $this->get_config('truncatecoursecodes'); $createnewcourses = $this->get_config('createnewcourses'); + $updatecourses = $this->get_config('updatecourses'); $createnewcategories = $this->get_config('createnewcategories'); + $categoryseparator = trim($this->get_config('categoryseparator')); + + // Ensure a default is set for the category separator. + if (empty($categoryseparator)) { + $categoryseparator = '|'; + } if ($createnewcourses) { require_once("$CFG->dirroot/course/lib.php"); @@ -320,12 +342,13 @@ protected function process_group_tag($tagcontents) { // Third, check if the course(s) exist. foreach ($group->coursecode as $coursecode) { $coursecode = trim($coursecode); - if (!$DB->get_field('course', 'id', array('idnumber' => $coursecode))) { + $dbcourse = $DB->get_field('course', 'id', array('idnumber' => $coursecode)); + if (!$dbcourse) { if (!$createnewcourses) { $this->log_line("Course $coursecode not found in Moodle's course idnumbers."); } else { - // Create the (hidden) course(s) if not found + // Create the (hidden) course(s) if not found. $courseconfig = get_config('moodlecourse'); // Load Moodle Course shell defaults. // New course. @@ -361,27 +384,49 @@ protected function process_group_tag($tagcontents) { // Insert default names for teachers/students, from the current language. // Handle course categorisation (taken from the group.org.orgunit field if present). - if (!empty($group->category)) { - // If the category is defined and exists in Moodle, we want to store it in that one. - if ($catid = $DB->get_field('course_categories', 'id', array('name' => $group->category))) { - $course->category = $catid; - } else if ($createnewcategories) { - // Else if we're allowed to create new categories, let's create this one. - $newcat = new stdClass(); - $newcat->name = $group->category; - $newcat->visible = 0; - $catid = $DB->insert_record('course_categories', $newcat); - $course->category = $catid; - $this->log_line("Created new (hidden) category, #$catid: $newcat->name"); - } else { - // If not found and not allowed to create, stick with default. - $this->log_line('Category '.$group->category.' not found in Moodle database, so using '. - 'default category instead.'); - $course->category = $this->get_default_category_id(); + if (strlen($group->category) > 0) { + $sep = '{\\'.$categoryseparator.'}'; + $matches = preg_split($sep, $group->category, -1, PREG_SPLIT_NO_EMPTY); + + // Categories can be nested. + // For example: "Fall 2013|Biology" is the "Biology" category nested under the "Fall 2013" category. + // Iterate through each category and create it if necessary. + + $catid = 0; + $fullnestedcatname = ''; + foreach ($matches as $catname) { + $catname = trim($catname); + if (strlen($fullnestedcatname)) { + $fullnestedcatname .= ' / '; + } + $fullnestedcatname .= $catname; + $parentid = $catid; + if ($catid = $DB->get_field('course_categories', 'id', + array('name' => $catname, 'parent' => $parentid))) { + $course->category = $catid; + continue; // This category already exists. + } + if ($createnewcategories) { + // Else if we're allowed to create new categories, let's create this one. + $newcat = new stdClass(); + $newcat->name = $catname; + $newcat->visible = 0; + $newcat->parent = $parentid; + $catid = $DB->insert_record('course_categories', $newcat); + $this->log_line("Created new (hidden) category '$fullnestedcatname'"); + $course->category = $catid; + } else { + // If not found and not allowed to create, stick with default. + $this->log_line('Category '.$group->category.' not found in Moodle database, so using '. + 'default category instead.'); + $course->category = $this->get_default_category_id(); + break; + } } } else { $course->category = $this->get_default_category_id(); } + $course->startdate = time(); // Choose a sort order that puts us at the start of the list! $course->sortorder = 0; @@ -390,9 +435,41 @@ protected function process_group_tag($tagcontents) { $this->log_line("Created course $coursecode in Moodle (Moodle ID is $course->id)"); } - } else if ($recstatus == 3 && ($courseid = $DB->get_field('course', 'id', array('idnumber' => $coursecode)))) { + } else if (($recstatus == self::IMSENTERPRISE_UPDATE) && $dbcourse) { + if ($updatecourses) { + // Update course. Allowed fields to be updated are: + // Short Name, and Full Name. + $hasupdates = false; + if (!empty($group->short)) { + if ($group->short != $dbcourse->shortname) { + $dbcourse->shortname = $group->short; + $hasupdates = true; + } + } + if (!empty($group->full)) { + if ($group->full != $dbcourse->fullname) { + $dbcourse->fullname = $group->full; + $hasupdates = true; + } + } + if ($hasupdates) { + $DB->update_record('course', $dbcourse); + $courseid = $dbcourse->id; + add_to_log(SITEID, "course", "update", "view.php?id=$courseid", "ID $courseid"); + $this->log_line("Updated course $coursecode in Moodle (Moodle ID is $courseid)"); + } + } else { + // Update courses option is not enabled. Ignore. + $this->log_line("Ignoring update to course $coursecode"); + } + } else if (($recstatus == self::IMSENTERPRISE_DELETE) && $dbcourse) { // If course does exist, but recstatus==3 (delete), then set the course as hidden. - $DB->set_field('course', 'visible', '0', array('id' => $courseid)); + $courseid = $dbcourse->id; + $dbcourse->visible = 0; + $DB->update_record('course', $dbcourse); + add_to_log(SITEID, "course", "update", "view.php?id=$courseid", + "Updated (set to hidden) course $coursecode (Moodle ID is $courseid)"); + $this->log_line("Updated (set to hidden) course $coursecode in Moodle (Moodle ID is $courseid)"); } } } @@ -400,8 +477,7 @@ protected function process_group_tag($tagcontents) { /** * Process the person tag. This defines a Moodle user. - * - * @param string $tagcontents The raw contents of the XML element + * @param string $tagconents The raw contents of the XML element */ protected function process_person_tag($tagcontents) { global $CFG, $DB; @@ -412,6 +488,7 @@ protected function process_person_tag($tagcontents) { $fixcasepersonalnames = $this->get_config('fixcasepersonalnames'); $imsdeleteusers = $this->get_config('imsdeleteusers'); $createnewusers = $this->get_config('createnewusers'); + $imsupdateusers = $this->get_config('imsupdateusers'); $person = new stdClass(); if (preg_match('{.*?(.+?).*?}is', $tagcontents, $matches)) { @@ -423,11 +500,14 @@ protected function process_person_tag($tagcontents) { if (preg_match('{.*?.*?(.+?).*?.*?}is', $tagcontents, $matches)) { $person->lastname = trim($matches[1]); } - if (preg_match('{(.*?)}is', $tagcontents, $matches)) { + if (preg_match('{(.*?)}is', $tagcontents, $matches)) { $person->username = trim($matches[1]); } + if (preg_match('{.*?}is', $tagcontents, $matches)) { + $person->auth = trim($matches[1]); + } if ($imssourcedidfallback && trim($person->username) == '') { - // This is the point where we can fall back to useing the "sourcedid" if "userid" is not supplied + // This is the point where we can fall back to useing the "sourcedid" if "userid" is not supplied. // NB We don't use an "elseif" because the tag may be supplied-but-empty. $person->username = $person->idnumber; } @@ -460,7 +540,7 @@ protected function process_person_tag($tagcontents) { $recstatus = ($this->get_recstatus($tagcontents, 'person')); // Now if the recstatus is 3, we should delete the user if-and-only-if the setting for delete users is turned on. - if ($recstatus == 3) { + if ($recstatus == self::IMSENTERPRISE_DELETE) { if ($imsdeleteusers) { // If we're allowed to delete user records. // Do not dare to hack the user.deleted field directly in database!!! @@ -477,6 +557,18 @@ protected function process_person_tag($tagcontents) { } else { $this->log_line("Ignoring deletion request for user '$person->username' (ID number $person->idnumber)."); } + } else if ($recstatus == self::IMSENTERPRISE_UPDATE) { // Update user. + if ($imsupdateusers) { + if ($id = $DB->get_field('user', 'id', array('idnumber' => $person->idnumber))) { + $person->id = $id; + $DB->update_record('user', $person); + $this->log_line("Updated user $person->username"); + } else { + $this->log_line("Ignoring update request for non-existent user $person->username"); + } + } else { + $this->log_line("Ignoring update request for user $person->username"); + } } else { // Add or update record. @@ -494,9 +586,11 @@ protected function process_person_tag($tagcontents) { // If they don't exist and they have a defined username, and $createnewusers == true, we create them. $person->lang = $CFG->lang; // TODO: MDL-15863 this needs more work due to multiauth changes, use first auth for now. - $auth = explode(',', $CFG->auth); - $auth = reset($auth); - $person->auth = $auth; + if (empty($person->auth)) { + $auth = explode(',', $CFG->auth); + $auth = reset($auth); + $person->auth = $auth; + } $person->confirmed = 1; $person->timemodified = time(); $person->mnethostid = $CFG->mnet_localhost_id; @@ -568,7 +662,7 @@ protected function process_membership_tag($tagcontents) { } $recstatus = ($this->get_recstatus($mmatch[1], 'role')); - if ($recstatus == 3) { + if ($recstatus == self::IMSENTERPRISE_DELETE) { // See above - recstatus of 3 (==delete) is treated the same as status of 0. $member->status = 0; } diff --git a/enrol/imsenterprise/settings.php b/enrol/imsenterprise/settings.php index 8f7c734ee3383..a20d92a05f46b 100644 --- a/enrol/imsenterprise/settings.php +++ b/enrol/imsenterprise/settings.php @@ -50,6 +50,9 @@ $settings->add(new admin_setting_configcheckbox('enrol_imsenterprise/createnewusers', get_string('createnewusers', 'enrol_imsenterprise'), get_string('createnewusers_desc', 'enrol_imsenterprise'), 0)); + $settings->add(new admin_setting_configcheckbox('enrol_imsenterprise/imsupdateusers', + get_string('updateusers', 'enrol_imsenterprise'), get_string('updateusers_desc', 'enrol_imsenterprise'), 0)); + $settings->add(new admin_setting_configcheckbox('enrol_imsenterprise/imsdeleteusers', get_string('deleteusers', 'enrol_imsenterprise'), get_string('deleteusers_desc', 'enrol_imsenterprise'), 0)); @@ -88,10 +91,17 @@ $settings->add(new admin_setting_configcheckbox('enrol_imsenterprise/createnewcourses', get_string('createnewcourses', 'enrol_imsenterprise'), get_string('createnewcourses_desc', 'enrol_imsenterprise'), 0)); + $settings->add(new admin_setting_configcheckbox('enrol_imsenterprise/updatecourses', + get_string('updatecourses', 'enrol_imsenterprise'), get_string('updatecourses_desc', 'enrol_imsenterprise'), 0)); + $settings->add(new admin_setting_configcheckbox('enrol_imsenterprise/createnewcategories', get_string('createnewcategories', 'enrol_imsenterprise'), get_string('createnewcategories_desc', 'enrol_imsenterprise'), 0)); + $settings->add(new admin_setting_configtext('enrol_imsenterprise/categoryseparator', + get_string('categoryseparator', 'enrol_imsenterprise'), get_string('categoryseparator_desc', 'enrol_imsenterprise'), '|', + PARAM_TEXT, 3)); + $settings->add(new admin_setting_configcheckbox('enrol_imsenterprise/imsunenrol', get_string('allowunenrol', 'enrol_imsenterprise'), get_string('allowunenrol_desc', 'enrol_imsenterprise'), 0)); diff --git a/enrol/imsenterprise/tests/imsenterprise_test.php b/enrol/imsenterprise/tests/imsenterprise_test.php index 113ef11e08167..f407a4d95f4cb 100644 --- a/enrol/imsenterprise/tests/imsenterprise_test.php +++ b/enrol/imsenterprise/tests/imsenterprise_test.php @@ -96,6 +96,7 @@ public function test_users_add() { $prevnusers = $DB->count_records('user'); $user1 = new StdClass(); + $user1->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD; $user1->username = 'u1'; $user1->email = 'u1@example.com'; $user1->firstname = 'U'; @@ -108,6 +109,72 @@ public function test_users_add() { $this->assertEquals(($prevnusers + 1), $DB->count_records('user')); } + /** + * Add new users and set an auth type + */ + public function test_users_add_with_auth() { + global $DB; + + $prevnusers = $DB->count_records('user'); + + $user2 = new StdClass(); + $user2->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD; + $user2->username = 'u2'; + $user2->auth = 'cas'; + $user2->email = 'u2@u2.org'; + $user2->firstname = 'U'; + $user2->lastname = '2'; + + $users = array($user2); + $this->set_xml_file($users); + $this->imsplugin->cron(); + + $dbuser = $DB->get_record('user', array('username' => $user2->username)); + // TODO: MDL-15863 this needs more work due to multiauth changes, use first auth for now. + $dbauth = explode(',', $dbuser->auth); + $dbauth = reset($dbauth); + + $this->assertEquals(($prevnusers + 1), $DB->count_records('user')); + $this->assertEquals($dbauth, $user2->auth); + } + + + /** + * Update user + */ + public function test_user_update() { + global $DB; + + $user3 = new StdClass(); + $user3->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD; + $user3->username = 'u3'; + $user3->email = 'u3@u3.org'; + $user3->firstname = 'U'; + $user3->lastname = '3'; + + $users = array($user3); + $this->set_xml_file($users); + $this->imsplugin->cron(); + + $user3u = $DB->get_record('user', array('username' => $user3->username)); + + $user3u->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_UPDATE; + $user3u->email = 'updated_u3@updated_u3.org'; + $user3u->firstname = 'updated_U'; + $user3u->lastname = 'updated_3'; + + $users = array($user3u); + $this->set_xml_file($users); + $this->imsplugin->cron(); + + $dbuser = $DB->get_record('user', array('username' => $user3->username)); + + $this->assertEquals($dbuser->email, $user3u->email); + $this->assertEquals($dbuser->firstname, $user3u->firstname); + $this->assertEquals($dbuser->lastname, $user3u->lastname); + } + + /** * Existing courses are not created again */ @@ -139,11 +206,13 @@ public function test_courses_add() { $prevncourses = $DB->count_records('course'); $course1 = new StdClass(); + $course1->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD; $course1->idnumber = 'id1'; $course1->imsshort = 'id1'; $course1->category = 'DEFAULT CATNAME'; $course2 = new StdClass(); + $course2->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD; $course2->idnumber = 'id2'; $course2->imsshort = 'id2'; $course2->category = 'DEFAULT CATNAME'; @@ -194,6 +263,7 @@ public function test_courses_attrmapping() { $this->imsplugin->set_config('imscoursemapsummary', 'coursecode'); $course1 = new StdClass(); + $course1->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD; $course1->idnumber = 'id1'; $course1->imsshort = 'description_short1'; $course1->imslong = 'description_long'; @@ -215,6 +285,7 @@ public function test_courses_attrmapping() { $this->imsplugin->set_config('imscoursemapsummary', 'full'); $course2 = new StdClass(); + $course2->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD; $course2->idnumber = 'id2'; $course2->imsshort = 'description_short2'; $course2->imslong = 'description_long'; @@ -236,6 +307,7 @@ public function test_courses_attrmapping() { $this->imsplugin->set_config('imscoursemapsummary', 'full'); $course3 = new StdClass(); + $course3->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD; $course3->idnumber = 'id3'; $course3->imsshort = 'description_short3'; $course3->category = 'DEFAULT CATNAME'; @@ -251,6 +323,138 @@ public function test_courses_attrmapping() { } + /** + * Course updates + */ + public function test_course_update() { + global $DB; + + $course4 = new StdClass(); + $course4->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD; + $course4->idnumber = 'id4'; + $course4->imsshort = 'id4'; + $course4->imsfull = 'id4'; + $course4->category = 'DEFAULT CATNAME'; + + $this->set_xml_file(false, array($course4)); + $this->imsplugin->cron(); + + $course4u = $DB->get_record('course', array('idnumber' => $course4->idnumber)); + + $course4u->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_UPDATE; + $course4u->imsshort = 'description_short_updated'; + $course4u->imsfull = 'description_full_updated'; + + $this->set_xml_file(false, array($course4u)); + $this->imsplugin->cron(); + + $dbcourse = $DB->get_record('course', array('idnumber' => $course4->idnumber)); + $this->assertFalse(!$dbcourse); + $this->assertEquals($dbcourse->shortname, $course4u->imsshort); + $this->assertEquals($dbcourse->fullname, $course4u->imsfull); + } + + + /** + * Nested categories during course creation + */ + public function test_nested_categories() { + global $DB; + + $catsep = trim($this->imsplugin->get_config('categoryseparator')); + + $topcat = 'DEFAULT CATNAME'; + $subcat = 'DEFAULT SUB CATNAME'; + + $fullcat = $topcat.$catsep.$subcat; + + $course5 = new StdClass(); + $course5->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD; + $course5->idnumber = 'id5'; + $course5->imsshort = 'description_short'; + $course5->imslong = 'description_long'; + $course5->imsfull = 'description_full'; + $course5->category = $fullcat; + + $this->set_xml_file(false, array($course5)); + $this->imsplugin->cron(); + + $parentcatid = $DB->get_field('course_categories', 'id', array('name' => $topcat)); + $subcatid = $DB->get_field('course_categories', 'id', array('name' => $subcat,'parent' => $parentcatid)); + + $this->assertTrue(isset($subcatid)); + $this->assertTrue($subcatid > 0); + + # Change the category separator character + $this->imsplugin->set_config('categoryseparator', ':'); + + $catsep = trim($this->imsplugin->get_config('categoryseparator')); + + $topcat = 'DEFAULT CATNAME'; + $subcat = 'DEFAULT SUB CATNAME TEST2'; + + $fullcat = $topcat.$catsep.$subcat; + + $course6 = new StdClass(); + $course6->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD; + $course6->idnumber = 'id6'; + $course6->imsshort = 'description_short'; + $course6->imslong = 'description_long'; + $course6->imsfull = 'description_full'; + $course6->category = $fullcat; + + $this->set_xml_file(false, array($course6)); + $this->imsplugin->cron(); + + $parentcatid = $DB->get_field('course_categories', 'id', array('name' => $topcat)); + $subcatid = $DB->get_field('course_categories', 'id', array('name' => $subcat,'parent' => $parentcatid)); + + $this->assertTrue(isset($subcatid)); + $this->assertTrue($subcatid > 0); + } + + + /** + * Test that duplicate nested categories are not created + */ + public function test_nested_categories_for_dups() { + global $DB; + + $catsep = trim($this->imsplugin->get_config('categoryseparator')); + + $topcat = 'DEFAULT CATNAME'; + $subcat = 'DEFAULT SUB CATNAME DUPTEST'; + + $fullcat = $topcat.$catsep.$subcat; + + $course7 = new StdClass(); + $course7->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD; + $course7->idnumber = 'id7'; + $course7->imsshort = 'description_short'; + $course7->imslong = 'description_long'; + $course7->imsfull = 'description_full'; + $course7->category = $fullcat; + + $this->set_xml_file(false, array($course7)); + $this->imsplugin->cron(); + + $prevncategories = $DB->count_records('course_categories'); + + $course8 = new StdClass(); + $course8->recstatus = enrol_imsenterprise_plugin::IMSENTERPRISE_ADD; + $course8->idnumber = 'id8'; + $course8->imsshort = 'description_short'; + $course8->imslong = 'description_long'; + $course8->imsfull = 'description_full'; + $course8->category = $fullcat; + + $this->set_xml_file(false, array($course8)); + $this->imsplugin->cron(); + + $this->assertEquals($prevncategories, $DB->count_records('course_categories')); + } + + /** * Sets the plugin configuration for testing */ @@ -258,7 +462,9 @@ public 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('imsupdateusers', true); $this->imsplugin->set_config('createnewcourses', true); + $this->imsplugin->set_config('updatecourses', true); $this->imsplugin->set_config('createnewcategories', true); } @@ -276,12 +482,26 @@ public function set_xml_file($users = false, $courses = false) { if (!empty($users)) { foreach ($users as $user) { $xmlcontent .= ' - + recstatus)) { + $xmlcontent .= ' recstatus="'.$user->recstatus.'"'; + } + + $xmlcontent .= '> TestSource '.$user->username.' - '.$user->username.' + auth)) { + $xmlcontent .= ' authenticationtype="'.$user->auth.'"'; + } + + $xmlcontent .= '>'.$user->username.' '.$user->firstname.' '.$user->lastname.' @@ -300,7 +520,14 @@ public function set_xml_file($users = false, $courses = false) { foreach ($courses as $course) { $xmlcontent .= ' - + recstatus)) { + $xmlcontent .= ' recstatus="'.$course->recstatus.'"'; + } + + $xmlcontent .= '> TestSource '.$course->idnumber.'