diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d646a17..2669a5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,3 +9,4 @@ jobs: with: disable_behat: true disable_phpcpd: true + extra_plugin_runners: 'moodle-plugin-ci add-plugin --branch MOODLE_404_STABLE catalyst/moodle-tool_dynamic_cohorts' diff --git a/classes/helper.php b/classes/helper.php new file mode 100644 index 0000000..adbb414 --- /dev/null +++ b/classes/helper.php @@ -0,0 +1,265 @@ +. + +namespace tool_enrolprofile; + +use context_system; +use stdClass; +use tool_dynamic_cohorts\cohort_manager; +use tool_dynamic_cohorts\condition_base; +use tool_dynamic_cohorts\rule; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/cohort/lib.php'); + +/** + * Helper class. + * + * @package tool_enrolprofile + * @copyright 2024 Dmitrii Metelkin + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class helper { + + /** + * Tag item type. + */ + public const ITEM_TYPE_TAG = 'tag'; + + /** + * Category item type. + */ + public const ITEM_TYPE_CATEGORY = 'category'; + + /** + * Course item type. + */ + public const ITEM_TYPE_COURSE = 'course'; + + /** + * Field shortname + */ + public const FIELD_ENROLLED_UNTIL = 'enrolleduntil'; + + /** + * Course field name. + */ + public const COURSE_NAME = 'fullname'; + + /** + * Field shortname + */ + public const COHORT_FIELD_ID = 'id'; + + /** + * Field shortname + */ + public const COHORT_FIELD_TYPE = 'type'; + + /** + * Role shortname. + */ + public const STUDENT_ROLE = 'student'; + + /** + * Set up configuration item. + * + * @param int $itemid Item ID number + * @param string $itemtype Item type (tag, course, category). + * @param string $itemname Item name. + * @param stdClass|null $course Course to set up enrolment method. If not set, the no enrolment method will be created. + * @return void + */ + public static function set_up_item(int $itemid, string $itemtype, string $itemname, ?stdClass $course = null): void { + $cohort = self::get_cohort_by_item($itemid, $itemtype); + + if (empty($cohort)) { + $cohort = new stdClass(); + $cohort->contextid = context_system::instance()->id; + $cohort->name = $itemname; + $cohort->idnumber = $itemname; + $cohort->description = ucfirst($itemtype) . ' related'; + $typefieled = 'customfield_' . self::COHORT_FIELD_TYPE; + $cohort->$typefieled = $itemtype; + $idfieled = 'customfield_' . self::COHORT_FIELD_ID; + $cohort->$idfieled = $itemid; + + // Create a new cohort. + $cohort->id = self::add_cohort($cohort); + } + + // Create a dynamic cohort rule associated with this cohort. + self::add_rule($cohort, $itemtype); + // Add a tag to a custom profile field. + self::update_profile_field($itemtype, $itemname); + + // Create enrolment method for the cohort for a given course. + if (!empty($course)) { + self::add_enrolment_method($course, $cohort); + } + } + + /** + * Get cohort by provided item type and item id. + * + * @param int $itemid Item ID. + * @param string $itemtype Item type. + * + * @return stdClass|null + */ + public static function get_cohort_by_item(int $itemid, string $itemtype): ?stdClass { + $systemcontext = context_system::instance(); + + $allcohorts = cohort_get_cohorts($systemcontext->id, 0, 0, '', true); + // Load custom fields data and filter bby custom field type and id. + $cohorts = array_filter($allcohorts['cohorts'], function ($cohortdata) use ($itemid, $itemtype) { + foreach ($cohortdata->customfields as $customfield) { + $name = 'customfield_' . $customfield->get_field()->get('shortname'); + $cohortdata->$name = $customfield->export_value(); + } + $typefieled = 'customfield_' . self::COHORT_FIELD_TYPE; + $idfieled = 'customfield_' . self::COHORT_FIELD_ID; + + return $cohortdata->$typefieled == $itemtype && $cohortdata->$idfieled == $itemid; + }); + + if (!empty($cohorts)) { + return reset($cohorts); + } else { + return null; + } + } + + /** + * Helper method to add enrolment method to a course. + * + * @param stdClass $course Course. + * @param stdClass $cohort Cohort. + * + * @return void + */ + public static function add_enrolment_method(stdClass $course, stdClass $cohort): void { + global $DB; + + $studentrole = $DB->get_record('role', ['shortname' => self::STUDENT_ROLE]); + + $fields = [ + 'customint1' => $cohort->id, + 'roleid' => $studentrole->id, + 'courseid' => $course->id, + ]; + + if (!$DB->record_exists('enrol', $fields)) { + $enrol = enrol_get_plugin('cohort'); + $enrol->add_instance($course, $fields); + } + } + + /** + * Update profile field with new item. + * + * @param string $shortname Field short name. + * @param string $newitem A new item to add to the field. + * + * @return void + */ + public static function update_profile_field(string $shortname, string $newitem): void { + global $DB; + + $field = $DB->get_record('user_info_field', ['shortname' => $shortname]); + $fielddata = []; + if (!empty($field->param1)) { + $fielddata = explode("\n", $field->param1); + } + + if (!in_array($newitem, $fielddata)) { + $fielddata[] = $newitem; + sort($fielddata); + $field->param1 = implode("\n", $fielddata); + $DB->update_record('user_info_field', $field); + } + } + + /** + * Add cohort. + * + * @param stdClass $cohort Cohort. + * + * @return int + */ + public static function add_cohort(stdClass $cohort): int { + global $DB; + if (!$existingcohort = $DB->get_record('cohort', ['name' => $cohort->name])) { + return cohort_add_cohort($cohort); + } else { + return $existingcohort->id; + } + } + + /** + * A helper method to set up rule for given cohort. + * + * @param stdClass $cohort Cohort. + * @param string $fieldshortname Related profile field shortname. + * + * @return void + */ + public static function add_rule(stdClass $cohort, string $fieldshortname): void { + if (rule::get_record(['cohortid' => $cohort->id])) { + return; + } + + cohort_manager::manage_cohort($cohort->id); + $rule = new rule(0, (object)[ + 'name' => $cohort->name, + 'cohortid' => $cohort->id, + 'description' => $cohort->description, + ]); + $rule->save(); + + $condition = condition_base::get_instance(0, (object)[ + 'classname' => 'tool_dynamic_cohorts\local\tool_dynamic_cohorts\condition\user_custom_profile', + ]); + + $fieldname = 'profile_field_' . $fieldshortname; + $condition->set_config_data([ + 'profilefield' => $fieldname, + $fieldname . '_operator' => condition_base::TEXT_IS_EQUAL_TO, + $fieldname . '_value' => $cohort->name, + ]); + $condition->get_record()->set('ruleid', $rule->get('id')); + $condition->get_record()->set('sortorder', 0); + $condition->get_record()->save(); + + $condition = condition_base::get_instance(0, (object)[ + 'classname' => 'tool_dynamic_cohorts\local\tool_dynamic_cohorts\condition\user_custom_profile', + ]); + + $fieldname = 'profile_field_' . self::FIELD_ENROLLED_UNTIL; + $condition->set_config_data([ + 'profilefield' => $fieldname, + $fieldname . '_operator' => condition_base::DATE_IN_THE_FUTURE, + $fieldname . '_value' => 0, + ]); + $condition->get_record()->set('ruleid', $rule->get('id')); + $condition->get_record()->set('sortorder', 0); + $condition->get_record()->save(); + + $rule->set('enabled', 1); + $rule->save(); + } +} diff --git a/classes/observer.php b/classes/observer.php new file mode 100644 index 0000000..094795b --- /dev/null +++ b/classes/observer.php @@ -0,0 +1,77 @@ +. + +namespace tool_enrolprofile; + +use core\event\course_category_created; +use core\event\course_created; +use core\event\tag_added; + +/** + * Event observer class. + * + * @package tool_enrolprofile + * @copyright 2024 Dmitrii Metelkin + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class observer { + + /** + * Process tag_added event. + * + * @param tag_added $event The event. + */ + public static function tag_added(tag_added $event): void { + // Check context is course context. + $context = $event->get_context(); + if ($context->contextlevel != CONTEXT_COURSE && $event->other['itemtype'] != 'course') { + return; + } + + $tagid = $event->other['tagid']; + $tagname = $event->other['tagrawname']; + $course = get_course($event->other['itemid']); + + helper::set_up_item($tagid, helper::ITEM_TYPE_TAG, $tagname, $course); + } + + /** + * Process course_created event. + * + * @param course_created $event The event. + */ + public static function course_created(course_created $event): void { + global $DB; + + $course = get_course($event->courseid); + helper::set_up_item($course->id, helper::ITEM_TYPE_COURSE, $course->{helper::COURSE_NAME}, $course); + + $category = $DB->get_record('course_categories', ['id' => $course->category]); + helper::set_up_item($category->id, helper::ITEM_TYPE_CATEGORY, $category->name, $course); + } + + /** + * Process course_category_created event. + * + * @param course_category_created $event The event. + */ + public static function course_category_created(course_category_created $event): void { + global $DB; + + $category = $DB->get_record('course_categories', ['id' => $event->objectid]); + helper::set_up_item($category->id, helper::ITEM_TYPE_CATEGORY, $category->name); + } +} diff --git a/db/events.php b/db/events.php new file mode 100644 index 0000000..8d5dd1e --- /dev/null +++ b/db/events.php @@ -0,0 +1,40 @@ +. + +/** + * Plugin event observers are registered here. + * + * @package tool_enrolprofile + * @copyright 2024 Dmitrii Metelkin + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$observers = [ + [ + 'eventname' => '\core\event\tag_added', + 'callback' => '\tool_enrolprofile\observer::tag_added', + ], + [ + 'eventname' => '\core\event\course_created', + 'callback' => '\tool_enrolprofile\observer::course_created', + ], + [ + 'eventname' => '\core\event\course_category_created', + 'callback' => '\tool_enrolprofile\observer::course_category_created', + ], +]; diff --git a/lang/en/tool_enrolprofile.php b/lang/en/tool_enrolprofile.php index ab312c1..1d5fe01 100644 --- a/lang/en/tool_enrolprofile.php +++ b/lang/en/tool_enrolprofile.php @@ -19,7 +19,7 @@ * * @package tool_enrolprofile * @category string - * @copyright 2024 Your Name + * @copyright 2024 Dmitrii Metelkin * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ diff --git a/tests/helper_test.php b/tests/helper_test.php new file mode 100644 index 0000000..ac51a76 --- /dev/null +++ b/tests/helper_test.php @@ -0,0 +1,91 @@ +. + +namespace tool_enrolprofile; + +use advanced_testcase; +use core_customfield\field_controller; + +/** + * Unit tests for helper class. + * + * @package tool_enrolprofile + * @copyright 2024 Dmitrii Metelkin + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @covers \tool_enrolprofile\observer + */ +class helper_test extends advanced_testcase { + + /** + * Create cohort custom field for testing. + * + * @param string $shortname Field shortname + * @param string $datatype $field data type. + * + * @return field_controller + */ + protected function create_cohort_custom_field(string $shortname, string $datatype = 'text'): field_controller { + $fieldcategory = self::getDataGenerator()->create_custom_field_category([ + 'component' => 'core_cohort', + 'area' => 'cohort', + 'name' => 'Other fields', + ]); + + return self::getDataGenerator()->create_custom_field([ + 'shortname' => $shortname, + 'name' => 'Custom field ' . $shortname, + 'type' => $datatype, + 'categoryid' => $fieldcategory->get('id'), + ]); + } + + /** + * Test getting a cohort by item id. + */ + public function test_get_cohort_by_item(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $this->create_cohort_custom_field(helper::COHORT_FIELD_ID); + $this->create_cohort_custom_field(helper::COHORT_FIELD_TYPE); + + $typefieled = 'customfield_' . helper::COHORT_FIELD_TYPE; + $idfieled = 'customfield_' . helper::COHORT_FIELD_ID; + + $cohort1 = self::getDataGenerator()->create_cohort([ + $typefieled => 'tag', + $idfieled => 12, + ]); + + $cohort2 = self::getDataGenerator()->create_cohort([ + $typefieled => 'tag', + $idfieled => 13, + ]); + + $cohort3 = self::getDataGenerator()->create_cohort([ + $typefieled => 'course', + $idfieled => 13, + ]); + + $this->assertNull(helper::get_cohort_by_item(1, 'tag')); + $this->assertNull(helper::get_cohort_by_item(1, 'course')); + + $this->assertEquals($cohort1->id, helper::get_cohort_by_item(12, 'tag')->id); + $this->assertEquals($cohort2->id, helper::get_cohort_by_item(13, 'tag')->id); + $this->assertEquals($cohort3->id, helper::get_cohort_by_item(13, 'course')->id); + } +} diff --git a/tests/observer_test.php b/tests/observer_test.php new file mode 100644 index 0000000..ee5e586 --- /dev/null +++ b/tests/observer_test.php @@ -0,0 +1,289 @@ +. + +namespace tool_enrolprofile; + +use advanced_testcase; +use core_customfield\field_controller; +use core_tag_tag; +use context_course; + +/** + * Unit tests for observer class. + * + * @package tool_enrolprofile + * @copyright 2024 Dmitrii Metelkin + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @covers \tool_enrolprofile\observer + */ +class observer_test extends advanced_testcase { + + /** + * Tag profile field for testing. + * @var \stdClass + */ + protected $tagprofilefield; + + /** + * Course profile field for testing. + * @var \stdClass + */ + protected $courseprofilefield; + + /** + * Category profile field for testing. + * @var \stdClass + */ + protected $categoryprofilefield; + + /** + * Set up before every test. + * + * @return void + */ + public function setUp(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $this->tagprofilefield = $this->add_user_profile_field(helper::ITEM_TYPE_TAG, 'autocomplete'); + $this->courseprofilefield = $this->add_user_profile_field(helper::ITEM_TYPE_COURSE, 'autocomplete'); + $this->categoryprofilefield = $this->add_user_profile_field(helper::ITEM_TYPE_CATEGORY, 'autocomplete'); + + $this->create_cohort_custom_field(helper::COHORT_FIELD_ID); + $this->create_cohort_custom_field(helper::COHORT_FIELD_TYPE); + } + + /** + * A helper function to create a custom profile field. + * + * @param string $shortname Short name of the field. + * @param string $datatype Type of the field, e.g. text, checkbox, datetime, menu and etc. + * @param array $extras A list of extra fields for the field (e.g. forceunique, param1 and etc) + * + * @return \stdClass + */ + protected function add_user_profile_field(string $shortname, string $datatype, array $extras = []): \stdClass { + global $DB; + + $data = new \stdClass(); + $data->shortname = $shortname; + $data->datatype = $datatype; + $data->name = 'Test ' . $shortname; + $data->description = 'This is a test field'; + $data->required = false; + $data->locked = false; + $data->forceunique = false; + $data->signup = false; + $data->visible = '0'; + $data->categoryid = '0'; + + foreach ($extras as $name => $value) { + $data->{$name} = $value; + } + + $data->id = $DB->insert_record('user_info_field', $data); + + return $data; + } + + /** + * Create cohort custom field for testing. + * + * @param string $shortname Field shortname + * @param string $datatype $field data type. + * + * @return field_controller + */ + protected function create_cohort_custom_field(string $shortname, string $datatype = 'text'): field_controller { + $fieldcategory = self::getDataGenerator()->create_custom_field_category([ + 'component' => 'core_cohort', + 'area' => 'cohort', + 'name' => 'Other fields', + ]); + + return self::getDataGenerator()->create_custom_field([ + 'shortname' => $shortname, + 'name' => 'Custom field ' . $shortname, + 'type' => $datatype, + 'categoryid' => $fieldcategory->get('id'), + ]); + } + + /** + * Check logic when adding a tag. + */ + public function test_tag_added(): void { + global $DB; + + $course = $this->getDataGenerator()->create_course(); + $tagname = 'A tag'; + + $this->assertEmpty($DB->get_record('cohort', ['name' => $tagname])); + $this->assertEmpty($DB->get_field('user_info_field', 'param1', ['id' => $this->tagprofilefield->id])); + $this->assertEmpty($DB->get_record('tag', ['rawname' => $tagname])); + $this->assertEmpty($DB->get_record('tool_dynamic_cohorts', ['name' => $tagname])); + + // Should be already course and category cohorts. + $this->assertCount(2, $DB->get_records('enrol', ['courseid' => $course->id, 'enrol' => 'cohort'])); + + core_tag_tag::set_item_tags('core', 'course', $course->id, context_course::instance($course->id), [$tagname]); + + $tag = $DB->get_record('tag', ['rawname' => $tagname]); + $this->assertNotEmpty($tag); + + $cohort = $DB->get_record('cohort', ['name' => $tagname]); + $this->assertNotEmpty($cohort); + + $cohort = cohort_get_cohort($cohort->id, context_course::instance($course->id), true); + foreach ($cohort->customfields as $customfield) { + if ($customfield->get_field()->get('shortname') == helper::COHORT_FIELD_ID) { + $this->assertSame($tag->id, $customfield->export_value()); + } + if ($customfield->get_field()->get('shortname') == helper::COHORT_FIELD_TYPE) { + $this->assertSame(helper::ITEM_TYPE_TAG, $customfield->export_value()); + } + } + + $profilefielddata = $DB->get_field('user_info_field', 'param1', ['id' => $this->tagprofilefield->id]); + $this->assertNotEmpty($profilefielddata); + $this->assertTrue(in_array($tagname, explode("\n", $profilefielddata))); + + $rule = $DB->get_record('tool_dynamic_cohorts', ['name' => $tagname]); + $this->assertNotEmpty($rule); + $this->assertEquals($cohort->id, $rule->cohortid); + $this->assertEquals(1, $rule->enabled); + $conditions = $DB->get_records('tool_dynamic_cohorts_c', ['ruleid' => $rule->id]); + $this->assertCount(2, $conditions); + + $this->assertCount(3, $DB->get_records('enrol', ['courseid' => $course->id, 'enrol' => 'cohort'])); + $enrol = $DB->get_record('enrol', ['courseid' => $course->id, 'enrol' => 'cohort', 'customint1' => $cohort->id]); + $this->assertNotEmpty($enrol); + } + + /** + * Check logic when creating a course. + */ + public function test_course_created(): void { + global $DB; + + $coursename = 'Course name'; + $this->assertEmpty($DB->get_record('cohort', ['name' => $coursename])); + $this->assertEmpty($DB->get_field('user_info_field', 'param1', ['id' => $this->courseprofilefield->id])); + $this->assertEmpty($DB->get_record('tool_dynamic_cohorts', ['name' => $coursename])); + + $course = $this->getDataGenerator()->create_course(['fullname' => $coursename]); + + // Should be course and category cohorts. + $this->assertCount(2, $DB->get_records('enrol', ['courseid' => $course->id, 'enrol' => 'cohort'])); + + // Check everything about course cohort. + $coursecohort = $DB->get_record('cohort', ['name' => $coursename]); + $this->assertNotEmpty($coursecohort); + + $cohort = cohort_get_cohort($coursecohort->id, context_course::instance($course->id), true); + foreach ($cohort->customfields as $customfield) { + if ($customfield->get_field()->get('shortname') == helper::COHORT_FIELD_ID) { + $this->assertSame($course->id, $customfield->export_value()); + } + if ($customfield->get_field()->get('shortname') == helper::COHORT_FIELD_TYPE) { + $this->assertSame(helper::ITEM_TYPE_COURSE, $customfield->export_value()); + } + } + + $profilefielddata = $DB->get_field('user_info_field', 'param1', ['id' => $this->courseprofilefield->id]); + $this->assertNotEmpty($profilefielddata); + $this->assertTrue(in_array($coursename, explode("\n", $profilefielddata))); + + $rule = $DB->get_record('tool_dynamic_cohorts', ['name' => $coursename]); + $this->assertNotEmpty($rule); + $this->assertEquals($cohort->id, $rule->cohortid); + $this->assertEquals(1, $rule->enabled); + $conditions = $DB->get_records('tool_dynamic_cohorts_c', ['ruleid' => $rule->id]); + $this->assertCount(2, $conditions); + + $enrol = $DB->get_record('enrol', ['courseid' => $course->id, 'enrol' => 'cohort', 'customint1' => $cohort->id]); + $this->assertNotEmpty($enrol); + + // Check everything about category cohort. + $category = $DB->get_record('course_categories', ['id' => $course->category]); + $categorycohort = $DB->get_record('cohort', ['name' => $category->name]); + $this->assertNotEmpty($categorycohort); + + $cohort = cohort_get_cohort($categorycohort->id, context_course::instance($category->id), true); + foreach ($cohort->customfields as $customfield) { + if ($customfield->get_field()->get('shortname') == helper::COHORT_FIELD_ID) { + $this->assertSame($category->id, $customfield->export_value()); + } + if ($customfield->get_field()->get('shortname') == helper::COHORT_FIELD_TYPE) { + $this->assertSame(helper::ITEM_TYPE_CATEGORY, $customfield->export_value()); + } + } + + $profilefielddata = $DB->get_field('user_info_field', 'param1', ['id' => $this->categoryprofilefield->id]); + $this->assertNotEmpty($profilefielddata); + $this->assertTrue(in_array($category->name, explode("\n", $profilefielddata))); + + $rule = $DB->get_record('tool_dynamic_cohorts', ['name' => $category->name]); + $this->assertNotEmpty($rule); + $this->assertEquals($cohort->id, $rule->cohortid); + $this->assertEquals(1, $rule->enabled); + $conditions = $DB->get_records('tool_dynamic_cohorts_c', ['ruleid' => $rule->id]); + $this->assertCount(2, $conditions); + + $enrol = $DB->get_record('enrol', ['courseid' => $course->id, 'enrol' => 'cohort', 'customint1' => $cohort->id]); + $this->assertNotEmpty($enrol); + } + + /** + * Check logic when creating a course category. + */ + public function test_course_category_created(): void { + global $DB; + + $categoryname = 'Category name'; + $this->assertEmpty($DB->get_record('cohort', ['name' => $categoryname])); + $this->assertEmpty($DB->get_field('user_info_field', 'param1', ['id' => $this->categoryprofilefield->id])); + $this->assertEmpty($DB->get_record('tool_dynamic_cohorts', ['name' => $categoryname])); + + $category = $this->getDataGenerator()->create_category(['name' => $categoryname]); + + // Check everything about category cohort. + $categorycohort = $DB->get_record('cohort', ['name' => $category->name]); + $this->assertNotEmpty($categorycohort); + + $cohort = cohort_get_cohort($categorycohort->id, \context_coursecat::instance($category->id), true); + foreach ($cohort->customfields as $customfield) { + if ($customfield->get_field()->get('shortname') == helper::COHORT_FIELD_ID) { + $this->assertSame($category->id, $customfield->export_value()); + } + if ($customfield->get_field()->get('shortname') == helper::COHORT_FIELD_TYPE) { + $this->assertSame(helper::ITEM_TYPE_CATEGORY, $customfield->export_value()); + } + } + + $profilefielddata = $DB->get_field('user_info_field', 'param1', ['id' => $this->categoryprofilefield->id]); + $this->assertNotEmpty($profilefielddata); + $this->assertTrue(in_array($categoryname, explode("\n", $profilefielddata))); + + $rule = $DB->get_record('tool_dynamic_cohorts', ['name' => $categoryname]); + $this->assertNotEmpty($rule); + $this->assertEquals($cohort->id, $rule->cohortid); + $this->assertEquals(1, $rule->enabled); + $conditions = $DB->get_records('tool_dynamic_cohorts_c', ['ruleid' => $rule->id]); + $this->assertCount(2, $conditions); + } +}