From f1979b60a164584db8b42df0e2e23e1f0153735a Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 30 Mar 2016 17:05:55 +0800 Subject: [PATCH] MDL-52743 tool_lp: Course competency statistics Note - the statistics you see depends on whether you can recieve competency ratings in the course. So students see their own progress as a %. Graders see the least proficient competencies for the course. --- admin/tool/lp/classes/api.php | 48 ++++++++ .../classes/course_competency_statistics.php | 58 ++++++++++ admin/tool/lp/classes/external.php | 2 + .../course_competency_statistics_exporter.php | 103 ++++++++++++++++++ .../output/course_competencies_page.php | 6 + .../lp/classes/user_competency_course.php | 56 ++++++++++ admin/tool/lp/lang/en/tool_lp.php | 2 + admin/tool/lp/styles.css | 2 + .../course_competencies_page.mustache | 4 + .../course_competency_statistics.mustache | 76 +++++++++++++ admin/tool/lp/tests/api_test.php | 90 +++++++++++++++ 11 files changed, 447 insertions(+) create mode 100644 admin/tool/lp/classes/course_competency_statistics.php create mode 100644 admin/tool/lp/classes/external/course_competency_statistics_exporter.php create mode 100644 admin/tool/lp/templates/course_competency_statistics.mustache diff --git a/admin/tool/lp/classes/api.php b/admin/tool/lp/classes/api.php index ef26a0403ac..6070053d3f8 100644 --- a/admin/tool/lp/classes/api.php +++ b/admin/tool/lp/classes/api.php @@ -1037,6 +1037,30 @@ class api { return $result; } + /** + * Count the proficient competencies in a course for one user. + * + * @param int $courseid The id of the course to check. + * @param int $userid The id of the user to check. + * @return int + */ + public static function count_proficient_competencies_in_course_for_user($courseid, $userid) { + static::require_enabled(); + // Check the user have access to the course. + self::validate_course($courseid); + + // First we do a permissions check. + $context = context_course::instance($courseid); + + $capabilities = array('tool/lp:coursecompetencyview', 'tool/lp:coursecompetencymanage'); + if (!has_any_capability($capabilities, $context)) { + throw new required_capability_exception($context, 'tool/lp:coursecompetencyview', 'nopermissions', ''); + } + + // OK - all set. + return user_competency_course::count_proficient_competencies($courseid, $userid); + } + /** * Count all the competencies in a course. * @@ -4772,6 +4796,7 @@ class api { * @return int */ public static function count_plans_for_template($templateorid, $status = 0) { + static::require_enabled(); $template = $templateorid; if (!is_object($template)) { $template = new template($template); @@ -4795,6 +4820,7 @@ class api { * @return int */ public static function count_user_competency_plans_for_template($templateorid, $proficiency = null) { + static::require_enabled(); $template = $templateorid; if (!is_object($template)) { $template = new template($template); @@ -4833,6 +4859,27 @@ class api { return plan::get_records_for_template($template->get_id(), $status, $skip, $limit); } + /** + * Get the most often not completed competency for this course. + * + * Requires tool/lp:coursecompetencyview capability at the course context. + * + * @param int $courseid The course id + * @param int $skip The number of records to skip + * @param int $limit The max number of records to return + * @return competency[] + */ + public static function get_least_proficient_competencies_for_course($courseid, $skip = 0, $limit = 100) { + static::require_enabled(); + $coursecontext = context_course::instance($courseid); + + if (!has_any_capability(array('tool/lp:competencyview', 'tool/lp:competencymanage'), $coursecontext)) { + throw new required_capability_exception($coursecontext, 'tool/lp:competencyview', 'nopermissions', ''); + } + + return user_competency_course::get_least_proficient_competencies_for_course($courseid, $skip, $limit); + } + /** * Get the most often not completed competency for this template. * @@ -4844,6 +4891,7 @@ class api { * @return competency[] */ public static function get_least_proficient_competencies_for_template($templateorid, $skip = 0, $limit = 100) { + static::require_enabled(); $template = $templateorid; if (!is_object($template)) { $template = new template($template); diff --git a/admin/tool/lp/classes/course_competency_statistics.php b/admin/tool/lp/classes/course_competency_statistics.php new file mode 100644 index 00000000000..d5bad5c9cff --- /dev/null +++ b/admin/tool/lp/classes/course_competency_statistics.php @@ -0,0 +1,58 @@ +. + +/** + * Course competency statistics class + * + * @package tool_lp + * @copyright 2016 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp; +defined('MOODLE_INTERNAL') || die(); + +/** + * Course competency statistics class. + * + * @package tool_lp + * @copyright 2016 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_competency_statistics { + + /** @var $competencycount The number of competencies in the course */ + public $competencycount = 0; + + /** @var $proficientcompetencycount The number of proficient competencies for the current user */ + public $proficientcompetencycount = 0; + + /** @var $leastproficientcompetencies The competencies in this course that were proficient the least times */ + public $leastproficientcompetencies = array(); + + /** + * Return the custom definition of the properties of this model. + * + * @param int $courseid The course we want to generate statistics for. + */ + public function __construct($courseid) { + global $USER; + + $this->competencycount = api::count_competencies_in_course($courseid); + $this->proficientcompetencycount = api::count_proficient_competencies_in_course_for_user($courseid, $USER->id); + $this->leastproficientcompetencies = api::get_least_proficient_competencies_for_course($courseid, 0, 3); + } +} diff --git a/admin/tool/lp/classes/external.php b/admin/tool/lp/classes/external.php index 9a1c270601a..ad60f8bd4fd 100644 --- a/admin/tool/lp/classes/external.php +++ b/admin/tool/lp/classes/external.php @@ -59,6 +59,7 @@ use tool_lp\external\user_evidence_summary_exporter; use tool_lp\external\user_evidence_competency_exporter; use tool_lp\external\competency_exporter; use tool_lp\external\course_competency_exporter; +use tool_lp\external\course_competency_statistics_exporter; use tool_lp\external\course_summary_exporter; use tool_lp\external\course_module_summary_exporter; use tool_lp\external\plan_exporter; @@ -1892,6 +1893,7 @@ class external extends external_api { 'canmanagecoursecompetencies' => new external_value(PARAM_BOOL, 'User can manage linked course competencies'), 'canconfigurecoursecompetencies' => new external_value(PARAM_BOOL, 'User can configure course competency settings'), 'settings' => course_competency_settings_exporter::get_read_structure(), + 'statistics' => course_competency_statistics_exporter::get_read_structure(), 'competencies' => new external_multiple_structure(new external_single_structure(array( 'competency' => competency_exporter::get_read_structure(), 'coursecompetency' => course_competency_exporter::get_read_structure(), diff --git a/admin/tool/lp/classes/external/course_competency_statistics_exporter.php b/admin/tool/lp/classes/external/course_competency_statistics_exporter.php new file mode 100644 index 00000000000..32a88efb061 --- /dev/null +++ b/admin/tool/lp/classes/external/course_competency_statistics_exporter.php @@ -0,0 +1,103 @@ +. + +/** + * Class for exporting a course competency statistics summary. + * + * @package tool_lp + * @copyright 2016 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\external; +defined('MOODLE_INTERNAL') || die(); + +use renderer_base; +use moodle_url; + +/** + * Class for exporting a course competency statistics summary. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_competency_statistics_exporter extends exporter { + + public static function define_properties() { + return array( + 'competencycount' => array( + 'type' => PARAM_INT, + ), + 'proficientcompetencycount' => array( + 'type' => PARAM_INT, + ), + ); + } + + public static function define_other_properties() { + return array( + 'proficientcompetencypercentage' => array( + 'type' => PARAM_FLOAT + ), + 'proficientcompetencypercentageformatted' => array( + 'type' => PARAM_RAW + ), + 'leastproficient' => array( + 'type' => competency_exporter::read_properties_definition(), + 'multiple' => true + ), + 'leastproficientcount' => array( + 'type' => PARAM_INT + ), + 'canbegradedincourse' => array( + 'type' => PARAM_BOOL + ), + 'canmanagecoursecompetencies' => array( + 'type' => PARAM_BOOL + ), + ); + } + + protected static function define_related() { + return array('context' => 'context'); + } + + protected function get_other_values(renderer_base $output) { + $proficientcompetencypercentage = 0; + $proficientcompetencypercentageformatted = ''; + if ($this->data->competencycount > 0) { + $proficientcompetencypercentage = ((float) $this->data->proficientcompetencycount / (float) $this->data->competencycount) * 100.0; + $proficientcompetencypercentageformatted = format_float($proficientcompetencypercentage); + } + $competencies = array(); + $contextcache = array(); + foreach ($this->data->leastproficientcompetencies as $competency) { + if (!isset($contextcache[$competency->get_competencyframeworkid()])) { + $contextcache[$competency->get_competencyframeworkid()] = $competency->get_context(); + } + $context = $contextcache[$competency->get_competencyframeworkid()]; + $exporter = new competency_exporter($competency, array('context' => $context)); + $competencies[] = $exporter->export($output); + } + return array( + 'proficientcompetencypercentage' => $proficientcompetencypercentage, + 'proficientcompetencypercentageformatted' => $proficientcompetencypercentageformatted, + 'leastproficient' => $competencies, + 'leastproficientcount' => count($competencies), + 'canbegradedincourse' => has_capability('tool/lp:coursecompetencygradable', $this->related['context']), + 'canmanagecoursecompetencies' => has_capability('tool/lp:coursecompetencymanage', $this->related['context']) + ); + } +} diff --git a/admin/tool/lp/classes/output/course_competencies_page.php b/admin/tool/lp/classes/output/course_competencies_page.php index d6446514f90..d30a92afbd7 100644 --- a/admin/tool/lp/classes/output/course_competencies_page.php +++ b/admin/tool/lp/classes/output/course_competencies_page.php @@ -33,10 +33,12 @@ use context_system; use context_course; use tool_lp\api; use tool_lp\course_competency; +use tool_lp\course_competency_statistics; use tool_lp\competency; use tool_lp\external\competency_exporter; use tool_lp\external\competency_path_exporter; use tool_lp\external\course_competency_exporter; +use tool_lp\external\course_competency_statistics_exporter; use tool_lp\external\course_competency_settings_exporter; use tool_lp\external\course_module_summary_exporter; use tool_lp\external\user_competency_exporter; @@ -78,6 +80,7 @@ class course_competencies_page implements renderable, templatable { $this->canmanagecoursecompetencies = has_capability('tool/lp:coursecompetencymanage', $this->context); $this->canconfigurecoursecompetencies = has_capability('tool/lp:coursecompetencyconfigure', $this->context); $this->coursecompetencysettings = api::read_course_competency_settings($courseid); + $this->coursecompetencystatistics = new course_competency_statistics($courseid); // Check the lowest level in which the user can manage the competencies. $this->manageurl = null; @@ -178,6 +181,9 @@ class course_competencies_page implements renderable, templatable { $data->canconfigurecoursecompetencies = $this->canconfigurecoursecompetencies; $exporter = new course_competency_settings_exporter($this->coursecompetencysettings); $data->settings = $exporter->export($output); + $related = array('context' => $this->context); + $exporter = new course_competency_statistics_exporter($this->coursecompetencystatistics, $related); + $data->statistics = $exporter->export($output); $data->manageurl = null; if ($this->canmanagecompetencyframeworks) { $data->manageurl = $this->manageurl->out(true); diff --git a/admin/tool/lp/classes/user_competency_course.php b/admin/tool/lp/classes/user_competency_course.php index 7e9172aead8..056291427d0 100644 --- a/admin/tool/lp/classes/user_competency_course.php +++ b/admin/tool/lp/classes/user_competency_course.php @@ -229,4 +229,60 @@ class user_competency_course extends persistent { return self::get_records_select("userid = :userid AND courseid = :courseid AND $sql", $params); } + + /** + * Count the proficient competencies in this course for one user. + * + * @param int $courseid The course id + * @param int $userid The user id + * @return int + */ + public static function count_proficient_competencies($courseid, $userid) { + global $DB; + + $sql = 'SELECT COUNT(comp.id) + FROM {' . self::TABLE . '} usercoursecomp + JOIN {' . competency::TABLE . '} comp + ON usercoursecomp.competencyid = comp.id + WHERE usercoursecomp.courseid = ? AND usercoursecomp.userid = ? AND usercoursecomp.proficiency = ?'; + $params = array($courseid, $userid, true); + + $results = $DB->count_records_sql($sql, $params); + + return $results; + } + + /** + * Get the list of competencies that were completed the least times in a course. + * + * @param int $courseid + * @param int $skip The number of competencies to skip + * @param int $limit The max number of competencies to return + * @return competency[] + */ + public static function get_least_proficient_competencies_for_course($courseid, $skip = 0, $limit = 0) { + global $DB; + + $fields = competency::get_sql_fields('c'); + $params = array('courseid' => $courseid); + $sql = 'SELECT ' . $fields . ', SUM(COALESCE(ucc.proficiency, 0)) AS timesproficient ' . + ' FROM {' . competency::TABLE . '} c + JOIN {' . course_competency::TABLE . '} cc + ON c.id = cc.competencyid + LEFT JOIN {' . user_competency_course::TABLE . '} ucc + ON ucc.competencyid = c.id AND ucc.courseid = cc.courseid + WHERE cc.courseid = :courseid + GROUP BY c.id + ORDER BY timesproficient ASC, c.id DESC'; + + $results = $DB->get_records_sql($sql, $params, $skip, $limit); + $a = $DB->get_records_sql('SELECT * from {' . user_competency_course::TABLE . '}'); + + $comps = array(); + foreach ($results as $r) { + $c = competency::extract_record($r); + $comps[] = new competency(0, $c); + } + return $comps; + } } diff --git a/admin/tool/lp/lang/en/tool_lp.php b/admin/tool/lp/lang/en/tool_lp.php index 54191153b15..b8377d8bab6 100644 --- a/admin/tool/lp/lang/en/tool_lp.php +++ b/admin/tool/lp/lang/en/tool_lp.php @@ -52,6 +52,7 @@ $string['competencies'] = 'Competencies'; $string['competenciesarenotenabled'] = 'The competencies module is not enabled.'; $string['competenciesforframework'] = 'Competencies for {$a}'; $string['competenciesmostoftennotproficient'] = 'Competencies most often not proficient in completed plans'; +$string['competenciesmostoftennotproficientincourse'] = 'Competencies most often not proficient in this course'; $string['competenciessettings'] = 'Competencies settings'; $string['competencycannotbedeleted'] = 'The competency \'{$a}\' can not be deleted'; $string['competencycreated'] = 'Competency created'; @@ -427,5 +428,6 @@ $string['viewdetails'] = 'View details'; $string['visible'] = 'Visible'; $string['visible_help'] = 'A competency framework can be hidden from teachers. This could be useful if a framework is still in the process of being developed.'; $string['when'] = 'When'; +$string['xcompetenciesproficientoutofyincourse'] = 'You are proficient in {$a->x} out of {$a->y} competencies in this course.'; $string['xcompetencieslinkedoutofy'] = '{$a->x} out of {$a->y} competencies linked to courses'; $string['xplanscompletedoutofy'] = '{$a->x} out of {$a->y} plans completed for this template'; diff --git a/admin/tool/lp/styles.css b/admin/tool/lp/styles.css index 6530232968b..745a8319d0e 100644 --- a/admin/tool/lp/styles.css +++ b/admin/tool/lp/styles.css @@ -4,11 +4,13 @@ .path-admin-tool-lp [data-region="competencylinktree"] ul li { list-style-type: none; } +.path-admin-tool-lp [data-region="coursecompetencystatistics"] .progresstext, .path-admin-tool-lp [data-region="templatestatistics"] .progresstext { display: inline-block; height: 40px; vertical-align: top; } +.path-admin-tool-lp [data-region="coursecompetencystatistics"] .progress, .path-admin-tool-lp [data-region="templatestatistics"] .progress { width: 10em; display: inline-block; diff --git a/admin/tool/lp/templates/course_competencies_page.mustache b/admin/tool/lp/templates/course_competencies_page.mustache index fb4a05576db..84944323cd6 100644 --- a/admin/tool/lp/templates/course_competencies_page.mustache +++ b/admin/tool/lp/templates/course_competencies_page.mustache @@ -43,6 +43,9 @@

{{/canconfigurecoursecompetencies}} +{{#statistics}} +{{> tool_lp/course_competency_statistics }} +{{/statistics}}
@@ -59,6 +62,7 @@ {{/canmanagecoursecompetencies}} {{#competency}}

{{competency.shortname}} {{competency.idnumber}}

diff --git a/admin/tool/lp/templates/course_competency_statistics.mustache b/admin/tool/lp/templates/course_competency_statistics.mustache new file mode 100644 index 00000000000..5915a63f205 --- /dev/null +++ b/admin/tool/lp/templates/course_competency_statistics.mustache @@ -0,0 +1,76 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_lp/course_competency_statistics + + IDs required for JS: + * none + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + + See admin/tool/lp/classes/external/course_competency_statistics_exporter + + Example context (json): + { + competencycount: 4, + proficientcompetencycount: 3, + proficientcompetencypercentage: 75, + proficientcompetencypercentageformatted: 75.0 + } + +}} +{{! + Template statistics template. +}} +{{#competencycount}} +
+ {{#canbegradedincourse}} +
+
+ {{#str}}xcompetenciesproficientoutofyincourse, tool_lp, { "x": "{{proficientcompetencycount}}", "y": "{{competencycount}}" } {{/str}} +
+
+
+ {{proficientcompetencypercentageformatted}} % +
+
+
+ {{/canbegradedincourse}} + {{#canmanagecoursecompetencies}} + {{#leastproficientcount}} +
+
+

{{#str}}competenciesmostoftennotproficientincourse, tool_lp{{/str}}

+
+
+ {{#leastproficient}} + +

{{shortname}} {{idnumber}}

+
+ {{/leastproficient}} +
+
+ {{/leastproficientcount}} + {{/canmanagecoursecompetencies}} +
+{{/competencycount}} diff --git a/admin/tool/lp/tests/api_test.php b/admin/tool/lp/tests/api_test.php index e8292299e6a..2971ea18067 100644 --- a/admin/tool/lp/tests/api_test.php +++ b/admin/tool/lp/tests/api_test.php @@ -4039,6 +4039,96 @@ class tool_lp_api_testcase extends advanced_testcase { $this->assertEquals(null, $uc2->get_proficiency()); } + /** + * Test course statistics api functions. + */ + public function test_course_statistics() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('tool_lp'); + $this->setAdminUser(); + + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $u3 = $dg->create_user(); + $u4 = $dg->create_user(); + $c1 = $dg->create_course(); + $framework = $lpg->create_framework(); + // Enrol students in the course. + $studentarch = get_archetype_roles('student'); + $studentrole = array_shift($studentarch); + $coursecontext = context_course::instance($c1->id); + $dg->role_assign($studentrole->id, $u1->id, $coursecontext->id); + $dg->enrol_user($u1->id, $c1->id, $studentrole->id); + $dg->role_assign($studentrole->id, $u2->id, $coursecontext->id); + $dg->enrol_user($u2->id, $c1->id, $studentrole->id); + $dg->role_assign($studentrole->id, $u3->id, $coursecontext->id); + $dg->enrol_user($u3->id, $c1->id, $studentrole->id); + $dg->role_assign($studentrole->id, $u4->id, $coursecontext->id); + $dg->enrol_user($u4->id, $c1->id, $studentrole->id); + + // Create 6 competencies. + $comp1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $comp2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $comp3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $comp4 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $comp5 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $comp6 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + + // Link 6 out of 6 to a course. + $lpg->create_course_competency(array('competencyid' => $comp1->get_id(), 'courseid' => $c1->id)); + $lpg->create_course_competency(array('competencyid' => $comp2->get_id(), 'courseid' => $c1->id)); + $lpg->create_course_competency(array('competencyid' => $comp3->get_id(), 'courseid' => $c1->id)); + $lpg->create_course_competency(array('competencyid' => $comp4->get_id(), 'courseid' => $c1->id)); + $lpg->create_course_competency(array('competencyid' => $comp5->get_id(), 'courseid' => $c1->id)); + $lpg->create_course_competency(array('competencyid' => $comp6->get_id(), 'courseid' => $c1->id)); + + // Rate some competencies. + // User 1. + api::grade_competency_in_course($c1, $u1->id, $comp1->get_id(), 4, 'Unit test'); + api::grade_competency_in_course($c1, $u1->id, $comp2->get_id(), 4, 'Unit test'); + api::grade_competency_in_course($c1, $u1->id, $comp3->get_id(), 4, 'Unit test'); + api::grade_competency_in_course($c1, $u1->id, $comp4->get_id(), 4, 'Unit test'); + // User 2. + api::grade_competency_in_course($c1, $u2->id, $comp1->get_id(), 1, 'Unit test'); + api::grade_competency_in_course($c1, $u2->id, $comp2->get_id(), 1, 'Unit test'); + api::grade_competency_in_course($c1, $u2->id, $comp3->get_id(), 1, 'Unit test'); + api::grade_competency_in_course($c1, $u2->id, $comp4->get_id(), 1, 'Unit test'); + // User 3. + api::grade_competency_in_course($c1, $u3->id, $comp1->get_id(), 3, 'Unit test'); + api::grade_competency_in_course($c1, $u3->id, $comp2->get_id(), 3, 'Unit test'); + // User 4. + api::grade_competency_in_course($c1, $u4->id, $comp1->get_id(), 2, 'Unit test'); + api::grade_competency_in_course($c1, $u4->id, $comp2->get_id(), 2, 'Unit test'); + + // OK we have enough data - lets call some API functions and check for expected results. + + $result = api::count_proficient_competencies_in_course_for_user($c1->id, $u1->id); + $this->assertEquals(4, $result); + $result = api::count_proficient_competencies_in_course_for_user($c1->id, $u2->id); + $this->assertEquals(0, $result); + $result = api::count_proficient_competencies_in_course_for_user($c1->id, $u3->id); + $this->assertEquals(2, $result); + $result = api::count_proficient_competencies_in_course_for_user($c1->id, $u4->id); + $this->assertEquals(0, $result); + + $result = api::get_least_proficient_competencies_for_course($c1->id, 0, 2); + // We should get 5 and 6 in repeatable order. + $valid = false; + if (($comp5->get_id() == $result[0]->get_id()) || ($comp6->get_id() == $result[0]->get_id())) { + $valid = true; + } + $this->assertTrue($valid); + $valid = false; + if (($comp5->get_id() == $result[1]->get_id()) || ($comp6->get_id() == $result[1]->get_id())) { + $valid = true; + } + $this->assertTrue($valid); + $expected = $result[1]->get_id(); + $result = api::get_least_proficient_competencies_for_course($c1->id, 1, 1); + $this->assertEquals($result[0]->get_id(), $expected); + } + /** * Test template statistics api functions. */