diff --git a/.github/workflows/moodle-ci.yml b/.github/workflows/moodle-ci.yml index f5b1072..cd82f6f 100644 --- a/.github/workflows/moodle-ci.yml +++ b/.github/workflows/moodle-ci.yml @@ -16,7 +16,7 @@ jobs: - 5432:5432 options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 3 mariadb: - image: mariadb:10 + image: mariadb:10.6.7 env: MYSQL_USER: 'root' MYSQL_ALLOW_EMPTY_PASSWORD: "true" @@ -29,9 +29,19 @@ jobs: strategy: fail-fast: false matrix: - php: ['8.0'] - moodle-branch: ['MOODLE_401_STABLE'] - database: [pgsql, mariadb] + include: + - php: '8.2' + moodle-branch: 'MOODLE_403_STABLE' + database: 'pgsql' + - php: '8.0' + moodle-branch: 'MOODLE_402_STABLE' + database: 'mariadb' + - php: '8.0' + moodle-branch: 'MOODLE_401_STABLE' + database: 'pgsql' + - php: '7.4' + moodle-branch: 'MOODLE_400_STABLE' + database: 'mariadb' steps: - name: Check out repository code diff --git a/Documentation.md b/Documentation.md new file mode 100644 index 0000000..ee13766 --- /dev/null +++ b/Documentation.md @@ -0,0 +1,208 @@ +# Skills - Monitoring skills and learning time + +## Overview + +The "Skills" feature is designed to enhance learner engagement within Moodle. As a tool-type plugin, Skills facilitates the awarding of skills based on points. Learners can acquire skills by completing linked courses or activities, fostering a sense of achievement and progress. + +The "Skills" feature allows administrators and teachers to configure up to 10 levels for each skill. Each level corresponds to a specific set of points, creating a structured progression system. This system enhances the granularity of skill development and provides learners with clear milestones to achieve. + +This documentation provides an in-depth guide to the various aspects and functionalities of the Skills feature. + +## Installation and initial setup + +### Installation + +You can install the Pulse plugin using the Moodle plugin installer. Here are the steps to follow: + +1. Download the "Skills" plugin from the Moodle plugins repository or from the bdecent website. +2. Log in to your Moodle site as an administrator. +3. Go to "Site administration > Plugins > Install plugins". +4. Upload the downloaded plugin ZIP file. +5. Follow the prompts to install the plugin. +6. Once the admin tool plugin is installed, you can manage it by going to Site Administration > Plugins > Admin Tools > Skills. From there, you can set up the skills and levels system, assign to courses and modules. + +Alternatively, you can also install the Skills plugin manually. Here are the steps to follow: + +1. Download the "Skills" plugin from the Moodle plugins repository +2. Unzip the downloaded file. +3. Upload the skills folder to the moodle/admin/tool directory on your Moodle server. +4. Log in to your Moodle site as an administrator. +5. Go to "Site administration > Notifications". +6. Follow the prompts to install the plugin. + +## Key Features: + +1. **Skill Creation and Management**: Create, edit, and delete skills seamlessly. + +2. **Add Levels**: Create multiple levels with specific set of points for each skill. + +3. **User Points System**: Allow users to earn points for mastering skills and track their progress. + +4. **Logs and Reports**: Access detailed logs of user points and generate reports on skill usage. + +5. **Privacy Controls**: Configure privacy settings to handle user consent and data protection. + +6. **Integration with Courses**: Link skills to courses, providing a holistic view of a user's achievements. + + +## How it works. + +Admins initiate the process by creating a skill and establishing levels with assigned points for the skill. The skill is then linked to a course through the "Manage Skills" page. Teachers wield multiple options for awarding points: + +1. **Points**: Assign a specific number of points to the user. + +2. **Force Levels**: Compel the user's points to align with a designated level (potentially decreasing their current points). + +3. **Set Levels**: Adjust the user's points to match a specified level, provided the user has sufficient points. + +Upon course completion, users earn points and acquire skills based on the established setup. + + +# Manage skills: + + Manage skills to create a new skill and edit existing skills. + +1. **Filter:** The "Filter" option is used to filter the list of skills within the category lists. + +### Active Skills: + +List of skills currently active and user can earn skills and teachers can use it in there courses. + +The "Active Skills" tab displays a full list of created skills or the filtered skills within the categories. + +1. **Key:** + + Each skill should be uniquely identified by a distinct key to maintain clarity and organization in the system. + +2. **Skill Name:** + + The designation 'Skill Name' serves as the unique identifier for each individual skill. + +3. **Description:** + + The "Description" refers to a detailed explanation or information provided about various skills. + +4. **Time created:** + + Time Created" indicates the record of when specific skills were established. + +5. **Course Categories:** + + It displays the list of categories added for specific skills. + +6. **Actions:** + + 1. ***Edit settings:*** Click the 'Edit' icon in the table to make changes to a specific skill. + 2. ***Status:*** Use this toggle icon in the table to enable or disable the status of the specific skill. + 3. **Archive:** Click the "Archive" option in the table to Archive the specific skill. + +### Archived Skills: + +Archived skills are not available in course list, and not awared to students. + +The "Archived Skills" tab displays a list of archived skills or the filtered skills within the categories. + +**Actions:** + + 1. ***Delete:*** Click the 'delete' option to remove a specific skill. + 2. ***Active:*** Click the 'active' option to move a specific skill to the "Active" tab. + + +## General Configuration + +Use the "**Create Skill**" button to create a new skills. Skills comes with following configurations. + +These configurations establish the rules and standards for assessing, tracking, and awarding skills. + +1. **Skill Name:** + + The designation 'Skill Name' serves as the unique identifier for each individual skill. + +2. **Key:** + + Each skill should be uniquely identified by a distinct key to maintain clarity and organization in the system. + +3. **Description:** + + The term "Skills Description" refers to a detailed explanation or information provided about various skills. + +4. **Status:** + + Choose the status for this skill: + + ***Enabled:*** The skill will be added to all courses that match the course categories setting below and can be configured by teachers. + + ***Disabled:*** The skill will not be added to any courses and cannot be used by teachers. + +5. **Learning time:** + + The time required to complete this skill within the course. + +6. **Skill color:** + + Choose a color to represent the skill level. + +7. **Available in course categories:** + + Select the categories to make this skill available exclusively to courses within the chosen category. If no category is selected, the course will be available globally across all categories. + +## Levels - General settings + +1. **Number of levels** + +Select the number of levels available for this skill. Each level have a specific point requirement for achievement and other following configurations. + +2. **Level Name** + +Provide the level name for the specific skill. + +3. **Level Point** + +Please specify the point value for the specific skill level. + +4. **Level Color** + +Select a color to represent the level. This will override the general skill color for visualization purposes. + +5. **Level Image** + +Please upload an image that represents the level of skill. This will be used for visualization. + + +# Course skills settings + +To access the skills list and assign them to a course, utilize the "Manage Skills" link found in the secondary navigation of the course. Within this interface, you have the option to grant a precise number of points or set the points required to reach a specific skill level. + +Simply employ the "Edit" icon in the table to activate the skill for the course and configure the settings for "Upon Course Completion" and "Points." + +1. **Status:** + + Choose the status for this skill: + + ***Enabled:*** The skill will be added to the course that match the upon completion setting below and can be configured by teachers. + + ***Disabled:*** The skill will not be added to any courses and cannot be used by teachers. + +2. **Upon course completion:** + + Upon course completion, you can choose from several options to determine what should happen at the end of the course. + + ***Nothing:*** Choose 'Nothing' to use activity completion, instead of course completion, for awarding points. + + ***Points:*** Select 'Add points' to have the specified number of skill points added upon course completion. Please note that using negative numbers will result in a deduction of points. + + ***Set level:*** Choose 'Set level' to have the completion of the course add the necessary number of points required to reach that level, unless the student already has more points. + + ***Force level:*** Select 'Force level' to set the number of points to the amount required for that level upon course completion, regardless of the student's previous level/points. This may result in students having fewer points than before. + +3. **Points:** + + Enter the number of skill points to be awarded or deducted. Use a positive number to add points and a negative number to deduct points. + + ***Example:*** + Entering "50" will add 50 points. + Entering "-20" will deduct 20 points. + +4. **Level:** + + Choose the desired skill level for this course. Upon completion, the student will receive the corresponding number of points required to achieve the selected level. diff --git a/README.md b/README.md index eee015a..0f868c7 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ See http://docs.moodle.org/en/Installing_plugins for details on installing Moodl Admins or users with the capability tool/skills:manage (by default given to managers) need to create skills under Site Administration > Plugins > Tools > Skills and make them available either globally or for specific course categories. -Teachers (or users with the capability tool/skills:managecourseskills) can then manage skills in their course from the "Manage skills" page which is found in the secondary navigation of the course. From there, teachers can then enable specific skills and configure how many points students earn upon completion of the course. +Teachers (or users with the capability tool/skills:managecourseskillslist) can then manage skills in their course from the "Manage skills" page which is found in the secondary navigation of the course. From there, teachers can then enable specific skills and configure how many points students earn upon completion of the course. # Documentation diff --git a/addon/README.md b/addon/README.md new file mode 100644 index 0000000..aad6063 --- /dev/null +++ b/addon/README.md @@ -0,0 +1,12 @@ +# Skills Plugin Addons + +Welcome to the Skills plugin addons! This addon houses additional features and sub-plugin addons for the "Tool skills" plugin in Moodle. + +## Overview + +The Skills plugin enhances the functionality to providing advanced features related to skills management. This addon focuses on extending the plugin with extra addons, specifically designed for extend user points allocation methods. + +# Copyright + +bdecent gmbh +bdecent.de diff --git a/classes/courseskills.php b/classes/courseskills.php index 05ae2cd..0d938c9 100644 --- a/classes/courseskills.php +++ b/classes/courseskills.php @@ -24,10 +24,14 @@ namespace tool_skills; +defined('MOODLE_INTERNAL') || die(); + use completion_info; use moodle_exception; use stdClass; +require_once($CFG->dirroot.'/admin/tool/skills/lib.php'); + /** * Manage the skills for courses. Trigger skills to assign point for users. */ @@ -60,7 +64,7 @@ protected function __construct(int $courseid) { } /** - * Create the retunr the clas instance for this skillcourse id. + * Create the retun the clas instance for this skillcourse id. * * @param int $courseid * @return self @@ -70,7 +74,7 @@ public static function get(int $courseid) : self { } /** - * Create the retunr the clas instance for this skillcourse id. + * Fetch to the skills course data . * * @param int $skillid * @return self @@ -105,6 +109,7 @@ public function get_instance_skills(): array { return array_map(fn($sk) => skills::get($sk->skill), $skills); } + /** * Remove the course skills records. * @@ -115,6 +120,8 @@ public function remove_instance_skills() { $DB->delete_records('tool_skills_courses', ['courseid' => $this->courseid]); + \tool_skills\helper::extend_addons_remove_course_instance($this->courseid); + $this->get_logs()->delete_method_log($this->courseid, 'course'); } @@ -193,7 +200,7 @@ public function get_user_earned_points(int $userid) { * @return void */ public function manage_course_completions(int $userid) { - global $CFG; + global $CFG, $DB; require_once($CFG->dirroot . '/lib/completionlib.php'); @@ -204,13 +211,37 @@ public function manage_course_completions(int $userid) { // Get course skills records. $skills = $this->get_instance_skills(); foreach ($skills as $skillcourseid => $skill) { + // Create a skill course record instance for this skill. $this->set_skill_instance($skillcourseid); - $skill->assign_skills($this, $userid); + // Get the data. + $csdata = $this->build_data(); + + // Start the database transaction. + $transaction = $DB->start_delegated_transaction(); + + switch ($csdata->uponcompletion) { + + case skills::COMPLETIONFORCELEVEL: + $skill->force_level($this, $csdata->level, $userid); + break; + + case skills::COMPLETIONSETLEVEL: + $skill->moveto_level($this, $csdata->level, $userid); + break; + + case skills::COMPLETIONPOINTS: + $skill->increase_points($this, $csdata->points, $userid); + break; + } + + // End the database transaction. + $transaction->allow_commit(); } } } + /** * Manage users completion. * diff --git a/classes/events/observer.php b/classes/events/observer.php index 2dbe2ef..e9cfd17 100644 --- a/classes/events/observer.php +++ b/classes/events/observer.php @@ -59,7 +59,7 @@ public static function course_completed(\core\event\course_completed $event) { public static function user_deleted(\core\event\user_deleted $event) { // Fetch the event data. $data = $event->get_data(); - $relateduserid = $data['userid']; // Completed user id. + $relateduserid = $data['objectid']; // Completed user id. // Remove the user skill points. user::get($relateduserid)->remove_user_skillpoints(); } diff --git a/classes/form/course_form.php b/classes/form/course_form.php index 7e69e0e..11eee9d 100644 --- a/classes/form/course_form.php +++ b/classes/form/course_form.php @@ -96,6 +96,15 @@ public function definition() { $mform->addHelpButton('level', 'completionlevel', 'tool_skills'); } + /** + * Check the access for the submit data to this form. + * + * @return bool + */ + protected function check_access_for_dynamic_submission(): void { + // TODO: Validatation of user capability goes here. + } + /** * Get the context of this form used. * @@ -108,15 +117,6 @@ protected function get_context_for_dynamic_submission(): \context { return $courseid ? \context_course::instance($courseid) : \context_system::instance(); } - /** - * Check the access for the submit data to this form. - * - * @return bool - */ - protected function check_access_for_dynamic_submission(): void { - // TODO: Validatation of user capability goes here. - } - /** * Process the submission from AJAX. * diff --git a/classes/form/skills_form.php b/classes/form/skills_form.php index 468ecdc..288fd8a 100644 --- a/classes/form/skills_form.php +++ b/classes/form/skills_form.php @@ -31,7 +31,7 @@ use context_system; use html_writer; -use \tool_skills\skills; +use tool_skills\skills; /** * Skills create/edit form. @@ -178,7 +178,7 @@ public function definition_after_data() { if ($i == 0 && !$mform->getElementValue("levels[$i][name]")) { $mform->setDefaults([ "levels[$i][name]" => get_string('skillslevel', 'tool_skills') . ' ' . $i, - "levels[$i][points]" => '0' + "levels[$i][points]" => '0', ]); } @@ -233,7 +233,7 @@ public function data_preprocessing(&$defaultvalues) { $defaultvalues = (object) $defaultvalues; $filemanagers = [ - 'image' => 'image' + 'image' => 'levelimage', ]; // Levels count. @@ -242,13 +242,11 @@ public function data_preprocessing(&$defaultvalues) { // Prepare the file manager fields to store images. foreach ($filemanagers as $configname => $filearea) { // For all levels in this skills. - for ($i = 1; $i <= $levelscount; $i++) { + for ($i = 0; $i <= $levelscount; $i++) { if (empty($defaultvalues->levels[$i])) { continue; } - // Fileare for this level. - $filearea .= '_' . $i; // Draft item id. $draftitemid = file_get_submitted_draft_itemid($filearea); // Use the level id as item id. @@ -263,6 +261,7 @@ public function data_preprocessing(&$defaultvalues) { $defaultvalues->levels[$i][$configname] = $draftitemid; } } + } /** @@ -282,7 +281,7 @@ public function data_postprocessing(&$data) { $data = (object) $data; $filemanagers = [ - 'image' => 'image' + 'image' => 'levelimage', ]; $levelscount = $data->levelscount; @@ -290,16 +289,16 @@ public function data_postprocessing(&$data) { // Prepare the file manager fields to store images. foreach ($filemanagers as $configname => $filearea) { - for ($i = 1; $i <= $levelscount; $i++) { + for ($i = 0; $i <= $levelscount; $i++) { if (empty($data->levels[$i])) { continue; } + // Level id used as item id. $levelid = $data->levels[$i]['id'] ?: 0; - // Now save the files in correct part of the File API. - $filearea .= '_' . $i; + // Now save the files in correct part of the File API. file_save_draft_area_files( $data->levels[$i][$configname], $context->id, 'tool_skills', $filearea, $levelid, $this->get_editor_options($context) @@ -321,7 +320,7 @@ protected function get_editor_options($context=null) { 'subdirs' => true, 'maxfiles' => 1, 'maxbytes' => 1000000, - 'context' => $context ?: $PAGE->context + 'context' => $context ?: $PAGE->context, ]; } diff --git a/classes/helper.php b/classes/helper.php index d4b52c0..dbb2d9e 100644 --- a/classes/helper.php +++ b/classes/helper.php @@ -49,7 +49,7 @@ public static function skills_buttons($tab, $filtered=false) { if (has_capability('tool/skills:manage', $PAGE->context)) { // Setup create template button on page. $caption = get_string('createskill', 'tool_skills'); - $editurl = new \moodle_url('/admin/tool/skills/manage/edit.php', array('sesskey' => sesskey())); + $editurl = new \moodle_url('/admin/tool/skills/manage/edit.php', ['sesskey' => sesskey()]); // IN Moodle 4.2, primary button param depreceted. $primary = defined('single_button::BUTTON_PRIMARY') ? single_button::BUTTON_PRIMARY : true; @@ -61,7 +61,7 @@ public static function skills_buttons($tab, $filtered=false) { $button .= \html_writer::start_div('filter-form-container'); $button .= \html_writer::link('javascript:void(0)', $OUTPUT->pix_icon('i/filter', 'Filter'), [ 'id' => 'tool-skills-filter', - 'class' => 'sort-toolskills btn btn-primary ml-2 ' . ($filtered ? 'filtered' : '') + 'class' => 'sort-toolskills btn btn-primary ml-2 ' . ($filtered ? 'filtered' : ''), ]); $filter = new \tool_skills_table_filter(null, ['t' => $tab]); $button .= \html_writer::tag('div', $filter->render(), ['id' => 'tool-skills-filterform', 'class' => 'hide']); @@ -70,4 +70,169 @@ public static function skills_buttons($tab, $filtered=false) { return $button; } + /** + * Undocumented function + * + * @return void + */ + public static function get_skills_list() { + global $DB; + // List of skills available. + $skills = $DB->get_records('tool_skills', []); + array_walk($skills, function(&$skill) { + $skill = \tool_skills\skills::get($skill->id); + }); + + return $skills; + } + + /** + * Get the list of completed skills of the user. + * + * @param int $userid + * @return array + */ + public static function get_user_completedskills(int $userid) { + global $DB; + // List of skills available. + $skills = \tool_skills\user::get($userid)->get_user_skills(); + + $completed = []; + foreach ($skills as $skill) { + $skillpoint = $skill->skillobj->get_points_to_earnskill(); + if ($skillpoint <= 0) { + continue; + } + + $points = $skill->userpoints->points ?? 0; + $percentage = ($points / $skillpoint) * 100; + + if ($percentage >= 100) { + $completed[] = $skill->skill; + } + } + + return !empty($completed) ? array_unique($completed) : []; + } + + /** + * Calculate the skills total points assigned for the given courses. + * + * @param array $courseids + * @return int + */ + public static function get_courses_skill_points(array $courseids) { + global $DB; + + list($insql, $inparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED, 'skp'); + + $sql = "SELECT tsl.skill, MAX(tsl.points) AS skillpoints + FROM {tool_skills_levels} tsl + JOIN {tool_skills_courses} tsc ON tsc.skill = tsl.skill + WHERE tsc.status = 1 AND tsc.courseid $insql + GROUP BY tsl.skill"; + + $skills = $DB->get_records_sql($sql, $inparams); + + $skillpoints = array_sum(array_column($skills, 'skillpoints')); + + return $skillpoints; + } + + /** + * Get addon extend method. + * + * @param string $method + * @return array + */ + public static function get_addon_extend_method($method) { + $addon = new \tool_skills\plugininfo\skilladdon(); + $methods = $addon->get_plugins_base($method); + return $methods; + } + + /** + * Extend the remove skills addon. + * + * @param int $skillid Id of the skill. + * @return void + */ + public static function extend_addons_remove_skills(int $skillid) { + // Extend the method from sub plugins. + $methods = self::get_addon_extend_method('remove_skills'); + foreach ($methods as $method) { + // Trigger the skill id. + $method->remove_skills($skillid); + } + + } + + + /** + * Remove course instance. + * + * @param int $courseid Course ID. + * @return void + */ + public static function extend_addons_remove_course_instance(int $courseid) { + // Extend the method from sub plugins. + $methods = self::get_addon_extend_method('remove_course_instance'); + foreach ($methods as $method) { + // Trigger the skill id. + $method->remove_course_instance($courseid); + } + } + + /** + * Add the activity method user skills data . + * + * @param int $point + * @return void + */ + public static function extend_addons_add_userskills_data(&$point) { + // Extend the method from sub plugins. + $methods = self::get_addon_extend_method('add_userskills_data'); + foreach ($methods as $method) { + // Trigger the skill id. + $method->add_userskills_data($point); + } + } + + /** + * Add to the user points content in profile page. + * + * @param int $skillstr Course ID. + * @param stdclass $data Data. + * @return void + */ + public static function extend_addons_add_user_points_content(&$skillstr, $data) { + // Extend the method from sub plugins. + $methods = self::get_addon_extend_method('add_user_points_content'); + foreach ($methods as $method) { + // Trigger the skill id. + $method->add_user_points_content($skillstr, $data); + } + } + + /** + * Add the activity method user skills data . + * + * @param \tool_skills\allocation_method $skillobj + * @return string + */ + public static function extend_addons_get_allocation_method($skillobj) { + // Extend the method from sub plugins. + $methods = self::get_addon_extend_method('get_allocation_method'); + foreach ($methods as $method) { + // Trigger the skill id. + $result = $method->get_allocation_method($skillobj); + + // Find the allocation method, break the check. + if ($result) { + break; + } + } + return $result ?? ''; + } + } diff --git a/classes/logs.php b/classes/logs.php index b3ed97b..1297c39 100644 --- a/classes/logs.php +++ b/classes/logs.php @@ -22,6 +22,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_skills; +use moodle_exception; /** * Maintain the user log of points allocations. @@ -49,9 +50,33 @@ public static function get() { return self::$instance; } + /** + * Get logs function. + * + * @param int $skillid ID of the skill + * @param int $userid ID of the user earned the point + * @param int $methodid Method id. ID of the table. + * @param string $method Method of the allocation, Course and activity methods are available current now. + * @param int $status Type of the points awarded. 1 for increase, 0 for negative points. + * @return void + */ + public function get_log(int $skillid, int $userid, int $methodid, string $method, int $status=1) { + global $DB; + + if ($log = $DB->get_record('tool_skills_awardlogs', ['skill' => $skillid, 'userid' => $userid, + 'method' => $method, 'methodid' => $methodid, ])) { + return $log; + } else { + throw new moodle_exception('skillawardnotfound', 'tool_skills'); + } + + return false; + } + /** * Add the allocated user points and method of allocation to the logs. * + * @param int $skillid ID of the skill * @param int $userid ID of the user earned the point * @param int $points Points the user earned, Contains negative points. * @param int $methodid Method id. ID of the table. @@ -59,19 +84,22 @@ public static function get() { * @param int $status Type of the points awarded. 1 for increase, 0 for negative points. * @return int ID of the logs inserted ID. */ - public function add(int $userid, int $points, int $methodid, string $method, int $status=1) { + public function add(int $skillid, int $userid, int $points, int $methodid, string $method, int $status=1) { global $DB; - $record = [ - 'userid' => $userid, - 'points' => $points, - 'methodid' => $methodid, - 'method' => $method, - 'status' => $status, - 'timecreated' => time() - ]; - - return $DB->insert_record('tool_skills_awardlogs', $record); + if (!$DB->record_exists('tool_skills_awardlogs', ['skill' => $skillid, 'userid' => $userid, + 'method' => $method, 'methodid' => $methodid, ])) { + $record = [ + 'skill' => $skillid, + 'userid' => $userid, + 'points' => $points, + 'methodid' => $methodid, + 'method' => $method, + 'status' => $status, + 'timecreated' => time(), + ]; + return $DB->insert_record('tool_skills_awardlogs', $record); + } } /** diff --git a/classes/plugininfo/skilladdon.php b/classes/plugininfo/skilladdon.php new file mode 100644 index 0000000..de5616a --- /dev/null +++ b/classes/plugininfo/skilladdon.php @@ -0,0 +1,80 @@ +. + +/** + * Subplugin type for tool skills - defined. + * + * @package tool_skills + * @copyright 2023, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_skills\plugininfo; + +/** + * Skilladdon is subplugin of tool_skills. + */ +class skilladdon extends \core\plugininfo\base { + /** + * Returns the information about plugin availability + * + * True means that the plugin is enabled. False means that the plugin is + * disabled. Null means that the information is not available, or the + * plugin does not support configurable availability or the availability + * can not be changed. + * + * @return null|bool + */ + public function is_enabled() { + return true; + } + + /** + * Should there be a way to uninstall the plugin via the administration UI. + * + * By default uninstallation is not allowed, plugin developers must enable it explicitly! + * + * @return bool + */ + public function is_uninstall_allowed() { + return true; + } + + /** + * Get the list of action plugins with its base class. + * + * @param string $method + * @return array + */ + public function get_plugins_base($method) { + $plugins = \core_component::get_plugin_list('skilladdon'); + + if (!empty($plugins)) { + foreach ($plugins as $componentname => $pluginpath) { + $classname = "skilladdon_$componentname\manage_skills"; + if (!class_exists($classname)) { + continue; + } + + if (method_exists("skilladdon_$componentname\manage_skills", $method)) { + $instance = new $classname(); + $extend[] = $instance; + } + } + } + return $extend ?? []; + } +} diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index f841144..cac3689 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -27,10 +27,10 @@ use context; use core_privacy\local\metadata\collection; -use \core_privacy\local\request\contextlist; -use \core_privacy\local\request\userlist; -use \core_privacy\local\request\approved_userlist; -use \core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\contextlist; +use core_privacy\local\request\userlist; +use core_privacy\local\request\approved_userlist; +use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\helper; use core_privacy\local\request\transform; use core_privacy\local\request\writer; @@ -57,7 +57,7 @@ public static function get_metadata(collection $collection): collection { 'skill' => 'privacy:metadata:userpoints:skill', 'points' => 'privacy:metadata:userpoints:points', 'timecreated' => 'privacy:metadata:userpoints:timecreated', - 'timemodified' => 'privacy:metadata:userpoints:timemodified' + 'timemodified' => 'privacy:metadata:userpoints:timemodified', ]; $collection->add_database_table('tool_skills_userpoints', $userpointsmetadata, 'privacy:metadata:userpoints'); @@ -67,7 +67,7 @@ public static function get_metadata(collection $collection): collection { 'points' => 'privacy:metadata:awardlogs:points', 'methodid' => 'privacy:metadata:awardlogs:methodid', 'method' => 'privacy:metadata:awardlogs:method', - 'timecreated' => 'privacy:metadata:awardlogs:timecreated' + 'timecreated' => 'privacy:metadata:awardlogs:timecreated', ]; $collection->add_database_table('tool_skills_awardlogs', $awardlogsmetadata, 'privacy:metadata:awardlogs'); @@ -191,7 +191,7 @@ public static function delete_data_for_all_users_in_context(\context $context) { return; } - $courses = $DB->get_records('tool_skills_courses', array('courseid' => $course->id)); + $courses = $DB->get_records('tool_skills_courses', ['courseid' => $course->id]); foreach ($courses as $skillcourse) { $log = $DB->get_record('tool_skills_awardlogs', ['methodid' => $skillcourse->id, 'method' => 'course']); $points = $DB->get_record('tool_skill_userpoints', ['skill' => $log->skill, 'userid' => $log->userid]); diff --git a/classes/skills.php b/classes/skills.php index 39ecd51..80ced6a 100644 --- a/classes/skills.php +++ b/classes/skills.php @@ -24,11 +24,17 @@ namespace tool_skills; +defined('MOODLE_INTERNAL') || die(); + use moodle_exception; use stdClass; use context_system; use tool_skills\allocation_method; +require_once($CFG->dirroot.'/grade/lib.php'); +require_once($CFG->dirroot.'/grade/querylib.php'); +require_once($CFG->dirroot.'/admin/tool/skills/lib.php'); + /** * Skills manage instance, doing manage of skills tasks. */ @@ -72,6 +78,12 @@ class skills { */ public const COMPLETIONFORCELEVEL = 3; + /** + * Represent the upon completion result is points grade achieve in the activity. + * @var int + */ + public const COMPLETIONPOINTSGRADE = 4; + /** * Skill instance id. * @@ -238,6 +250,10 @@ public function delete_skill() { \tool_skills\level::remove_skill_levels($this->skillid); // Delete all its actions. \tool_skills\courseskills::remove_skills($this->skillid); + // Extend the addons remove skills. + \tool_skills\helper::extend_addons_remove_skills($this->skillid); + + $DB->delete_records('tool_skills_userpoints', ['skill' => $this->skillid]); return true; } @@ -286,14 +302,14 @@ public function duplicate() { */ public function get_points_to_earnskill() { - $levels = $this->get_levels(); + $levels = $this->get_levels(); // List of levels, available in the skill. - $pointstocomplete = 0; - foreach ($levels as $levelid => $level) { - $pointstocomplete += $level->points; + if (!empty($levels)) { + $levelpoints = array_column($levels, 'points'); + $pointstocomplete = max($levelpoints); } - return $pointstocomplete; + return $pointstocomplete ?? 0; } /** @@ -332,39 +348,6 @@ public function get_levels_count() : int { return count($this->get_levels()); } - /** - * Assign the skills to users. - * - * @param \tool_skills\allocation_method $skillobj - * @param int $userid - * - * @return void - */ - public function assign_skills($skillobj, int $userid) { - global $DB; - - $csdata = $skillobj->get_data(); - // Start the database transaction. - $transaction = $DB->start_delegated_transaction(); - - switch ($csdata->uponcompletion) { - - case self::COMPLETIONFORCELEVEL: - $this->force_level($skillobj, $csdata->level, $userid); - break; - - case self::COMPLETIONSETLEVEL: - $this->moveto_level($skillobj, $csdata->level, $userid); - break; - - case self::COMPLETIONPOINTS: - $this->increase_points($skillobj, $csdata->points, $userid); - break; - } - - $transaction->allow_commit(); - } - /** * Fetch the user skill points table record. * @@ -387,6 +370,7 @@ public function get_user_skill(int $userid, $create=true) { $record['timecreated'] = time(); $DB->insert_record('tool_skills_userpoints', $record); + return $this->get_user_skill($userid, false); // Don't need to create again. } @@ -402,7 +386,7 @@ public function get_user_skill(int $userid, $create=true) { * * @return void */ - protected function force_level(allocation_method $skillobj, int $levelid, int $userid) { + public function force_level($skillobj, int $levelid, int $userid) { // Fetch the level instance for this level. $level = level::get($levelid); @@ -422,7 +406,7 @@ protected function force_level(allocation_method $skillobj, int $levelid, int $u * * @return void */ - protected function moveto_level(allocation_method $skillobj, int $levelid, int $userid) { + public function moveto_level($skillobj, int $levelid, int $userid) { // User skill. $userskill = $this->get_user_skill($userid); // Fetch the level instance for this level. @@ -447,13 +431,12 @@ protected function moveto_level(allocation_method $skillobj, int $levelid, int $ * * @return void */ - protected function increase_points(allocation_method $skillobj, int $points, int $userid) { + public function increase_points($skillobj, int $points, int $userid) { // Get user skill current record, create new one if not found. $userskill = $this->get_user_skill($userid); // Increase the allocated points with current user points. $levelpoints = $userskill->points + $points; - // Find the method of the course skills. - $method = ($skillobj instanceof \tool_skills\courseskills) ? 'course' : ''; + // Update the new points for this user in db. $this->set_userskill_points($userid, $levelpoints); @@ -468,7 +451,7 @@ protected function increase_points(allocation_method $skillobj, int $points, int * @param int $points * @return int */ - protected function set_userskill_points(int $userid, int $points) : int { + public function set_userskill_points(int $userid, int $points) : int { global $DB; $record = ['skill' => $this->skillid, 'userid' => $userid]; @@ -476,6 +459,7 @@ protected function set_userskill_points(int $userid, int $points) : int { if ($data = $DB->get_record('tool_skills_userpoints', $record)) { $id = $data->id; $data->points = $points; + $data->timemodified = time(); $DB->update_record('tool_skills_userpoints', $data); @@ -497,15 +481,43 @@ protected function set_userskill_points(int $userid, int $points) : int { * @param int $points * @return void */ - protected function create_user_point_award(allocation_method $skillobj, int $userid, int $points) { + public function create_user_point_award($skillobj, int $userid, int $points) { + // Find the method of the course skills. - $method = ($skillobj instanceof \tool_skills\courseskills) ? 'course' : ''; + if ($skillobj instanceof \tool_skills\courseskills) { + $method = 'course'; + } else { + $method = helper::extend_addons_get_allocation_method($skillobj); + } // Allocation method id. $methodid = $skillobj->get_data()->id; // Log the point awarded to users and the method. - $this->log->add($userid, $points, $methodid, $method); + $this->log->add($this->skillid, $userid, $points, $methodid, $method); + } + + /** + * Get the list of proficient users in this skill. + * + * @return array + */ + public function get_proficient() { + global $DB; + + // Points to earn this skill. + $proficientpoint = $this->get_points_to_earnskill(); + // User points. + $userpoints = $DB->get_records('tool_skills_userpoints', ['skill' => $this->skillid]); + $list = []; + foreach ($userpoints as $id => $points) { + // Verify the user is reached the maximum points for the level. + if ($points->points >= $proficientpoint) { + $list[] = $points->userid; // This user is proficient in this skill. + } + } + // Return the list of proficient users id. + return $list; } /** @@ -582,4 +594,5 @@ public static function manage_instance($formdata) { return $skillid ?? false; } + } diff --git a/classes/table/archived_skills.php b/classes/table/archived_skills.php index c3d19ff..8989906 100644 --- a/classes/table/archived_skills.php +++ b/classes/table/archived_skills.php @@ -75,6 +75,7 @@ public function query_db($pagesize, $useinitialsbar = true) { global $DB; $condition = 'archived = 1'; + // Filter the category. if ($this->filterset->has_filter('category')) { $values = $this->filterset->get_filter('category')->get_filter_values(); @@ -90,23 +91,23 @@ public function query_db($pagesize, $useinitialsbar = true) { } /** - * Name of the skill column. Format the string to support multilingual. + * Description of the skill. * * @param stdClass $row * @return string */ - public function col_name(stdClass $row) : string { - return format_string($row->name); + public function col_description(stdClass $row) : string { + return format_text($row->description, FORMAT_HTML, ['overflow' => false]); } /** - * Description of the skill. + * Name of the skill column. Format the string to support multilingual. * * @param stdClass $row * @return string */ - public function col_description(stdClass $row) : string { - return format_text($row->description, FORMAT_HTML, ['overflow' => false]); + public function col_name(stdClass $row) : string { + return format_string($row->name); } /** @@ -165,13 +166,13 @@ public function col_actions(stdClass $row) : string { // Base url to edit the skills. $baseurl = new \moodle_url('/admin/tool/skills/manage/edit.php', [ 'id' => $row->id, - 'sesskey' => \sesskey() + 'sesskey' => \sesskey(), ]); // Skills List URL. $listurl = new \moodle_url('/admin/tool/skills/manage/list.php', [ 'id' => $row->id, - 'sesskey' => \sesskey() + 'sesskey' => \sesskey(), ]); $actions = []; @@ -180,16 +181,16 @@ public function col_actions(stdClass $row) : string { $actions[] = [ 'url' => new \moodle_url($listurl, ['action' => 'delete', 't' => 'archive']), 'icon' => new \pix_icon('t/delete', \get_string('delete')), - 'attributes' => array('class' => 'action-delete'), - 'action' => new \confirm_action(get_string('deleteskill', 'tool_skills')) + 'attributes' => ['class' => 'action-delete'], + 'action' => new \confirm_action(get_string('deleteskill', 'tool_skills')), ]; // Unarchive the skills. $actions[] = [ 'url' => new \moodle_url($listurl, ['action' => 'active']), 'icon' => new \pix_icon('f/active', \get_string('active', 'tool_skills'), 'tool_skills'), - 'attributes' => array('class' => 'action-active'), - 'action' => new \confirm_action(get_string('activeskillwarning', 'tool_skills')) + 'attributes' => ['class' => 'action-active'], + 'action' => new \confirm_action(get_string('activeskillwarning', 'tool_skills')), ]; $actionshtml = []; @@ -203,7 +204,7 @@ public function col_actions(stdClass $row) : string { $action['url'], $action['icon'], ($action['action'] ?? null), - $action['attributes'] + $action['attributes'], ); } return \html_writer::div(join('', $actionshtml), 'skill-item-actions item-actions mr-0'); diff --git a/classes/table/course_skills_table.php b/classes/table/course_skills_table.php index 399fb11..7c38331 100644 --- a/classes/table/course_skills_table.php +++ b/classes/table/course_skills_table.php @@ -176,13 +176,13 @@ public function col_actions(stdClass $row) : string { $baseurl = new \moodle_url('/admin/tool/skills/manage/editcourse.php', [ 'skill' => $row->id, 'courseid' => $row->courseid ?: $this->courseid, - 'sesskey' => \sesskey() + 'sesskey' => \sesskey(), ]); // Skills List URL. $listurl = new \moodle_url('/admin/tool/skills/manage/courselist.php', [ 'courseid' => $row->courseid ?: $this->courseid, - 'sesskey' => \sesskey() + 'sesskey' => \sesskey(), ]); $actions = []; @@ -191,7 +191,7 @@ public function col_actions(stdClass $row) : string { $actions[] = [ 'url' => $baseurl, 'icon' => new \pix_icon('t/edit', \get_string('edit')), - 'attributes' => array('class' => 'action-edit', 'data-target' => "toolskill-edit", "data-skillid" => $row->id) + 'attributes' => ['class' => 'action-edit', 'data-target' => "toolskill-edit", "data-skillid" => $row->id], ]; // Show/Hide. @@ -204,8 +204,9 @@ public function col_actions(stdClass $row) : string { ); // Skills status switch. - $statusurl = new \moodle_url($listurl, array('t' => 'archive', 'skill' => $row->id, - 'action' => ($row->coursestatus) ? 'disable' : 'enable')); + $statusurl = new \moodle_url($listurl, ['t' => 'archive', 'skill' => $row->id, + 'action' => ($row->coursestatus) ? 'disable' : 'enable', + ]); $statusclass = ' toolskills-status-switch '; $statusclass .= $row->coursestatus ? 'action-hide' : 'action-show'; $actions[] = html_writer::link($statusurl->out(false), $checkbox, ['class' => $statusclass]); @@ -221,7 +222,7 @@ public function col_actions(stdClass $row) : string { $action['url'], $action['icon'], ($action['action'] ?? null), - $action['attributes'] + $action['attributes'], ); } return html_writer::div(join('', $actionshtml), 'skill-course-actions skill-actions mr-0'); @@ -238,13 +239,13 @@ public function edit_switch($row) { $temp = (object) [ 'legacyseturl' => (new moodle_url('/admin/tool/skills/manage/courselist.php', [ 'id' => $row->id, - 'sesskey' => sesskey() + 'sesskey' => sesskey(), ]))->out(false), 'pagecontextid' => $PAGE->context->id, 'pageurl' => $PAGE->url, 'sesskey' => sesskey(), 'checked' => $row->coursestatus, - 'id' => $row->skillcourseid + 'id' => $row->skillcourseid, ]; return $OUTPUT->render_from_template('tool_skills/status_switch', $temp); } diff --git a/classes/table/skills_table.php b/classes/table/skills_table.php index cf4d9f1..89865d9 100644 --- a/classes/table/skills_table.php +++ b/classes/table/skills_table.php @@ -158,13 +158,13 @@ public function col_actions(stdClass $row) : string { // Base url to edit the skills. $baseurl = new \moodle_url('/admin/tool/skills/manage/edit.php', [ 'id' => $row->id, - 'sesskey' => \sesskey() + 'sesskey' => \sesskey(), ]); // Skills List URL. $listurl = new \moodle_url('/admin/tool/skills/manage/list.php', [ 'id' => $row->id, - 'sesskey' => \sesskey() + 'sesskey' => \sesskey(), ]); $actions = []; @@ -173,7 +173,7 @@ public function col_actions(stdClass $row) : string { $actions[] = [ 'url' => $baseurl, 'icon' => new \pix_icon('t/edit', \get_string('edit')), - 'attributes' => array('class' => 'action-edit') + 'attributes' => ['class' => 'action-edit'], ]; // Show/Hide. @@ -184,7 +184,7 @@ public function col_actions(stdClass $row) : string { ) . html_writer::tag('span', '', ['class' => 'custom-control-label']), 'custom-control custom-switch' ); - $statusurl = new \moodle_url($listurl, array('action' => ($row->status) ? 'disable' : 'enable')); + $statusurl = new \moodle_url($listurl, ['action' => ($row->status) ? 'disable' : 'enable']); $statusclass = ' toolskills-status-switch '; $statusclass .= $row->status ? 'action-hide' : 'action-show'; $actions[] = html_writer::link($statusurl->out(false), $checkbox, ['class' => $statusclass]); @@ -193,8 +193,8 @@ public function col_actions(stdClass $row) : string { $actions[] = [ 'url' => new \moodle_url($listurl, ['t' => 'archive', 'action' => 'archive']), 'icon' => new \pix_icon('f/archive', \get_string('archive', 'tool_skills'), 'tool_skills'), - 'attributes' => array('class' => 'action-archive'), - 'action' => new \confirm_action(get_string('archiveskill', 'tool_skills')) + 'attributes' => ['class' => 'action-archive'], + 'action' => new \confirm_action(get_string('archiveskill', 'tool_skills')), ]; $actionshtml = []; @@ -208,7 +208,7 @@ public function col_actions(stdClass $row) : string { $action['url'], $action['icon'], ($action['action'] ?? null), - $action['attributes'] + $action['attributes'], ); } return html_writer::div(join('', $actionshtml), 'skill-item-actions item-actions mr-0'); @@ -225,13 +225,13 @@ public function edit_switch($row) { $temp = (object) [ 'legacyseturl' => (new moodle_url('/admin/tool/skills/manage/list.php', [ 'id' => $row->id, - 'sesskey' => sesskey() + 'sesskey' => sesskey(), ]))->out(false), 'pagecontextid' => $PAGE->context->id, 'pageurl' => $PAGE->url, 'sesskey' => sesskey(), 'checked' => $row->status, - 'id' => $row->id + 'id' => $row->id, ]; return $OUTPUT->render_from_template('tool_skills/status_switch', $temp); } diff --git a/classes/table/users_skills.php b/classes/table/users_skills.php index 8fa9d59..c75eb7a 100644 --- a/classes/table/users_skills.php +++ b/classes/table/users_skills.php @@ -126,6 +126,6 @@ public function query_db($pagesize, $useinitialsbar = true) { public function col_fullname($data) { global $OUTPUT; - return $OUTPUT->user_picture($data, array('size' => 35, 'includefullname' => true)); + return $OUTPUT->user_picture($data, ['size' => 35, 'includefullname' => true]); } } diff --git a/classes/user.php b/classes/user.php index 36a1535..8eb4522 100644 --- a/classes/user.php +++ b/classes/user.php @@ -98,7 +98,7 @@ public function get_userid() { * @return array */ public function get_user_skills() { - global $DB; + global $DB, $CFG; // Fetch the list of user enrolled courses. $courses = enrol_get_users_courses($this->userid, true, 'id'); @@ -116,7 +116,7 @@ public function get_user_skills() { FROM {tool_skills_courses} tscs JOIN {tool_skills} ts ON ts.id = tscs.skill LEFT JOIN {tool_skills_userpoints} up ON up.skill = ts.id AND up.userid = :userid - WHERE tscs.status = :enabled AND tscs.courseid $insql"; + WHERE tscs.status = :enabled AND ts.status = 1 AND ts.archived = 0 AND tscs.courseid $insql"; $list = $DB->get_records_sql($sql, ['userid' => $this->userid, 'enabled' => 1] + $inparams); @@ -129,6 +129,9 @@ public function get_user_skills() { $point->skillcourse = courseskills::get($point->courseid); $point->skillcourse->set_skill_instance($point->id); + // Extend addons to inlcude its skill data. + \tool_skills\helper::extend_addons_add_userskills_data($point); + $point->userpoints = $DB->get_record('tool_skills_userpoints', ['skill' => $point->skill, 'userid' => $this->userid]); // Skill levels. $point->levels = $DB->get_records('tool_skills_levels', ['skill' => $point->skill]); @@ -194,5 +197,37 @@ public function get_user_award_by_method(string $method, int $methodid) { return null; } + /** + * Get user proficiency level. + * + * @param int $skillid + * @param int $points + * @return string + */ + public function get_user_proficency_level(int $skillid, int $points) { + + $skill = skills::get($skillid); + $levels = $skill->get_levels(); + foreach ($levels as $level) { + if ($points >= $level->points) { + $proficiencylevel = $level->name; + } + } + + return $proficiencylevel ?? ''; + } + + /** + * Get the user percentage in the skill. + * + * @param int $skillid Skill ID + * @param int $points + * @return string + */ + public function get_user_percentage(int $skillid, $points) { + $skillpoint = skills::get($skillid)->get_points_to_earnskill(); + $percentage = ($points / $skillpoint) * 100; + return ((int) $percentage) . '%'; + } } diff --git a/db/access.php b/db/access.php index 5fe697d..3995a4d 100644 --- a/db/access.php +++ b/db/access.php @@ -24,35 +24,36 @@ defined('MOODLE_INTERNAL') || die(); -$capabilities = array( +$capabilities = [ // Capability to manage skills. - 'tool/skills:manage' => array( + 'tool/skills:manage' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_SYSTEM, 'riskbitmask' => RISK_XSS | RISK_CONFIG, - 'archetypes' => array( - 'manager' => CAP_ALLOW - ), - ), + 'archetypes' => [ + 'manager' => CAP_ALLOW, + ], + ], - 'tool/skills:managecourseskills' => array( + 'tool/skills:managecourseskillslist' => [ 'captype' => 'write', - 'contextlevel' => CONTEXT_SYSTEM, + 'contextlevel' => CONTEXT_COURSE, 'riskbitmask' => RISK_XSS | RISK_CONFIG, - 'archetypes' => array( - 'manager' => CAP_ALLOW, + 'archetypes' => [ + 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - ), - ), + 'manager' => CAP_ALLOW, + ], + ], - 'tool/skills:viewotherspoints' => array( + 'tool/skills:viewotherspoints' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_SYSTEM, 'riskbitmask' => RISK_XSS | RISK_CONFIG, - 'archetypes' => array( - 'manager' => CAP_ALLOW - ), - ), + 'archetypes' => [ + 'manager' => CAP_ALLOW, + ], + ], -); +]; diff --git a/db/events.php b/db/events.php index 7b4d07e..776ef2b 100644 --- a/db/events.php +++ b/db/events.php @@ -26,19 +26,18 @@ $observers = [ - array( + [ 'eventname' => 'core\event\course_completed', 'callback' => '\tool_skills\events\observer::course_completed', - ), + ], - array( + [ 'eventname' => 'core\event\course_deleted', 'callback' => '\tool_skills\events\observer::course_deleted', - ), + ], - array( + [ 'eventname' => 'core\event\user_deleted', 'callback' => '\tool_skills\events\observer::user_deleted', - ), - + ], ]; diff --git a/db/install.xml b/db/install.xml index 9fda7d9..32b3337 100644 --- a/db/install.xml +++ b/db/install.xml @@ -73,6 +73,7 @@