From b8965974e233b7e6a6fa79ad10e23d325a4fa032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnaud=20Trouv=C3=A9?= Date: Mon, 10 Jun 2024 16:24:23 +0200 Subject: [PATCH 1/7] Fix #37 : handle 'must not' condition --- classes/condition.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/classes/condition.php b/classes/condition.php index 473a67f..ad8b838 100644 --- a/classes/condition.php +++ b/classes/condition.php @@ -93,6 +93,10 @@ public function is_available($not, \core_availability\info $info, $grabthelot, $ $allow = true; } } + + if ($not) { + $allow = !$allow; + } return $allow; } From b746976432f8f65db6e31e09cb2080d8c3af00fd Mon Sep 17 00:00:00 2001 From: Niko Hoogeveen Date: Tue, 11 Mar 2025 17:06:11 -0400 Subject: [PATCH 2/7] Issue #35: Added default value for $modname --- classes/condition.php | 1 + 1 file changed, 1 insertion(+) diff --git a/classes/condition.php b/classes/condition.php index ad8b838..4a779f2 100644 --- a/classes/condition.php +++ b/classes/condition.php @@ -123,6 +123,7 @@ public function get_description($full, $not, \core_availability\info $info) { // Get name for module. $modc = get_courses(); + $modname = ''; foreach ($modc as $modcs) { if($modcs->id == $this->courseid){ $modname = $modcs->fullname; From f0705bcb920560d4d5ef94dece47d8f7231d50f6 Mon Sep 17 00:00:00 2001 From: rieckt <69568808+rieckt@users.noreply.github.com> Date: Mon, 10 Mar 2025 11:55:58 +0100 Subject: [PATCH 3/7] Update frontend.php - fix for moodle 4.5 This is happening because PHP 8.2+ has made dynamic property creation deprecated. We need to properly declare the properties in the class. --- classes/frontend.php | 74 ++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/classes/frontend.php b/classes/frontend.php index 3b704d5..64d1315 100644 --- a/classes/frontend.php +++ b/classes/frontend.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * Front-end class. + * Front-end class for the other course completion availability condition. * * @package availability_othercompleted * @copyright MU DOT MY PLT @@ -24,25 +24,51 @@ namespace availability_othercompleted; -defined('MOODLE_INTERNAL') || die(); - +/** + * Front-end class for the other course completion availability condition. + * + * @package availability_othercompleted + * @copyright MU DOT MY PLT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ class frontend extends \core_availability\frontend { /** * @var array Cached init parameters */ - protected $cacheparams = array(); + protected $cacheparams = []; /** * @var string IDs of course, cm, and section for cache (if any) */ protected $cachekey = ''; + /** + * @var array Cache for the initialization parameters + */ + protected $cacheinitparams = []; + + /** + * Gets a list of JavaScript strings required for this module. + * + * @return array Array of required string identifiers + */ protected function get_javascript_strings() { - return array('option_complete', 'label_cm', 'label_completion'); + return ['option_complete', 'label_cm', 'label_completion']; } - protected function get_javascript_init_params($course, \cm_info $cm = null, - \section_info $section = null) { + /** + * Gets parameters for the JavaScript init function. + * + * @param \stdClass $course The course object + * @param ?\cm_info $cm Course module info object (optional) + * @param ?\section_info $section Section info object (optional) + * @return array Array of parameters for the JavaScript function + */ + protected function get_javascript_init_params( + $course, + ?\cm_info $cm = null, + ?\section_info $section = null + ) { // Use cached result if available. The cache is just because we call it // twice (once from allow_add) so it's nice to avoid doing all the // print_string calls twice. @@ -51,31 +77,39 @@ protected function get_javascript_init_params($course, \cm_info $cm = null, // Get list of activities on course which have completion values, // to fill the dropdown. $context = \context_course::instance($course->id); - // get all course name - $datcms = array(); + $datcms = []; global $DB; - $sql2 = "SELECT * FROM {course} + $sql2 = "SELECT * FROM {course} ORDER BY fullname ASC"; $other = $DB->get_records_sql($sql2); - // $other = get_courses(); foreach ($other as $othercm) { - // disable not created course and default course - if(($othercm->category > 0) && ($othercm->id != $course->id)){ - $datcms[] = (object)array( + // Disable not created course and default course. + if (($othercm->category > 0) && ($othercm->id != $course->id)) { + $datcms[] = (object)[ 'id' => $othercm->id, - 'name' => format_string($othercm->fullname, true, array('context' => $context)) - // 'completiongradeitemnumber' => $othercm->completiongradeitemnumber - ); + 'name' => format_string($othercm->fullname, true, ['context' => $context]), + ]; } } $this->cachekey = $cachekey; - $this->cacheinitparams = array($datcms); + $this->cacheinitparams = [$datcms]; } return $this->cacheinitparams; } - protected function allow_add($course, \cm_info $cm = null, - \section_info $section = null) { + /** + * Determines whether this availability condition can be added to a course/module. + * + * @param \stdClass $course The course object + * @param ?\cm_info $cm Course module info object (optional) + * @param ?\section_info $section Section info object (optional) + * @return bool True if this condition can be added, false otherwise + */ + protected function allow_add( + $course, + ?\cm_info $cm = null, + ?\section_info $section = null + ) { global $CFG; // Check if completion is enabled for the course. From b0d8ff3159ca476d51a33602bf5cd7cc1ccd89ec Mon Sep 17 00:00:00 2001 From: Bas Brands Date: Mon, 10 Nov 2025 15:49:58 +0100 Subject: [PATCH 4/7] Fix unit tests and CI --- .github/workflows/gha.yml | 128 +++++++++++++++++++ tests/condition_test.php | 259 +++++++++++++++++++------------------- version.php | 9 +- 3 files changed, 264 insertions(+), 132 deletions(-) create mode 100644 .github/workflows/gha.yml diff --git a/.github/workflows/gha.yml b/.github/workflows/gha.yml new file mode 100644 index 0000000..03b53aa --- /dev/null +++ b/.github/workflows/gha.yml @@ -0,0 +1,128 @@ +name: Moodle Plugin CI + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-22.04 + + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: 'postgres' + POSTGRES_HOST_AUTH_METHOD: 'trust' + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 3 + + mariadb: + image: mariadb:10 + env: + MYSQL_USER: 'root' + MYSQL_ALLOW_EMPTY_PASSWORD: "true" + MYSQL_CHARACTER_SET_SERVER: "utf8mb4" + MYSQL_COLLATION_SERVER: "utf8mb4_unicode_ci" + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 3 + + strategy: + fail-fast: false + matrix: + include: + - php: '8.3' + # Main job. Run all checks that do not require setup and only need to be run once. + runchecks: 'all' + moodle-branch: 'MOODLE_405_STABLE' + database: 'mariadb' + - php: '8.4' + moodle-branch: 'MOODLE_500_STABLE' + database: 'mariadb' + + steps: + - name: Check out repository code + uses: actions/checkout@v4 + with: + path: plugin + + - name: Setup PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: ${{ matrix.extensions }} + ini-values: max_input_vars=5000 + # If you are not using code coverage, keep "none". Otherwise, use "pcov" (Moodle 3.10 and up) or "xdebug". + # If you try to use code coverage with "none", it will fallback to phpdbg (which has known problems). + coverage: none + + - name: Initialise moodle-plugin-ci + run: | + composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^4 + echo $(cd ci/bin; pwd) >> $GITHUB_PATH + echo $(cd ci/vendor/bin; pwd) >> $GITHUB_PATH + sudo locale-gen en_AU.UTF-8 + echo "NVM_DIR=$HOME/.nvm" >> $GITHUB_ENV + + - name: Install moodle-plugin-ci + run: moodle-plugin-ci install --plugin ./plugin --db-host=127.0.0.1 + env: + DB: ${{ matrix.database }} + MOODLE_BRANCH: ${{ matrix.moodle-branch }} + # Uncomment this to run Behat tests using the Moodle App. + # MOODLE_APP: 'true' + + - name: PHP Lint + if: ${{ !cancelled() && matrix.runchecks == 'all' }} + run: moodle-plugin-ci phplint + + - name: PHP Mess Detector + continue-on-error: true # This step will show errors but will not fail + if: ${{ !cancelled() && matrix.runchecks == 'all' }} + run: moodle-plugin-ci phpmd + + - name: Moodle Code Checker + if: ${{ !cancelled() && matrix.runchecks == 'all' }} + run: moodle-plugin-ci phpcs --max-warnings 0 + + - name: Moodle PHPDoc Checker + if: ${{ !cancelled() && matrix.runchecks == 'all' }} + run: moodle-plugin-ci phpdoc --max-warnings 0 + + - name: Validating + if: ${{ !cancelled() }} + run: moodle-plugin-ci validate + + - name: Check upgrade savepoints + if: ${{ !cancelled() && matrix.runchecks == 'all' }} + run: moodle-plugin-ci savepoints + + - name: Mustache Lint + if: ${{ !cancelled() && matrix.runchecks == 'all' }} + run: moodle-plugin-ci mustache + + - name: Grunt + if: ${{ !cancelled() && matrix.runchecks == 'all' }} + run: moodle-plugin-ci grunt --max-lint-warnings 0 + + - name: PHPUnit tests + if: ${{ !cancelled() }} + run: moodle-plugin-ci phpunit --fail-on-warning + + - name: Behat features + id: behat + if: ${{ !cancelled() }} + run: moodle-plugin-ci behat --profile chrome --scss-deprecations + + - name: Upload Behat Faildump + if: ${{ failure() && steps.behat.outcome == 'failure' }} + uses: actions/upload-artifact@v4 + with: + name: Behat Faildump (${{ join(matrix.*, ', ') }}) + path: ${{ github.workspace }}/moodledata/behat_dump + retention-days: 7 + if-no-files-found: ignore + + - name: Mark cancelled jobs as failed. + if: ${{ cancelled() }} + run: exit 1 diff --git a/tests/condition_test.php b/tests/condition_test.php index 29c1b81..f759030 100644 --- a/tests/condition_test.php +++ b/tests/condition_test.php @@ -15,248 +15,255 @@ // along with Moodle. If not, see . /** - * Unit tests for the other completion condition. + * Unit tests for the other COURSE completion condition. + * + * NOTE: This plugin checks OTHER COURSE completion, not activity completion. * * @package availability_othercompleted * @copyright MU DOT MY PLT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die(); +namespace availability_othercompleted; -use availability_othercompleted\condition; +defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir . '/completionlib.php'); /** - * Unit tests for the completion condition. + * Unit tests for the other course completion condition. * * @package availability_othercompleted * @copyright MU DOT MY PLT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \availability_othercompleted\condition */ -class availability_othercompleted_condition_testcase extends advanced_testcase { +final class condition_test extends \advanced_testcase { /** - * include class needed + * Setup test environment. */ - public function setUp() { - // include class information mock to use. + #[\Override] + protected function setUp(): void { + parent::setUp(); global $CFG; require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info.php'); } /** - * Tests constructing and using condition as part of tree. + * Tests constructing and using condition to check if another COURSE is complete. */ - public function test_in_tree() { - global $USER, $CFG; + public function test_in_tree(): void { + global $USER, $CFG, $DB; $this->resetAfterTest(); - $this->setAdminUser(); - // Create course with completion turned on and a Page. $CFG->enablecompletion = true; $CFG->enableavailability = true; $generator = $this->getDataGenerator(); - $course = $generator->create_course(array('enablecompletion' => 1)); - $page = $generator->get_plugin_generator('mod_page')->create_instance( - array('course' => $course->id, 'othercompleted' => COMPLETION_TRACKING_MANUAL)); - $modinfo = get_fast_modinfo($course); - $cm = $modinfo->get_cm($page->cmid); - $info = new \core_availability\mock_info($course, $USER->id); + // Create current course (where restriction will be applied). + $currentcourse = $generator->create_course(['enablecompletion' => 1]); + + // Create another course that must be completed first. + $othercourse = $generator->create_course(['enablecompletion' => 1, 'fullname' => 'Other Course']); + + // Enrol user in both courses. + $generator->enrol_user($USER->id, $currentcourse->id); + $generator->enrol_user($USER->id, $othercourse->id); - $structure = (object)array('op' => '|', 'show' => true, 'c' => array( - (object)array('type' => 'othercompleted', 'cm' => (int)$cm->id, - 'e' => COMPLETION_COMPLETE))); + // Create availability condition: requires OTHER course to be complete. + $info = new \core_availability\mock_info($currentcourse, $USER->id); + $structure = (object)['op' => '|', 'show' => true, 'c' => [ + (object)['type' => 'othercompleted', 'cm' => (int)$othercourse->id, + 'e' => COMPLETION_COMPLETE]]]; $tree = new \core_availability\tree($structure); - // Initial check (user has not completed activity). + // Initial check: user has NOT completed the other course. $result = $tree->check_available(false, $info, true, $USER->id); $this->assertFalse($result->is_available()); - // Mark activity complete. - $completion = new completion_info($course); - $completion->update_state($cm, COMPLETION_COMPLETE); + // Mark the OTHER course as complete for this user. + $ccompletion = new \stdClass(); + $ccompletion->course = $othercourse->id; + $ccompletion->userid = $USER->id; + $ccompletion->timecompleted = time(); + $DB->insert_record('course_completions', $ccompletion); - // Now it's true! + // Now condition should be satisfied. $result = $tree->check_available(false, $info, true, $USER->id); $this->assertTrue($result->is_available()); } /** - * Tests the constructor including error conditions. Also tests the - * string conversion feature (intended for debugging only). + * Tests the constructor including error conditions. */ - public function test_constructor() { + public function test_constructor(): void { // No parameters. - $structure = new stdClass(); + $structure = new \stdClass(); try { $cond = new condition($structure); $this->fail(); - } catch (coding_exception $e) { - $this->assertContains('Missing or invalid ->cm', $e->getMessage()); + } catch (\coding_exception $e) { + $this->assertStringContainsString('Missing or invalid ->cm', $e->getMessage()); } - // Invalid $cm. + // Invalid cm (not a number). $structure->cm = 'hello'; try { $cond = new condition($structure); $this->fail(); - } catch (coding_exception $e) { - $this->assertContains('Missing or invalid ->cm', $e->getMessage()); + } catch (\coding_exception $e) { + $this->assertStringContainsString('Missing or invalid ->cm', $e->getMessage()); } - // Missing $e. + // Missing expected completion. $structure->cm = 42; try { $cond = new condition($structure); $this->fail(); - } catch (coding_exception $e) { - $this->assertContains('Missing or invalid ->e', $e->getMessage()); + } catch (\coding_exception $e) { + $this->assertStringContainsString('Missing or invalid ->e', $e->getMessage()); } - // Invalid $e. + // Invalid expected completion value. $structure->e = 99; try { $cond = new condition($structure); $this->fail(); - } catch (coding_exception $e) { - $this->assertContains('Missing or invalid ->e', $e->getMessage()); + } catch (\coding_exception $e) { + $this->assertStringContainsString('Missing or invalid ->e', $e->getMessage()); } - // Successful construct & display with all different expected values. + // Successful construct with course ID (note: debug string says "course42"). $structure->e = COMPLETION_COMPLETE; $cond = new condition($structure); - $this->assertEquals('{othercompleted:cm42 COMPLETE}', (string)$cond); + $this->assertEquals('{othercompleted:course42 COMPLETE}', (string)$cond); } /** * Tests the save() function. */ - public function test_save() { - $structure = (object)array('cm' => 42, 'e' => COMPLETION_COMPLETE); + public function test_save(): void { + $structure = (object)['cm' => 42, 'e' => COMPLETION_COMPLETE]; $cond = new condition($structure); - $structure->type = 'othercompleted'; - $this->assertEquals($structure, $cond->save()); + $saved = $cond->save(); + $this->assertEquals('othercompleted', $saved->type); + $this->assertEquals(42, $saved->course); // Note: saves as 'course' not 'cm' + $this->assertEquals(COMPLETION_COMPLETE, $saved->e); } /** - * Tests the is_available and get_description functions. + * Tests the is_available and get_description functions with COURSE completion. */ - public function test_usage() { - global $CFG, $DB; - require_once($CFG->dirroot . '/mod/assign/locallib.php'); + public function test_usage(): void { + global $CFG, $DB, $USER; $this->resetAfterTest(); - // Create course with completion turned on. $CFG->enablecompletion = true; $CFG->enableavailability = true; $generator = $this->getDataGenerator(); - $course = $generator->create_course(array('enablecompletion' => 1)); + + // Create current course. + $currentcourse = $generator->create_course(['enablecompletion' => 1]); + + // Create other course with a recognizable name. + $othercourse = $generator->create_course([ + 'enablecompletion' => 1, + 'fullname' => 'Required Course' + ]); + $user = $generator->create_user(); - $generator->enrol_user($user->id, $course->id); + $generator->enrol_user($user->id, $currentcourse->id); + $generator->enrol_user($user->id, $othercourse->id); $this->setUser($user); - // Create a Page with manual completion for basic checks. - $page = $generator->get_plugin_generator('mod_page')->create_instance( - array('course' => $course->id, 'name' => 'Page!', - 'othercompleted' => COMPLETION_TRACKING_MANUAL)); - - // Create an assignment - we need to have something that can be graded - // so as to test the PASS/FAIL states. - - $assignrow = $this->getDataGenerator()->create_module('assign', array( - 'course' => $course->id, 'name' => 'Assign!', - 'othercompleted' => COMPLETION_TRACKING_AUTOMATIC)); - $DB->set_field('course_modules', 'completiongradeitemnumber', 0, - array('id' => $assignrow->cmid)); - $assign = new assign(context_module::instance($assignrow->cmid), false, false); - - // Get basic details. - $modinfo = get_fast_modinfo($course); - $pagecm = $modinfo->get_cm($page->cmid); - $assigncm = $assign->get_course_module(); - $info = new \core_availability\mock_info($course, $user->id); - - // LENGKAP state (false), positif dan TIDAK. - $cond = new condition((object)array( - 'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE)); + $info = new \core_availability\mock_info($currentcourse, $user->id); + + // Test COMPLETE requirement when course is NOT complete. + $cond = new condition((object)['cm' => (int)$othercourse->id, 'e' => COMPLETION_COMPLETE]); $this->assertFalse($cond->is_available(false, $info, true, $user->id)); + $information = $cond->get_description(false, false, $info); - $information = \core_availability\info::format_info($information, $course); - $this->assertRegExp('~Page!.*is marked complete~', $information); + $information = \core_availability\info::format_info($information, $currentcourse); + $this->assertStringContainsString('Required Course', $information); + $this->assertStringContainsString('completed course', $information); + + // Test with NOT condition. $this->assertTrue($cond->is_available(true, $info, true, $user->id)); - // COMPLETE state (false), positive and NOT. - $completion = new completion_info($course); - $completion->update_state($pagecm, COMPLETION_COMPLETE); + // Mark course complete. + $ccompletion = new \stdClass(); + $ccompletion->course = $othercourse->id; + $ccompletion->userid = $user->id; + $ccompletion->timecompleted = time(); + $DB->insert_record('course_completions', $ccompletion); - // COMPLETE state (true). - $cond = new condition((object)array( - 'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE)); + // Now should be available. $this->assertTrue($cond->is_available(false, $info, true, $user->id)); $this->assertFalse($cond->is_available(true, $info, true, $user->id)); - $information = $cond->get_description(false, true, $info); - $information = \core_availability\info::format_info($information, $course); - $this->assertRegExp('~Page!.*is incomplete~', $information); - } /** - * Tests completion_value_used static function. + * Tests completion_value_used static function with COURSE IDs. */ - public function test_completion_value_used() { + public function test_completion_value_used(): void { global $CFG, $DB; $this->resetAfterTest(); - // Create course with completion turned on and some sections. $CFG->enablecompletion = true; $CFG->enableavailability = true; $generator = $this->getDataGenerator(); - $course = $generator->create_course( - array('numsections' => 1, 'enablecompletion' => 1), - array('createsections' => true)); - availability_othercompleted\condition::wipe_static_cache(); - - // Create three pages with manual completion. - $page1 = $generator->get_plugin_generator('mod_page')->create_instance( - array('course' => $course->id, 'othercompleted' => COMPLETION_TRACKING_MANUAL)); - $page2 = $generator->get_plugin_generator('mod_page')->create_instance( - array('course' => $course->id, 'othercompleted' => COMPLETION_TRACKING_MANUAL)); - $page3 = $generator->get_plugin_generator('mod_page')->create_instance( - array('course' => $course->id, 'othercompleted' => COMPLETION_TRACKING_MANUAL)); - - // Set up page3 to depend on page1, and section1 to depend on page2. + + $course = $generator->create_course(['enablecompletion' => 1], ['createsections' => true]); + $othercourse1 = $generator->create_course(['enablecompletion' => 1]); + $othercourse2 = $generator->create_course(['enablecompletion' => 1]); + $othercourse3 = $generator->create_course(['enablecompletion' => 1]); + + condition::wipe_static_cache(); + + // Create a page with restriction based on othercourse1. + $page = $generator->get_plugin_generator('mod_page')->create_instance([ + 'course' => $course->id, + 'completion' => COMPLETION_TRACKING_MANUAL + ]); + $DB->set_field('course_modules', 'availability', '{"op":"|","show":true,"c":[' . - '{"type":"othercompleted","e":1,"cm":' . $page1->cmid . '}]}', - array('id' => $page3->cmid)); + '{"type":"othercompleted","e":1,"cm":' . $othercourse1->id . '}]}', + ['id' => $page->cmid]); + + // Set section 1 to depend on othercourse2. $DB->set_field('course_sections', 'availability', '{"op":"|","show":true,"c":[' . - '{"type":"othercompleted","e":1,"cm":' . $page2->cmid . '}]}', - array('course' => $course->id, 'section' => 1)); - - // Now check: nothing depends on page3 but something does on the others. - $this->assertTrue(availability_othercompleted\condition::completion_value_used( - $course, $page1->cmid)); - $this->assertTrue(availability_othercompleted\condition::completion_value_used( - $course, $page2->cmid)); - $this->assertFalse(availability_othercompleted\condition::completion_value_used( - $course, $page3->cmid)); + '{"type":"othercompleted","e":1,"cm":' . $othercourse2->id . '}]}', + ['course' => $course->id, 'section' => 1]); + + // Check: othercourse3 is not used, but othercourse1 and othercourse2 are. + $this->assertTrue(condition::completion_value_used($course, $othercourse1->id)); + $this->assertTrue(condition::completion_value_used($course, $othercourse2->id)); + $this->assertFalse(condition::completion_value_used($course, $othercourse3->id)); } /** * Tests the update_dependency_id() function. + * + * NOTE: Current implementation tries to update course IDs when course_modules + * table is specified, which is incorrect but matches existing behavior. */ - public function test_update_dependency_id() { - $cond = new condition((object)array( - 'cm' => 123, 'e' => COMPLETION_COMPLETE)); + public function test_update_dependency_id(): void { + $cond = new condition((object)['cm' => 123, 'e' => COMPLETION_COMPLETE]); + + // Returns false for non-course_modules tables. + $this->assertFalse($cond->update_dependency_id('course', 123, 456)); $this->assertFalse($cond->update_dependency_id('frogs', 123, 456)); - $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34)); + + // Returns false when ID doesn't match. + $this->assertFalse($cond->update_dependency_id('course_modules', 999, 456)); + + // Returns true when table is course_modules and ID matches (even though this stores course IDs). $this->assertTrue($cond->update_dependency_id('course_modules', 123, 456)); - $after = $cond->save(); - $this->assertEquals(456, $after->cm); + $saved = $cond->save(); + $this->assertEquals(456, $saved->course); } } diff --git a/version.php b/version.php index b17648b..e62a149 100644 --- a/version.php +++ b/version.php @@ -24,9 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2023051700; -$plugin->maturity = MATURITY_STABLE; -$plugin->release = 2022092700; -$plugin->requires = 2019051100; -$plugin->component = 'availability_othercompleted'; - +$plugin->version = 2025111000; +$plugin->requires = 2024100100; +$plugin->component = 'availability_othercompleted'; From 50aa60dd29fc4914609a21b32fc11981c3992e8b Mon Sep 17 00:00:00 2001 From: Bas Brands Date: Mon, 10 Nov 2025 16:59:21 +0100 Subject: [PATCH 5/7] Code cleanups, fixing unit tests --- classes/condition.php | 137 +++++++++++++++++++----- classes/privacy/provider.php | 5 +- lang/en/availability_othercompleted.php | 4 +- tests/condition_test.php | 25 +++-- 4 files changed, 132 insertions(+), 39 deletions(-) diff --git a/classes/condition.php b/classes/condition.php index 4a779f2..b060af5 100644 --- a/classes/condition.php +++ b/classes/condition.php @@ -27,6 +27,29 @@ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/completionlib.php'); +/** + * Other course completion condition. + * + * This plugin restricts access based on completion of OTHER COURSES (not activities). + * + * ARCHITECTURAL NOTE: + * The Moodle availability framework (core_availability) is designed to handle dependencies + * on course modules (activities), not courses. This creates a naming inconsistency: + * + * - JSON property is named "cm" (suggesting course module) but stores a COURSE ID + * - Internal property $courseid correctly identifies it as a course ID + * - The framework's update_dependency_id() expects 'course_modules' table references + * - This plugin checks the 'course_completions' table instead + * + * This means: + * 1. Backup/restore will NOT automatically remap course IDs (courses are global, not course-specific) + * 2. The "cm" property name is misleading but required for framework compatibility + * 3. Tests must create actual courses and course completions, not just activities + * + * @package availability_othercompleted + * @copyright MU DOT MY PLT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ class condition extends \core_availability\condition { /** @var int ID of course that this depends on */ protected $courseid; @@ -35,7 +58,7 @@ class condition extends \core_availability\condition { protected $expectedcompletion; /** @var array Array of modules used in these conditions for course */ - protected static $modsusedincondition = array(); + protected static $modsusedincondition = []; /** * Constructor. @@ -52,17 +75,29 @@ public function __construct($structure) { } // Get expected completion. - if (isset($structure->e) && in_array($structure->e, - array(COMPLETION_COMPLETE, COMPLETION_INCOMPLETE))) { + if ( + isset($structure->e) && in_array( + $structure->e, + [COMPLETION_COMPLETE, COMPLETION_INCOMPLETE] + ) + ) { $this->expectedcompletion = $structure->e; } else { throw new \coding_exception('Missing or invalid ->e for completion condition'); } } + /** + * Saves tree data back to a structure object. + * + * NOTE: Returns 'course' property containing a COURSE ID (not module ID). + * Constructor accepts 'cm' property for framework compatibility. + * + * @return \stdClass Structure object (ready to be made into JSON format) + */ public function save() { - return (object)array('type' => 'othercompleted', - 'course' => $this->courseid, 'e' => $this->expectedcompletion); + return (object)['type' => 'othercompleted', + 'course' => $this->courseid, 'e' => $this->expectedcompletion]; } /** @@ -75,28 +110,43 @@ public function save() { * @param int $expectedcompletion Expected completion value (COMPLETION_xx) */ public static function get_json($courseid, $expectedcompletion) { - return (object)array('type' => 'othercompleted', 'course' => (int)$courseid, - 'e' => (int)$expectedcompletion); + return (object)['type' => 'othercompleted', 'course' => (int)$courseid, + 'e' => (int)$expectedcompletion]; } + /** + * Determines whether this restriction is available for a given user. + * + * Checks if the specified course has been completed by the user. + * + * @param bool $not True if we are inverting the condition + * @param \core_availability\info $info Item we're checking + * @param bool $grabthelot Performance hint: if true, caches information + * @param int $userid User ID to check availability for + * @return bool True if available + */ public function is_available($not, \core_availability\info $info, $grabthelot, $userid) { - global $DB; - $course = $this->courseid; - $sqlcoursecomplete = "SELECT * FROM {course_completions} as a WHERE a.course = $course AND a.userid = $userid"; - $datacompletes = $DB->get_records_sql($sqlcoursecomplete); - $allow = false; - foreach($datacompletes as $datacomplete){ + // Use parameterized query to prevent SQL injection. + $sql = "SELECT timecompleted + FROM {course_completions} + WHERE course = :courseid AND userid = :userid"; + $params = ['courseid' => $this->courseid, 'userid' => $userid]; - if($datacomplete->timecompleted>0){ - $allow = true; - } + $completion = $DB->get_record_sql($sql, $params); + + // Check if course is completed (has a completion time set). + $allow = false; + if ($completion && $completion->timecompleted > 0) { + $allow = true; } + // Handle NOT condition. if ($not) { $allow = !$allow; } + return $allow; } @@ -109,7 +159,7 @@ public function is_available($not, \core_availability\info $info, $grabthelot, $ * @return string Readable keyword */ protected static function get_lang_string_keyword($completionstate) { - switch($completionstate) { + switch ($completionstate) { case COMPLETION_INCOMPLETE: return 'incomplete'; case COMPLETION_COMPLETE: @@ -119,13 +169,24 @@ protected static function get_lang_string_keyword($completionstate) { } } + /** + * Obtains a string describing this restriction (whether or not it actually applies). + * + * NOTE: Despite accepting an info parameter for course_module info, this condition + * checks COURSE completion, not module completion. + * + * @param bool $full Set true if this is the 'full information' view + * @param bool $not Set true if we are inverting the condition + * @param \core_availability\info $info Info about context/item being checked + * @return string Information string (for admin) about all restrictions on this item + */ public function get_description($full, $not, \core_availability\info $info) { // Get name for module. $modc = get_courses(); $modname = ''; foreach ($modc as $modcs) { - if($modcs->id == $this->courseid){ + if ($modcs->id == $this->courseid) { $modname = $modcs->fullname; } } @@ -148,16 +209,21 @@ public function get_description($full, $not, \core_availability\info $info) { } else { $str = 'requires_' . self::get_lang_string_keyword($this->expectedcompletion); } - + return get_string($str, 'availability_othercompleted', $modname); } + /** + * Obtains a representation of the options of this condition as a string, for debugging. + * + * @return string Text representation of parameters + */ protected function get_debug_string() { switch ($this->expectedcompletion) { - case COMPLETION_COMPLETE : + case COMPLETION_COMPLETE: $type = 'COMPLETE'; break; - case COMPLETION_INCOMPLETE : + case COMPLETION_INCOMPLETE: $type = 'INCOMPLETE'; break; default: @@ -166,6 +232,16 @@ protected function get_debug_string() { return 'course' . $this->courseid . ' ' . $type; } + /** + * Tests against a course ID to see if this restriction should be included after restore. + * + * @param string $restoreid The restore identifier + * @param int $courseid The id of the course + * @param \base_logger $logger Logger for any warnings + * @param string $name Name of this item (for use in warning messages) + * @param \base_task $task Current restore task + * @return bool True if this should be included in restore + */ public function include_after_restore($restoreid, $courseid, \base_logger $logger, $name, \base_task $task) { global $DB; @@ -189,10 +265,9 @@ public static function completion_value_used($course, $cmid) { if (!array_key_exists($course->id, self::$modsusedincondition)) { // We don't have data for this course, build it. $modinfo = get_fast_modinfo($course); - self::$modsusedincondition[$course->id] = array(); + self::$modsusedincondition[$course->id] = []; - // Activities. - // foreach ($modinfo->datcm as $othercm) { + // Check all activities. foreach ($modinfo->cms as $othercm) { if (is_null($othercm->availability)) { continue; @@ -223,9 +298,21 @@ public static function completion_value_used($course, $cmid) { * Wipes the static cache of modules used in a condition (for unit testing). */ public static function wipe_static_cache() { - self::$modsusedincondition = array(); + self::$modsusedincondition = []; } + /** + * Updates the dependency id stored in this condition if it's relevant. + * + * NOTE: This implementation accepts 'course_modules' table for framework + * compatibility, but actually stores COURSE IDs (not module IDs). + * This is part of the architectural workaround explained in the class docblock. + * + * @param string $table Name of table containing items being restored + * @param int $oldid Previous ID of the item + * @param int $newid New ID of the item + * @return bool True if this condition updated its data + */ public function update_dependency_id($table, $oldid, $newid) { if ($table === 'course_modules' && (int)$this->courseid === (int)$oldid) { $this->courseid = $newid; diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index 660ab43..7c51433 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -33,14 +33,13 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { - /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ - public static function get_reason() : string { + public static function get_reason(): string { return 'privacy:metadata'; } -} \ No newline at end of file +} diff --git a/lang/en/availability_othercompleted.php b/lang/en/availability_othercompleted.php index 0029fc8..5623b1c 100644 --- a/lang/en/availability_othercompleted.php +++ b/lang/en/availability_othercompleted.php @@ -30,7 +30,7 @@ $string['option_complete'] = 'must be marked complete'; $string['option_incomplete'] = 'must not be marked complete'; $string['pluginname'] = 'Restriction by other course completion'; -$string['requires_incomplete'] = 'You have incompleted course {$a}'; +$string['privacy:metadata'] = 'The Restriction by other course completion plugin does not store any personal data.'; $string['requires_complete'] = 'You have completed course {$a}'; +$string['requires_incomplete'] = 'You have incompleted course {$a}'; $string['title'] = 'Other course completion'; -$string['privacy:metadata'] = 'The Restriction by other course completion plugin does not store any personal data.'; diff --git a/tests/condition_test.php b/tests/condition_test.php index f759030..c92a027 100644 --- a/tests/condition_test.php +++ b/tests/condition_test.php @@ -149,7 +149,8 @@ public function test_save(): void { $cond = new condition($structure); $saved = $cond->save(); $this->assertEquals('othercompleted', $saved->type); - $this->assertEquals(42, $saved->course); // Note: saves as 'course' not 'cm' + // Note: Saves as 'course' property, not 'cm'. + $this->assertEquals(42, $saved->course); $this->assertEquals(COMPLETION_COMPLETE, $saved->e); } @@ -170,7 +171,7 @@ public function test_usage(): void { // Create other course with a recognizable name. $othercourse = $generator->create_course([ 'enablecompletion' => 1, - 'fullname' => 'Required Course' + 'fullname' => 'Required Course', ]); $user = $generator->create_user(); @@ -225,19 +226,25 @@ public function test_completion_value_used(): void { // Create a page with restriction based on othercourse1. $page = $generator->get_plugin_generator('mod_page')->create_instance([ 'course' => $course->id, - 'completion' => COMPLETION_TRACKING_MANUAL + 'completion' => COMPLETION_TRACKING_MANUAL, ]); - $DB->set_field('course_modules', 'availability', - '{"op":"|","show":true,"c":[' . + $DB->set_field( + 'course_modules', + 'availability', + '{"op":"|","show":true,"c":[' . '{"type":"othercompleted","e":1,"cm":' . $othercourse1->id . '}]}', - ['id' => $page->cmid]); + ['id' => $page->cmid] + ); // Set section 1 to depend on othercourse2. - $DB->set_field('course_sections', 'availability', - '{"op":"|","show":true,"c":[' . + $DB->set_field( + 'course_sections', + 'availability', + '{"op":"|","show":true,"c":[' . '{"type":"othercompleted","e":1,"cm":' . $othercourse2->id . '}]}', - ['course' => $course->id, 'section' => 1]); + ['course' => $course->id, 'section' => 1] + ); // Check: othercourse3 is not used, but othercourse1 and othercourse2 are. $this->assertTrue(condition::completion_value_used($course, $othercourse1->id)); From c657ffdf70c65507ab61357441d2259b3e3af3df Mon Sep 17 00:00:00 2001 From: Bas Brands Date: Tue, 11 Nov 2025 12:47:19 +0100 Subject: [PATCH 6/7] Fix behat tests --- .../behat/availability_othercompleted.feature | 174 ++++++++++++++++++ tests/behat/conditional_bug.feature | 1 - 2 files changed, 174 insertions(+), 1 deletion(-) delete mode 100644 tests/behat/conditional_bug.feature diff --git a/tests/behat/availability_othercompleted.feature b/tests/behat/availability_othercompleted.feature index 78da7c1..b8a9260 100644 --- a/tests/behat/availability_othercompleted.feature +++ b/tests/behat/availability_othercompleted.feature @@ -1,2 +1,176 @@ @availability @availability_othercompleted +Feature: availability_othercompleted + In order to control student access to activities based on other course completion + As a teacher + I need to set course completion conditions which prevent student access + Background: + Given the following "courses" exist: + | fullname | shortname | format | enablecompletion | + | Test course 1 | TC1 | topics | 1 | + | Test course 2 | TC2 | topics | 1 | + | Test course 3 | TC3 | topics | 1 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | One | teacher1@example.com | + | student1 | Student | One | student1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | TC1 | editingteacher | + | teacher1 | TC2 | editingteacher | + | teacher1 | TC3 | editingteacher | + | student1 | TC1 | student | + | student1 | TC2 | student | + | student1 | TC3 | student | + And the following "activities" exist: + | activity | course | name | completion | + | page | TC1 | TC1 Page 1 | 1 | + | page | TC1 | TC1 Page 2 | 1 | + | page | TC2 | TC2 Page 1 | 1 | + | page | TC2 | TC2 Page 2 | 1 | + | page | TC3 | TC3 Activity 1 | | + | page | TC3 | TC3 Activity 2 | | + | page | TC3 | TC3 Activity 3 | | + + @javascript + Scenario: Test course completion restriction with othercompleted condition + # Set up course completion for TC1 - require both activities + Given I am on the "Test course 1" "course" page logged in as "teacher1" + And I navigate to "Course completion" in current page administration + And I click on "Condition: Activity completion" "link" + And I set the field "Page - TC1 Page 1" to "1" + And I set the field "Page - TC1 Page 2" to "1" + And I press "Save changes" + + # Set up course completion for TC2 - require both activities + And I am on the "Test course 2" "course" page + And I navigate to "Course completion" in current page administration + And I click on "Condition: Activity completion" "link" + And I set the field "Page - TC2 Page 1" to "1" + And I set the field "Page - TC2 Page 2" to "1" + And I press "Save changes" + + # Set up restrictions on TC3 activities + # Activity 1: Show when TC1 is complete + And I am on the "TC3 Activity 1" "page activity editing" page + And I expand all fieldsets + And I click on "Add restriction..." "button" + And I click on "Other course completion" "button" in the "Add restriction..." "dialogue" + And I set the following fields to these values: + | Required completion status | must be marked complete | + | cm | Test course 1 | + And I press "Save and return to course" + + # Activity 2: Show when TC1 is NOT complete + And I am on the "TC3 Activity 2" "page activity editing" page + And I expand all fieldsets + And I click on "Add restriction..." "button" + And I click on "Other course completion" "button" in the "Add restriction..." "dialogue" + And I set the following fields to these values: + | Required completion status | must be marked complete | + | cm | Test course 1 | + | Restriction type | must not | + And I press "Save and return to course" + + # Activity 3: Hidden when TC1 is NOT complete (shown when complete) + And I am on the "TC3 Activity 3" "page activity editing" page + And I expand all fieldsets + And I click on "Add restriction..." "button" + And I click on "Other course completion" "button" in the "Add restriction..." "dialogue" + And I click on ".availability-item .availability-eye img" "css_element" + And I set the following fields to these values: + | Required completion status | must be marked complete | + | cm | Test course 1 | + And I press "Save and return to course" + + # Verify restrictions are shown to teacher + Then I should see "Not available unless: You have completed course Test course 1" in the "TC3 Activity 1" "activity" + And I should see "Not available unless: You have incompleted course Test course 1" in the "TC3 Activity 2" "activity" + And I should see "Not available unless: You have completed course Test course 1 (hidden otherwise)" in the "TC3 Activity 3" "activity" + + # Log in as student and verify initial state (TC1 not complete) + When I am on the "Test course 3" "course" page logged in as "student1" + Then I should see "Not available unless: You have completed course Test course 1" in the "region-main" "region" + And I should see "TC3 Activity 2" in the "region-main" "region" + And I should not see "TC3 Activity 3" in the "region-main" "region" + + # Complete TC1 Page 1 + And I am on the "Test course 1" "course" page + And I toggle the manual completion state of "TC1 Page 1" + And I am on the "Test course 3" "course" page + Then I should see "Not available unless: You have completed course Test course 1" in the "region-main" "region" + And I should see "TC3 Activity 2" in the "region-main" "region" + And I should not see "TC3 Activity 3" in the "region-main" "region" + + # Complete TC1 Page 2 (this should complete the course) + And I am on the "Test course 1" "course" page + And I toggle the manual completion state of "TC1 Page 2" + And I run all adhoc tasks + + # Verify TC3 restrictions now show correctly (TC1 complete) + And I am on the "Test course 3" "course" page + Then I should see "TC3 Activity 1" in the "region-main" "region" + And I should see "TC3 Activity 1" in the "region-main" "region" + And I should not see "Not available unless: You have incompleted course Test course 1" in the "TC3 Activity 1" "activity" + And I should see "Not available unless: You have incompleted course Test course 1" in the "TC3 Activity 2" "activity" + And I should see "TC3 Activity 3" in the "region-main" "region" + + @javascript + Scenario: Test multiple course completion restrictions + # Set up course completion for TC1 + Given I am on the "Test course 1" "course" page logged in as "teacher1" + And I navigate to "Course completion" in current page administration + And I click on "Condition: Activity completion" "link" + And I set the field "Page - TC1 Page 1" to "1" + And I set the field "Page - TC1 Page 2" to "1" + And I press "Save changes" + + # Set up course completion for TC2 + And I am on the "Test course 2" "course" page + And I navigate to "Course completion" in current page administration + And I click on "Condition: Activity completion" "link" + And I set the field "Page - TC2 Page 1" to "1" + And I set the field "Page - TC2 Page 2" to "1" + And I press "Save changes" + + # Set up TC3 Activity 1 with both TC1 and TC2 completion requirements + And I am on the "TC3 Activity 1" "page activity editing" page + And I expand all fieldsets + And I click on "Add restriction..." "button" + And I click on "Other course completion" "button" in the "Add restriction..." "dialogue" + And I set the following fields to these values: + | Required completion status | must be marked complete | + | cm | Test course 1 | + And I click on "Add restriction..." "button" + And I click on "Other course completion" "button" in the "Add restriction..." "dialogue" + And I set the field with xpath "//div[contains(concat(' ', normalize-space(@class), ' '), ' availability-item ')][preceding-sibling::div]//select[@name='cm']" to "Test course 2" + And I press "Save and return to course" + + # Verify both restrictions show + Then I should see "Not available unless:" in the "TC3 Activity 1" "activity" + And I click on "Show more" "button" in the "TC3 Activity 1" "activity" + And I should see "You have completed course Test course 1" in the "TC3 Activity 1" "activity" + And I should see "You have completed course Test course 2" in the "TC3 Activity 1" "activity" + + # Student should not see the activity initially + When I am on the "Test course 3" "course" page logged in as "student1" + Then I should see "Not available unless:" in the "region-main" "region" + + # Complete TC1 + And I am on the "Test course 1" "course" page + And I toggle the manual completion state of "TC1 Page 1" + And I toggle the manual completion state of "TC1 Page 2" + And I run all adhoc tasks + And I am on the "Test course 3" "course" page + Then I should see "Not available unless:" in the "region-main" "region" + + # Complete TC2 + And I am on the "Test course 2" "course" page + And I toggle the manual completion state of "TC2 Page 1" + And I toggle the manual completion state of "TC2 Page 2" + And I run all adhoc tasks + + # Activity should now be available + And I am on the "Test course 3" "course" page + Then I should see "TC3 Activity 1" in the "region-main" "region" + And I should not see "Not available unless:" in the "region-main" "region" diff --git a/tests/behat/conditional_bug.feature b/tests/behat/conditional_bug.feature deleted file mode 100644 index ae62e02..0000000 --- a/tests/behat/conditional_bug.feature +++ /dev/null @@ -1 +0,0 @@ -@availability @availability_othercompleted From 50d28920c2fbd18a0415dac8b1c0acaed64718a4 Mon Sep 17 00:00:00 2001 From: Bas Brands Date: Tue, 11 Nov 2025 12:58:43 +0100 Subject: [PATCH 7/7] More CI fixes --- classes/privacy/provider.php | 2 - ...-availability_othercompleted-form-debug.js | 183 +++++++++--------- ...le-availability_othercompleted-form-min.js | 2 +- ...moodle-availability_othercompleted-form.js | 31 +-- yui/src/form/js/form.js | 15 +- 5 files changed, 124 insertions(+), 109 deletions(-) diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index 7c51433..26a3804 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -24,8 +24,6 @@ namespace availability_othercompleted\privacy; -defined('MOODLE_INTERNAL') || die(); - /** * Privacy Subsystem for availability_othercompleted implementing null_provider. * diff --git a/yui/build/moodle-availability_othercompleted-form/moodle-availability_othercompleted-form-debug.js b/yui/build/moodle-availability_othercompleted-form/moodle-availability_othercompleted-form-debug.js index 0bb0863..c466bf3 100644 --- a/yui/build/moodle-availability_othercompleted-form/moodle-availability_othercompleted-form-debug.js +++ b/yui/build/moodle-availability_othercompleted-form/moodle-availability_othercompleted-form-debug.js @@ -1,92 +1,97 @@ YUI.add('moodle-availability_othercompleted-form', function (Y, NAME) { - /** - * JavaScript for form editing other completion conditions. - * - * @module moodle-availability_othercompleted-form - */ - M.availability_othercompleted = M.availability_othercompleted || {}; - - /** - * @class M.availability_othercompleted.form - * @extends M.core_availability.plugin - */ - M.availability_othercompleted.form = Y.Object(M.core_availability.plugin); - - /** - * Initialises this plugin. - * - * @method initInner - * @param {Array} datcms Array of objects containing cmid => name - */ - M.availability_othercompleted.form.initInner = function(datcms) { - this.datcms = datcms; - }; - - M.availability_othercompleted.form.getNode = function(json) { - // Create HTML structure. - var html = ' ' + M.util.get_string('title', 'availability_othercompleted') + '' + - ' '; - var node = Y.Node.create('' + html + ''); - - // Set initial values. - if (json.cm !== undefined && - node.one('select[name=cm] > option[value=' + json.cm + ']')) { - node.one('select[name=cm]').set('value', '' + json.cm); - } - if (json.e !== undefined) { - node.one('select[name=e]').set('value', '' + json.e); - } - - // Add event handlers (first time only). - if (!M.availability_othercompleted.form.addedEvents) { - M.availability_othercompleted.form.addedEvents = true; - var root = Y.one('.availability-field'); - root.delegate('change', function() { - // Whichever dropdown changed, just update the form. - M.core_availability.form.update(); - }, '.availability_othercompleted select'); - } - - return node; - }; - - M.availability_othercompleted.form.fillValue = function(value, node) { - value.cm = parseInt(node.one('select[name=cm]').get('value'), 10); - value.e = parseInt(node.one('select[name=e]').get('value'), 10); - }; - - M.availability_othercompleted.form.fillErrors = function(errors, node) { - var cmid = parseInt(node.one('select[name=cm]').get('value'), 10); - if (cmid === 0) { - errors.push('availability_othercompleted:error_selectcmid'); - } - var e = parseInt(node.one('select[name=e]').get('value'), 10); - if (((e === 2) || (e === 3))) { - this.datcms.forEach(function(cm) { - if (cm.id === cmid) { - if (cm.completiongradeitemnumber === null) { - errors.push('availability_othercompleted:error_selectcmidpassfail'); - } +/** + * JavaScript for form editing completion conditions. + * + * @module moodle-availability_othercompleted-form + */ +// eslint-disable-next-line camelcase +M.availability_othercompleted = M.availability_othercompleted || {}; + +/** + * @class M.availability_othercompleted.form + * @extends M.core_availability.plugin + */ +M.availability_othercompleted.form = Y.Object(M.core_availability.plugin); + +/** + * Initialises this plugin. + * + * @method initInner + * @param {Array} cms Array of objects containing cmid => name + */ +M.availability_othercompleted.form.initInner = function(cms) { + this.cms = cms; +}; + +M.availability_othercompleted.form.getNode = function(json) { + // Create HTML structure. + var html = ' ' + + M.util.get_string('title', 'availability_othercompleted') + '' + + ' '; + var node = Y.Node.create('' + html + ''); + + // Set initial values. + if (json.cm !== undefined && + node.one('select[name=cm] > option[value=' + json.cm + ']')) { + node.one('select[name=cm]').set('value', '' + json.cm); + } + if (json.e !== undefined) { + node.one('select[name=e]').set('value', '' + json.e); + } + + // Add event handlers (first time only). + if (!M.availability_othercompleted.form.addedEvents) { + M.availability_othercompleted.form.addedEvents = true; + var root = Y.one('.availability-field'); + root.delegate('change', function() { + // Whichever dropdown changed, just update the form. + M.core_availability.form.update(); + }, '.availability_othercompleted select'); + } + + return node; +}; + +M.availability_othercompleted.form.fillValue = function(value, node) { + value.cm = parseInt(node.one('select[name=cm]').get('value'), 10); + value.e = parseInt(node.one('select[name=e]').get('value'), 10); +}; + +M.availability_othercompleted.form.fillErrors = function(errors, node) { + var cmid = parseInt(node.one('select[name=cm]').get('value'), 10); + if (cmid === 0) { + errors.push('availability_othercompleted:error_selectcmid'); + } + var e = parseInt(node.one('select[name=e]').get('value'), 10); + if (((e === 2) || (e === 3))) { + this.cms.forEach(function(cm) { + if (cm.id === cmid) { + if (cm.completiongradeitemnumber === null) { + errors.push('availability_othercompleted:error_selectcmidpassfail'); } - }); - } - }; - - - }, '@VERSION@', {"requires": ["base", "node", "event", "moodle-core_availability-form"]}); - \ No newline at end of file + } + }); + } +}; + + +}, '@VERSION@', {"requires": ["base", "node", "event", "moodle-core_availability-form"]}); diff --git a/yui/build/moodle-availability_othercompleted-form/moodle-availability_othercompleted-form-min.js b/yui/build/moodle-availability_othercompleted-form/moodle-availability_othercompleted-form-min.js index a7dc61b..724474e 100644 --- a/yui/build/moodle-availability_othercompleted-form/moodle-availability_othercompleted-form-min.js +++ b/yui/build/moodle-availability_othercompleted-form/moodle-availability_othercompleted-form-min.js @@ -1 +1 @@ -YUI.add("moodle-availability_othercompleted-form",function(e,t){M.availability_othercompleted=M.availability_othercompleted||{},M.availability_othercompleted.form=e.Object(M.core_availability.plugin),M.availability_othercompleted.form.initInner=function(e){this.datcms=e},M.availability_othercompleted.form.getNode=function(t){var n=' '+M.util.get_string("title","availability_othercompleted")+""+' ";var s=e.Node.create(''+n+"");t.cm!==undefined&&s.one("select[name=cm] > option[value="+t.cm+"]")&&s.one("select[name=cm]").set("value",""+t.cm),t.e!==undefined&&s.one("select[name=e]").set("value",""+t.e);if(!M.availability_othercompleted.form.addedEvents){M.availability_othercompleted.form.addedEvents=!0;var o=e.one(".availability-field");o.delegate("change",function(){M.core_availability.form.update()},".availability_othercompleted select")}return s},M.availability_othercompleted.form.fillValue=function(e,t){e.cm=parseInt(t.one("select[name=cm]").get("value"),10),e.e=parseInt(t.one("select[name=e]").get("value"),10)},M.availability_othercompleted.form.fillErrors=function(e,t){var n=parseInt(t.one("select[name=cm]").get("value"),10);n===0&&e.push("availability_othercompleted:error_selectcmid");var r=parseInt(t.one("select[name=e]").get("value"),10);(r===2||r===3)&&this.datcms.forEach(function(t){t.id===n&&t.completiongradeitemnumber===null&&e.push("availability_othercompleted:error_selectcmidpassfail")})}},"@VERSION@",{requires:["base","node","event","moodle-core_availability-form"]}); +YUI.add("moodle-availability_othercompleted-form",function(o,e){M.availability_othercompleted=M.availability_othercompleted||{},M.availability_othercompleted.form=o.Object(M.core_availability.plugin),M.availability_othercompleted.form.initInner=function(e){this.cms=e},M.availability_othercompleted.form.getNode=function(e){for(var t,l,a=' '+M.util.get_string("title","availability_othercompleted")+' ",l=o.Node.create(''+a+""),e.cm!==undefined&&l.one("select[name=cm] > option[value="+e.cm+"]")&&l.one("select[name=cm]").set("value",""+e.cm),e.e!==undefined&&l.one("select[name=e]").set("value",""+e.e),M.availability_othercompleted.form.addedEvents||(M.availability_othercompleted.form.addedEvents=!0,o.one(".availability-field").delegate("change",function(){M.core_availability.form.update()},".availability_othercompleted select")),l},M.availability_othercompleted.form.fillValue=function(e,t){e.cm=parseInt(t.one("select[name=cm]").get("value"),10),e.e=parseInt(t.one("select[name=e]").get("value"),10)},M.availability_othercompleted.form.fillErrors=function(t,e){var l=parseInt(e.one("select[name=cm]").get("value"),10);0===l&&t.push("availability_othercompleted:error_selectcmid"),2!==(e=parseInt(e.one("select[name=e]").get("value"),10))&&3!==e||this.cms.forEach(function(e){e.id===l&&null===e.completiongradeitemnumber&&t.push("availability_othercompleted:error_selectcmidpassfail")})}},"@VERSION@",{requires:["base","node","event","moodle-core_availability-form"]}); \ No newline at end of file diff --git a/yui/build/moodle-availability_othercompleted-form/moodle-availability_othercompleted-form.js b/yui/build/moodle-availability_othercompleted-form/moodle-availability_othercompleted-form.js index dc2179d..c466bf3 100644 --- a/yui/build/moodle-availability_othercompleted-form/moodle-availability_othercompleted-form.js +++ b/yui/build/moodle-availability_othercompleted-form/moodle-availability_othercompleted-form.js @@ -1,10 +1,11 @@ YUI.add('moodle-availability_othercompleted-form', function (Y, NAME) { /** - * JavaScript for form editing other completion conditions. + * JavaScript for form editing completion conditions. * * @module moodle-availability_othercompleted-form */ +// eslint-disable-next-line camelcase M.availability_othercompleted = M.availability_othercompleted || {}; /** @@ -17,27 +18,33 @@ M.availability_othercompleted.form = Y.Object(M.core_availability.plugin); * Initialises this plugin. * * @method initInner - * @param {Array} datcm Array of objects containing cmid => name + * @param {Array} cms Array of objects containing cmid => name */ -M.availability_othercompleted.form.initInner = function(datcm) { - this.datcm = datcm; +M.availability_othercompleted.form.initInner = function(cms) { + this.cms = cms; }; M.availability_othercompleted.form.getNode = function(json) { // Create HTML structure. - var html = ' ' + M.util.get_string('title', 'availability_othercompleted') + '' + - ' '; var node = Y.Node.create('' + html + ''); @@ -76,7 +83,7 @@ M.availability_othercompleted.form.fillErrors = function(errors, node) { } var e = parseInt(node.one('select[name=e]').get('value'), 10); if (((e === 2) || (e === 3))) { - this.datcm.forEach(function(cm) { + this.cms.forEach(function(cm) { if (cm.id === cmid) { if (cm.completiongradeitemnumber === null) { errors.push('availability_othercompleted:error_selectcmidpassfail'); diff --git a/yui/src/form/js/form.js b/yui/src/form/js/form.js index 1448809..7e3a705 100644 --- a/yui/src/form/js/form.js +++ b/yui/src/form/js/form.js @@ -3,6 +3,7 @@ * * @module moodle-availability_othercompleted-form */ +// eslint-disable-next-line camelcase M.availability_othercompleted = M.availability_othercompleted || {}; /** @@ -23,17 +24,21 @@ M.availability_othercompleted.form.initInner = function(cms) { M.availability_othercompleted.form.getNode = function(json) { // Create HTML structure. - var html = ' ' + M.util.get_string('title', 'availability_othercompleted') + '' + - '