diff --git a/enrol/database/lang/en/enrol_database.php b/enrol/database/lang/en/enrol_database.php index d6dba449a8d72..12984ae06b7a8 100644 --- a/enrol/database/lang/en/enrol_database.php +++ b/enrol/database/lang/en/enrol_database.php @@ -49,11 +49,15 @@ $string['localcoursefield'] = 'Local course field'; $string['localrolefield'] = 'Local role field'; $string['localuserfield'] = 'Local user field'; +$string['newcourseenddate'] = 'New course end date field'; +$string['newcourseenddate_desc'] = 'Optional. If not set, then the course end date will be set as configured default course duration. If set, then template value will be overridden.'; $string['newcoursetable'] = 'Remote new courses table'; $string['newcoursetable_desc'] = 'Specify of the name of the table that contains list of courses that should be created automatically. Empty means no courses are created.'; $string['newcoursecategory'] = 'New course category field'; $string['newcoursefullname'] = 'New course full name field'; $string['newcourseidnumber'] = 'New course ID number field'; +$string['newcoursestartdate'] = 'New course start date field'; +$string['newcoursestartdate_desc'] = 'Optional. If not set then, the course start date will be set to the current date. If set, then template value will be overridden.'; $string['newcourseshortname'] = 'New course short name field'; $string['pluginname'] = 'External database'; $string['pluginname_desc'] = 'You can use an external database (of nearly any kind) to control your enrolments. It is assumed your external database contains at least a field containing a course ID, and a field containing a user ID. These are compared against fields that you choose in the local course and user tables.'; diff --git a/enrol/database/lib.php b/enrol/database/lib.php index 97c99e6fac291..ec3d8d65e6495 100644 --- a/enrol/database/lib.php +++ b/enrol/database/lib.php @@ -669,17 +669,24 @@ public function sync_courses(progress_trace $trace) { return 1; } + $courseconfig = get_config('moodlecourse'); + $table = $this->get_config('newcoursetable'); $fullname = trim($this->get_config('newcoursefullname')); $shortname = trim($this->get_config('newcourseshortname')); $idnumber = trim($this->get_config('newcourseidnumber')); $category = trim($this->get_config('newcoursecategory')); + $startdate = trim($this->get_config('newcoursestartdate')); + $enddate = trim($this->get_config('newcourseenddate')); + // Lowercased versions - necessary because we normalise the resultset with array_change_key_case(). $fullname_l = strtolower($fullname); $shortname_l = strtolower($shortname); $idnumber_l = strtolower($idnumber); $category_l = strtolower($category); + $startdatelowercased = strtolower($startdate); + $enddatelowercased = strtolower($enddate); $localcategoryfield = $this->get_config('localcategoryfield', 'id'); $defaultcategory = $this->get_config('defaultcategory'); @@ -698,6 +705,13 @@ public function sync_courses(progress_trace $trace) { if ($idnumber) { $sqlfields[] = $idnumber; } + if ($startdate) { + $sqlfields[] = $startdate; + } + if ($enddate) { + $sqlfields[] = $enddate; + } + $sql = $this->db_get_sql($table, array(), $sqlfields, true); $createcourses = array(); if ($rs = $extdb->Execute($sql)) { @@ -722,6 +736,7 @@ public function sync_courses(progress_trace $trace) { $course->fullname = $fields[$fullname_l]; $course->shortname = $fields[$shortname_l]; $course->idnumber = $idnumber ? $fields[$idnumber_l] : ''; + if ($category) { if (empty($fields[$category_l])) { // Empty category means use default. @@ -738,6 +753,35 @@ public function sync_courses(progress_trace $trace) { } else { $course->category = $defaultcategory; } + + if ($startdate) { + if (!empty($fields[$startdatelowercased])) { + $course->startdate = is_number($fields[$startdatelowercased]) + ? $fields[$startdatelowercased] + : strtotime($fields[$startdatelowercased]); + + // Broken start date. Stop syncing this course. + if ($course->startdate === false) { + $trace->output('error: invalid external course start date value: ' . json_encode($fields), 1); + continue; + } + } + } + + if ($enddate) { + if (!empty($fields[$enddatelowercased])) { + $course->enddate = is_number($fields[$enddatelowercased]) + ? $fields[$enddatelowercased] + : strtotime($fields[$enddatelowercased]); + + // Broken end date. Stop syncing this course. + if ($course->enddate === false) { + $trace->output('error: invalid external course end date value: ' . json_encode($fields), 1); + continue; + } + } + } + $createcourses[] = $course; } } @@ -769,7 +813,6 @@ public function sync_courses(progress_trace $trace) { } } if (!$template) { - $courseconfig = get_config('moodlecourse'); $template = new stdClass(); $template->summary = ''; $template->summaryformat = FORMAT_HTML; @@ -798,6 +841,25 @@ public function sync_courses(progress_trace $trace) { $newcourse->idnumber = $fields->idnumber; $newcourse->category = $fields->category; + if (isset($fields->startdate)) { + $newcourse->startdate = $fields->startdate; + } + + if (isset($fields->enddate)) { + // Validating end date. + if ($fields->enddate > 0 && $newcourse->startdate > $fields->enddate) { + $trace->output( + "can not insert new course, the end date must be after the start date: " . $newcourse->shortname, 1 + ); + continue; + } + $newcourse->enddate = $fields->enddate; + } else { + if ($courseconfig->courseenddateenabled) { + $newcourse->enddate = $newcourse->startdate + $courseconfig->courseduration; + } + } + // Detect duplicate data once again, above we can not find duplicates // in external data using DB collation rules... if ($DB->record_exists('course', array('shortname' => $newcourse->shortname))) { diff --git a/enrol/database/settings.php b/enrol/database/settings.php index 92d0833b3fd5c..78d222bc3ecfa 100644 --- a/enrol/database/settings.php +++ b/enrol/database/settings.php @@ -113,6 +113,16 @@ $settings->add(new admin_setting_configtext('enrol_database/newcourseidnumber', get_string('newcourseidnumber', 'enrol_database'), '', 'idnumber')); + $settings->add(new admin_setting_configtext( + 'enrol_database/newcoursestartdate', + get_string('newcoursestartdate', 'enrol_database'), + get_string('newcoursestartdate_desc', 'enrol_database'), '')); + + $settings->add(new admin_setting_configtext( + 'enrol_database/newcourseenddate', + get_string('newcourseenddate', 'enrol_database'), + get_string('newcourseenddate_desc', 'enrol_database'), '')); + $settings->add(new admin_setting_configtext('enrol_database/newcoursecategory', get_string('newcoursecategory', 'enrol_database'), '', '')); $settings->add(new admin_settings_coursecat_select('enrol_database/defaultcategory', diff --git a/enrol/database/tests/sync_test.php b/enrol/database/tests/sync_test.php index 4b98153eafce9..b1cbf7d2e107d 100644 --- a/enrol/database/tests/sync_test.php +++ b/enrol/database/tests/sync_test.php @@ -151,6 +151,8 @@ protected function init_enrol_database() { $table->add_field('shortname', XMLDB_TYPE_CHAR, '255', null, null, null); $table->add_field('idnumber', XMLDB_TYPE_CHAR, '255', null, null, null); $table->add_field('category', XMLDB_TYPE_CHAR, '255', null, null, null); + $table->add_field('startdate', XMLDB_TYPE_CHAR, '255', null, null, null); + $table->add_field('enddate', XMLDB_TYPE_CHAR, '255', null, null, null); $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); if ($dbman->table_exists($table)) { $dbman->drop_table($table); @@ -441,7 +443,6 @@ public function test_sync_users(): void { // Test basic enrol sync for one user after login. - $this->reset_enrol_database(); $plugin->set_config('localcoursefield', 'idnumber'); $plugin->set_config('localuserfield', 'idnumber'); $plugin->set_config('localrolefield', 'shortname'); @@ -799,8 +800,148 @@ public function test_sync_courses(): void { $this->assertEquals(1+2+1+4+1+count(self::$courses), $DB->count_records('course')); $this->assertTrue($DB->record_exists('course', array('idnumber' => 'ncid9'))); - // Final cleanup - remove extra tables, fixtures and caches. $this->cleanup_enrol_database(); } + + /** + * Test syncing courses with start and end dates. + * + * @covers \enrol_database_plugin::sync_courses + */ + public function test_sync_courses_start_end_dates(): void { + global $DB; + + $this->resetAfterTest(); + $this->preventResetByRollback(); + $this->init_enrol_database(); + + $courseconfig = get_config('moodlecourse'); + $nextyear = (int) date('Y') + 1; + $prev = (int) date('Y') - 1; + + $midnightstartdate = usergetmidnight(time()); + $midnightenddate = usergetmidnight(time()) + $courseconfig->courseduration; + + $plugin = enrol_get_plugin('database'); + + $trace = new \null_progress_trace(); + + $course1 = ['fullname' => 'C1', 'shortname' => 'c1', 'idnumber' => '', 'startdate' => 0, + 'enddate' => 0]; + $course2 = ['fullname' => 'C2', 'shortname' => 'c2', 'idnumber' => '', 'startdate' => null, + 'enddate' => null]; + // This course won't be created. Broken start date. + $course3 = ['fullname' => 'C3', 'shortname' => 'c3', 'idnumber' => '', 'startdate' => 'not date', + 'enddate' => 0]; + // This course won't be created. Broken end date. + $course4 = ['fullname' => 'C4', 'shortname' => 'c4', 'idnumber' => '', 'startdate' => 0, + 'enddate' => 'not date']; + // This course won't be created. Start date after end date. + $course5 = ['fullname' => 'C5', 'shortname' => 'c5', 'idnumber' => '', 'startdate' => '12.05.2024', + 'enddate' => '12.05.2021']; + $course6 = ['fullname' => 'C6', 'shortname' => 'c6', 'idnumber' => '', 'startdate' => '2024-05-22', + 'enddate' => '2027-05-12']; + $course7 = ['fullname' => 'C7', 'shortname' => 'c7', 'idnumber' => '', 'startdate' => null, + 'enddate' => '12.05.' . $nextyear]; + $course8 = ['fullname' => 'C8', 'shortname' => 'c8', 'idnumber' => '', 'startdate' => '12.05.2024', + 'enddate' => null]; + // This course won't be created. Start date is not set, but it should be set to date after end date. + $course9 = ['fullname' => 'C9', 'shortname' => 'c9', 'idnumber' => '', 'startdate' => null, + 'enddate' => '12.05.' . $prev]; + + $DB->insert_record('enrol_database_test_courses', $course1); + $DB->insert_record('enrol_database_test_courses', $course2); + $DB->insert_record('enrol_database_test_courses', $course3); + $DB->insert_record('enrol_database_test_courses', $course4); + $DB->insert_record('enrol_database_test_courses', $course5); + $DB->insert_record('enrol_database_test_courses', $course6); + $DB->insert_record('enrol_database_test_courses', $course7); + $DB->insert_record('enrol_database_test_courses', $course8); + $DB->insert_record('enrol_database_test_courses', $course9); + + // Mess with case as we need to check that fields are lower cased. + $plugin->set_config('newcoursestartdate', 'StartDaTE'); + $plugin->set_config('newcourseenddate', 'ENDdATE'); + + $plugin->sync_courses($trace); + + // Course 3, course 4, course 5 and course 9 should not be created. + $this->assertTrue($DB->record_exists('course', ['shortname' => $course1['shortname']])); + $this->assertTrue($DB->record_exists('course', ['shortname' => $course2['shortname']])); + $this->assertFalse($DB->record_exists('course', ['shortname' => $course3['shortname']])); + $this->assertFalse($DB->record_exists('course', ['shortname' => $course4['shortname']])); + $this->assertFalse($DB->record_exists('course', ['shortname' => $course5['shortname']])); + $this->assertTrue($DB->record_exists('course', ['shortname' => $course6['shortname']])); + $this->assertTrue($DB->record_exists('course', ['shortname' => $course7['shortname']])); + $this->assertTrue($DB->record_exists('course', ['shortname' => $course8['shortname']])); + $this->assertFalse($DB->record_exists('course', ['shortname' => $course9['shortname']])); + + // Check dates for created courses. + $this->assertEquals($midnightstartdate, $DB->get_field('course', 'startdate', ['shortname' => $course1['shortname']])); + $this->assertEquals($midnightenddate, $DB->get_field('course', 'enddate', ['shortname' => $course1['shortname']])); + + $this->assertEquals($midnightstartdate, $DB->get_field('course', 'startdate', ['shortname' => $course2['shortname']])); + $this->assertEquals($midnightenddate, $DB->get_field('course', 'enddate', ['shortname' => $course2['shortname']])); + + $this->assertEquals(strtotime('22.05.2024'), $DB->get_field('course', 'startdate', ['shortname' => $course6['shortname']])); + $this->assertEquals(strtotime('12.05.2027'), $DB->get_field('course', 'enddate', ['shortname' => $course6['shortname']])); + + $this->assertEquals($midnightstartdate, $DB->get_field('course', 'startdate', ['shortname' => $course7['shortname']])); + $expected = strtotime('12.05.' . $nextyear); + $this->assertEquals($expected, $DB->get_field('course', 'enddate', ['shortname' => $course7['shortname']])); + + $this->assertEquals(strtotime('12.05.2024'), $DB->get_field('course', 'startdate', ['shortname' => $course8['shortname']])); + $expected = strtotime('12.05.2024') + $courseconfig->courseduration; + $this->assertEquals($expected, $DB->get_field('course', 'enddate', ['shortname' => $course8['shortname']])); + + // Push course with dates as timestamp. + $course10 = ['fullname' => 'C10', 'shortname' => 'c10', 'idnumber' => '', 'startdate' => 1810051200, + 'enddate' => 1810051211]; + $DB->insert_record('enrol_database_test_courses', $course10); + + $plugin->sync_courses($trace); + + $this->assertTrue($DB->record_exists('course', ['shortname' => $course10['shortname']])); + $this->assertEquals(1810051200, $DB->get_field('course', 'startdate', ['shortname' => $course10['shortname']])); + $this->assertEquals(1810051211, $DB->get_field('course', 'enddate', ['shortname' => $course10['shortname']])); + + // Push course with broken dates, but delete dates from plugin configuration before syncing. + $course11 = ['fullname' => 'C11', 'shortname' => 'c11', 'idnumber' => '', 'startdate' => 'not date', + 'enddate' => 'not date']; + $DB->insert_record('enrol_database_test_courses', $course11); + + $plugin->set_config('newcoursestartdate', ''); + $plugin->set_config('newcourseenddate', ''); + $plugin->sync_courses($trace); + + $this->assertTrue($DB->record_exists('course', ['shortname' => $course11['shortname']])); + $this->assertEquals($midnightstartdate, $DB->get_field('course', 'startdate', ['shortname' => $course11['shortname']])); + $this->assertEquals($midnightenddate, $DB->get_field('course', 'enddate', ['shortname' => $course11['shortname']])); + + // Push courses with correct dates, but set date configuration to not existing date fields. + $course12 = ['fullname' => 'C12', 'shortname' => 'c12', 'idnumber' => '', 'startdate' => '2024-05-22', + 'enddate' => '2027-05-12']; + $DB->insert_record('enrol_database_test_courses', $course11); + + $plugin->set_config('newcoursestartdate', 'startdate'); + $plugin->set_config('newcourseenddate', 'ed'); + $plugin->sync_courses($trace); + + // Course should not be synced to prevent setting up incorrect dates. + $this->assertFalse($DB->record_exists('course', ['shortname' => $course12['shortname']])); + + $course13 = ['fullname' => 'C13', 'shortname' => 'c13', 'idnumber' => '', 'startdate' => '2024-05-22', + 'enddate' => '2027-05-12']; + $DB->insert_record('enrol_database_test_courses', $course11); + + $plugin->set_config('newcoursestartdate', 'sd'); + $plugin->set_config('newcourseenddate', 'enddate'); + $plugin->sync_courses($trace); + + // Course should not be synced to prevent setting up incorrect dates. + $this->assertFalse($DB->record_exists('course', ['shortname' => $course13['shortname']])); + + $this->cleanup_enrol_database(); + } } diff --git a/enrol/database/version.php b/enrol/database/version.php index 3297172be4e70..afc419d60be05 100644 --- a/enrol/database/version.php +++ b/enrol/database/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX). +$plugin->version = 2024042201; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2024041600; // Requires this Moodle version. $plugin->component = 'enrol_database'; // Full name of the plugin (used for diagnostics)