Commit 41487c38 authored by Ferran Recio Calderó's avatar Ferran Recio Calderó
Browse files

MDL-72660 core_courseformat: course index completion icons

parent e5894c04
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
......@@ -30,6 +30,7 @@ import CmItem from 'core_courseformat/local/content/section/cmitem';
// Course actions is needed for actions that are not migrated to components.
import courseActions from 'core_course/actions';
import DispatchActions from 'core_courseformat/local/content/actions';
import * as CourseEvents from 'core_course/events';
export default class Component extends BaseComponent {
......@@ -98,6 +99,13 @@ export default class Component extends BaseComponent {
// Mark content as state ready.
this.element.classList.add(this.classes.STATEDREADY);
}
// Capture completion events.
this.addEventListener(
this.element,
CourseEvents.manualCompletionToggled,
this._completionHandler
);
}
/**
......@@ -211,6 +219,18 @@ export default class Component extends BaseComponent {
this.dettachedSections = {};
}
/**
* Activity manual completion listener.
*
* @param {Event} event the custom ecent
*/
_completionHandler({detail}) {
if (detail === undefined) {
return;
}
this.reactive.dispatch('cmCompletion', [detail.cmid], detail.completed);
}
/**
* Update a course section when the section number changes.
*
......
......@@ -31,6 +31,11 @@ export default class {
*/
constructor(reactive) {
this.reactive = reactive;
// Completions states are defined in lib/completionlib.php. There are 4 different completion
// state values, however, the course index uses the same state for complete and complete_pass.
// This is the reason why completed appears twice in the array.
this.COMPLETIONS = ['incomplete', 'complete', 'complete', 'fail'];
}
/**
......@@ -150,4 +155,25 @@ export default class {
number: sectioninfo.number,
};
}
/**
* Generate a compoetion export data from the cm element.
*
* @param {Object} state the current state.
* @param {Object} cminfo the course module state data.
* @returns {Object}
*/
cmCompletion(state, cminfo) {
const data = {
statename: '',
state: 'NaN',
};
if (cminfo.completionstate !== undefined) {
data.state = cminfo.completionstate;
data.hasstate = true;
const statename = this.COMPLETIONS[cminfo.completionstate] ?? 'NaN';
data[`is${statename}`] = true;
}
return data;
}
}
......@@ -153,6 +153,18 @@ export default class {
this._setElementsValue(stateManager, 'section', sectionIds, 'dragging', dragValue);
}
/**
* Mark or unmark course modules as complete.
*
* @param {StateManager} stateManager the current state manager
* @param {array} cmIds the list of course modules ids
* @param {bool} complete the new completion value
*/
cmCompletion(stateManager, cmIds, complete) {
const newValue = (complete) ? 1 : 0;
this._setElementsValue(stateManager, 'cm', cmIds, 'completionstate', newValue);
}
/**
* Lock or unlock course modules.
*
......
......@@ -25,6 +25,12 @@
*/
import DndCmItem from 'core_courseformat/local/courseeditor/dndcmitem';
import Templates from 'core/templates';
import Prefetch from 'core/prefetch';
// Prefetch the completion icons template.
const completionTemplate = 'core_courseformat/local/courseindex/cmcompletion';
Prefetch.prefetchTemplate(completionTemplate);
export default class Component extends DndCmItem {
......@@ -37,6 +43,7 @@ export default class Component extends DndCmItem {
// Default query selectors.
this.selectors = {
CM_NAME: `[data-for='cm_name']`,
CM_COMPLETION: `[data-for='cm_completion']`,
};
// Default classes to toggle on refresh.
this.classes = {
......@@ -63,9 +70,16 @@ export default class Component extends DndCmItem {
/**
* Initial state ready method.
*
* @param {Object} state the course state.
*/
stateReady() {
stateReady(state) {
this.configDragDrop(this.id);
// Refresh completion icon.
this._refreshCompletion({
state,
element: state.cm.get(this.id),
});
}
/**
......@@ -77,6 +91,7 @@ export default class Component extends DndCmItem {
return [
{watch: `cm[${this.id}]:deleted`, handler: this.remove},
{watch: `cm[${this.id}]:updated`, handler: this._refreshCm},
{watch: `cm[${this.id}].completionstate:updated`, handler: this._refreshCompletion},
];
}
......@@ -95,4 +110,34 @@ export default class Component extends DndCmItem {
this.locked = element.locked;
}
/**
* Update the activity completion icon.
*
* @param {Object} details the update details
* @param {Object} details.state the state data
* @param {Object} details.element the element data
*/
async _refreshCompletion({state, element}) {
// No completion icons are displayed in edit mode.
if (this.reactive.isEditing || !element.istrackeduser) {
return;
}
// Check if the completion value has changed.
const completionElement = this.getElement(this.selectors.CM_COMPLETION);
if (completionElement.dataset.value == element.completionstate) {
return;
}
// Collect section information from the state.
const exporter = this.reactive.getExporter();
const data = exporter.cmCompletion(state, element);
try {
const {html, js} = await Templates.renderForPromise(completionTemplate, data);
Templates.replaceNode(completionElement, html, js);
} catch (error) {
throw error;
}
}
}
......@@ -17,12 +17,17 @@
namespace core_courseformat\output\local\state;
use core_courseformat\base as course_format;
use completion_info;
use section_info;
use cm_info;
use renderable;
use stdClass;
use core_availability\info_module;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/completionlib.php');
/**
* Contains the ajax update course module structure.
*
......@@ -66,10 +71,12 @@ class cm implements renderable {
* @return stdClass data context for a mustache template
*/
public function export_for_template(\renderer_base $output): stdClass {
global $USER;
$format = $this->format;
$section = $this->section;
$cm = $this->cm;
$course = $format->get_course();
$data = (object)[
'id' => $cm->id,
......@@ -94,6 +101,14 @@ class cm implements renderable {
$data->content = $output->course_section_updated_cm_item($format, $section, $cm);
}
// Completion status.
$completioninfo = new completion_info($course);
$data->istrackeduser = $completioninfo->is_tracked_user($USER->id);
if ($data->istrackeduser && $completioninfo->is_enabled($cm)) {
$completiondata = $completioninfo->get_data($cm);
$data->completionstate = $completiondata->completionstate;
}
return $data;
}
}
......@@ -28,7 +28,7 @@
"isactive": 1,
"uniqid": "0",
"isactive": 1,
"accessvisible": 1,
"accessvisible": 1
}
}}
<li class="courseindex-item
......@@ -39,6 +39,7 @@
data-for="cm"
data-id="{{id}}"
>
<span class="completioninfo" data-for="cm_completion" data-value="NaN"></span>
{{#url}}
{{#uservisible}}
<a class="courseindex-link text-truncate" href="{{{url}}}" data-for="cm_name">
......
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_courseformat/local/courseindex/cmcompletion
Displays a course index course-module entry.
Example context (json):
{
"state": 1,
"iscomplete": true,
"isincomplete": true,
"isfail": true,
"hasstate": true
}
}}
{{#iscomplete}}
<span class="completioninfo completion_complete" data-for="cm_completion" data-value="{{state}}">
{{#pix}} t/completion_complete, moodle, {{#str}} done, completion {{/str}} {{/pix}}
</span>
{{/iscomplete}}
{{#isincomplete}}
<span class="completioninfo completion_incomplete" data-for="cm_completion" data-value="{{state}}">
{{#pix}} t/completion_incomplete, moodle, {{#str}} todo, completion {{/str}} {{/pix}}
</span>
{{/isincomplete}}
{{#isfail}}
<span class="completioninfo completion_fail" data-for="cm_completion" data-value="{{state}}">
{{#pix}} t/completion_fail, moodle, {{#str}} failed, completion {{/str}} {{/pix}}
</span>
{{/isfail}}
{{^hasstate}}
<span class="completioninfo completion_none" data-for="cm_completion" data-value="{{state}}"></span>
{{/hasstate}}
@core @core_course @core_courseformat
Feature: Course index completion icons
In order to quickly check my activities completions
As a student
I need to see the activity completion in the course index.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| enablecompletion | 1 |
| numsections | 4 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section | completion |
| assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 | 1 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| teacher1 | C1 | editingteacher |
@javascript
Scenario: Teacher does not see completion icons.
Given I am on the "C1" "Course" page logged in as "teacher1"
And I click on "Side panel" "button"
When I click on "Open course index drawer" "button"
Then I should see "Topic 1" in the "courseindex-content" "region"
And I should see "Activity sample 1" in the "courseindex-content" "region"
And "To do" "icon" should not exist in the "courseindex-content" "region"
@javascript
Scenario: User should see the completion icons
Given I am on the "C1" "Course" page logged in as "student1"
And I click on "Side panel" "button"
When I click on "Open course index drawer" "button"
Then I should see "Topic 1" in the "courseindex-content" "region"
And I should see "Activity sample 1" in the "courseindex-content" "region"
And "To do" "icon" should exist in the "courseindex-content" "region"
@javascript
Scenario: Manual completion shoudl update the course index completion
Given I am on the "C1" "Course" page logged in as "student1"
And I click on "Side panel" "button"
And I click on "Open course index drawer" "button"
And "To do" "icon" should exist in the "courseindex-content" "region"
When I press "Mark as done"
And I wait until "Done" "button" exists
Then "Done" "icon" should exist in the "courseindex-content" "region"
And I press "Done"
And I wait until "Mark as done" "button" exists
And "To do" "icon" should exist in the "courseindex-content" "region"
@javascript
Scenario: Refresh the page should keep the completion consistent
Given I am on the "C1" "Course" page logged in as "student1"
And I click on "Side panel" "button"
And I click on "Open course index drawer" "button"
And "To do" "icon" should exist in the "courseindex-content" "region"
When I press "Mark as done"
And I wait until "Done" "button" exists
And I reload the page
Then "Done" "icon" should exist in the "courseindex-content" "region"
@javascript
Scenario: Auto completion should appear in the course index
Given the following "activities" exist:
| activity | name | intro | course | idnumber | section | completion | completionview |
| assign | Activity sample 2 | Test assignment description | C1 | sample2 | 1 | 1 | 1 |
When I am on the "sample2" "Activity" page logged in as "student1"
And I am on the "C1" "Course" page
And I click on "Side panel" "button"
And I click on "Open course index drawer" "button"
Then "Done" "icon" should exist in the "courseindex-content" "region"
@javascript
Scenario: Completion failed should appear in the course index
Given the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | First question | Answer the first question |
And the following "activities" exist:
| activity | name | course | idnumber | attempts | gradepass | completion | completionusegrade | completionpass | completionattemptsexhausted |
| quiz | Test quiz name | C1 | quiz1 | 1 | 5.00 | 2 | 1 | 1 | 1 |
And quiz "Test quiz name" contains the following questions:
| question | page |
| First question | 1 |
And user "student1" has attempted "Test quiz name" with responses:
| slot | response |
| 1 | False |
When I am on the "C1" "Course" page logged in as "student1"
And I click on "Side panel" "button"
And I click on "Open course index drawer" "button"
And "Failed" "icon" should exist in the "courseindex-content" "region"
@javascript
Scenario: Completion passed should appear in the course index
Given the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | First question | Answer the first question |
And the following "activities" exist:
| activity | name | course | idnumber | attempts | gradepass | completion | completionusegrade | completionpass | completionattemptsexhausted |
| quiz | Test quiz name | C1 | quiz1 | 1 | 5.00 | 2 | 1 | 1 | 1 |
And quiz "Test quiz name" contains the following questions:
| question | page |
| First question | 1 |
And user "student1" has attempted "Test quiz name" with responses:
| slot | response |
| 1 | True |
When I am on the "C1" "Course" page logged in as "student1"
And I click on "Side panel" "button"
And I click on "Open course index drawer" "button"
And "Done" "icon" should exist in the "courseindex-content" "region"
......@@ -148,6 +148,7 @@ $string['dependenciescompleted'] = 'Completion of other courses';
$string['detail_desc:receivegrade'] = 'Receive a grade';
$string['detail_desc:receivepassgrade'] = 'Receive a passing grade';
$string['detail_desc:view'] = 'View';
$string['done'] = 'Done';
$string['hiddenrules'] = 'Some settings specific to <b>{$a}</b> have been hidden. To view unselect other activities';
$string['editcoursecompletionsettings'] = 'Edit course completion settings';
$string['enablecompletion'] = 'Enable completion tracking';
......@@ -166,6 +167,7 @@ $string['eventcoursecompletionupdated'] = 'Course completion updated';
$string['eventcoursemodulecompletionupdated'] = 'Course activity completion updated';
$string['eventdefaultcompletionupdated'] = 'Default for course activity completion updated';
$string['excelcsvdownload'] = 'Download in Excel-compatible format (.csv)';
$string['failed'] = 'Failed';
$string['fraction'] = 'Fraction';
$string['graderequired'] = 'Required course grade';
$string['gradexrequired'] = '{$a} required';
......@@ -229,6 +231,7 @@ $string['selfcompletion'] = 'Self completion';
$string['showcompletionconditions'] = 'Show activity completion conditions';
$string['showcompletionconditions_help'] = 'Activity completion conditions are always shown on the activity page. This setting determines whether activity completion conditions are also shown below each activity on the course page.';
$string['showinguser'] = 'Showing user';
$string['todo'] = 'To do';
$string['unenrolingfromcourse'] = 'Unenrolling from course';
$string['unenrolment'] = 'Unenrolment';
$string['unit'] = 'Unit';
......
......@@ -368,6 +368,9 @@ class icon_system_fontawesome extends icon_system_font {
'core:t/collapsed' => 'fa-caret-right',
'core:t/collapsedcaret' => 'fa-caret-right',
'core:t/collapsedchevron' => 'fa-chevron-right',
'core:t/completion_complete' => 'fa-circle',
'core:t/completion_fail' => 'fa-times',
'core:t/completion_incomplete' => 'fa-circle-thin',
'core:t/contextmenu' => 'fa-cog',
'core:t/copy' => 'fa-copy',
'core:t/delete' => 'fa-trash',
......
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg4"
version="1.1"
overflow="visible"
preserveAspectRatio="xMinYMid meet"
viewBox="0 0 12 12"
height="12"
width="12">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<path
id="path817"
d="M 6.0000002,2.4999999 C 4.0728255,2.4999999 2.5,4.0728255 2.5,6.0000001 c 0,1.9271748 1.5728255,3.5 3.5000002,3.5 C 7.927175,9.5000001 9.5,7.9271749 9.5,6.0000001 9.5,4.0728255 7.927175,2.4999999 6.0000002,2.4999999 Z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#387936;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.80178976;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
</svg>
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment