├── .github └── workflows │ └── moodle-ci.yml ├── .gitignore ├── CHANGES.md ├── LICENSE ├── README.md ├── backup └── moodle2 │ ├── backup_subcourse_activity_task.class.php │ ├── backup_subcourse_stepslib.php │ ├── restore_subcourse_activity_task.class.php │ └── restore_subcourse_stepslib.php ├── classes ├── completion │ └── custom_completion.php ├── event │ ├── course_module_instance_list_viewed.php │ ├── course_module_viewed.php │ └── subcourse_grades_fetched.php ├── external │ └── view_subcourse.php ├── observers.php ├── output │ └── mobile.php ├── privacy │ └── provider.php └── task │ ├── check_completed_refcourses.php │ └── fetch_grades.php ├── db ├── access.php ├── events.php ├── install.xml ├── mobile.php ├── services.php ├── tasks.php └── upgrade.php ├── index.php ├── lang └── en │ ├── deprecated.txt │ └── subcourse.php ├── lib.php ├── locallib.php ├── mod_form.php ├── pix ├── howto.svg ├── icon-250.png ├── icon.png ├── icon.svg └── monologo.svg ├── settings.php ├── styles.css ├── templates ├── .mustachelintignore ├── mobile_view_latest.mustache └── subcourseinfo.mustache ├── tests ├── behat │ ├── auto_fetch_grades.feature │ ├── completion_course.feature │ ├── course_page_display.feature │ ├── fetch_percentage_grades.feature │ ├── hidden_grades.feature │ └── instant_redirect.feature ├── external.php ├── generator │ └── lib.php ├── locallib_test.php └── output_mobile_test.php ├── version.php └── view.php /.github/workflows/moodle-ci.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/ci.yml 2 | name: ci 3 | 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | test: 8 | uses: catalyst/catalyst-moodle-workflows/.github/workflows/ci.yml@main 9 | secrets: 10 | moodle_org_token: ${{ secrets.MOODLE_ORG_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /phpunit.xml 2 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ### 10.1.0 ### 2 | 3 | * Ionic 5 support added to make the plugin compatible with the Moodle App version 4 | 3.9.5. Credit goes to Dani Palou (@dpalou). 5 | * Removed legacy Bootstrap 2 classes. Credit goes to Daniel Escobar (@Daryhez). 6 | * Coding style cleanups and improvements. 7 | 8 | ### 10.0.0 ### 9 | 10 | * Display of progress and grade in referenced course on the main course page can be 11 | now configured (issue #32). Credit goes to Arnaud Trouvé (@ak4t0sh). 12 | * Supported Moodle versions 3.9 (LTS) and 3.10. 13 | 14 | ### 9.0.1 ### 15 | 16 | * Added example JSON context to the ionic template to pass the mustache lint. 17 | 18 | ### 9.0.0 ### 19 | 20 | * Added support for the Moodle Mobile App. The app displays a simple screen with the 21 | information about the progress in the referenced course, the current final grade 22 | there and a button to go to the referenced course. Teacher specific features such as 23 | fetching grades are out of mobile app support scope (issue #19). 24 | * Added standard subcourse:view allowing to fine control what can view the module 25 | instances in the course. 26 | * Added ability to mark the Subcourse activity as completed automatically upon opening 27 | by users - uses standard view tracking feature. 28 | 29 | ### 8.0.1 ### 30 | 31 | * Fixed issue #38 causing the grades fetch fail and throw error in certain cases. This 32 | is a regression of the hidden grades support introduced in 8.0.0 (issue #28). 33 | 34 | ### 8.0.0 ### 35 | 36 | * Referenced course selector uses the autocomplete form widget (issue #34). 37 | * Referenced course final grades can now be fetched either as real values (existing 38 | and default behaviour) or as percentual values (new optional behaviour). This allows 39 | teacher to keep the percentage displayed in the referenced course matching with the 40 | grade in the subcourse activity even if there are excluded grades (issue #29). 41 | * If the grades are hidden in the referenced course, they are now correctly marked as 42 | hidden in subcourse activity, too. This supports both hidden grade item (i.e. whole 43 | columns in the gradebook) and individual grades (issue #28). 44 | * Supported and tested on Moodle 3.8 and 3.9. Likely to work on lower versions, too. 45 | 46 | ### 7.2.0 ### 47 | 48 | * Links to the referenced course can open in a new window/tab (issue #27). 49 | 50 | ### 7.1.1 ### 51 | 52 | * Do not call a method deprecated in Moodle 3.6 53 | * Updated Behat tests to work in Moodle 3.6 54 | 55 | ### 7.1.0 ### 56 | 57 | * Improved handling of hidden grade items in the referenced course (issue #28). 58 | 59 | ### 7.0.0 ### 60 | 61 | * Progress in the referenced course is displayed. 62 | * The main view page now provides links to the gradebook in the referenced course 63 | instead of the link to the gradebook in the current course (issue #22). 64 | * Overall cleanup and UI improvements of the main view page. 65 | 66 | ### 6.0.0 ### 67 | 68 | * Filters are applied displaying course names - credit goes to Philipp Hager 69 | * The "should be completed" event is displayed on the dashboard if expected completion 70 | date/time is set. 71 | * Privacy API implemented to make it GDPR friendly. No personal data are stored by the 72 | module itself. 73 | * Requires Moodle 3.3 or higher, updated tests for Moodle 3.5. 74 | 75 | ### 5.0.1 ### 76 | 77 | * Fixed a typo in the string (currrent -> current). 78 | * Updated Behat tests to work in Moodle 3.3 and 3.4. 79 | 80 | ### 5.0.0 ### 81 | 82 | * Performance improvements: On triggered events (such as when a student 83 | received a grade in the referenced course), the subcourse used to fetch all 84 | students grades. This led to performance troubles in courses with many 85 | students, or when performing bulk changes (such as enrolling multiple 86 | students at once). 87 | * Completion: The subcourse can now be automatically marked as completed when 88 | the student completes the referenced course. 89 | * Behat tests pass on Moodle 3.1 and 3.2, manually tested on 3.3. 90 | 91 | ### 4.0.1 ### 92 | 93 | * Fixed Behat tests syntax for Moodle 3.1 and 3.2 94 | 95 | ### 4.0.0 ### 96 | 97 | * Fixed Behat test failure in Moodle 3.0 due to MDL-51051. 98 | * Fixed coding style violations reported by the codechecker. 99 | * Added support for Travis CI. 100 | 101 | ### 3.1 ### 102 | 103 | * Added support for Moodle 2.9 - does not use `add_intro_editor()` for this and higher versions. 104 | * Improved Behat tests to prevent accidental false positive matches of certain selectors. 105 | 106 | ### 3.0 ### 107 | 108 | * The module now observes "user graded" and "role assigned" events and fetches grades instantly on them (no need to rely on pressing 109 | the "Fetch now" button or the cron task running). Credit goes to Vadim Dvorovenko for implementing this. 110 | * Added Behat tests. 111 | * Fixed missing name of the "addinstance" capability. 112 | * Requires Moodle 2.8. 113 | * Changed versioning scheme of the plugin. The "master" branch now contains the most recent stable code. 114 | 115 | ### 2.7.0 ### 116 | 117 | * Added an option to instantly redirect to the referenced course when attempting to view the subcourse module page. This does not 118 | affect users with the permission to fetch grades manually so they do not loose that option. Credit goes to Matt Gibson for the 119 | original idea and implementation in his fork. 120 | * Legacy cron function replaced with the new scheduled task API. This allows administrators to define the schedule of fetching 121 | grades from subcourses to fit their needs (especially on heavy loaded sites with many users). 122 | * Legacy add_to_log() replaced with the new event API. This allows to use the new logging storage introduced in Moodle 2.7. Credit 123 | goes to Vadim Dvorovenko for the original patch. 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Subcourse module for Moodle 2 | =========================== 3 | 4 | ![Moodle Plugin CI](https://github.com/catalyst/moodle-mod_subcourse/workflows/Moodle%20Plugin%20CI/badge.svg) 5 | 6 | This Moodle module provides very simple yet useful functionality. When added into a 7 | course, it behaves as a graded activity. The grade for each student is took from a 8 | final grade in another course. Combined with 9 | [metacourses](http://docs.moodle.org/en/Course_meta_link), this allows course 10 | designers to effectively organise courses into separate units. 11 | 12 | 13 | Branches 14 | ------------ 15 | The git branches here support the following versions. 16 | 17 | | Moodle version | Branch | 18 | |-----------------------|-------------------| 19 | | Moodle 4.1 | MOODLE_401_STABLE | 20 | | Moodle 4.2 | MOODLE_402_STABLE | 21 | | Moodle 4.3 | MOODLE_403_STABLE | 22 | | Moodle 4.4 | MOODLE_404_STABLE | 23 | | Moodle 4.5 | MOODLE_405_STABLE | 24 | 25 | 26 | Installation 27 | ------------ 28 | 29 | Please follow for 30 | the general instructions on how to install Moodle plugins. 31 | 32 | When installing from uploaded ZIP package or via Git, the "subcourse" directory is 33 | expected to be place under the "/mod" directory of your Moodle installation. 34 | 35 | Usage 36 | ----- 37 | 38 | * Create a main course and one or few other courses that should act as a sub-courses of 39 | the main course. 40 | * Into the main course, add a new instance of the Subcourse activity module for each 41 | of the referenced course (sub-course) to fetch grades from. 42 | * Enrol students into all courses (the main one as well as the referenced ones). 43 | * Let students receive the final grades in the referenced courses. 44 | * Check that the final grade in the referenced courses now appears as the grade for 45 | the Subcourse activity in the main course. 46 | 47 | Support 48 | ------ 49 | 50 | Free support for this plugin is available in the moodle.org forums - https://moodle.org/mod/forum/view.php?id=44 51 | 52 | Commercial level support is available from the Moodle Partner Catalyst IT https://www.catalyst.net.nz/contact-us 53 | 54 | Author 55 | ------ 56 | 57 | The module was originally written by David Mudrák and is now maintained by Catalyst IT. 58 | 59 | Useful links 60 | ------------ 61 | 62 | * [Bug tracker](https://github.com/catalyst/moodle-mod_subcourse/issues) 63 | 64 | License 65 | ------- 66 | 67 | This program is free software: you can redistribute it and/or modify it under the 68 | terms of the GNU General Public License as published by the Free Software Foundation, 69 | either version 3 of the License, or (at your option) any later version. 70 | 71 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 72 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 73 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 74 | 75 | You should have received a copy of the GNU General Public License along with this 76 | program. If not, see . 77 | -------------------------------------------------------------------------------- /backup/moodle2/backup_subcourse_activity_task.class.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides {@see backup_subcourse_activity_task} class. 19 | * 20 | * @package mod_subcourse 21 | * @category backup 22 | * @copyright 2013 David Mudrak 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | 28 | require_once($CFG->dirroot.'/mod/subcourse/backup/moodle2/backup_subcourse_stepslib.php'); 29 | 30 | /** 31 | * Provides settings and steps to perform a complete backup of the activity. 32 | * 33 | * @copyright 2013 David Mudrak 34 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 | */ 36 | class backup_subcourse_activity_task extends backup_activity_task { 37 | 38 | /** 39 | * Define (add) particular settings this activity can have 40 | */ 41 | protected function define_my_settings() { 42 | // No particular settings for this activity. 43 | } 44 | 45 | /** 46 | * Define (add) particular steps this activity can have 47 | */ 48 | protected function define_my_steps() { 49 | $this->add_step(new backup_subcourse_activity_structure_step('subcourse_structure', 'subcourse.xml')); 50 | } 51 | 52 | /** 53 | * Code the transformations to perform in the activity in order to get transportable (encoded) links 54 | * 55 | * @param string $content User text content 56 | */ 57 | public static function encode_content_links($content) { 58 | global $CFG; 59 | 60 | $base = preg_quote($CFG->wwwroot, "/"); 61 | 62 | // Link to the list of subcourses. 63 | $search = "/(".$base."\/mod\/subcourse\/index.php\?id\=)([0-9]+)/"; 64 | $content = preg_replace($search, '$@SUBCOURSEINDEX*$2@$', $content); 65 | 66 | // Link to subcourse by moduleid. 67 | $search = "/(".$base."\/mod\/subcourse\/view.php\?id\=)([0-9]+)/"; 68 | $content = preg_replace($search, '$@SUBCOURSEVIEWBYID*$2@$', $content); 69 | 70 | return $content; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /backup/moodle2/backup_subcourse_stepslib.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides {@see backup_subcourse_activity_structure_step} class. 19 | * 20 | * @package mod_subcourse 21 | * @category backup 22 | * @copyright 2013 David Mudrak 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | /** 27 | * Defines the complete subcourse structure for backup 28 | * 29 | * @copyright 2013 David Mudrak 30 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 31 | */ 32 | class backup_subcourse_activity_structure_step extends backup_activity_structure_step { 33 | 34 | /** 35 | * Defines the complete subcourse structure for backup 36 | */ 37 | protected function define_structure() { 38 | 39 | $subcourse = new backup_nested_element('subcourse', ['id'], [ 40 | 'name', 'intro', 'introformat', 'timecreated', 'timemodified', 'timefetched', 41 | 'refcourse', 'instantredirect', 'completioncourse', 'blankwindow', 'fetchpercentage', 42 | 'coursepageprintgrade', 'coursepageprintprogress' 43 | ]); 44 | 45 | $subcourse->set_source_table('subcourse', ['id' => backup::VAR_ACTIVITYID]); 46 | 47 | return $this->prepare_activity_structure($subcourse); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /backup/moodle2/restore_subcourse_activity_task.class.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides {@see restore_subcourse_activity_task} class. 19 | * 20 | * @package mod_subcourse 21 | * @category backup 22 | * @copyright 2013 David Mudrak 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | 28 | require_once($CFG->dirroot . '/mod/subcourse/backup/moodle2/restore_subcourse_stepslib.php'); 29 | 30 | /** 31 | * Defines all settings and step to perform the activity restore. 32 | * 33 | * @copyright 2013 David Mudrak 34 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 | */ 36 | class restore_subcourse_activity_task extends restore_activity_task { 37 | 38 | /** 39 | * Define (add) particular settings this activity can have 40 | */ 41 | protected function define_my_settings() { 42 | // No particular settings for this activity. 43 | } 44 | 45 | /** 46 | * Define (add) particular steps this activity can have 47 | */ 48 | protected function define_my_steps() { 49 | // Choice only has one structure step. 50 | $this->add_step(new restore_subcourse_activity_structure_step('subcourse_structure', 'subcourse.xml')); 51 | } 52 | 53 | /** 54 | * Define the contents in the activity that must be 55 | * processed by the link decoder 56 | */ 57 | public static function define_decode_contents() { 58 | $contents = []; 59 | 60 | $contents[] = new restore_decode_content('subcourse', ['intro'], 'subcourse'); 61 | 62 | return $contents; 63 | } 64 | 65 | /** 66 | * Define the decoding rules for links belonging 67 | * to the activity to be executed by the link decoder 68 | */ 69 | public static function define_decode_rules() { 70 | $rules = []; 71 | 72 | $rules[] = new restore_decode_rule('SUBCOURSEVIEWBYID', '/mod/subcourse/view.php?id=$1', 'course_module'); 73 | $rules[] = new restore_decode_rule('SUBCOURSEINDEX', '/mod/subcourse/index.php?id=$1', 'course'); 74 | 75 | return $rules; 76 | 77 | } 78 | 79 | /** 80 | * Define the restore log rules that will be applied 81 | * by the {@see restore_logs_processor} when restoring 82 | * subcourse logs. It must return one array 83 | * of {@see restore_log_rule} objects 84 | */ 85 | public static function define_restore_log_rules() { 86 | $rules = []; 87 | 88 | $rules[] = new restore_log_rule('subcourse', 'add', 'view.php?id={course_module}', '{subcourse}'); 89 | $rules[] = new restore_log_rule('subcourse', 'update', 'view.php?id={course_module}', '{subcourse}'); 90 | $rules[] = new restore_log_rule('subcourse', 'view', 'view.php?id={course_module}', '{subcourse}'); 91 | $rules[] = new restore_log_rule('subcourse', 'fetch', 'view.php?id={course_module}', '{subcourse}'); 92 | 93 | return $rules; 94 | } 95 | 96 | /** 97 | * Define the restore log rules that will be applied 98 | * by the {@see restore_logs_processor} when restoring 99 | * course logs. It must return one array 100 | * of {@see restore_log_rule} objects 101 | * 102 | * Note this rules are applied when restoring course logs 103 | * by the restore final task, but are defined here at 104 | * activity level. All them are rules not linked to any module instance (cmid = 0) 105 | */ 106 | public static function define_restore_log_rules_for_course() { 107 | $rules = []; 108 | 109 | // Fix old wrong uses (missing extension). 110 | $rules[] = new restore_log_rule('subcourse', 'view all', 'index?id={course}', null, 111 | null, null, 'index.php?id={course}'); 112 | $rules[] = new restore_log_rule('subcourse', 'view all', 'index.php?id={course}', null); 113 | 114 | return $rules; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /backup/moodle2/restore_subcourse_stepslib.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides {@see restore_subcourse_activity_structure_step} class 19 | * 20 | * @package mod_subcourse 21 | * @category backup 22 | * @copyright 2013 David Mudrak 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | /** 27 | * Structure step to restore one subcourse activity 28 | * 29 | * @copyright 2017 David Mudrak 30 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 31 | */ 32 | class restore_subcourse_activity_structure_step extends restore_activity_structure_step { 33 | 34 | /** 35 | * Attaches the handlers of the backup XML tree parts. 36 | * 37 | * @return array of restore_path_element 38 | */ 39 | protected function define_structure() { 40 | 41 | $paths = []; 42 | 43 | $paths[] = new restore_path_element('subcourse', '/activity/subcourse'); 44 | 45 | return $this->prepare_activity_structure($paths); 46 | } 47 | 48 | /** 49 | * Process the /activity/subcourse path element. 50 | * 51 | * @param object|array $data node contents 52 | */ 53 | protected function process_subcourse($data) { 54 | global $DB; 55 | 56 | $data = (object)$data; 57 | $data->course = $this->get_courseid(); 58 | 59 | $data->timefetched = 0; 60 | 61 | if (!$this->task->is_samesite() || !$DB->record_exists('course', ['id' => $data->refcourse])) { 62 | $data->refcourse = 0; 63 | } 64 | 65 | $newitemid = $DB->insert_record('subcourse', $data); 66 | // Immediately after inserting "activity" record, call this. 67 | $this->apply_activity_instance($newitemid); 68 | } 69 | 70 | /** 71 | * Callback to be executed after the restore. 72 | */ 73 | protected function after_execute() { 74 | // Add subcourse related files, no need to match by itemname (just internally handled context). 75 | $this->add_related_files('mod_subcourse', 'intro', null); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /classes/completion/custom_completion.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | declare(strict_types=1); 18 | 19 | namespace mod_subcourse\completion; 20 | 21 | /** 22 | * Custom completion rules for mod_subcourse 23 | * 24 | * @package mod_subcourse 25 | * @copyright Catalyst IT 26 | * @author Dan Marsden 27 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 28 | */ 29 | class custom_completion extends \core_completion\activity_custom_completion { 30 | /** 31 | * Returns completion state of the custom completion rules 32 | * 33 | * @param string $rule 34 | * @return integer 35 | */ 36 | public function get_state(string $rule): int { 37 | global $CFG, $DB; 38 | require_once($CFG->dirroot.'/completion/completion_completion.php'); 39 | 40 | $this->validate_rule($rule); 41 | 42 | $subcourse = $DB->get_record('subcourse', ['id' => $this->cm->instance], 'id,refcourse,completioncourse', MUST_EXIST); 43 | 44 | if (empty($subcourse->completioncourse)) { 45 | // The rule not enabled, return early. 46 | return COMPLETION_UNKNOWN; 47 | } 48 | 49 | if (empty($subcourse->refcourse)) { 50 | // Misconfigured subcourse instance, behave as if was not enabled. 51 | return COMPLETION_INCOMPLETE; 52 | } 53 | 54 | // Check if the referenced course is completed. 55 | $coursecompletion = new \completion_completion(['userid' => $this->userid, 'course' => $subcourse->refcourse]); 56 | 57 | return $coursecompletion->is_complete() ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE; 58 | } 59 | 60 | /** 61 | * Fetch the list of custom completion rules that this module defines. 62 | * @return array 63 | */ 64 | public static function get_defined_custom_rules(): array { 65 | return ['completioncourse']; 66 | } 67 | 68 | /** 69 | * Returns an associative array of the descriptions of custom completion rules. 70 | * @return array 71 | */ 72 | public function get_custom_rule_descriptions(): array { 73 | return ['completioncourse' => get_string('completioncourse', 'subcourse')]; 74 | } 75 | 76 | /** 77 | * Returns an array of all completion rules, in the order they should be displayed to users. 78 | * @return array 79 | */ 80 | public function get_sort_order(): array { 81 | return [ 82 | 'completionview', 83 | 'completionusegrade', 84 | 'completioncourse', 85 | ]; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /classes/event/course_module_instance_list_viewed.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides the {@see mod_subcourse\even\course_module_instance_list_viewed} class. 19 | * 20 | * @package mod_subcourse 21 | * @category event 22 | * @copyright 2014 Vadim Dvorovenko 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | namespace mod_subcourse\event; 27 | 28 | /** 29 | * Represents the "course module instance list viewed" event. 30 | * 31 | * @copyright 2017 David Mudrak 32 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 | */ 34 | class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed { 35 | // No need for any code here as everything is handled by the parent class. 36 | } 37 | -------------------------------------------------------------------------------- /classes/event/course_module_viewed.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides the {@see mod_subcourse\event\course_module_viewed} class. 19 | * 20 | * @package mod_subcourse 21 | * @category event 22 | * @copyright 2014 Vadim Dvorovenko 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | namespace mod_subcourse\event; 27 | 28 | /** 29 | * Represents the "course module viewed" event. 30 | * 31 | * @copyright 2017 David Mudrak 32 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 | */ 34 | class course_module_viewed extends \core\event\course_module_viewed { 35 | 36 | /** 37 | * Initialize the event - set the objecttable. 38 | */ 39 | protected function init() { 40 | $this->data['objecttable'] = 'subcourse'; 41 | parent::init(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /classes/event/subcourse_grades_fetched.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides the {@see mod_subcourse\event\subcourse_grades_fetched} class. 19 | * 20 | * @package mod_subcourse 21 | * @category event 22 | * @copyright 2014 Vadim Dvorovenko 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | namespace mod_subcourse\event; 27 | 28 | /** 29 | * Represents the "grades fetched" event. 30 | * 31 | * @copyright 2017 David Mudrak 32 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 | */ 34 | class subcourse_grades_fetched extends \core\event\base { 35 | 36 | /** 37 | * Initialize the event. 38 | */ 39 | protected function init() { 40 | $this->data['crud'] = 'u'; 41 | $this->data['edulevel'] = self::LEVEL_TEACHING; 42 | $this->data['objecttable'] = 'subcourse'; 43 | } 44 | 45 | /** 46 | * Return the event's human readable name. 47 | * 48 | * @return string 49 | */ 50 | public static function get_name() { 51 | return get_string('eventgradesfetched', 'subcourse'); 52 | } 53 | 54 | /** 55 | * Return the event's human readable description. 56 | * 57 | * @return string 58 | */ 59 | public function get_description() { 60 | return "The user with id '{$this->userid}' fetched grades from the course with id '{$this->other['refcourse']}' ". 61 | "into the 'subcourse' activity with the course module id '{$this->contextinstanceid}'."; 62 | } 63 | 64 | /** 65 | * Return the URL of the subcourse module to which the grades were fetched. 66 | * 67 | * @return moodle_url 68 | */ 69 | public function get_url() { 70 | return new \moodle_url('/mod/subcourse/view.php', ['id' => $this->contextinstanceid]); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /classes/external/view_subcourse.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides {@see \mod_subcourse\external\view_subcourse} class. 19 | * 20 | * @copyright 2020 David Mudrák 21 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 22 | */ 23 | 24 | namespace mod_subcourse\external; 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | 28 | require_once($CFG->libdir . '/externallib.php'); 29 | 30 | use external_api; 31 | use external_function_parameters; 32 | use external_multiple_structure; 33 | use external_single_structure; 34 | use external_value; 35 | use external_warnings; 36 | 37 | /** 38 | * Implements the mod_subcourse_view_subcourse external function. 39 | * 40 | * @package mod_subcourse 41 | * @category external 42 | * @copyright 2020 David Mudrák 43 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 44 | */ 45 | class view_subcourse extends external_api { 46 | 47 | /** 48 | * Describes the parameters for view_subcourse. 49 | * 50 | * @return external_function_parameters 51 | */ 52 | public static function execute_parameters() { 53 | 54 | return new external_function_parameters([ 55 | 'subcourseid' => new external_value(PARAM_INT, 'Subcourse instance id'), 56 | ]); 57 | } 58 | 59 | /** 60 | * Trigger the course module viewed event and update the module completion status. 61 | * 62 | * @param int $subcourseid subcourse instance id 63 | * @return array of warnings and status result 64 | * @throws moodle_exception 65 | */ 66 | public static function execute($subcourseid) { 67 | global $CFG, $DB; 68 | require_once($CFG->dirroot . '/mod/subcourse/locallib.php'); 69 | 70 | $params = ['subcourseid' => $subcourseid]; 71 | $params = self::validate_parameters(self::execute_parameters(), $params); 72 | $warnings = []; 73 | 74 | $subcourse = $DB->get_record('subcourse', ['id' => $params['subcourseid']], '*', MUST_EXIST); 75 | list($course, $cm) = get_course_and_cm_from_instance($subcourse, 'subcourse'); 76 | $context = \context_module::instance($cm->id); 77 | 78 | self::validate_context($context); 79 | 80 | subcourse_set_module_viewed($subcourse, $context, $course, $cm); 81 | 82 | $result = [ 83 | 'status' => true, 84 | 'warnings' => $warnings, 85 | ]; 86 | 87 | return $result; 88 | } 89 | 90 | /** 91 | * Describes the view_subcourse return value. 92 | * 93 | * @return external_single_structure 94 | */ 95 | public static function execute_returns() { 96 | return new external_single_structure( 97 | [ 98 | 'status' => new external_value(PARAM_BOOL, 'Status: true if success'), 99 | 'warnings' => new external_warnings(), 100 | ] 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /classes/observers.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides the {@see mod_subcourse\observers} class. 19 | * 20 | * @package mod_subcourse 21 | * @copyright 2014 Vadim Dvorovenko (Vadimon@mail.ru) 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | namespace mod_subcourse; 26 | 27 | use completion_info; 28 | 29 | defined('MOODLE_INTERNAL') || die(); 30 | 31 | require_once($CFG->dirroot . '/mod/subcourse/locallib.php'); 32 | 33 | /** 34 | * Implements the module's event observers. 35 | * 36 | * @copyright 2017 David Mudrak 37 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 38 | */ 39 | class observers { 40 | 41 | /** 42 | * User graded 43 | * 44 | * @param \core\event\user_graded $event 45 | * @return void 46 | */ 47 | public static function user_graded(\core\event\user_graded $event) { 48 | global $DB; 49 | 50 | $courseid = $event->courseid; 51 | $userid = $event->relateduserid; 52 | 53 | $subcourses = $DB->get_records('subcourse', ['refcourse' => $courseid], '', 'id, course, refcourse, fetchpercentage'); 54 | 55 | foreach ($subcourses as $subcourse) { 56 | subcourse_grades_update($subcourse->course, $subcourse->id, $subcourse->refcourse, 57 | null, false, false, $userid, $subcourse->fetchpercentage); 58 | } 59 | } 60 | 61 | /** 62 | * Role assigned 63 | * 64 | * Every time new user is enrolled into course we should fetch all subcourses again, 65 | * because user can be previously enrolled into subcourse 66 | * 67 | * @param \core\event\role_assigned $event 68 | * @return void 69 | */ 70 | public static function role_assigned(\core\event\role_assigned $event) { 71 | global $DB; 72 | 73 | $courseid = $event->courseid; 74 | $userid = $event->relateduserid; 75 | 76 | $subcourses = $DB->get_records('subcourse', ['course' => $courseid], '', 'id, course, refcourse, fetchpercentage'); 77 | 78 | foreach ($subcourses as $subcourse) { 79 | subcourse_grades_update($subcourse->course, $subcourse->id, $subcourse->refcourse, 80 | null, false, false, $userid, $subcourse->fetchpercentage); 81 | } 82 | } 83 | 84 | /** 85 | * Handle the course_completed event. 86 | * 87 | * Notify all subcourse instances with the relevant completion rule enabled 88 | * that the user completed the referenced course - so that they can be eventually 89 | * marked as completed, too. 90 | * 91 | * @param \core\event\course_completed $event 92 | * @return void 93 | */ 94 | public static function course_completed(\core\event\course_completed $event) { 95 | global $CFG, $DB; 96 | require_once($CFG->dirroot.'/lib/completionlib.php'); 97 | 98 | $courseid = $event->courseid; 99 | $userid = $event->relateduserid; 100 | 101 | // Get all subcourses that have the completed course as the referenced one. 102 | $subcourses = $DB->get_records('subcourse', ['refcourse' => $courseid, 'completioncourse' => 1]); 103 | 104 | if (empty($subcourses)) { 105 | // No subcourse interested in this. 106 | return; 107 | } 108 | 109 | // Load the courses where the subcourses are located in. 110 | $courseids = []; 111 | 112 | foreach ($subcourses as $subcourse) { 113 | $courseids[$subcourse->course] = true; 114 | } 115 | 116 | $courses = $DB->get_records_list('course', 'id', array_keys($courseids), '', '*'); 117 | 118 | foreach ($subcourses as $subcourse) { 119 | $course = $courses[$subcourse->course]; 120 | $cm = get_coursemodule_from_instance('subcourse', $subcourse->id, $course->id); 121 | $completion = new completion_info($course); 122 | 123 | if ($completion->is_enabled($cm)) { 124 | // Notify the subcourse to check the completion status. 125 | $completion->update_state($cm, COMPLETION_COMPLETE, $userid); 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /classes/output/mobile.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides {@see \mod_subcourse\output\mobile} class. 19 | * 20 | * @copyright 2020 David Mudrák 21 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 22 | */ 23 | 24 | namespace mod_subcourse\output; 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | 28 | require_once($CFG->dirroot . '/mod/subcourse/locallib.php'); 29 | 30 | /** 31 | * Controls the display of the plugin in the Mobile App. 32 | * 33 | * @package mod_subcourse 34 | * @category output 35 | * @copyright 2020 David Mudrák 36 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 | */ 38 | class mobile { 39 | 40 | /** 41 | * Return the data for the CoreCourseModuleDelegate delegate. 42 | * 43 | * @param object $args 44 | * @return object 45 | */ 46 | public static function main_view($args) { 47 | global $OUTPUT, $USER, $DB; 48 | 49 | $args = (object) $args; 50 | $cm = get_coursemodule_from_id('subcourse', $args->cmid); 51 | $context = \context_module::instance($cm->id); 52 | 53 | require_login($args->courseid, false, $cm, true, true); 54 | require_capability('mod/subcourse:view', $context); 55 | 56 | $subcourse = $DB->get_record('subcourse', ['id' => $cm->instance], '*', MUST_EXIST); 57 | 58 | $warning = null; 59 | $progress = null; 60 | 61 | if (empty($subcourse->refcourse)) { 62 | $refcourse = false; 63 | 64 | if (has_capability('mod/subcourse:fetchgrades', $context)) { 65 | $warning = get_string('refcoursenull', 'subcourse'); 66 | } 67 | 68 | } else { 69 | $refcourse = $DB->get_record('course', ['id' => $subcourse->refcourse], 'id, fullname', IGNORE_MISSING); 70 | } 71 | 72 | if ($refcourse) { 73 | $refcourse->fullname = \core_external\util::format_string($refcourse->fullname, $context); 74 | $refcourse->url = new \moodle_url('/course/view.php', ['id' => $refcourse->id]); 75 | $progress = \core_completion\progress::get_course_progress_percentage($refcourse); 76 | } 77 | 78 | $currentgrade = subcourse_get_current_grade($subcourse, $USER->id); 79 | 80 | // Pre-format some of the texts for the mobile app. 81 | $subcourse->name = \core_external\util::format_string($subcourse->name, $context); 82 | [$subcourse->intro, $subcourse->introformat] = \core_external\util::format_text($subcourse->intro, $subcourse->introformat, 83 | $context, 'mod_subcourse', 'intro'); 84 | 85 | $data = [ 86 | 'cmid' => $cm->id, 87 | 'subcourse' => $subcourse, 88 | 'refcourse' => $refcourse, 89 | 'progress' => $progress, 90 | 'hasprogress' => isset($progress), 91 | 'currentgrade' => $currentgrade, 92 | 'hasgrade' => isset($currentgrade), 93 | 'warning' => $warning, 94 | 'canusemoduleinfo' => $args->appversioncode >= 44000, 95 | ]; 96 | 97 | return [ 98 | 'templates' => [ 99 | [ 100 | 'id' => 'main', 101 | 'html' => $OUTPUT->render_from_template('mod_subcourse/mobile_view_latest', $data), 102 | ], 103 | ], 104 | 'javascript' => '', 105 | 'otherdata' => '', 106 | 'files' => [], 107 | ]; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /classes/privacy/provider.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Defines {@see \mod_subcourse\privacy\provider} class. 19 | * 20 | * @package mod_subcourse 21 | * @category privacy 22 | * @copyright 2018 David Mudrák 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | namespace mod_subcourse\privacy; 27 | 28 | /** 29 | * Privacy API implementation for the Subcourse plugin. 30 | * 31 | * @copyright 2018 David Mudrák 32 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 | */ 34 | class provider implements \core_privacy\local\metadata\null_provider { 35 | 36 | use \core_privacy\local\legacy_polyfill; 37 | 38 | // phpcs:disable PSR2.Methods.MethodDeclaration.Underscore 39 | 40 | /** 41 | * Returns stringid of a text explaining that this plugin stores no personal data. 42 | * 43 | * @return string 44 | */ 45 | public static function _get_reason() { 46 | return 'privacy:metadata'; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /classes/task/check_completed_refcourses.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides {@see \mod_subcourse\task\check_completed_refcourses} class 19 | * 20 | * @package mod_subcourse 21 | * @category task 22 | * @copyright 2017 David Mudrák 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | namespace mod_subcourse\task; 27 | 28 | use completion_completion; 29 | use completion_info; 30 | use context_course; 31 | 32 | defined('MOODLE_INTERNAL') || die(); 33 | 34 | require_once($CFG->dirroot.'/mod/subcourse/locallib.php'); 35 | 36 | /** 37 | * Makes sure that all subcourse instances are marked as completed when they should be. 38 | * 39 | * Normally, completed course triggers the subcourse completion automatically 40 | * via observing the event. This task is there for rechecking the completions to catch 41 | * up with courses that were completed in the past (and the event was missed). 42 | * 43 | * @copyright 2017 David Mudrak 44 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 45 | */ 46 | class check_completed_refcourses extends \core\task\scheduled_task { 47 | 48 | /** 49 | * Returns a descriptive name for this task shown to admins 50 | * 51 | * @return string 52 | */ 53 | public function get_name() { 54 | return get_string('taskcheckcompletedrefcourses', 'mod_subcourse'); 55 | } 56 | 57 | /** 58 | * Performs the task 59 | * 60 | * @throws moodle_exception on an error (the job will be retried) 61 | */ 62 | public function execute() { 63 | global $CFG, $DB; 64 | require_once($CFG->dirroot.'/lib/completionlib.php'); 65 | require_once($CFG->dirroot.'/completion/completion_completion.php'); 66 | 67 | if (!completion_info::is_enabled_for_site()) { 68 | mtrace("Completion tracking not enabled on this site"); 69 | return; 70 | } 71 | 72 | // Get all subcourses that have completion rule based on refcourse completed. 73 | $rs = $DB->get_recordset('subcourse', ['completioncourse' => 1], 'course'); 74 | 75 | // Note the subcourses are sorted by their course. We cache the course 76 | // record and the list of enrolled participants for all subcourse 77 | // instances within one course in the following variable. 78 | $cache = []; 79 | 80 | foreach ($rs as $subcourse) { 81 | $cm = get_coursemodule_from_instance('subcourse', $subcourse->id); 82 | 83 | if (empty($subcourse->refcourse)) { 84 | mtrace("Subcourse {$subcourse->id}: no referenced course configured ... skipped"); 85 | continue; 86 | } 87 | 88 | if (!isset($cache[$subcourse->course])) { 89 | // Load the course with this subcourse and students enrolled to it. 90 | // We do not need data from the previous course any more. 91 | $course = $DB->get_record('course', ['id' => $subcourse->course]); 92 | $coursecontext = context_course::instance($course->id); 93 | $cache = [ 94 | $course->id => (object)[ 95 | 'course' => $course, 96 | 'participants' => get_enrolled_users($coursecontext, 'mod/subcourse:begraded', 0, "u.id"), 97 | ] 98 | ]; 99 | } 100 | 101 | $completion = new completion_info($cache[$subcourse->course]->course); 102 | 103 | if (!$completion->is_enabled($cm)) { 104 | mtrace("Subcourse {$subcourse->id}: completion tracking not enabled ... skipped"); 105 | continue; 106 | } 107 | 108 | mtrace("Subcourse {$subcourse->id}: checking refcourse {$subcourse->refcourse} completions ... "); 109 | 110 | foreach (array_keys($cache[$subcourse->course]->participants) as $userid) { 111 | $coursecompletion = new completion_completion(['userid' => $userid, 'course' => $subcourse->refcourse]); 112 | if ($coursecompletion->is_complete()) { 113 | // Notify the subcourse to check the completion status. 114 | mtrace(" - user {$userid}: has completed referenced course, checking subcourse completion"); 115 | $completion->update_state($cm, COMPLETION_COMPLETE, $userid); 116 | } 117 | } 118 | 119 | mtrace(" ... checked ".count($cache[$subcourse->course]->participants)." users"); 120 | } 121 | 122 | $rs->close(); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /classes/task/fetch_grades.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides the {@see mod_subcourse\task\fetch_grades} class. 19 | * 20 | * @package mod_subcourse 21 | * @category task 22 | * @copyright 2014 David Mudrak 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | namespace mod_subcourse\task; 27 | 28 | defined('MOODLE_INTERNAL') || die(); 29 | 30 | require_once($CFG->dirroot.'/mod/subcourse/locallib.php'); 31 | 32 | /** 33 | * Fetches remote grades into all subcourse instances 34 | * 35 | * @copyright 2014 David Mudrak 36 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 | */ 38 | class fetch_grades extends \core\task\scheduled_task { 39 | 40 | /** 41 | * Returns a descriptive name for this task shown to admins 42 | * 43 | * @return string 44 | */ 45 | public function get_name() { 46 | return get_string('taskfetchgrades', 'mod_subcourse'); 47 | } 48 | 49 | /** 50 | * Performs the task 51 | * 52 | * @throws moodle_exception on an error (the job will be retried) 53 | */ 54 | public function execute() { 55 | global $DB; 56 | 57 | $subcourses = $DB->get_records("subcourse", null, "", "id, course, refcourse, fetchpercentage"); 58 | 59 | if (empty($subcourses)) { 60 | return; 61 | } 62 | 63 | $updatedids = []; 64 | 65 | foreach ($subcourses as $subcourse) { 66 | 67 | if (empty($subcourse->refcourse)) { 68 | mtrace("Subcourse {$subcourse->id}: no referenced course configured ... skipped"); 69 | continue; 70 | } 71 | 72 | mtrace("Subcourse {$subcourse->id}: fetching grades from course {$subcourse->refcourse} ". 73 | "to course {$subcourse->course} ... ", ""); 74 | $result = subcourse_grades_update($subcourse->course, $subcourse->id, $subcourse->refcourse, 75 | null, false, false, [], $subcourse->fetchpercentage); 76 | 77 | if ($result == GRADE_UPDATE_OK) { 78 | $updatedids[] = $subcourse->id; 79 | mtrace("ok"); 80 | 81 | } else { 82 | mtrace("failed with error code ".$result); 83 | } 84 | } 85 | 86 | if (!empty($updatedids)) { 87 | subcourse_update_timefetched($updatedids); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /db/access.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Capability definitions for the subcourse module. 19 | * 20 | * @package mod_subcourse 21 | * @category access 22 | * @copyright 2008 David Mudrak 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | 28 | $capabilities = [ 29 | 'mod/subcourse:view' => [ 30 | 'captype' => 'read', 31 | 'contextlevel' => CONTEXT_MODULE, 32 | 'archetypes' => [ 33 | 'guest' => CAP_ALLOW, 34 | 'student' => CAP_ALLOW, 35 | 'teacher' => CAP_ALLOW, 36 | 'editingteacher' => CAP_ALLOW, 37 | 'manager' => CAP_ALLOW, 38 | ], 39 | ], 40 | 41 | 'mod/subcourse:addinstance' => [ 42 | 'riskbitmask' => RISK_XSS, 43 | 'captype' => 'write', 44 | 'contextlevel' => CONTEXT_COURSE, 45 | 'archetypes' => [ 46 | 'editingteacher' => CAP_ALLOW, 47 | 'manager' => CAP_ALLOW 48 | ], 49 | 'clonepermissionsfrom' => 'moodle/course:manageactivities', 50 | ], 51 | 52 | 'mod/subcourse:begraded' => [ 53 | 'captype' => 'write', 54 | 'contextlevel' => CONTEXT_MODULE, 55 | 'legacy' => [ 56 | 'student' => CAP_ALLOW 57 | ] 58 | ], 59 | 60 | 'mod/subcourse:fetchgrades' => [ 61 | 'captype' => 'write', 62 | 'contextlevel' => CONTEXT_MODULE, 63 | 'legacy' => [ 64 | 'teacher' => CAP_ALLOW, 65 | 'editingteacher' => CAP_ALLOW, 66 | ] 67 | ], 68 | ]; 69 | -------------------------------------------------------------------------------- /db/events.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Defines the list of module's event observers. 19 | * 20 | * @package mod_subcourse 21 | * @copyright 2014 Vadim Dvorovenko (Vadimon@mail.ru) 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | $observers = [ 28 | [ 29 | 'eventname' => '\core\event\user_graded', 30 | 'callback' => '\mod_subcourse\observers::user_graded', 31 | ], 32 | [ 33 | 'eventname' => '\core\event\role_assigned', 34 | 'callback' => '\mod_subcourse\observers::role_assigned', 35 | ], 36 | [ 37 | 'eventname' => '\core\event\course_completed', 38 | 'callback' => '\mod_subcourse\observers::course_completed', 39 | ], 40 | ]; 41 | -------------------------------------------------------------------------------- /db/install.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /db/mobile.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Declares the Mobile App addons provided by this plugin. 19 | * 20 | * @package mod_subcourse 21 | * @copyright 2020 David Mudrák 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | $addons = [ 28 | 'mod_subcourse' => [ 29 | 'handlers' => [ 30 | 'subcourse' => [ 31 | 'displaydata' => [ 32 | 'icon' => $CFG->wwwroot . '/mod/subcourse/pix/icon.png', 33 | 'class' => '', 34 | ], 35 | 36 | 'delegate' => 'CoreCourseModuleDelegate', 37 | 'method' => 'main_view', 38 | ], 39 | ], 40 | 'lang' => [ 41 | ['pluginname', 'mod_subcourse'], 42 | ['gotorefcourse', 'mod_subcourse'], 43 | ['currentgrade', 'mod_subcourse'], 44 | ], 45 | ], 46 | ]; 47 | -------------------------------------------------------------------------------- /db/services.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Defines the external functions and services provided by the plugin. 19 | * 20 | * @package mod_subcourse 21 | * @category external 22 | * @copyright 2020 David Mudrák 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | 28 | $functions = [ 29 | 'mod_subcourse_view_subcourse' => [ 30 | 'classname' => 'mod_subcourse\external\view_subcourse', 31 | 'methodname' => 'execute', 32 | 'description' => 'Trigger the course module viewed event and update the module completion status.', 33 | 'type' => 'write', 34 | 'capabilities' => 'mod/subcourse:view', 35 | 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], 36 | ], 37 | ]; 38 | -------------------------------------------------------------------------------- /db/tasks.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Defines the list of the module's scheduled tasks. 19 | * 20 | * @package mod_subcourse 21 | * @category task 22 | * @copyright 2014 David Mudrak 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | 28 | $tasks = [ 29 | [ 30 | 'classname' => '\mod_subcourse\task\fetch_grades', 31 | 'blocking' => 0, 32 | 'minute' => 'R', 33 | 'hour' => '3', 34 | 'day' => '*', 35 | 'dayofweek' => '1-5', 36 | 'month' => '*' 37 | ], 38 | [ 39 | 'classname' => '\mod_subcourse\task\check_completed_refcourses', 40 | 'blocking' => 0, 41 | 'minute' => 'R', 42 | 'hour' => '4', 43 | 'day' => '*', 44 | 'dayofweek' => '1-5', 45 | 'month' => '*' 46 | ], 47 | ]; 48 | -------------------------------------------------------------------------------- /db/upgrade.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Keeps track of upgrades to the subcourse module 19 | * 20 | * @package mod_subcourse 21 | * @category upgrade 22 | * @copyright 2008 David Mudrak 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | /** 27 | * Performs upgrade of the database structure and data 28 | * 29 | * @param int $oldversion the version we are upgrading from 30 | * @return bool true 31 | */ 32 | function xmldb_subcourse_upgrade($oldversion=0) { 33 | global $DB; 34 | 35 | $dbman = $DB->get_manager(); 36 | 37 | if ($oldversion < 2013102501) { 38 | // Drop the 'grade' field from the 'subcourse' table. 39 | 40 | $table = new xmldb_table('subcourse'); 41 | $field = new xmldb_field('grade'); 42 | if ($dbman->field_exists($table, $field)) { 43 | $dbman->drop_field($table, $field); 44 | } 45 | 46 | upgrade_mod_savepoint(true, 2013102501, 'subcourse'); 47 | } 48 | 49 | if ($oldversion < 2014060900) { 50 | // Add the field 'instantredirect' to the table 'subcourse'. 51 | $table = new xmldb_table('subcourse'); 52 | $field = new xmldb_field('instantredirect', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'refcourse'); 53 | 54 | if (!$dbman->field_exists($table, $field)) { 55 | $dbman->add_field($table, $field); 56 | } 57 | 58 | upgrade_mod_savepoint(true, 2014060900, 'subcourse'); 59 | } 60 | 61 | if ($oldversion < 2017071300) { 62 | // Add the field completioncourse to the table 'subcourse'. 63 | $table = new xmldb_table('subcourse'); 64 | $field = new xmldb_field('completioncourse', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'instantredirect'); 65 | 66 | if (!$dbman->field_exists($table, $field)) { 67 | $dbman->add_field($table, $field); 68 | } 69 | 70 | upgrade_mod_savepoint(true, 2017071300, 'subcourse'); 71 | } 72 | 73 | if ($oldversion < 2018121600) { 74 | // Add field 'blankwindow' to the table 'subcourse'. 75 | $table = new xmldb_table('subcourse'); 76 | $field = new xmldb_field('blankwindow', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'completioncourse'); 77 | 78 | if (!$dbman->field_exists($table, $field)) { 79 | $dbman->add_field($table, $field); 80 | } 81 | 82 | upgrade_mod_savepoint(true, 2018121600, 'subcourse'); 83 | } 84 | 85 | if ($oldversion < 2020071100) { 86 | // Add field 'fetchpercentage' to the table 'subcourse'. 87 | $table = new xmldb_table('subcourse'); 88 | $field = new xmldb_field('fetchpercentage', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'blankwindow'); 89 | 90 | if (!$dbman->field_exists($table, $field)) { 91 | $dbman->add_field($table, $field); 92 | } 93 | 94 | upgrade_mod_savepoint(true, 2020071100, 'subcourse'); 95 | } 96 | 97 | if ($oldversion < 2021021400) { 98 | // Add the field 'coursepageprintgrade' to the table 'subcourse'. 99 | $table = new xmldb_table('subcourse'); 100 | $field = new xmldb_field('coursepageprintgrade', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '1', 101 | 'fetchpercentage'); 102 | 103 | if (!$dbman->field_exists($table, $field)) { 104 | $dbman->add_field($table, $field); 105 | } 106 | 107 | // Add the field 'coursepageprintprogress' to the table 'subcourse'. 108 | $field = new xmldb_field('coursepageprintprogress', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '1', 109 | 'coursepageprintgrade'); 110 | 111 | if (!$dbman->field_exists($table, $field)) { 112 | $dbman->add_field($table, $field); 113 | } 114 | 115 | upgrade_mod_savepoint(true, 2021021400, 'subcourse'); 116 | } 117 | 118 | return true; 119 | } 120 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * View all instances of subcourse in a particular course 19 | * 20 | * @package mod_subcourse 21 | * @copyright 2008 David Mudrak 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | require_once(dirname(dirname(dirname(__FILE__))).'/config.php'); 26 | require_once(dirname(__FILE__).'/lib.php'); 27 | 28 | $id = required_param('id', PARAM_INT); 29 | 30 | $course = $DB->get_record('course', ['id' => $id], '*', MUST_EXIST); 31 | 32 | require_course_login($course); 33 | 34 | $PAGE->set_url(new moodle_url('/mod/subcourse/index.php', ['id' => $id])); 35 | $PAGE->set_title($course->fullname); 36 | $PAGE->set_heading($course->shortname); 37 | $PAGE->set_pagelayout('incourse'); 38 | $PAGE->navbar->add(get_string('modulenameplural', 'subcourse')); 39 | 40 | $event = \mod_subcourse\event\course_module_instance_list_viewed::create([ 41 | 'context' => context_course::instance($course->id) 42 | ]); 43 | $event->add_record_snapshot('course', $course); 44 | $event->trigger(); 45 | 46 | echo $OUTPUT->header(); 47 | 48 | if (!$subcourses = get_all_instances_in_course('subcourse', $course)) { 49 | echo $OUTPUT->heading(get_string('nosubcourses', 'subcourse'), 2); 50 | echo $OUTPUT->continue_button(new moodle_url('/course/view.php', ['id' => $course->id])); 51 | echo $OUTPUT->footer(); 52 | die(); 53 | } 54 | 55 | $usesections = course_format_uses_sections($course->format); 56 | 57 | $timenow = time(); 58 | $strsectionname = get_string('sectionname', 'format_'.$course->format); 59 | $strname = get_string('subcoursename', 'subcourse'); 60 | $strdesc = get_string('moduleintro'); 61 | 62 | $table = new html_table(); 63 | $table->id = 'subcourseslist'; 64 | 65 | if ($usesections) { 66 | $table->head = array ($strsectionname, $strname, $strdesc); 67 | $table->align = array ('center', 'left', 'left'); 68 | } else { 69 | $table->head = array ($strname, $strdesc); 70 | $table->align = array ('left', 'left'); 71 | } 72 | 73 | foreach ($subcourses as $subcourse) { 74 | $attributes = []; 75 | if (empty($subcourse->visible)) { 76 | $attributes['class'] = 'dimmed'; 77 | } 78 | $link = html_writer::link(new moodle_url('/mod/subcourse/view.php', 79 | ['id' => $subcourse->coursemodule]), format_string($subcourse->name), $attributes); 80 | $description = format_module_intro('subcourse', $subcourse, $subcourse->coursemodule); 81 | if ($usesections) { 82 | $table->data[] = [get_section_name($course, $subcourse->section), $link, $description]; 83 | } else { 84 | $table->data[] = [$link, $description]; 85 | } 86 | } 87 | 88 | echo $OUTPUT->heading(get_string('modulenameplural', 'subcourse')); 89 | echo html_writer::table($table); 90 | echo $OUTPUT->footer(); 91 | -------------------------------------------------------------------------------- /lang/en/deprecated.txt: -------------------------------------------------------------------------------- 1 | gotocoursename,mod_subcourse 2 | -------------------------------------------------------------------------------- /lang/en/subcourse.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Defines English strings of the subcourse module 19 | * 20 | * @package mod_subcourse 21 | * @category string 22 | * @copyright 2008 David Mudrak 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | 28 | $string['blankwindow'] = 'Open in a new window'; 29 | $string['blankwindow_help'] = 'When selected, the link will open the referenced course in a new browser window.'; 30 | $string['currentgrade'] = 'Current grade: {$a}'; 31 | $string['currentprogress'] = 'Progress: {$a}%'; 32 | $string['displayoption:coursepageprintgrade'] = 'Display grade from referenced course on course page'; 33 | $string['displayoption:coursepageprintprogress'] = 'Display progress from referenced course on course page'; 34 | $string['errfetch'] = 'Unable to fetch grades: error code {$a}'; 35 | $string['errlocalremotescale'] = 'Unable to fetch grades: the remote final grade item uses local scale.'; 36 | $string['eventgradesfetched'] = 'Grades fetched'; 37 | $string['fetchgradesmode'] = 'Fetch grades as'; 38 | $string['fetchgradesmode0'] = 'Real values'; 39 | $string['fetchgradesmode1'] = 'Percentual values'; 40 | $string['fetchgradesmode_help'] = 'Depending on the gradebook setup in the referenced course, the raw value and the percentual value of the final course grade may not always match the values shown in this subcourse activity. This setting determines which of the values should match. 41 | 42 | * Real values - the real value of the final grade in the referenced is fetched as an activity grade in this subcourse. If there are some excluded grades in the referenced course, then the percentual final grade calculated in the referenced course may not match the percentage in the subcourse activity. 43 | * Percentual values - the final grade received in the referenced course is recalculated so that the percentage displayed in the referenced course matches the percentage displayed in this subcourse activity. If there are some excluded grades in the referenced course, the actual real grade value may not match.'; 44 | $string['fetchnow'] = 'Fetch grades now'; 45 | $string['gotorefcourse'] = 'Go to {$a}'; 46 | $string['gotorefcoursegrader'] = 'All grades in {$a}'; 47 | $string['gotorefcoursemygrades'] = 'My grades in {$a}'; 48 | $string['gradesfetching'] = 'Grades fetching'; 49 | $string['hiddencourse'] = '*hidden*'; 50 | $string['instantredirect'] = 'Redirect to the referenced course'; 51 | $string['instantredirect_help'] = 'If enabled, users will be redirected to the referenced course when attempting to view the subcourse module page. Does not affect users with the permission to fetch grades manually.'; 52 | $string['lastfetchnever'] = 'The grades have not been fetched yet'; 53 | $string['lastfetchtime'] = 'Last fetch: {$a}'; 54 | $string['linkcontrol'] = 'Subcourse activity link'; 55 | $string['modulename'] = 'Subcourse'; 56 | $string['modulename_help'] = 'The module provides very simple yet useful functionality. When added into a course, it behaves as a graded activity. The grade for each student is taken from a final grade in another course. Combined with metacourses, this allows course designers to organize courses into separate units.'; 57 | $string['modulenameplural'] = 'Subcourses'; 58 | $string['nocoursesavailable'] = 'No courses you could fetch grades from'; 59 | $string['nosubcourses'] = 'There are no subcourses in this course'; 60 | $string['pluginadministration'] = 'Subcourse administration'; 61 | $string['pluginname'] = 'Subcourse'; 62 | $string['privacy:metadata'] = 'Subcourse does not store any personal data'; 63 | $string['refcourse'] = 'Referenced course'; 64 | $string['refcourse_help'] = 'The referenced course is the one the grade of the activity is taken from. Students should be enroled into the referenced course. 65 | 66 | You need to be a teacher in the course to have it listed here. You may need to ask your site administrator to set up this activity for you to fetch grades from other courses.'; 67 | $string['refcoursecurrent'] = 'Keep current reference'; 68 | $string['refcourselabel'] = 'Fetch grades from'; 69 | $string['refcoursenull'] = 'No referenced course configured'; 70 | $string['settings:coursepageprintgrade'] = 'Grade on course page'; 71 | $string['settings:coursepageprintgrade_desc'] = 'Display grade from referenced course on course page.'; 72 | $string['settings:displayhiddencourses'] = 'Display hidden courses'; 73 | $string['settings:displayhiddencourses_desc'] = 'Allow hidden courses to be selected when editing a subcourse.'; 74 | $string['settings:coursepageprintprogress'] = 'Progress on course page'; 75 | $string['settings:coursepageprintprogress_desc'] = 'Display progress from referenced course on course page.'; 76 | $string['subcourse:addinstance'] = 'Add a new subcourse'; 77 | $string['subcourse:begraded'] = 'Receive grade from the referenced course'; 78 | $string['subcourse:fetchgrades'] = 'Fetch grades manually from the referenced course'; 79 | $string['subcourse:view'] = 'View subcourse activity'; 80 | $string['subcoursename'] = 'Subcourse name'; 81 | $string['taskcheckcompletedrefcourses'] = 'Check referenced courses completion'; 82 | $string['taskfetchgrades'] = 'Fetch subcourse grades'; 83 | $string['completioncourse'] = 'Require course completed'; 84 | $string['completioncourse_help'] = 'If enabled, the activity is considered complete when a student completes the referenced course.'; 85 | $string['completioncourse_text'] = 'Student must complete the referenced course to complete this activity.'; 86 | 87 | // Deprecated and no longer used. 88 | $string['gotocoursename'] = 'Go to the course {$a->name}'; 89 | -------------------------------------------------------------------------------- /lib.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Library of functions, classes and constants for module subcourse 19 | * 20 | * @package mod_subcourse 21 | * @copyright 2008 David Mudrak 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | /** 26 | * Returns the information if the module supports a feature 27 | * 28 | * @see plugin_supports() in lib/moodlelib.php 29 | * @param string $feature FEATURE_xx constant for requested feature 30 | * @return mixed true if the feature is supported, null if unknown 31 | */ 32 | function subcourse_supports($feature) { 33 | 34 | if (defined('FEATURE_MOD_PURPOSE')) { 35 | if ($feature === FEATURE_MOD_PURPOSE) { 36 | return MOD_PURPOSE_CONTENT; 37 | } 38 | } 39 | 40 | switch($feature) { 41 | case FEATURE_GRADE_HAS_GRADE: 42 | return true; 43 | case FEATURE_MOD_INTRO: 44 | return true; 45 | case FEATURE_SHOW_DESCRIPTION: 46 | return true; 47 | case FEATURE_GROUPS: 48 | return true; 49 | case FEATURE_GROUPINGS: 50 | return true; 51 | case FEATURE_GROUPMEMBERSONLY: 52 | return true; 53 | case FEATURE_BACKUP_MOODLE2: 54 | return true; 55 | case FEATURE_COMPLETION_TRACKS_VIEWS: 56 | return true; 57 | case FEATURE_COMPLETION_HAS_RULES: 58 | return true; 59 | default: 60 | return null; 61 | } 62 | } 63 | 64 | /** 65 | * Given an object containing all the necessary data, (defined by the form) 66 | * this function will create a new instance and return the id number of the new 67 | * instance. 68 | * 69 | * @param stdClass $subcourse 70 | * @return int The id of the newly inserted subcourse record 71 | */ 72 | function subcourse_add_instance(stdClass $subcourse) { 73 | global $CFG, $DB; 74 | require_once($CFG->dirroot.'/mod/subcourse/locallib.php'); 75 | 76 | $subcourse->timecreated = time(); 77 | 78 | if (empty($subcourse->instantredirect)) { 79 | $subcourse->instantredirect = 0; 80 | } 81 | 82 | if (empty($subcourse->blankwindow)) { 83 | $subcourse->blankwindow = 0; 84 | } 85 | 86 | if (empty($subcourse->coursepageprintprogress)) { 87 | $subcourse->coursepageprintprogress = 0; 88 | } 89 | 90 | if (empty($subcourse->coursepageprintgrade)) { 91 | $subcourse->coursepageprintgrade = 0; 92 | } 93 | 94 | $newid = $DB->insert_record("subcourse", $subcourse); 95 | 96 | if (!empty($subcourse->refcourse)) { 97 | // Create grade_item but do not fetch grades. 98 | // The context does not exist yet and we can't get users by capability. 99 | subcourse_grades_update($subcourse->course, $newid, $subcourse->refcourse, $subcourse->name, true); 100 | } 101 | 102 | if (!empty($subcourse->completionexpected)) { 103 | \core_completion\api::update_completion_date_event($subcourse->coursemodule, 'subcourse', $newid, 104 | $subcourse->completionexpected); 105 | } 106 | 107 | return $newid; 108 | } 109 | 110 | /** 111 | * Given an object containing all the necessary data, (defined by the form) 112 | * this function will update an existing instance with new data. 113 | * 114 | * @param stdClass $subcourse 115 | * @return boolean success/failure 116 | */ 117 | function subcourse_update_instance(stdClass $subcourse) { 118 | global $CFG, $DB; 119 | require_once($CFG->dirroot.'/mod/subcourse/locallib.php'); 120 | 121 | $cmid = $subcourse->coursemodule; 122 | 123 | $subcourse->timemodified = time(); 124 | $subcourse->id = $subcourse->instance; 125 | 126 | if (!empty($subcourse->refcoursecurrent)) { 127 | unset($subcourse->refcourse); 128 | } 129 | 130 | if (empty($subcourse->instantredirect)) { 131 | $subcourse->instantredirect = 0; 132 | } 133 | 134 | if (empty($subcourse->blankwindow)) { 135 | $subcourse->blankwindow = 0; 136 | } 137 | 138 | if (empty($subcourse->coursepageprintprogress)) { 139 | $subcourse->coursepageprintprogress = 0; 140 | } 141 | 142 | if (empty($subcourse->coursepageprintgrade)) { 143 | $subcourse->coursepageprintgrade = 0; 144 | } 145 | 146 | $DB->update_record('subcourse', $subcourse); 147 | 148 | if (!empty($subcourse->refcourse)) { 149 | if (has_capability('mod/subcourse:fetchgrades', context_module::instance($cmid))) { 150 | subcourse_grades_update($subcourse->course, $subcourse->id, $subcourse->refcourse, $subcourse->name, 151 | false, false, [], $subcourse->fetchpercentage); 152 | subcourse_update_timefetched($subcourse->id); 153 | } 154 | } 155 | 156 | \core_completion\api::update_completion_date_event($cmid, 'subcourse', $subcourse->id, $subcourse->completionexpected); 157 | 158 | return true; 159 | } 160 | 161 | /** 162 | * Given an ID of an instance of this module, 163 | * this function will permanently delete the instance 164 | * and any data that depends on it. 165 | * 166 | * @param int $id Id of the module instance 167 | * @return boolean success/failure 168 | */ 169 | function subcourse_delete_instance($id) { 170 | global $CFG, $DB; 171 | require_once($CFG->libdir.'/gradelib.php'); 172 | 173 | // Check the instance exists. 174 | if (!$subcourse = $DB->get_record("subcourse", ["id" => $id])) { 175 | return false; 176 | } 177 | 178 | // Remove the instance record. 179 | $DB->delete_records("subcourse", ["id" => $subcourse->id]); 180 | 181 | // Clean up the gradebook items. 182 | grade_update('mod/subcourse', $subcourse->course, 'mod', 'subcourse', $subcourse->id, 0, null, ['deleted' => true]); 183 | 184 | return true; 185 | } 186 | 187 | /** 188 | * Must return an array of user records (all data) who are participants 189 | * for a given instance of subcourse. Must include every user involved 190 | * in the instance, independient of his role (student, teacher, admin...) 191 | * See other modules as example. 192 | * 193 | * @param int $subcourseid ID of an instance of this module 194 | * @return mixed boolean/array of students 195 | */ 196 | function subcourse_get_participants($subcourseid) { 197 | return false; 198 | } 199 | 200 | /** 201 | * Return a small object with summary information about what a 202 | * user has done with a given particular instance of this module 203 | * Used for user activity reports. 204 | * $return->time = the time they did it 205 | * $return->info = a short text description 206 | * 207 | * @param stdClass $course The course record. 208 | * @param stdClass $user The user record. 209 | * @param cm_info|stdClass $mod The course module info object or record. 210 | * @param stdClass $subcourse The subcourse instance record. 211 | * @return null 212 | */ 213 | function subcourse_user_outline($course, $user, $mod, $subcourse) { 214 | return true; 215 | } 216 | 217 | /** 218 | * Print a detailed representation of what a user has done with 219 | * a given particular instance of this module, for user activity reports. 220 | * 221 | * @param stdClass $course The course record. 222 | * @param stdClass $user The user record. 223 | * @param cm_info|stdClass $mod The course module info object or record. 224 | * @param stdClass $subcourse The subcourse instance record. 225 | * @return boolean 226 | */ 227 | function subcourse_user_complete($course, $user, $mod, $subcourse) { 228 | return true; 229 | } 230 | 231 | /** 232 | * Given a course and a time, this module should find recent activity 233 | * that has occurred in subcourse activities and print it out. 234 | * Return true if there was output, or false is there was none. 235 | * 236 | * @param stdClass $course 237 | * @param bool $viewfullnames 238 | * @param int $timestart 239 | * @return boolean true if anything was printed, otherwise false 240 | */ 241 | function subcourse_print_recent_activity($course, $viewfullnames, $timestart) { 242 | return false; 243 | } 244 | 245 | /** 246 | * Is a scale used by the given subcourse instance? 247 | * 248 | * The subcourse itself does not generate grades so we always return 249 | * false here in order not to block the scale removal. 250 | * 251 | * @param int $subcourseid id of an instance of this module 252 | * @param int $scaleid 253 | * @return bool 254 | */ 255 | function subcourse_scale_used($subcourseid, $scaleid) { 256 | return false; 257 | } 258 | 259 | /** 260 | * Is a scale used by some subcourse instance? 261 | * 262 | * The subcourse itself does not generate grades so we always return 263 | * false here in order not to block the scale removal. 264 | * 265 | * @param int $scaleid 266 | * @return boolean True if the scale is used by any subcourse 267 | */ 268 | function subcourse_scale_used_anywhere($scaleid) { 269 | return false; 270 | } 271 | 272 | /** 273 | * This will provide summary info about the user's grade in the subcourse below the link on 274 | * the course/view.php page 275 | * 276 | * @param cm_info $cm 277 | * @return void 278 | */ 279 | function mod_subcourse_cm_info_view(cm_info $cm) { 280 | global $CFG, $USER, $DB; 281 | 282 | if (isset($cm->customdata->coursepageprintgrade) && isset($cm->customdata->coursepageprintprogress)) { 283 | $displayoptions = (object) [ 284 | 'coursepageprintgrade' => $cm->customdata->coursepageprintgrade, 285 | 'coursepageprintprogress' => $cm->customdata->coursepageprintprogress, 286 | ]; 287 | 288 | } else { 289 | // This is unexpected - the customdata should be set in {@see subcourse_get_coursemodule_info()}. 290 | $displayoptions = $DB->get_record('subcourse', ['id' => $cm->instance], 'coursepageprintgrade, coursepageprintprogress'); 291 | } 292 | 293 | $html = ''; 294 | 295 | if ($displayoptions->coursepageprintprogress) { 296 | $sql = "SELECT r.* 297 | FROM {course} r 298 | JOIN {subcourse} s ON s.refcourse = r.id 299 | WHERE s.id = :subcourseid"; 300 | 301 | $refcourse = $DB->get_record_sql($sql, ['subcourseid' => $cm->instance], IGNORE_MISSING); 302 | $percentage = null; 303 | if ($refcourse) { 304 | $percentage = \core_completion\progress::get_course_progress_percentage($refcourse); 305 | } 306 | if ($percentage !== null) { 307 | $percentage = floor($percentage); 308 | $html .= html_writer::tag('div', get_string('currentprogress', 'subcourse', $percentage), 309 | ['class' => 'contentafterlink']); 310 | } 311 | } 312 | 313 | if ($displayoptions->coursepageprintgrade) { 314 | require_once($CFG->libdir.'/gradelib.php'); 315 | 316 | $grades = grade_get_grades($cm->course, 'mod', 'subcourse', $cm->instance, $USER->id); 317 | $currentgrade = (empty($grades->items[0]->grades)) ? null : reset($grades->items[0]->grades); 318 | 319 | if (($currentgrade !== null) && isset($currentgrade->grade) && !($currentgrade->hidden)) { 320 | $strgrade = $currentgrade->str_grade; 321 | $html .= html_writer::tag('div', get_string('currentgrade', 'subcourse', $strgrade), 322 | ['class' => 'contentafterlink']); 323 | } 324 | } 325 | 326 | if ($html !== '') { 327 | $cm->set_after_link($html); 328 | } 329 | } 330 | 331 | /** 332 | * Return the action associated with the given calendar event, or null if there is none. 333 | * 334 | * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event 335 | * is not displayed on the block. 336 | * 337 | * @param calendar_event $event 338 | * @param \core_calendar\action_factory $factory 339 | * @return \core_calendar\local\event\entities\action_interface|null 340 | */ 341 | function mod_subcourse_core_calendar_provide_event_action(calendar_event $event, \core_calendar\action_factory $factory) { 342 | 343 | $cm = get_fast_modinfo($event->courseid)->instances['subcourse'][$event->instance]; 344 | 345 | return $factory->create_instance( 346 | get_string('view'), 347 | new \moodle_url('/mod/subcourse/view.php', ['id' => $cm->id]), 348 | 1, 349 | true 350 | ); 351 | } 352 | 353 | /** 354 | * Given a course_module object, this function returns any 355 | * "extra" information that may be needed when printing 356 | * this activity in a course listing. 357 | * 358 | * See {@see get_array_of_activities()} in course/lib.php 359 | * 360 | * @param object $coursemodule 361 | * @return cached_cm_info info 362 | */ 363 | function subcourse_get_coursemodule_info($coursemodule) { 364 | global $CFG, $DB; 365 | 366 | $subcourse = $DB->get_record('subcourse', ['id' => $coursemodule->instance], 367 | 'id, name, intro, introformat, instantredirect, blankwindow, coursepageprintgrade, coursepageprintprogress, ' . 368 | 'completioncourse'); 369 | 370 | if (!$subcourse) { 371 | return null; 372 | } 373 | 374 | $info = new cached_cm_info(); 375 | $info->name = $subcourse->name; 376 | $info->customdata = (object) [ 377 | 'coursepageprintgrade' => $subcourse->coursepageprintgrade, 378 | 'coursepageprintprogress' => $subcourse->coursepageprintprogress, 379 | ]; 380 | 381 | if ($subcourse->instantredirect && $subcourse->blankwindow) { 382 | $url = new moodle_url('/mod/subcourse/view.php', ['id' => $coursemodule->id, 'isblankwindow' => 1]); 383 | $info->onclick = "window.open('".$url->out(false)."'); return false;"; 384 | } 385 | 386 | if ($coursemodule->showdescription) { 387 | // Set content from intro and introformat. Filters are disabled because we filter with format_text at display time. 388 | $info->content = format_module_intro('subcourse', $subcourse, $coursemodule->id, false); 389 | } 390 | 391 | if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) { 392 | $info->customdata->customcompletionrules['completioncourse'] = $subcourse->completioncourse; 393 | } 394 | 395 | return $info; 396 | } 397 | 398 | 399 | /** 400 | * Create or update the grade item for given subcourse 401 | * 402 | * @category grade 403 | * @param object $subcourse object 404 | * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook 405 | * @return int 0 if ok, error code otherwise 406 | */ 407 | function subcourse_grade_item_update($subcourse, $grades = null) { 408 | global $CFG; 409 | require_once($CFG->dirroot . '/mod/subcourse/locallib.php'); 410 | 411 | $reset = false; 412 | if ($grades === 'reset') { 413 | $reset = true; 414 | } 415 | $gradeitemonly = true; 416 | if (!empty($grades)) { 417 | $gradeitemonly = false; 418 | } 419 | return subcourse_grades_update($subcourse->course, $subcourse->id, $subcourse->refcourse, 420 | $subcourse->name, $gradeitemonly, $reset); 421 | } 422 | 423 | /** 424 | * Update activity grades. 425 | * 426 | * @param stdClass $subcourse subcourse record 427 | * @param int $userid specific user only, 0 means all 428 | * @param bool $nullifnone - not used 429 | */ 430 | function subcourse_update_grades($subcourse, $userid=0, $nullifnone=true) { 431 | global $CFG; 432 | require_once($CFG->dirroot . '/mod/subcourse/locallib.php'); 433 | require_once($CFG->libdir.'/gradelib.php'); 434 | 435 | if ($subcourse->refcourse) { 436 | $refgrades = subcourse_fetch_refgrades($subcourse->id, $subcourse->refcourse, false, $userid, false); 437 | } else { 438 | // Prevent empty referenced course id coding error. 439 | return GRADE_UPDATE_FAILED; 440 | } 441 | 442 | if ($refgrades && $refgrades->grades) { 443 | if (!empty($refgrades->localremotescale)) { 444 | // Unable to fetch remote grades - local scale is used in the remote course. 445 | return GRADE_UPDATE_FAILED; 446 | } 447 | return subcourse_grade_item_update($subcourse, $refgrades->grades); 448 | } else { 449 | return subcourse_grade_item_update($subcourse); 450 | } 451 | } 452 | -------------------------------------------------------------------------------- /locallib.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Library providing functions that implement the module's features. 19 | * 20 | * @package mod_subcourse 21 | * @copyright 2017 David Mudrák 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | require_once($CFG->libdir.'/gradelib.php'); 28 | 29 | /** 30 | * Returns the list of courses the grades can be taken from 31 | * 32 | * Returned are courses in which the user has permission to view the grade 33 | * book. Never returns the current course (as a course cannot be a subcourse of 34 | * itself) and the site course (the front page course). If the userid is not 35 | * passed, the current user is expected. 36 | * 37 | * @param int $userid Id of user for which we want to get the list of courses 38 | * @return array list of course records 39 | */ 40 | function subcourse_available_courses($userid = null) { 41 | global $COURSE, $USER; 42 | 43 | $courses = []; 44 | 45 | if (empty($userid)) { 46 | $userid = $USER->id; 47 | } 48 | 49 | $fields = 'fullname,shortname,idnumber,category,visible,sortorder'; 50 | $mycourses = get_user_capability_course('moodle/grade:viewall', $userid, true, $fields, 'sortorder'); 51 | 52 | if ($mycourses) { 53 | $ignorecourses = [$COURSE->id, SITEID]; 54 | foreach ($mycourses as $mycourse) { 55 | if (in_array($mycourse->id, $ignorecourses)) { 56 | continue; 57 | } 58 | $courses[] = $mycourse; 59 | } 60 | } 61 | 62 | return $courses; 63 | } 64 | 65 | /** 66 | * Fetches grade_item info and grades from the referenced course 67 | * 68 | * Returned structure is 69 | * object( 70 | * ->grades = array[userid] of object(->userid ->rawgrade ->feedback ->feedbackformat ->hidden) 71 | * ->grademax 72 | * ->grademin 73 | * ->itemname 74 | * ... 75 | * ) 76 | * 77 | * @param int $subcourseid ID of subcourse instance 78 | * @param int $refcourseid ID of referenced course 79 | * @param bool $gradeitemonly If true, fetch only grade item info without grades 80 | * @param int|array $userids If fetching grades, limit only to this user(s), defaults to all. 81 | * @param bool $fetchpercentage Re-calculate the grade value so that the displayed percentage matches the original. 82 | * @return stdClass containing grades array and gradeitem info 83 | */ 84 | function subcourse_fetch_refgrades($subcourseid, $refcourseid, $gradeitemonly = false, $userids = [], $fetchpercentage = false) { 85 | 86 | if (empty($refcourseid)) { 87 | throw new coding_exception('Empty referenced course id'); 88 | } 89 | 90 | $fetchedfields = subcourse_get_fetched_item_fields(); 91 | 92 | $return = new stdClass(); 93 | $return->grades = []; 94 | 95 | $refgradeitem = grade_item::fetch_course_item($refcourseid); 96 | 97 | // Get grade_item info. 98 | foreach ($fetchedfields as $property) { 99 | if (isset($refgradeitem->$property)) { 100 | $return->$property = $refgradeitem->$property; 101 | } else { 102 | $return->$property = null; 103 | } 104 | } 105 | 106 | // If the remote grade_item is non-global scale, do not fetch grades - they can't be used. 107 | if (($refgradeitem->gradetype == GRADE_TYPE_SCALE) && (!subcourse_is_global_scale($refgradeitem->scaleid))) { 108 | $gradeitemonly = true; 109 | debugging(get_string('errlocalremotescale', 'subcourse')); 110 | $return->localremotescale = true; 111 | } 112 | 113 | if (!$gradeitemonly) { 114 | // Get grades. 115 | 116 | if (!is_array($userids)) { 117 | $userids = [$userids]; 118 | } 119 | 120 | $cm = get_coursemodule_from_instance("subcourse", $subcourseid); 121 | $context = context_module::instance($cm->id); 122 | 123 | $users = get_users_by_capability($context, 'mod/subcourse:begraded', 'u.id,u.lastname', 124 | 'u.lastname', '', '', '', '', false, true); 125 | 126 | foreach ($users as $user) { 127 | if ($userids && !in_array($user->id, $userids)) { 128 | continue; 129 | } 130 | 131 | $grade = new grade_grade(['itemid' => $refgradeitem->id, 'userid' => $user->id]); 132 | 133 | $return->grades[$user->id] = new stdClass(); 134 | $return->grades[$user->id]->userid = $user->id; 135 | $return->grades[$user->id]->feedback = $grade->feedback; 136 | $return->grades[$user->id]->feedbackformat = $grade->feedbackformat; 137 | $return->grades[$user->id]->hidden = $grade->hidden; 138 | 139 | if ($grade->finalgrade === null) { 140 | // No grade set yet. 141 | $return->grades[$user->id]->rawgrade = null; 142 | 143 | } else if (empty($fetchpercentage)) { 144 | // Fetch the raw value of the final grade in the referenced course. 145 | $return->grades[$user->id]->rawgrade = $grade->finalgrade; 146 | 147 | } else { 148 | // Re-calculate the value so that the displayed percentage matches. 149 | // This may make difference when there are excluded grades in the referenced course. 150 | if ($grade->rawgrademax > 0) { 151 | $ratio = ($grade->finalgrade - $grade->rawgrademin) / ($grade->rawgrademax - $grade->rawgrademin); 152 | $fakevalue = $return->grademin + $ratio * ($return->grademax - $return->grademin); 153 | $return->grades[$user->id]->rawgrade = grade_floatval($fakevalue); 154 | 155 | } else { 156 | $return->grades[$user->id]->rawgrade = 0; 157 | } 158 | } 159 | } 160 | } 161 | 162 | return $return; 163 | } 164 | 165 | /** 166 | * Create or update grade item and grades for given subcourse 167 | * 168 | * @param int $courseid ID of referencing course (the course containing the instance of 169 | * subcourse) 170 | * @param int $subcourseid ID of subcourse instance 171 | * @param int $refcourseid ID of referenced course (the course to take grades from) 172 | * @param str $itemname Set the itemname 173 | * @param bool $gradeitemonly If true, fetch only grade item info without grades 174 | * @param bool $reset Reset grades in gradebook 175 | * @param int|array $userids If fetching grades, limit only to this user(s), defaults to all. 176 | * @param bool $fetchpercentage Re-calculate the grade value so that the displayed percentage matches the original. 177 | * @return int GRADE_UPDATE_OK etc 178 | */ 179 | function subcourse_grades_update($courseid, $subcourseid, $refcourseid, $itemname = null, 180 | $gradeitemonly = false, $reset = false, $userids = [], $fetchpercentage = null) { 181 | global $DB; 182 | 183 | if (empty($refcourseid)) { 184 | return GRADE_UPDATE_FAILED; 185 | } 186 | 187 | if (!$DB->record_exists('course', ['id' => $refcourseid])) { 188 | return GRADE_UPDATE_FAILED; 189 | } 190 | 191 | if (!$gradeitemonly && $fetchpercentage === null) { 192 | debugging('Performance: The caller should provide the fetchpercentage value to avoid an extra DB call.', DEBUG_DEVELOPER); 193 | $fetchpercentage = $DB->get_field('subcourse', 'fetchpercentage', ['id' => $subcourseid]); 194 | } 195 | 196 | $fetchedfields = subcourse_get_fetched_item_fields(); 197 | 198 | $refgrades = subcourse_fetch_refgrades($subcourseid, $refcourseid, $gradeitemonly, $userids, $fetchpercentage); 199 | 200 | if (!empty($refgrades->localremotescale)) { 201 | // Unable to fetch remote grades - local scale is used in the remote course. 202 | return GRADE_UPDATE_FAILED; 203 | } 204 | 205 | $params = []; 206 | 207 | foreach ($fetchedfields as $property) { 208 | if (isset($refgrades->$property)) { 209 | $params[$property] = $refgrades->$property; 210 | } 211 | } 212 | if (!empty($itemname)) { 213 | $params['itemname'] = $itemname; 214 | } 215 | 216 | $grades = $refgrades->grades; 217 | 218 | if ($reset) { 219 | $params['reset'] = true; 220 | $grades = null; 221 | } 222 | 223 | $result = grade_update('mod/subcourse', $courseid, 'mod', 'subcourse', $subcourseid, 0, $grades, $params); 224 | 225 | // The {@see grade_update()} does not change the grade hidden state so we need to perform it manually now. 226 | if (!$gradeitemonly && $result == GRADE_UPDATE_OK) { 227 | $gi = grade_item::fetch([ 228 | 'source' => 'mod/subcourse', 229 | 'courseid' => $courseid, 230 | 'itemtype' => 'mod', 231 | 'itemmodule' => 'subcourse', 232 | 'iteminstance' => $subcourseid, 233 | 'itemnumber' => 0 234 | ]); 235 | 236 | $gs = grade_grade::fetch_all(['itemid' => $gi->id]); 237 | 238 | if (!empty($gs)) { 239 | foreach ($gs as $g) { 240 | if (isset($refgrades->grades[$g->userid])) { 241 | if ($refgrades->grades[$g->userid]->hidden != $g->hidden) { 242 | $g->grade_item = $gi; 243 | $g->set_hidden($refgrades->grades[$g->userid]->hidden); 244 | } 245 | } 246 | } 247 | } 248 | } 249 | 250 | return $result; 251 | } 252 | 253 | /** 254 | * Checks if a remote scale can be re-used, i.e. if it is global (standard, server wide) scale 255 | * 256 | * @param mixed $scaleid ID of the scale 257 | * @return boolean True if scale is global, false if not. 258 | */ 259 | function subcourse_is_global_scale($scaleid) { 260 | global $DB; 261 | 262 | if (!is_numeric($scaleid)) { 263 | throw new moodle_exception('errnonnumeric', 'subcourse'); 264 | } 265 | 266 | if (!$DB->get_record('scale', ['id' => $scaleid, 'courseid' => 0], 'id')) { 267 | // No such scale with courseid 0. 268 | return false; 269 | } else { 270 | // Found the global scale. 271 | return true; 272 | } 273 | } 274 | 275 | /** 276 | * Updates the timefetched timestamp for given subcourses 277 | * 278 | * @param array|int $subcourseids ID of subcourse instance or array of IDs 279 | * @param mixed $time The timestamp, defaults to the current time 280 | * @return bool 281 | */ 282 | function subcourse_update_timefetched($subcourseids, $time = null) { 283 | global $DB; 284 | 285 | if (empty($subcourseids)) { 286 | return false; 287 | } 288 | if (is_numeric($subcourseids)) { 289 | $subcourseids = [$subcourseids]; 290 | } 291 | if (!is_array($subcourseids)) { 292 | return false; 293 | } 294 | if (is_null($time)) { 295 | $time = time(); 296 | } 297 | if (!is_numeric($time)) { 298 | return false; 299 | } 300 | list($sql, $params) = $DB->get_in_or_equal($subcourseids); 301 | $DB->set_field_select('subcourse', 'timefetched', $time, "id $sql", $params); 302 | 303 | return true; 304 | } 305 | 306 | /** 307 | * The list of fields to copy from remote grade_item 308 | * @return array 309 | */ 310 | function subcourse_get_fetched_item_fields() { 311 | return ['gradetype', 'grademax', 'grademin', 'scaleid', 'hidden']; 312 | } 313 | 314 | /** 315 | * Return if the user has a grade for the activity and the string representation of the grade. 316 | * 317 | * @param stdClass $subcourse Subcourse activity record with id and course properties set 318 | * @param int $userid User id to get the grade for 319 | * @return string $strgrade 320 | */ 321 | function subcourse_get_current_grade(stdClass $subcourse, int $userid): ?string { 322 | 323 | $currentgrade = grade_get_grades($subcourse->course, 'mod', 'subcourse', $subcourse->id, $userid); 324 | $strgrade = null; 325 | 326 | if (!empty($currentgrade->items[0]->grades)) { 327 | $currentgrade = reset($currentgrade->items[0]->grades); 328 | 329 | if (isset($currentgrade->grade) && !($currentgrade->hidden)) { 330 | $strgrade = $currentgrade->str_grade; 331 | } 332 | } 333 | 334 | return $strgrade; 335 | } 336 | 337 | /** 338 | * Mark the course module as viewed by the user. 339 | * 340 | * @param stdClass $subcourse Subcourse record. 341 | * @param context $context Course module context. 342 | * @param stdClass $course Course record. 343 | * @param cm_info|object $cm Course module info. 344 | */ 345 | function subcourse_set_module_viewed(stdClass $subcourse, context $context, stdClass $course, $cm) { 346 | global $CFG; 347 | require_once($CFG->libdir . '/completionlib.php'); 348 | 349 | $completion = new completion_info($course); 350 | $completion->set_module_viewed($cm); 351 | 352 | $event = \mod_subcourse\event\course_module_viewed::create([ 353 | 'objectid' => $subcourse->id, 354 | 'context' => $context, 355 | ]); 356 | 357 | $event->add_record_snapshot('course_modules', $cm); 358 | $event->add_record_snapshot('course', $course); 359 | $event->add_record_snapshot('subcourse', $subcourse); 360 | 361 | $event->trigger(); 362 | } 363 | -------------------------------------------------------------------------------- /mod_form.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Defines the main subcourse settings form 19 | * 20 | * @package mod_subcourse 21 | * @category form 22 | * @copyright 2008 David Mudrak 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | 28 | require_once($CFG->dirroot.'/mod/subcourse/locallib.php'); 29 | require_once($CFG->dirroot.'/course/moodleform_mod.php'); 30 | 31 | /** 32 | * Subcourse settings form 33 | * 34 | * @copyright 2008 David Mudrak 35 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 36 | */ 37 | class mod_subcourse_mod_form extends moodleform_mod { 38 | 39 | /** 40 | * Form fields definition 41 | */ 42 | public function definition() { 43 | global $CFG, $DB, $COURSE; 44 | 45 | $mform = $this->_form; 46 | $config = get_config('mod_subcourse'); 47 | 48 | $mform->addElement('header', 'general', get_string('general', 'form')); 49 | 50 | $mform->addElement('text', 'name', get_string('subcoursename', 'subcourse'), ['size' => '64']); 51 | if (!empty($CFG->formatstringstriptags)) { 52 | $mform->setType('name', PARAM_TEXT); 53 | } else { 54 | $mform->setType('name', PARAM_CLEANHTML); 55 | } 56 | $mform->addRule('name', null, 'required', null, 'client'); 57 | $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); 58 | 59 | $this->standard_intro_elements(); 60 | 61 | $mform->addElement('header', 'section-refcourse', get_string('refcourse', 'subcourse')); 62 | $mform->setExpanded('section-refcourse'); 63 | $mform->addHelpButton('section-refcourse', 'refcourse', 'subcourse'); 64 | 65 | $mycourses = subcourse_available_courses(); 66 | 67 | $currentrefcourseid = isset($this->current->refcourse) ? $this->current->refcourse : null; 68 | $currentrefcoursename = null; 69 | $currentrefcourseavailable = false; 70 | 71 | if (!empty($currentrefcourseid)) { 72 | 73 | if ($currentrefcourseid == $COURSE->id) { 74 | // Invalid self-reference. 75 | $this->current->refcourse = 0; 76 | $includenoref = true; 77 | 78 | } else { 79 | $currentrefcoursename = $DB->get_field('course', 'fullname', ['id' => $currentrefcourseid], IGNORE_MISSING); 80 | } 81 | 82 | if ($currentrefcoursename === false) { 83 | // Reference to non-existing course. 84 | $this->current->refcourse = 0; 85 | $includenoref = true; 86 | 87 | } else { 88 | // Check if the currently set value is still available. 89 | foreach ($mycourses as $mycourse) { 90 | if ($mycourse->id == $currentrefcourseid) { 91 | $currentrefcourseavailable = true; 92 | break; 93 | } 94 | } 95 | } 96 | } 97 | 98 | if (!empty($currentrefcourseid) && !$currentrefcourseavailable) { 99 | // Currently referring to a course that is not available for us. 100 | // E.g. the admin has set up this Subcourse for the teacher or the teacher lost his role in the referred course etc. 101 | // Give them a chance to just keep such a reference. 102 | $mform->addElement('checkbox', 'refcoursecurrent', get_string('refcoursecurrent', 'subcourse'), 103 | format_string($currentrefcoursename)); 104 | $mform->setDefault('refcoursecurrent', 1); 105 | $includekeepref = true; 106 | } 107 | 108 | $options = [get_string('none')]; 109 | 110 | if (empty($mycourses)) { 111 | if (empty($includekeepref)) { 112 | $options = [0 => get_string('nocoursesavailable', 'subcourse')]; 113 | $mform->addElement('select', 'refcourse', get_string('refcourselabel', 'subcourse'), $options); 114 | } else { 115 | $mform->addElement('hidden', 'refcourse', 0); 116 | $mform->setType('refcourse', PARAM_INT); 117 | } 118 | 119 | } else { 120 | $catlist = core_course_category::make_categories_list('', 0, ' / '); 121 | 122 | foreach ($mycourses as $mycourse) { 123 | $courselabel = $catlist[$mycourse->category] . ' / ' . $mycourse->fullname.' ('.$mycourse->shortname.')'; 124 | if (empty($mycourse->visible)) { 125 | if ($config->displayhiddencourses || $mycourse->id == $currentrefcourseid) { 126 | $hiddenlabel = ' '.get_string('hiddencourse', 'subcourse'); 127 | $options[$mycourse->id] = $courselabel.$hiddenlabel; 128 | } 129 | } else { 130 | $options[$mycourse->id] = $courselabel; 131 | } 132 | } 133 | 134 | $mform->addElement('autocomplete', 'refcourse', get_string('refcourselabel', 'subcourse'), $options); 135 | 136 | if (!empty($includekeepref)) { 137 | $mform->disabledIf('refcourse', 'refcoursecurrent', 'checked'); 138 | } 139 | } 140 | 141 | $mform->addElement('header', 'section-gradesfetching', get_string('gradesfetching', 'subcourse')); 142 | 143 | $mform->addElement('select', 'fetchpercentage', get_string('fetchgradesmode', 'subcourse'), [ 144 | 0 => get_string('fetchgradesmode0', 'subcourse'), 145 | 1 => get_string('fetchgradesmode1', 'subcourse'), 146 | ]); 147 | $mform->addHelpButton('fetchpercentage', 'fetchgradesmode', 'subcourse'); 148 | 149 | $mform->addElement('header', 'section-subcourselink', get_string('linkcontrol', 'subcourse')); 150 | 151 | $mform->addElement('checkbox', 'instantredirect', get_string('instantredirect', 'subcourse')); 152 | $mform->addHelpButton('instantredirect', 'instantredirect', 'subcourse'); 153 | 154 | $mform->addElement('checkbox', 'blankwindow', get_string('blankwindow', 'subcourse')); 155 | $mform->addHelpButton('blankwindow', 'blankwindow', 'subcourse'); 156 | 157 | $mform->addElement('header', 'optionssection', get_string('appearance')); 158 | $mform->addElement('checkbox', 'coursepageprintprogress', get_string('displayoption:coursepageprintprogress', 'subcourse')); 159 | $mform->setDefault('coursepageprintprogress', $config->coursepageprintprogress); 160 | $mform->addElement('checkbox', 'coursepageprintgrade', get_string('displayoption:coursepageprintgrade', 'subcourse')); 161 | $mform->setDefault('coursepageprintgrade', $config->coursepageprintgrade); 162 | 163 | $this->standard_coursemodule_elements(); 164 | $this->add_action_buttons(); 165 | } 166 | 167 | /** 168 | * Add elements for setting the custom completion rules. 169 | * 170 | * @category completion 171 | * @return array List of added element names, or names of wrapping group elements. 172 | */ 173 | public function add_completion_rules() { 174 | $mform = $this->_form; 175 | $completionfieldname = 'completioncourse' . $this->get_suffix(); 176 | 177 | $mform->addElement('advcheckbox', $completionfieldname, get_string('completioncourse', 'mod_subcourse'), 178 | get_string('completioncourse_text', 'mod_subcourse')); 179 | $mform->addHelpButton($completionfieldname, 'completioncourse', 'mod_subcourse'); 180 | 181 | return [$completionfieldname]; 182 | } 183 | 184 | /** 185 | * Called during validation to see whether some module-specific completion rules are selected. 186 | * 187 | * @param array $data Input data not yet validated. 188 | * @return bool True if one or more rules is enabled, false if none are. 189 | */ 190 | public function completion_rule_enabled($data) { 191 | $completionfieldname = 'completioncourse' . $this->get_suffix(); 192 | return (!empty($data[$completionfieldname])); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /pix/icon-250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalyst/moodle-mod_subcourse/455213cd7dfaefe947d282d587cd8f1c30b9b3fa/pix/icon-250.png -------------------------------------------------------------------------------- /pix/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalyst/moodle-mod_subcourse/455213cd7dfaefe947d282d587cd8f1c30b9b3fa/pix/icon.png -------------------------------------------------------------------------------- /pix/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 25 | 28 | 32 | 36 | 37 | 40 | 44 | 48 | 49 | 52 | 56 | 60 | 61 | 64 | 68 | 72 | 73 | 76 | 80 | 84 | 85 | 88 | 92 | 96 | 97 | 100 | 104 | 108 | 109 | 112 | 116 | 120 | 121 | 123 | 127 | 131 | 132 | 134 | 138 | 142 | 143 | 145 | 149 | 153 | 154 | 156 | 160 | 164 | 165 | 167 | 171 | 175 | 176 | 178 | 182 | 186 | 187 | 189 | 193 | 197 | 198 | 207 | 210 | 214 | 218 | 219 | 228 | 237 | 246 | 255 | 264 | 273 | 282 | 291 | 300 | 309 | 318 | 327 | 330 | 334 | 338 | 339 | 348 | 357 | 366 | 375 | 384 | 394 | 404 | 414 | 415 | 433 | 435 | 436 | 438 | image/svg+xml 439 | 441 | 442 | 443 | 444 | 445 | 450 | 455 | 460 | 470 | 480 | 485 | 486 | 487 | -------------------------------------------------------------------------------- /pix/monologo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /settings.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Subcourse module admin settings and defaults 19 | * 20 | * @package mod_subcourse 21 | * @category admin 22 | * @copyright 2020 Arnaud Trouvé 23 | * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die; 27 | 28 | if ($hassiteconfig) { 29 | if ($ADMIN->fulltree) { 30 | $settings->add(new admin_setting_heading( 31 | 'subcoursemodeditdefaults', 32 | get_string('modeditdefaults', 'admin'), 33 | get_string('condifmodeditdefaults', 'admin') 34 | )); 35 | 36 | $settings->add(new admin_setting_configcheckbox( 37 | 'mod_subcourse/coursepageprintprogress', 38 | get_string('settings:coursepageprintprogress', 'mod_subcourse'), 39 | get_string('settings:coursepageprintprogress_desc', 'mod_subcourse'), 40 | 1 41 | )); 42 | 43 | $settings->add(new admin_setting_configcheckbox( 44 | 'mod_subcourse/coursepageprintgrade', 45 | get_string('settings:coursepageprintgrade', 'mod_subcourse'), 46 | get_string('settings:coursepageprintgrade_desc', 'mod_subcourse'), 47 | 1 48 | )); 49 | 50 | $settings->add(new admin_setting_configcheckbox( 51 | 'mod_subcourse/displayhiddencourses', 52 | get_string('settings:displayhiddencourses', 'mod_subcourse'), 53 | get_string('settings:displayhiddencourses_desc', 'mod_subcourse'), 54 | 1 55 | )); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | #page-mod-subcourse-view .subcourseinfo { 2 | background-color: #f5f5f5; 3 | padding: 1em; 4 | min-height: 100px; 5 | margin-bottom: 2px; 6 | } 7 | 8 | #page-mod-subcourse-view .subcourseinfo .infotext { 9 | font-size: 125%; 10 | margin-bottom: 10px; 11 | } 12 | 13 | #page-mod-subcourse-view .subcourseinfo .subcourse-progress-bar { 14 | display: block; 15 | background-color: white; 16 | height: 1rem; 17 | margin-bottom: 10px; 18 | } 19 | 20 | #page-mod-subcourse-view .subcourseinfo .subcourse-progress-bar > div { 21 | background-color: green; 22 | height: 100%; 23 | } 24 | 25 | #page-mod-subcourse-view .actionbuttons { 26 | padding-top: 10px; 27 | padding-bottom: 10px; 28 | } 29 | 30 | #page-mod-subcourse-view .actionbuttons .btn { 31 | margin-right: 5px; 32 | } 33 | 34 | #page-mod-subcourse-index #subcourseslist { 35 | margin: 0 auto; 36 | } 37 | -------------------------------------------------------------------------------- /templates/.mustachelintignore: -------------------------------------------------------------------------------- 1 | # Exclude the Ionic templates from HTML validation. 2 | mobile_*.mustache 3 | -------------------------------------------------------------------------------- /templates/mobile_view_latest.mustache: -------------------------------------------------------------------------------- 1 | {{! 2 | This file is part of Moodle - https://moodle.org/ 3 | 4 | Moodle is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Moodle is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Moodle. If not, see . 16 | }} 17 | {{! 18 | @template mod_subcourse/mobile_view 19 | 20 | Render the main view for the mobile app. 21 | 22 | Classes required for JS: 23 | * none 24 | 25 | Data attributes required for JS: 26 | * none 27 | 28 | Context variables required for this template: 29 | * cmid - [int] Course module identifier. 30 | * subcourse - [object] 31 | * subcourse.id - [int] Subcourse instance ID. 32 | * subcourse.intro - [string] Formatted activity description. 33 | * refcourse - [object] 34 | * refcourse.fullname - [string] Formatted referenced course name. 35 | * refcourse.url - [string] Referenced course view URL. 36 | * hasprogress - [bool] Is the progress value set (not null). 37 | * progress - [float] Percentual value of the progress in the referenced course. 38 | * hasgrade - [bool] Is the currentgrade value set (not null). 39 | * currentgrade - [string] Textual representation of the final grade in the referenced course. 40 | * warning - [string] Warning to be displayed to the user. 41 | 42 | Example context (json): 43 | { 44 | "cmid": 42, 45 | "subcourse": { 46 | "id": 24, 47 | "intro": "

Subcourse activity description

" 48 | }, 49 | "refcourse": { 50 | "fullname": "Subcourse A.100", 51 | "url": "https://my.school.edu/lms/course/view.php?id=43" 52 | }, 53 | "hasprogress": true, 54 | "progress": 100, 55 | "hasgrade": true, 56 | "currentgrade": "80.00", 57 | "warning": "" 58 | } 59 | }} 60 | 61 | {{=<% %>=}} 62 |
63 | <%#canusemoduleinfo%> 64 | 70 | 71 | <%/canusemoduleinfo%> 72 | <%^canusemoduleinfo%> 73 | 77 | 78 | <%/canusemoduleinfo%> 79 | 80 | <%# warning %> 81 | 82 | 83 | 84 | 85 | <% warning %> 86 | 87 | 88 | 89 | <%/ warning %> 90 | 91 | 92 | <%# hasprogress %> 93 | 94 | 95 | 96 | 97 | 98 | <%/ hasprogress %> 99 | 100 | <%# hasgrade %> 101 | 102 | {{ 'plugin.mod_subcourse.currentgrade' | translate: {$a: '<% currentgrade %>'} }} 103 | 104 | <%/ hasgrade %> 105 | 106 | <%# refcourse %> 107 | 108 | {{ 'plugin.mod_subcourse.gotorefcourse' | translate: {$a: '<% refcourse.fullname %>'} }} 109 | 110 | <%/ refcourse %> 111 | 112 |
113 | 114 | 118 | 119 | -------------------------------------------------------------------------------- /templates/subcourseinfo.mustache: -------------------------------------------------------------------------------- 1 | {{! 2 | This file is part of Moodle - https://moodle.org/ 3 | 4 | Moodle is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Moodle is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Moodle. If not, see . 16 | }} 17 | {{! 18 | @template mod_subcourse/subcourseinfo 19 | 20 | Displays percentage progress and the current grade in the subcourse. 21 | 22 | Classes required for JS: 23 | * none 24 | 25 | Data attributes required for JS: 26 | * none 27 | 28 | Context variables required for this template: 29 | * haspercentage (bool) 30 | * hasstrgrade (bool) 31 | * percentage (int) 32 | * strgrade (string) 33 | 34 | Example context (json): 35 | { 36 | "haspercentage" : true, 37 | "hasstrgrade" : true, 38 | "percentage" : 42, 39 | "strgrade" : 12.00 40 | } 41 | }} 42 |
43 | {{#haspercentage}} 44 |
45 |
46 |
{{#str}} currentprogress, mod_subcourse, {{percentage}} {{/str}}
47 |
48 |
49 |
50 |
51 |
52 | {{/haspercentage}} 53 | 54 | {{#hasstrgrade}} 55 |
56 |
57 |
{{#str}} currentgrade, mod_subcourse, {{strgrade}} {{/str}}
58 |
59 |
60 | {{/hasstrgrade}} 61 |
62 | -------------------------------------------------------------------------------- /tests/behat/auto_fetch_grades.feature: -------------------------------------------------------------------------------- 1 | @mod @mod_subcourse 2 | Feature: Grades are fetched automatically from the referenced course 3 | In order to see student's final course grade as a grade item in another course 4 | As a teacher 5 | I need to give the final grade in a referenced course and that's enough 6 | 7 | @javascript 8 | Scenario: Grade is immediately copied from a subcourse to the master course 9 | Given the following "users" exist: 10 | | username | firstname | lastname | email | 11 | | teacher1 | Teacher | 1 | teacher1@example.com | 12 | | student1 | Student | 1 | student1@example.com | 13 | | student2 | Student | 2 | student2@example.com | 14 | And the following "courses" exist: 15 | | fullname | shortname | category | 16 | | MainCourse | M | 0 | 17 | | RefCourse | R | 0 | 18 | And the following "course enrolments" exist: 19 | | user | course | role | 20 | | teacher1 | M | editingteacher | 21 | | student1 | M | student | 22 | | teacher1 | R | editingteacher | 23 | | student1 | R | student | 24 | | student2 | R | student | 25 | And I log in as "admin" 26 | # 27 | # We use Mean of grades in this test to be able to override the maximum course grade. 28 | # 29 | And I set the following administration settings values: 30 | | grade_aggregations_visible | Mean of grades | 31 | And I log out 32 | # 33 | # Set grades in the referenced course. 34 | # 35 | And I log in as "teacher1" 36 | And I am on "RefCourse" course homepage 37 | And I navigate to "Setup > Gradebook setup" in the course gradebook 38 | And I set the following settings for grade item "RefCourse" of type "course" on "setup" page: 39 | | Aggregation | Mean of grades | 40 | | Maximum grade | 1000 | 41 | And the following "grade items" exist: 42 | | itemname | grademax | course | 43 | | Manual item 1 | 200 | R | 44 | And I navigate to "View > Grader report" in the course gradebook 45 | And I turn editing mode on 46 | And I give the grade "100" to the user "Student 1" for the grade item "Manual item 1" 47 | And I give the grade "50" to the user "Student 2" for the grade item "Manual item 1" 48 | And I press "Save changes" 49 | And I turn editing mode off 50 | # 51 | # Create the subcourse instance. 52 | # 53 | And I add a "subcourse" activity to course "MainCourse" section "1" and I fill the form with: 54 | | Subcourse name | Unit course 1 | 55 | | Fetch grades from | RefCourse (R) | 56 | | Redirect to the referenced course | 0 | 57 | And I turn editing mode off 58 | And I am on the "Unit course 1" "subcourse activity" page logged in as teacher1 59 | # 60 | # Upon creation, no grades are fetched yet. 61 | # 62 | Then I should see "The grades have not been fetched yet" 63 | And I follow "Fetch grades now" 64 | And I should see "Last fetch:" 65 | # 66 | # After fetching, the grades are copied. 67 | # 68 | And I am on "MainCourse" course homepage 69 | And I navigate to "View > User report" in the course gradebook 70 | And I click on "Student 1" in the "Search users" search combo box 71 | And the following should exist in the "user-grade" table: 72 | | Grade item | Grade | Range | 73 | | Unit course 1 | 500 | 0–1000 | 74 | # 75 | # Changing grades in the referenced course has instant effect. 76 | # 77 | And I am on "RefCourse" course homepage 78 | And I navigate to "View > Grader report" in the course gradebook 79 | And I turn editing mode on 80 | And I give the grade "150" to the user "Student 1" for the grade item "Manual item 1" 81 | And I press "Save changes" 82 | And I turn editing mode off 83 | And I am on "MainCourse" course homepage 84 | And I navigate to "View > Grader report" in the course gradebook 85 | And I should not see "Student 2" 86 | And I am on "MainCourse" course homepage 87 | And I navigate to "View > User report" in the course gradebook 88 | And I click on "Student 1" in the "Search users" search combo box 89 | And the following should exist in the "user-grade" table: 90 | | Grade item | Grade | Range | 91 | | Unit course 1 | 750 | 0–1000 | 92 | # 93 | # Enrolling a student into the master course brings her grades instantly 94 | # 95 | And the following "course enrolments" exist: 96 | | user | course | role | 97 | | student2 | M | student | 98 | And I am on "MainCourse" course homepage 99 | And I navigate to "View > User report" in the course gradebook 100 | And I click on "Student 2" in the "Search users" search combo box 101 | And the following should exist in the "user-grade" table: 102 | | Grade item | Grade | Range | 103 | | Unit course 1 | 250 | 0–1000 | 104 | -------------------------------------------------------------------------------- /tests/behat/completion_course.feature: -------------------------------------------------------------------------------- 1 | @mod @mod_subcourse 2 | Feature: Completing the referenced course can lead to completing the subcourse activity 3 | In order to complete to Subcourse activity 4 | As a student 5 | I need to complete the referenced course, given such a rule is enabled 6 | 7 | Background: 8 | Given the following "users" exist: 9 | | username | firstname | lastname | email | 10 | | teacher1 | Teacher | 1 | teacher1@example.com | 11 | | student1 | Student | 1 | student1@example.com | 12 | And the following "courses" exist: 13 | | fullname | shortname | category | enablecompletion | 14 | | MainCourse | M | 0 | 1 | 15 | | RefCourse | R | 0 | 1 | 16 | And the following "course enrolments" exist: 17 | | user | course | role | 18 | | teacher1 | M | editingteacher | 19 | | student1 | M | student | 20 | | teacher1 | R | editingteacher | 21 | | student1 | R | student | 22 | And I enable "selfcompletion" "block" plugin 23 | # Create the subcourse instance. 24 | When I am on the "MainCourse" course page logged in as "teacher1" 25 | And I add a "subcourse" activity to course "MainCourse" section "1" and I fill the form with: 26 | | Subcourse name | Unit course 1 | 27 | | Fetch grades from | RefCourse (R) | 28 | | Redirect to the referenced course | 0 | 29 | | Add requirements | 1 | 30 | | View the activity | 0 | 31 | | Require course completed | 1 | 32 | | id_completionexpected_enabled | 1 | 33 | # Add the block to a the referenced course to allow students to manually complete it 34 | And I am on "RefCourse" course homepage with editing mode on 35 | And I add the "Self completion" block 36 | And I navigate to "Course completion" in current page administration 37 | And I expand all fieldsets 38 | And I set the following fields to these values: 39 | | id_criteria_self | 1 | 40 | And I press "Save changes" 41 | And I log out 42 | 43 | @javascript 44 | Scenario: Student is informed about a subcourse to be completed 45 | When I log in as "student1" 46 | Then I should see "Unit course 1 should be completed" 47 | 48 | @javascript 49 | Scenario: Completing the referenced course leads to completing the subcourse 50 | Given I am on the "RefCourse" course page logged in as "student1" 51 | And I follow "Complete course" 52 | And I should see "Confirm self completion" 53 | And I press "Yes" 54 | # Running completion task just after clicking sometimes fail, as record should be created before the task runs. 55 | And I wait "1" seconds 56 | When I run the scheduled task "core\task\completion_regular_task" 57 | And I am on "MainCourse" course homepage 58 | Then the "Require course completed" completion condition of "Unit course 1" is displayed as "done" 59 | And I log out 60 | And I log in as "teacher1" 61 | And I am on "MainCourse" course homepage 62 | And "Student 1" user has completed "Unit course 1" activity 63 | -------------------------------------------------------------------------------- /tests/behat/course_page_display.feature: -------------------------------------------------------------------------------- 1 | @mod @mod_subcourse 2 | Feature: Progress and grade in referenced course can be displayed on the course main page 3 | In order to control the look and feel of my course outline page 4 | As a teacher 5 | I need to be able to configure whether progress and grade in the referenced course is displayed on my course main page 6 | 7 | Background: 8 | Given the following "users" exist: 9 | | username | firstname | lastname | email | 10 | | teacher1 | Teacher | 1 | teacher1@example.com | 11 | | student1 | Student | 1 | student1@example.com | 12 | And the following "courses" exist: 13 | | fullname | shortname | category | 14 | | MainCourse | M | 0 | 15 | | RefCourse | R | 0 | 16 | And the following "course enrolments" exist: 17 | | user | course | role | 18 | | teacher1 | M | editingteacher | 19 | | student1 | M | student | 20 | | teacher1 | R | editingteacher | 21 | | student1 | R | student | 22 | # 23 | # Set grades in the referenced course. 24 | # 25 | And I log in as "teacher1" 26 | And I am on "RefCourse" course homepage 27 | And I navigate to "Settings" in current page administration 28 | And I set the following fields to these values: 29 | | Enable completion tracking | Yes | 30 | And I press "Save and display" 31 | And I turn editing mode on 32 | And I add a "label" activity to course "RefCourse" section "1" and I fill the form with: 33 | | Text | Just a simple module to activate progress tracking | 34 | | Students must manually mark the activity as done | 1 | 35 | And I turn editing mode off 36 | And I navigate to "Setup > Gradebook setup" in the course gradebook 37 | And the following "grade items" exist: 38 | | itemname | grademax | course | 39 | | Manual item 1 | 10 | R | 40 | And I navigate to "View > Grader report" in the course gradebook 41 | And I turn editing mode on 42 | And I give the grade "5" to the user "Student 1" for the grade item "Manual item 1" 43 | And I press "Save" 44 | And I turn editing mode off 45 | 46 | @javascript 47 | Scenario: Progress and grade displayed on both course main page and subcourse view page. 48 | Given I add a "subcourse" activity to course "MainCourse" section "1" and I fill the form with: 49 | | Subcourse name | Unit course 1 | 50 | | Fetch grades from | RefCourse (R) | 51 | | Redirect to the referenced course | 0 | 52 | | Display progress from referenced course on course page | 1 | 53 | | Display grade from referenced course on course page | 1 | 54 | And I turn editing mode off 55 | And I am on "MainCourse" course homepage 56 | And I am on the "Unit course 1" "subcourse activity" page logged in as teacher1 57 | And I follow "Fetch grades now" 58 | And I log out 59 | When I log in as "student1" 60 | And I am on "MainCourse" course homepage 61 | Then I should see "Progress:" in the "[data-activityname='Unit course 1']" "css_element" 62 | And I should see "Current grade:" in the "[data-activityname='Unit course 1']" "css_element" 63 | And I am on the "Unit course 1" "subcourse activity" page logged in as student1 64 | And I should see "Progress:" in the ".subcourseinfo-progress" "css_element" 65 | And I should see "Current grade:" in the ".subcourseinfo-grade" "css_element" 66 | 67 | @javascript 68 | Scenario: Progress and grade displayed on subcourse view page only. 69 | Given I add a "subcourse" activity to course "MainCourse" section "1" and I fill the form with: 70 | | Subcourse name | Unit course 1 | 71 | | Fetch grades from | RefCourse (R) | 72 | | Redirect to the referenced course | 0 | 73 | | Display progress from referenced course on course page | 0 | 74 | | Display grade from referenced course on course page | 0 | 75 | And I turn editing mode off 76 | And I am on "MainCourse" course homepage 77 | And I am on the "Unit course 1" "subcourse activity" page logged in as teacher1 78 | And I follow "Fetch grades now" 79 | And I log out 80 | When I log in as "student1" 81 | And I am on "MainCourse" course homepage 82 | Then I should not see "Progress:" in the "[data-activityname='Unit course 1']" "css_element" 83 | And I should not see "Current grade:" in the "[data-activityname='Unit course 1']" "css_element" 84 | And I am on the "Unit course 1" "subcourse activity" page logged in as student1 85 | And I should see "Progress:" in the ".subcourseinfo-progress" "css_element" 86 | And I should see "Current grade:" in the ".subcourseinfo-grade" "css_element" 87 | -------------------------------------------------------------------------------- /tests/behat/fetch_percentage_grades.feature: -------------------------------------------------------------------------------- 1 | @mod @mod_subcourse 2 | Feature: Grades can be fetched either a real values or as percentages 3 | In order to have the same grade in the referenced course and in the target meta course 4 | As a teacher 5 | I need to be able to set whether grades are fetched as real values or as percentual values 6 | 7 | Background: 8 | Given the following "users" exist: 9 | | username | firstname | lastname | email | 10 | | teacher1 | Teacher | 1 | teacher1@example.com | 11 | | student1 | Student | 1 | student1@example.com | 12 | | student2 | Student | 2 | student2@example.com | 13 | | student3 | Student | 3 | student3@example.com | 14 | | student4 | Student | 4 | student4@example.com | 15 | | student5 | Student | 5 | student5@example.com | 16 | And the following "courses" exist: 17 | | fullname | shortname | category | 18 | | MainCourse | M | 0 | 19 | | RefCourse | R | 0 | 20 | And the following "course enrolments" exist: 21 | | user | course | role | 22 | | teacher1 | M | editingteacher | 23 | | student1 | M | student | 24 | | student2 | M | student | 25 | | student3 | M | student | 26 | | student4 | M | student | 27 | | student5 | M | student | 28 | | teacher1 | R | editingteacher | 29 | | student1 | R | student | 30 | | student2 | R | student | 31 | | student3 | R | student | 32 | | student4 | R | student | 33 | | student5 | R | student | 34 | # 35 | # Set grades in the referenced course. 36 | # 37 | And I log in as "teacher1" 38 | And I am on "RefCourse" course homepage 39 | And I navigate to "Setup > Gradebook setup" in the course gradebook 40 | And I set the following settings for grade item "RefCourse" of type "course" on "setup" page: 41 | | Aggregation | Natural | 42 | | Exclude empty grades | 1 | 43 | And the following "grade items" exist: 44 | | itemname | grademax | course | 45 | | Manual item 1 | 10 | R | 46 | | Manual item 2 | 10 | R | 47 | | Manual item 3 | 10 | R | 48 | And I navigate to "Setup > Course grade settings" in the course gradebook 49 | And I set the field "Grade display type" to "Real (percentage)" 50 | # 51 | # Set also Grader report preferences to 52 | # 53 | And I press "Save changes" 54 | And I am on the "RefCourse" course page logged in as "teacher1" 55 | And I navigate to "View > Grader report" in the course gradebook 56 | And I turn editing mode on 57 | # 58 | # Student 1 has all three grades. 59 | # 60 | And I give the grade "10" to the user "Student 1" for the grade item "Manual item 1" 61 | And I give the grade "5" to the user "Student 1" for the grade item "Manual item 2" 62 | And I give the grade "5" to the user "Student 1" for the grade item "Manual item 3" 63 | # 64 | # Student 2 has one grade empty. 65 | # 66 | And I give the grade "10" to the user "Student 2" for the grade item "Manual item 1" 67 | And I give the grade "10" to the user "Student 2" for the grade item "Manual item 2" 68 | # 69 | # Student 3 has one explicitly excluded grade. 70 | # 71 | And I give the grade "10" to the user "Student 3" for the grade item "Manual item 1" 72 | And I give the grade "5" to the user "Student 3" for the grade item "Manual item 2" 73 | And I give the grade "5" to the user "Student 3" for the grade item "Manual item 3" 74 | # 75 | # Student 4 has only one zero grade. 76 | # 77 | And I give the grade "0" to the user "Student 4" for the grade item "Manual item 1" 78 | # 79 | # Student 4 has no grade. 80 | # 81 | And I press "Save changes" 82 | And I turn editing mode off 83 | # 84 | # Explicitly exclude a grade from Student 3. 85 | # 86 | And I navigate to "View > Single view" in the course gradebook 87 | And I click on "Users" "link" in the ".page-toggler" "css_element" 88 | And I click on "Student 3" in the "Search users" search combo box 89 | And I turn editing mode on 90 | And I set the field "Exclude for Manual item 3" to "1" 91 | And I press "Save" 92 | And I should see "Grades were set for 1 item" 93 | And I press "Save" 94 | # 95 | # Check the grades in the referenced course. 96 | # 97 | And I navigate to "View > Grader report" in the course gradebook 98 | And I turn editing mode off 99 | And the following should exist in the "user-grades" table: 100 | | -1- | -2- | -6- | 101 | | Student 1 | student1@example.com | 20.00 (66.67 %) | 102 | | Student 2 | student2@example.com | 20.00 (100.00 %) | 103 | | Student 3 | student3@example.com | 15.00 (75.00 %) | 104 | | Student 4 | student4@example.com | 0.00 (0.00 %) | 105 | | Student 5 | student5@example.com | - | 106 | 107 | @javascript 108 | Scenario: Grades are fetched as real values by default 109 | Given I add a "subcourse" activity to course "MainCourse" section "1" and I fill the form with: 110 | | Subcourse name | Unit course 1 | 111 | | Fetch grades from | RefCourse (R) | 112 | | Redirect to the referenced course | 0 | 113 | And I turn editing mode off 114 | And I am on "MainCourse" course homepage 115 | And I navigate to "Setup > Gradebook setup" in the course gradebook 116 | And I click on grade item menu "Unit course 1" of type "gradeitem" on "setup" page 117 | And I choose "Edit grade item" in the open action menu 118 | And I click on "Show more..." "link" in the "Edit grade item" "dialogue" 119 | And I set the following fields to these values: 120 | | Grade display type | Real (percentage) | 121 | And I press "Save changes" 122 | And I am on "MainCourse" course homepage 123 | And I am on the "Unit course 1" "subcourse activity" page logged in as teacher1 124 | When I follow "Fetch grades now" 125 | And I am on "MainCourse" course homepage 126 | And I navigate to "View > Grader report" in the course gradebook 127 | Then the following should exist in the "user-grades" table: 128 | | -1- | -2- | -3- | 129 | | Student 1 | student1@example.com | 20.00 (66.67 %) | 130 | | Student 2 | student2@example.com | 20.00 (66.67 %) | 131 | | Student 3 | student3@example.com | 15.00 (50.00 %) | 132 | | Student 4 | student4@example.com | 0.00 (0.00 %) | 133 | | Student 5 | student5@example.com | - | 134 | 135 | @javascript 136 | Scenario: Grades can be fetched as percentual values 137 | Given I add a "subcourse" activity to course "MainCourse" section "1" and I fill the form with: 138 | | Subcourse name | Unit course 1 | 139 | | Fetch grades from | RefCourse (R) | 140 | | Redirect to the referenced course | 0 | 141 | | Fetch grades as | Percentual values | 142 | And I turn editing mode off 143 | And I am on "MainCourse" course homepage 144 | And I navigate to "Setup > Gradebook setup" in the course gradebook 145 | And I click on grade item menu "Unit course 1" of type "gradeitem" on "setup" page 146 | And I choose "Edit grade item" in the open action menu 147 | And I click on "Show more..." "link" in the "Edit grade item" "dialogue" 148 | And I set the following fields to these values: 149 | | Grade display type | Real (percentage) | 150 | And I press "Save changes" 151 | And I am on "MainCourse" course homepage 152 | And I am on the "Unit course 1" "subcourse activity" page logged in as teacher1 153 | When I follow "Fetch grades now" 154 | And I am on the "MainCourse" course page logged in as "teacher1" 155 | And I navigate to "View > Grader report" in the course gradebook 156 | Then the following should exist in the "user-grades" table: 157 | | -1- | -2- | -3- | 158 | | Student 1 | student1@example.com | 20.00 (66.67 %) | 159 | | Student 2 | student2@example.com | 30.00 (100.00 %) | 160 | | Student 3 | student3@example.com | 22.50 (75.00 %) | 161 | | Student 4 | student4@example.com | 0.00 (0.00 %) | 162 | | Student 5 | student5@example.com | - | 163 | -------------------------------------------------------------------------------- /tests/behat/hidden_grades.feature: -------------------------------------------------------------------------------- 1 | @mod @mod_subcourse 2 | Feature: Course final grades hidden in the referenced course are hidden in the target course, too. 3 | In order to not reveal the hidden grades to students 4 | As a teacher 5 | I need to be sure that grades hidden in the referenced course, are kept hidden when fetched into the subcourse activity 6 | 7 | Background: 8 | Given the following "users" exist: 9 | | username | firstname | lastname | email | 10 | | teacher1 | Teacher | 1 | teacher1@example.com | 11 | | student1 | Student | 1 | student1@example.com | 12 | | student2 | Student | 2 | student2@example.com | 13 | And the following "courses" exist: 14 | | fullname | shortname | category | 15 | | MainCourse | M | 0 | 16 | | RefCourse | R | 0 | 17 | And the following "course enrolments" exist: 18 | | user | course | role | 19 | | teacher1 | M | editingteacher | 20 | | student1 | M | student | 21 | | student2 | M | student | 22 | | teacher1 | R | editingteacher | 23 | | student1 | R | student | 24 | | student2 | R | student | 25 | # 26 | # Set grades in the referenced course. 27 | # 28 | And I log in as "teacher1" 29 | And I am on "RefCourse" course homepage 30 | And I navigate to "Setup > Gradebook setup" in the course gradebook 31 | And the following "grade items" exist: 32 | | itemname | grademax | course | 33 | | Manual item 1 | 10 | R | 34 | And I navigate to "View > Grader report" in the course gradebook 35 | And I turn editing mode on 36 | And I give the grade "5" to the user "Student 1" for the grade item "Manual item 1" 37 | And I give the grade "8" to the user "Student 2" for the grade item "Manual item 1" 38 | And I press "Save" 39 | And I click on grade item menu "RefCourse" of type "course" on "grader" page 40 | And I choose "Show totals only" in the open action menu 41 | And I click on "Course total" "core_grades > grade_actions" in the "Student 1" "table_row" 42 | And I choose "Hide" in the open action menu 43 | And I turn editing mode off 44 | 45 | @javascript 46 | Scenario: If the course final grade is hidden, the associated subcourse activity grade is marked as hidden, too. 47 | Given I add a "subcourse" activity to course "MainCourse" section "1" and I fill the form with: 48 | | Subcourse name | Unit course 1 | 49 | | Fetch grades from | RefCourse (R) | 50 | | Redirect to the referenced course | 0 | 51 | And I turn editing mode off 52 | And I am on "MainCourse" course homepage 53 | And I am on the "Unit course 1" "subcourse activity" page logged in as teacher1 54 | When I follow "Fetch grades now" 55 | And I am on "MainCourse" course homepage 56 | And I navigate to "View > Grader report" in the course gradebook 57 | Then the following should exist in the "user-grades" table: 58 | | -1- | -2- | -3- | 59 | | Student 1 | student1@example.com | 5.00 | 60 | | Student 2 | student2@example.com | 8.00 | 61 | And I log out 62 | # 63 | # Student 1 should not see the grade in the referenced course. 64 | # 65 | And I log in as "student1" 66 | And I am on "MainCourse" course homepage 67 | And I navigate to "User report" in the course gradebook 68 | And I should see "MainCourse" in the "user-grade" "table" 69 | And I should not see "Unit course 1" in the "user-grade" "table" 70 | And I should not see "5.00" in the "user-grade" "table" 71 | And I log out 72 | # 73 | # Student 2 should see the grade normally. 74 | # 75 | And I log in as "student2" 76 | And I am on "MainCourse" course homepage 77 | And I navigate to "User report" in the course gradebook 78 | And I should see "MainCourse" in the "user-grade" "table" 79 | And I should see "Unit course 1" in the "user-grade" "table" 80 | And I should see "8.00" in the "user-grade" "table" 81 | 82 | @javascript 83 | Scenario: If the whole course final grade item is hidden, the associated subcourse activity grade item is marked as hidden, too. 84 | Given I am on "RefCourse" course homepage 85 | And I navigate to "Setup > Gradebook setup" in the course gradebook 86 | And I set the following settings for grade item "RefCourse" of type "course" on "setup" page: 87 | | Hidden | 1 | 88 | And I add a "subcourse" activity to course "MainCourse" section "1" and I fill the form with: 89 | | Subcourse name | Unit course 1 | 90 | | Fetch grades from | RefCourse (R) | 91 | | Redirect to the referenced course | 0 | 92 | And I turn editing mode off 93 | And I am on "MainCourse" course homepage 94 | And I am on the "Unit course 1" "subcourse activity" page logged in as teacher1 95 | When I follow "Fetch grades now" 96 | And I am on "MainCourse" course homepage 97 | And I navigate to "View > Grader report" in the course gradebook 98 | Then the following should exist in the "user-grades" table: 99 | | -1- | -2- | -3- | 100 | | Student 1 | student1@example.com | 5.00 | 101 | | Student 2 | student2@example.com | 8.00 | 102 | And I log out 103 | # 104 | # Student 1 should not see the grade in the referenced course. 105 | # 106 | And I log in as "student1" 107 | And I am on "MainCourse" course homepage 108 | And I navigate to "User report" in the course gradebook 109 | And I should see "MainCourse" in the "user-grade" "table" 110 | And I should not see "Unit course 1" in the "user-grade" "table" 111 | And I should not see "5.00" in the "user-grade" "table" 112 | And I log out 113 | # 114 | # Student 2 should not see the grade, too. 115 | # 116 | And I log in as "student2" 117 | And I am on "MainCourse" course homepage 118 | And I navigate to "User report" in the course gradebook 119 | And I should see "MainCourse" in the "user-grade" "table" 120 | And I should not see "Unit course 1" in the "user-grade" "table" 121 | And I should not see "8.00" in the "user-grade" "table" 122 | -------------------------------------------------------------------------------- /tests/behat/instant_redirect.feature: -------------------------------------------------------------------------------- 1 | @mod @mod_subcourse 2 | Feature: Clicking the subcourse instance in the course outline may or may not redirect to the referenced course 3 | In order to visit the referenced course 4 | As a user 5 | I need to visit the subcourse activity and either click a link or there is no need to do so 6 | 7 | Background: 8 | Given the following "users" exist: 9 | | username | firstname | lastname | email | 10 | | teacher1 | Teacher | 1 | teacher1@example.com | 11 | | student1 | Student | 1 | student1@example.com | 12 | And the following "courses" exist: 13 | | fullname | shortname | category | 14 | | MainCourse | M | 0 | 15 | | RefCourse | R | 0 | 16 | And the following "course enrolments" exist: 17 | | user | course | role | 18 | | teacher1 | M | editingteacher | 19 | | student1 | M | student | 20 | | teacher1 | R | editingteacher | 21 | | student1 | R | student | 22 | And I log in as "teacher1" 23 | And I am on "MainCourse" course homepage 24 | And I turn editing mode on 25 | 26 | @javascript 27 | Scenario: Student has to click the link to the referenced course manually 28 | And I add a "subcourse" activity to course "MainCourse" section "1" and I fill the form with: 29 | | Subcourse name | Unit course 1 | 30 | | Fetch grades from | RefCourse (R) | 31 | | Redirect to the referenced course | 0 | 32 | And I log out 33 | When I log in as "student1" 34 | And I am on "MainCourse" course homepage 35 | And I follow "Unit course 1" 36 | Then I should see "Go to RefCourse" 37 | And I follow "RefCourse" 38 | And I should see "RefCourse" in the "page-header" "region" 39 | 40 | @javascript 41 | Scenario: Student is instantly redirected to the referenced course 42 | And I add a "subcourse" activity to course "MainCourse" section "1" and I fill the form with: 43 | | Subcourse name | Unit course 1 | 44 | | Fetch grades from | RefCourse (R) | 45 | | Redirect to the referenced course | 1 | 46 | And I log out 47 | When I log in as "student1" 48 | And I am on "MainCourse" course homepage 49 | And I follow "Unit course 1" 50 | Then I should see "RefCourse" in the "page-header" "region" 51 | 52 | @javascript 53 | Scenario: Teacher is not redirected instantly even if that is enabled 54 | And I add a "subcourse" activity to course "MainCourse" section "1" and I fill the form with: 55 | | Subcourse name | Unit course 1 | 56 | | Fetch grades from | RefCourse (R) | 57 | | Redirect to the referenced course | 1 | 58 | And I am on the "Unit course 1" "subcourse activity" page logged in as teacher1 59 | Then I should see "Go to RefCourse" 60 | And I follow "RefCourse" 61 | And I should see "RefCourse" in the "page-header" "region" 62 | 63 | @javascript 64 | Scenario: Teacher is redirected instantly if unable to fetch grades manually 65 | And I add a "subcourse" activity to course "MainCourse" section "1" and I fill the form with: 66 | | Subcourse name | Unit course 1 | 67 | | Fetch grades from | RefCourse (R) | 68 | | Redirect to the referenced course | 1 | 69 | | ID number | subcourse1 | 70 | And the following "permission overrides" exist: 71 | | capability | permission | role | contextlevel | reference | 72 | | mod/subcourse:fetchgrades | Prevent | teacher | Activity module | subcourse1 | 73 | | mod/subcourse:fetchgrades | Prevent | editingteacher | Activity module | subcourse1 | 74 | And I am on "MainCourse" course homepage 75 | And I am on the "Unit course 1" "subcourse activity" page logged in as teacher1 76 | And I am on "RefCourse" course homepage 77 | Then I should see "RefCourse" in the "page-header" "region" 78 | -------------------------------------------------------------------------------- /tests/external.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides {@see mod_subcourse_external_testcase} class. 19 | * 20 | * @copyright 2020 David Mudrák 21 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 22 | */ 23 | 24 | defined('MOODLE_INTERNAL') || die(); 25 | 26 | global $CFG; 27 | require_once($CFG->dirroot . '/webservice/tests/helpers.php'); 28 | 29 | /** 30 | * Unit tests for the external functions provided by the plugin. 31 | * 32 | * @package mod_subcourse 33 | * @category test 34 | * @copyright 2020 David Mudrák 35 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 36 | */ 37 | class mod_subcourse_external_testcase extends externallib_advanced_testcase { 38 | 39 | /** 40 | * Test the external function mod_subcourse_view_subcourse. 41 | */ 42 | public function test_view_subcourse() { 43 | global $USER; 44 | 45 | $this->resetAfterTest(); 46 | $this->setAdminUser(); 47 | 48 | $generator = $this->getDataGenerator(); 49 | 50 | $metacourse = $generator->create_course(); 51 | $student = $generator->create_user(); 52 | $subcourse = $generator->create_module('subcourse', [ 53 | 'course' => $metacourse->id, 54 | ]); 55 | $generator->enrol_user($student->id, $metacourse->id, 'student'); 56 | 57 | list($course, $cm) = get_course_and_cm_from_instance($subcourse->id, 'subcourse'); 58 | $context = context_module::instance($cm->id); 59 | 60 | $returnvalue = \mod_subcourse\external\view_subcourse::execute($subcourse->id); 61 | 62 | // Clean value to simulate the web service server. 63 | $returnvalue = external_api::clean_returnvalue(\mod_subcourse\external\view_subcourse::execute_returns(), $returnvalue); 64 | 65 | $this->assertEquals(true, $returnvalue['status']); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/generator/lib.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides the {@see mod_subcourse_generator} class. 19 | * 20 | * @package mod_subcourse 21 | * @copyright 2020 David Mudrák 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | /** 26 | * Subcourse module data generator. 27 | * 28 | * @copyright 2020 David Mudrák 29 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 30 | */ 31 | class mod_subcourse_generator extends testing_module_generator { 32 | } 33 | -------------------------------------------------------------------------------- /tests/locallib_test.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides {@see mod_subcourse_locallib_testcase} class. 19 | * 20 | * @package mod_subcourse 21 | * @category test 22 | * @copyright 2020 David Mudrák 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | namespace mod_subcourse; 27 | 28 | defined('MOODLE_INTERNAL') || die(); 29 | 30 | global $CFG; 31 | require_once($CFG->dirroot . '/mod/subcourse/locallib.php'); 32 | 33 | /** 34 | * Unit tests for the functions in the locallib.php file. 35 | * 36 | * @copyright 2020 David Mudrák 37 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 38 | */ 39 | class locallib_test extends \advanced_testcase { 40 | 41 | /** 42 | * Test that it is possible to fetch grades from the referenced course. 43 | * 44 | * @covers ::subcourse_grades_update 45 | */ 46 | public function test_subcourse_grades_update() { 47 | 48 | $this->resetAfterTest(); 49 | $this->setAdminUser(); 50 | 51 | $generator = $this->getDataGenerator(); 52 | 53 | $metacourse = $generator->create_course(); 54 | $refcourse = $generator->create_course(); 55 | 56 | $student1 = $generator->create_user(); 57 | $student2 = $generator->create_user(); 58 | 59 | $generator->enrol_user($student1->id, $metacourse->id, 'student'); 60 | $generator->enrol_user($student1->id, $refcourse->id, 'student'); 61 | $generator->enrol_user($student2->id, $metacourse->id, 'student'); 62 | $generator->enrol_user($student2->id, $refcourse->id, 'student'); 63 | 64 | // Give some grades in the referenced course. 65 | $gi = new \grade_item($generator->create_grade_item(['courseid' => $refcourse->id]), false); 66 | $gi->update_final_grade($student1->id, 90, 'test'); 67 | $gi->update_final_grade($student2->id, 60, 'test'); 68 | $gi->force_regrading(); 69 | grade_regrade_final_grades($refcourse->id); 70 | 71 | // Create the Subcourse module instance in the metacourse, representing the final grade in the referenced course. 72 | $subcourse = $generator->create_module('subcourse', [ 73 | 'course' => $metacourse->id, 74 | 'refcourse' => $refcourse->id, 75 | ]); 76 | 77 | $strgrade = subcourse_get_current_grade($subcourse, $student1->id); 78 | $this->assertNull($strgrade); 79 | 80 | // Fetch all students' grades from the refcourse to the metacourse. 81 | subcourse_grades_update($metacourse->id, $subcourse->id, $refcourse->id, null, false, false, [], false); 82 | 83 | // Check the grades were correctly fetched. 84 | $metagrades = grade_get_grades($metacourse->id, 'mod', 'subcourse', $subcourse->id, [$student1->id, $student2->id]); 85 | $this->assertEquals(90, $metagrades->items[0]->grades[$student1->id]->grade); 86 | $this->assertEquals(60, $metagrades->items[0]->grades[$student2->id]->grade); 87 | 88 | $strgrade = subcourse_get_current_grade($subcourse, $student1->id); 89 | $this->assertEquals('90.00', $strgrade); 90 | 91 | // Update the grades in the referenced course. 92 | $gi->update_final_grade($student1->id, 80, 'test'); 93 | $gi->update_final_grade($student2->id, 50, 'test'); 94 | $gi->force_regrading(); 95 | grade_regrade_final_grades($refcourse->id); 96 | 97 | // Fetch again, this time only one student's grades. 98 | subcourse_grades_update($metacourse->id, $subcourse->id, $refcourse->id, null, false, false, [$student1->id], false); 99 | 100 | // Re-check that the student1's grade was updated succesfully. 101 | $metagrades = grade_get_grades($metacourse->id, 'mod', 'subcourse', $subcourse->id, [$student1->id, $student2->id]); 102 | $this->assertEquals(80, $metagrades->items[0]->grades[$student1->id]->grade); 103 | } 104 | 105 | /** 106 | * Test that calling {see subcourse_set_module_viewed()} does not raise errors. 107 | * 108 | * @covers ::subcourse_set_module_viewed 109 | */ 110 | public function test_subcourse_set_module_viewed() { 111 | 112 | $this->resetAfterTest(); 113 | $this->setAdminUser(); 114 | 115 | $generator = $this->getDataGenerator(); 116 | 117 | $metacourse = $generator->create_course(); 118 | $student = $generator->create_user(); 119 | $subcourse = $generator->create_module('subcourse', [ 120 | 'course' => $metacourse->id, 121 | ]); 122 | $generator->enrol_user($student->id, $metacourse->id, 'student'); 123 | 124 | list($course, $cm) = get_course_and_cm_from_instance($subcourse->id, 'subcourse'); 125 | $context = \context_module::instance($cm->id); 126 | 127 | subcourse_set_module_viewed($subcourse, $context, $course, $cm); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/output_mobile_test.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides {@see mod_subcourse_output_mobile_testcase} class. 19 | * 20 | * @copyright 2020 David Mudrák 21 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 22 | */ 23 | 24 | namespace mod_subcourse; 25 | 26 | /** 27 | * Unit tests for the methods provided by the {@see \mod_subcourse\output\mobile} class. 28 | * 29 | * @package mod_subcourse 30 | * @category test 31 | * @copyright 2020 David Mudrák 32 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 | */ 34 | class output_mobile_test extends \advanced_testcase { 35 | 36 | /** 37 | * Test the return value of the main_view() method. 38 | * 39 | * @covers ::main_view 40 | */ 41 | public function test_main_view() { 42 | 43 | $this->resetAfterTest(); 44 | $this->setAdminUser(); 45 | 46 | $generator = $this->getDataGenerator(); 47 | 48 | $metacourse = $generator->create_course(); 49 | $refcourse = $generator->create_course(); 50 | $student = $generator->create_user(); 51 | 52 | $generator->enrol_user($student->id, $metacourse->id, 'student'); 53 | $generator->enrol_user($student->id, $refcourse->id, 'student'); 54 | 55 | // Give some grades in the referenced course. 56 | $gi = new \grade_item($generator->create_grade_item(['courseid' => $refcourse->id]), false); 57 | $gi->update_final_grade($student->id, 90, 'test'); 58 | $gi->force_regrading(); 59 | grade_regrade_final_grades($refcourse->id); 60 | 61 | // Create the Subcourse module instance in the metacourse, representing the final grade in the referenced course. 62 | $subcourse = $generator->create_module('subcourse', [ 63 | 'course' => $metacourse->id, 64 | 'refcourse' => $refcourse->id, 65 | ]); 66 | 67 | // Fetch all students' grades from the refcourse to the metacourse. 68 | subcourse_grades_update($metacourse->id, $subcourse->id, $refcourse->id, null, false, false, [], false); 69 | 70 | // Get the data for the student using the Mobile App. 71 | $this->setUser($student); 72 | 73 | // Ionic5 compatible view for the app version 3.9.5. 74 | $mainview3950 = \mod_subcourse\output\mobile::main_view([ 75 | 'cmid' => $subcourse->cmid, 76 | 'courseid' => $metacourse->id, 77 | 'appversioncode' => 3950, 78 | ]); 79 | 80 | $this->assertEquals('main', $mainview3950['templates'][0]['id']); 81 | $this->assertStringContainsString('plugin.mod_subcourse.currentgrade', $mainview3950['templates'][0]['html']); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /version.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Plugin meta-data 19 | * 20 | * @package mod_subcourse 21 | * @copyright 2008 David Mudrak 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | $plugin->component = 'mod_subcourse'; 28 | $plugin->release = 2025032000; 29 | $plugin->version = 2025032000; 30 | $plugin->maturity = MATURITY_STABLE; 31 | $plugin->requires = 2024100700; // Requires 4.5. 32 | $plugin->supported = [405, 405]; 33 | -------------------------------------------------------------------------------- /view.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * View a particular instance of the subcourse 19 | * 20 | * @package mod_subcourse 21 | * @copyright 2008 David Mudrak 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | require(__DIR__.'/../../config.php'); 26 | require_once($CFG->dirroot.'/mod/subcourse/locallib.php'); 27 | require_once($CFG->libdir.'/gradelib.php'); 28 | 29 | $id = required_param('id', PARAM_INT); 30 | $fetchnow = optional_param('fetchnow', 0, PARAM_INT); 31 | $isblankwindow = optional_param('isblankwindow', false, PARAM_BOOL); 32 | 33 | $cm = get_coursemodule_from_id('subcourse', $id, 0, false, MUST_EXIST); 34 | $course = $DB->get_record('course', ['id' => $cm->course], '*', MUST_EXIST); 35 | $subcourse = $DB->get_record('subcourse', ['id' => $cm->instance], '*', MUST_EXIST); 36 | 37 | $context = context_module::instance($cm->id); 38 | $coursecontext = context_course::instance($course->id); 39 | 40 | require_login($course, true, $cm); 41 | require_capability('mod/subcourse:view', $context); 42 | 43 | $PAGE->set_url(new moodle_url('/mod/subcourse/view.php', ['id' => $cm->id])); 44 | $PAGE->set_title($subcourse->name); 45 | $PAGE->set_heading($course->fullname); 46 | 47 | if (empty($subcourse->refcourse)) { 48 | $refcourse = false; 49 | 50 | } else { 51 | $refcourse = $DB->get_record('course', ['id' => $subcourse->refcourse], '*', IGNORE_MISSING); 52 | } 53 | 54 | if ($fetchnow && $refcourse) { 55 | require_sesskey(); 56 | require_capability('mod/subcourse:fetchgrades', $context); 57 | 58 | $event = \mod_subcourse\event\subcourse_grades_fetched::create([ 59 | 'objectid' => $subcourse->id, 60 | 'context' => $context, 61 | 'other' => ['refcourse' => $refcourse->id] 62 | ]); 63 | 64 | $event->add_record_snapshot('course_modules', $cm); 65 | $event->add_record_snapshot('course', $course); 66 | $event->add_record_snapshot('subcourse', $subcourse); 67 | $event->trigger(); 68 | 69 | $result = subcourse_grades_update($subcourse->course, $subcourse->id, $subcourse->refcourse, 70 | null, false, false, [], $subcourse->fetchpercentage); 71 | 72 | if ($result == GRADE_UPDATE_OK) { 73 | subcourse_update_timefetched($subcourse->id); 74 | redirect(new moodle_url('/mod/subcourse/view.php', ['id' => $cm->id])); 75 | 76 | } else { 77 | throw new moodle_exception('errfetch', 'subcourse', $CFG->wwwroot . '/mod/subcourse/view.php?id=' . $cm->id, $result); 78 | } 79 | } 80 | 81 | subcourse_set_module_viewed($subcourse, $context, $course, $cm); 82 | 83 | if ($refcourse && !empty($subcourse->instantredirect)) { 84 | if (!has_capability('mod/subcourse:fetchgrades', $context)) { 85 | redirect(new moodle_url('/course/view.php', ['id' => $refcourse->id])); 86 | } 87 | } 88 | 89 | echo $OUTPUT->header(); 90 | 91 | if ($refcourse) { 92 | $percentage = \core_completion\progress::get_course_progress_percentage($refcourse); 93 | $strgrade = subcourse_get_current_grade($subcourse, $USER->id); 94 | 95 | echo $OUTPUT->render_from_template('mod_subcourse/subcourseinfo', [ 96 | 'haspercentage' => ($percentage !== null), 97 | 'hasstrgrade' => ($strgrade !== null), 98 | 'percentage' => floor((float)$percentage), 99 | 'strgrade' => $strgrade, 100 | ]); 101 | 102 | echo html_writer::start_div('actionbuttons'); 103 | 104 | if ($subcourse->blankwindow && !$isblankwindow) { 105 | $target = '_blank'; 106 | } else { 107 | $target = ''; 108 | } 109 | 110 | echo html_writer::link( 111 | new moodle_url('/course/view.php', ['id' => $refcourse->id]), 112 | get_string('gotorefcourse', 'subcourse', format_string($refcourse->fullname)), 113 | ['class' => 'btn btn-primary', 'target' => $target] 114 | ); 115 | 116 | $refcoursecontext = context_course::instance($refcourse->id); 117 | 118 | if (has_all_capabilities(['gradereport/grader:view', 'moodle/grade:viewall'], $refcoursecontext)) { 119 | echo html_writer::link( 120 | new moodle_url('/grade/report/grader/index.php', ['id' => $refcourse->id]), 121 | get_string('gotorefcoursegrader', 'subcourse', format_string($refcourse->fullname)), 122 | ['class' => 'btn btn-secondary'] 123 | ); 124 | } 125 | 126 | if (has_all_capabilities(['gradereport/user:view', 'moodle/grade:view'], $refcoursecontext) 127 | && $refcourse->showgrades && ($strgrade !== null)) { 128 | echo html_writer::link( 129 | new moodle_url('/grade/report/user/index.php', ['id' => $refcourse->id]), 130 | get_string('gotorefcoursemygrades', 'subcourse', format_string($refcourse->fullname)), 131 | ['class' => 'btn btn-secondary'] 132 | ); 133 | } 134 | 135 | if (has_capability('mod/subcourse:fetchgrades', $context)) { 136 | echo html_writer::link( 137 | new moodle_url($PAGE->url, ['sesskey' => sesskey(), 'fetchnow' => 1]), 138 | get_string('fetchnow', 'subcourse'), 139 | ['class' => 'btn btn-link'] 140 | ); 141 | 142 | if (empty($subcourse->timefetched)) { 143 | $fetchinfo = get_string('lastfetchnever', 'subcourse'); 144 | } else { 145 | $fetchinfo = get_string('lastfetchtime', 'subcourse', userdate($subcourse->timefetched)); 146 | } 147 | 148 | echo html_writer::tag('small', $fetchinfo, ['class' => 'dimmed_text']); 149 | } 150 | 151 | // End of div.actionbuttons. 152 | echo html_writer::end_div(); 153 | 154 | } else { 155 | if (has_capability('mod/subcourse:fetchgrades', $context)) { 156 | echo $OUTPUT->notification(get_string('refcoursenull', 'subcourse')); 157 | } 158 | } 159 | 160 | echo $OUTPUT->footer(); 161 | --------------------------------------------------------------------------------