MDL-70817 core_course: Create an activity information output component

The activity information output component displays information about
an activity module that can contain:
1. Activity dates
2. Completion information
   a. A manual completion button; or
   b. A list of automatic completion conditions and their statuses.

This patch also includes a new JS module called
core_course/manual_completion_toggle for toggling the
completion state of activities that support manual completion.
This commit is contained in:
Jun Pataleta
2021-04-02 21:56:22 +08:00
parent a1b3596cb8
commit cafe2f95ee
15 changed files with 582 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
define ("core_course/manual_completion_toggle",["exports","core/templates","core/notification","core_course/repository"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=e(b);c=e(c);function e(a){return a&&a.__esModule?a:{default:a}}function f(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function g(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var i=a.apply(b,c);function g(a){f(i,d,e,g,h,"next",a)}function h(a){f(i,d,e,g,h,"throw",a)}g(void 0)})}}var h={MANUAL_TOGGLE:"button[data-action=toggle-manual-completion]"},i={TOGGLE_MARK_DONE:"manual:mark-done",TOGGLE_UNDO:"manual:undo"},j=!1,k=function(){if(j){return}document.addEventListener("click",function(a){var b=a.target.closest(h.MANUAL_TOGGLE);if(b){a.preventDefault();l(b).catch(c.default.exception)}});j=!0};a.init=k;var l=function(){var a=g(regeneratorRuntime.mark(function a(e){var f,g,h,j,k,l,m,n;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:f=e.innerHTML;e.setAttribute("disabled","disabled");g=e.getAttribute("data-toggletype");h=e.getAttribute("data-cmid");j=e.getAttribute("data-activityname");k=g===i.TOGGLE_MARK_DONE;a.next=8;return b.default.render("core/loading",{});case 8:l=a.sent;a.next=11;return b.default.replaceNodeContents(e,l,"");case 11:a.prev=11;a.next=14;return(0,d.toggleManualCompletion)(h,k);case 14:m={cmid:h,activityname:j,overallcomplete:k,overallincomplete:!k,istrackeduser:!0};a.next=17;return b.default.renderForPromise("core_course/completion_manual",m);case 17:n=a.sent;a.next=20;return b.default.replaceNode(e,n.html,n.js);case 20:a.next=27;break;case 22:a.prev=22;a.t0=a["catch"](11);e.removeAttribute("disabled");e.innerHTML=f;c.default.exception(a.t0);case 27:case"end":return a.stop();}}},a,null,[[11,22]])}));return function(){return a.apply(this,arguments)}}()});
//# sourceMappingURL=manual_completion_toggle.min.js.map
File diff suppressed because one or more lines are too long
+122
View File
@@ -0,0 +1,122 @@
// 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/>.
/**
* Provides the functionality for toggling the manual completion state of a course module through
* the manual completion button.
*
* @module core_course/manual_completion_toggle
* @package core_course
* @copyright 2021 Jun Pataleta <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Templates from 'core/templates';
import Notification from 'core/notification';
import {toggleManualCompletion} from 'core_course/repository';
/**
* Selectors in the manual completion template.
*
* @type {{MANUAL_TOGGLE: string}}
*/
const SELECTORS = {
MANUAL_TOGGLE: 'button[data-action=toggle-manual-completion]',
};
/**
* Toggle type values for the data-toggletype attribute in the core_course/completion_manual template.
*
* @type {{TOGGLE_UNDO: string, TOGGLE_MARK_DONE: string}}
*/
const TOGGLE_TYPES = {
TOGGLE_MARK_DONE: 'manual:mark-done',
TOGGLE_UNDO: 'manual:undo',
};
/**
* Whether the event listener has already been registered for this module.
*
* @type {boolean}
*/
let registered = false;
/**
* Registers the click event listener for the manual completion toggle button.
*/
export const init = () => {
if (registered) {
return;
}
document.addEventListener('click', (e) => {
const toggleButton = e.target.closest(SELECTORS.MANUAL_TOGGLE);
if (toggleButton) {
e.preventDefault();
toggleManualCompletionState(toggleButton).catch(Notification.exception);
}
});
registered = true;
};
/**
* Toggles the manual completion state of the module for the given user.
*
* @param {HTMLElement} toggleButton
* @returns {Promise<void>}
*/
const toggleManualCompletionState = async(toggleButton) => {
// Make a copy of the original content of the button.
const originalInnerHtml = toggleButton.innerHTML;
// Disable the button to prevent double clicks.
toggleButton.setAttribute('disabled', 'disabled');
// Get button data.
const toggleType = toggleButton.getAttribute('data-toggletype');
const cmid = toggleButton.getAttribute('data-cmid');
const activityname = toggleButton.getAttribute('data-activityname');
// Get the target completion state.
const completed = toggleType === TOGGLE_TYPES.TOGGLE_MARK_DONE;
// Replace the button contents with the loading icon.
const loadingHtml = await Templates.render('core/loading', {});
await Templates.replaceNodeContents(toggleButton, loadingHtml, '');
try {
// Call the webservice to update the manual completion status.
await toggleManualCompletion(cmid, completed);
// All good so far. Refresh the manual completion button to reflect its new state by re-rendering the template.
const templateContext = {
cmid: cmid,
activityname: activityname,
overallcomplete: completed,
overallincomplete: !completed,
istrackeduser: true, // We know that we're tracking completion for this user given the presence of this button.
};
const renderObject = await Templates.renderForPromise('core_course/completion_manual', templateContext);
// Replace the toggle button with the newly loaded template.
await Templates.replaceNode(toggleButton, renderObject.html, renderObject.js);
} catch (exception) {
// In case of an error, revert the original state and appearance of the button.
toggleButton.removeAttribute('disabled');
toggleButton.innerHTML = originalInnerHtml;
// Show the exception.
Notification.exception(exception);
}
};
@@ -0,0 +1,154 @@
<?php
// 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/>.
/**
* File containing the class activity information renderable.
*
* @package core_course
* @copyright 2021 Jun Pataleta <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\output;
defined('MOODLE_INTERNAL') || die();
use cm_info;
use completion_info;
use context;
use core\activity_dates;
use core_completion\cm_completion_details;
use core_user;
use core_user\fields;
use renderable;
use renderer_base;
use stdClass;
use templatable;
/**
* The activity information renderable class.
*
* @package core_course
* @copyright 2021 Jun Pataleta <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity_information implements renderable, templatable {
/** @var cm_info The course module information. */
protected $cminfo = null;
/** @var array The array of relevant dates for this activity. */
protected $activitydates = [];
/** @var cm_completion_details The user's completion details for this activity. */
protected $cmcompletion = null;
/**
* Constructor.
*
* @param cm_info $cminfo The course module information.
* @param cm_completion_details $cmcompletion The course module information.
* @param array $activitydates The activity dates.
*/
public function __construct(cm_info $cminfo, cm_completion_details $cmcompletion, array $activitydates) {
$this->cminfo = $cminfo;
$this->cmcompletion = $cmcompletion;
$this->activitydates = $activitydates;
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param renderer_base $output Renderer base.
* @return stdClass
*/
public function export_for_template(renderer_base $output): stdClass {
$data = $this->build_completion_data();
$data->cmid = $this->cminfo->id;
$data->activityname = $this->cminfo->name;
$data->activitydates = $this->activitydates;
return $data;
}
/**
* Builds the completion data for export.
*
* @return stdClass
*/
protected function build_completion_data(): stdClass {
$data = new stdClass();
$data->hascompletion = $this->cmcompletion->has_completion();
$data->isautomatic = $this->cmcompletion->is_automatic();
// Get the name of the user overriding the completion condition, if available.
$data->overrideby = null;
$overrideby = $this->cmcompletion->overridden_by();
$overridebyname = null;
if (!empty($overrideby)) {
$userfields = fields::for_name();
$overridebyrecord = core_user::get_user($overrideby, 'id ' . $userfields->get_sql()->selects, MUST_EXIST);
$data->overrideby = fullname($overridebyrecord);
}
// We'll show only the completion conditions and not the completion status if we're not tracking completion for this user
// (e.g. a teacher, admin).
$data->istrackeduser = $this->cmcompletion->is_tracked_user();
// Overall completion states.
$overallcompletion = $this->cmcompletion->get_overall_completion();
$data->overallcomplete = $overallcompletion == COMPLETION_COMPLETE;
$data->overallincomplete = $overallcompletion == COMPLETION_INCOMPLETE;
// Set an accessible description for manual completions with overridden completion state.
if (!$data->isautomatic && $data->overrideby) {
$setbydata = (object)[
'activityname' => $this->cminfo->name,
'setby' => $data->overrideby,
];
$setbylangkey = $data->overallcomplete ? 'completion_setby:manual:done' : 'completion_setby:manual:markdone';
$data->accessibledescription = get_string($setbylangkey, 'course', $setbydata);
}
// Build automatic completion details.
$details = [];
foreach ($this->cmcompletion->get_details() as $key => $detail) {
// Set additional attributes for the template.
$detail->key = $key;
$detail->statuscomplete = in_array($detail->status, [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS]);
$detail->statuscompletefail = $detail->status == COMPLETION_COMPLETE_FAIL;
$detail->statusincomplete = $detail->status == COMPLETION_INCOMPLETE;
// Add an accessible description to be used for title and aria-label attributes for overridden completion details.
if ($data->overrideby) {
$setbydata = (object)[
'condition' => $detail->description,
'setby' => $data->overrideby,
];
$detail->accessibledescription = get_string('completion_setby:auto', 'course', $setbydata);
}
// We don't need the status in the template.
unset($detail->status);
$details[] = $detail;
}
$data->completiondetails = $details;
return $data;
}
}
+13
View File
@@ -2188,6 +2188,19 @@ class core_course_renderer extends plugin_renderer_base {
return $this->coursecat_tree($chelper, $tree);
}
/**
* Renders the activity information.
*
* Defer to template.
*
* @param \core_course\output\activity_information $page
* @return string html for the page
*/
public function render_activity_information(\core_course\output\activity_information $page) {
$data = $page->export_for_template($this->output);
return $this->output->render_from_template('core_course/activity_info', $data);
}
/**
* Renders the activity navigation.
*
+30
View File
@@ -0,0 +1,30 @@
{{!
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_course/activity_date
Template for displaying the activity's dates.
Example context (json):
{
"label": "Opens:",
"timestamp": 1293876000
}
}}
<div>
<strong>{{label}}</strong> {{#userdate}} {{timestamp}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}}
</div>
+68
View File
@@ -0,0 +1,68 @@
{{!
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_course/activity_info
Container to display activity information such as:
- Activity dates
- Activity completion requirements (automatic completion)
- Manual completion button
Example context (json):
{
"hascompletion": true,
"isautomatic": true,
"istrackeduser": true,
"activitydates": [
{
"label": "Opens:",
"timestamp": 1293876000
}
],
"completiondetails": [
{
"statuscomplete": 1,
"description": "Viewed"
},
{
"statusincomplete": 1,
"description": "Receive a grade"
}
]
}
}}
<div data-region="activity-information" class="activity-information">
<div class="mb-1">
{{#activitydates}}
{{>core_course/activity_date}}
{{/activitydates}}
</div>
<div>
{{#hascompletion}}
{{#isautomatic}}
<div class="automatic-completion-conditions" data-region ="completionrequirements" role="list" aria-label="{{#str}}completionrequirements, core_course{{/str}}">
{{#completiondetails}}
{{> core_course/completion_automatic }}
{{/completiondetails}}
</div>
{{/isautomatic}}
{{^isautomatic}}
{{> core_course/completion_manual }}
{{/isautomatic}}
{{/hascompletion}}
</div>
</div>
@@ -0,0 +1,63 @@
{{!
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_course/completion_automatic
Template for displaying an automatic completion rule and its status.
Example context (json):
{
"statuscomplete": 1,
"description": "View",
"istrackeduser": true,
"accessibledescription": "Done: View (Set by Admin User)"
}
}}
{{#istrackeduser}}
{{#statuscomplete}}
<span class="badge badge-success rounded mb-1" role="listitem" {{!
}}{{#accessibledescription}}{{!
}}title="{{.}}" {{!
}}aria-label="{{.}}" {{!
}}{{/accessibledescription}}>
<strong>{{#str}}completion_automatic:done, core_course{{/str}}</strong> <span class="font-weight-normal">{{description}}</span>
</span>
{{/statuscomplete}}
{{#statuscompletefail}}
<span class="badge badge-danger rounded mb-1" role="listitem" {{!
}}{{#accessibledescription}}{{!
}}title="{{.}}" {{!
}}aria-label="{{.}}" {{!
}}{{/accessibledescription}}>
<strong>{{#str}}completion_automatic:failed, core_course{{/str}}</strong> <span class="font-weight-normal">{{description}}</span>
</span>
{{/statuscompletefail}}
{{#statusincomplete}}
<span class="badge badge-secondary rounded mb-1" role="listitem" {{!
}}{{#accessibledescription}}{{!
}}title="{{.}}" {{!
}}aria-label="{{.}}" {{!
}}{{/accessibledescription}}>
<strong>{{#str}}completion_automatic:todo, core_course{{/str}}</strong> <span class="font-weight-normal">{{description}}</span>
</span>
{{/statusincomplete}}
{{/istrackeduser}}
{{^istrackeduser}}
<span class="badge badge-secondary rounded mb-1" role="listitem">
<span class="font-weight-normal">{{description}}</span>
</span>
{{/istrackeduser}}
@@ -0,0 +1,67 @@
{{!
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_course/completion_manual
Template for displaying the manual completion button.
Example context (json):
{
"cmid": 0,
"overallcomplete": true,
"overallincomplete": false
}
}}
{{#istrackeduser}}
{{#overallcomplete}}
<button class="btn btn-outline-success" data-action="toggle-manual-completion" data-toggletype="manual:undo" data-cmid="{{cmid}}" data-activityname="{{activityname}}" {{!
}}{{#accessibledescription}}{{!
}}title="{{.}}" {{!
}}aria-label="{{.}}" {{!
}}{{/accessibledescription}}{{!
}}{{^accessibledescription}}{{!
}}title="{{#str}}completion_manual:aria:done, course, {{activityname}} {{/str}}" {{!
}}aria-label="{{#str}}completion_manual:aria:done, course, {{activityname}} {{/str}}" {{!
}}{{/accessibledescription}}>
<i class="fa fa-check" aria-hidden="true"></i> {{#str}} completion_manual:done, core_course {{/str}}
</button>
{{/overallcomplete}}
{{#overallincomplete}}
<button class="btn btn-outline-secondary" data-action="toggle-manual-completion" data-toggletype="manual:mark-done" data-cmid="{{cmid}}" data-activityname="{{activityname}}" {{!
}}{{#accessibledescription}}{{!
}}title="{{.}}" {{!
}}aria-label="{{.}}" {{!
}}{{/accessibledescription}}{{!
}}{{^accessibledescription}}{{!
}}title="{{#str}}completion_manual:aria:markdone, course, {{activityname}} {{/str}}" {{!
}}aria-label="{{#str}}completion_manual:aria:markdone, course, {{activityname}} {{/str}}" {{!
}}{{/accessibledescription}}>
{{#str}} completion_manual:markdone, core_course {{/str}}
</button>
{{/overallincomplete}}
{{/istrackeduser}}
{{^istrackeduser}}
<button class="btn btn-outline-secondary" disabled>
{{#str}} completion_manual:markdone, core_course {{/str}}
</button>
{{/istrackeduser}}
{{#js}}
require(['core_course/manual_completion_toggle'], toggle => {
toggle.init()
});
{{/js}}
+11
View File
@@ -46,6 +46,17 @@ $string['aria:favourite'] = 'Course is starred';
$string['aria:favouritestab'] = 'Starred activities';
$string['aria:recommendedtab'] = 'Recommended activities';
$string['aria:modulefavourite'] = 'Star {$a} activity';
$string['completion_automatic:done'] = 'Done:';
$string['completion_automatic:failed'] = 'Failed:';
$string['completion_automatic:todo'] = 'To do:';
$string['completion_manual:aria:done'] = '{$a} is marked as done. Press to undo.';
$string['completion_manual:aria:markdone'] = 'Mark {$a} as done';
$string['completion_manual:done'] = 'Done';
$string['completion_manual:markdone'] = 'Mark as done';
$string['completion_setby:auto'] = 'Done: {$a->condition} (set by {$a->setby})';
$string['completion_setby:manual:done'] = '{$a->activityname} is marked by {$a->setby} as done. Press to undo.';
$string['completion_setby:manual:markdone'] = '{$a->activityname} is marked by {$a->setby} as not done. Press to mark as done.';
$string['completionrequirements'] = 'Completion requirements for this activity';
$string['coursealreadyfinished'] = 'Course already finished';
$string['coursenotyetstarted'] = 'The course has not yet started';
$string['coursenotyetfinished'] = 'The course has not yet finished';
+18
View File
@@ -35,6 +35,9 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core_completion\cm_completion_details;
use core_course\output\activity_information;
defined('MOODLE_INTERNAL') || die();
/**
@@ -899,6 +902,21 @@ class core_renderer extends renderer_base {
return '<div role="main">'.$this->unique_main_content_token.'</div>';
}
/**
* Returns information about an activity.
*
* @param cm_info $cminfo The course module information.
* @param cm_completion_details $completiondetails The completion details for this activity module.
* @param array $activitydates The dates for this activity module.
* @return string the activity information HTML.
* @throws coding_exception
*/
public function activity_information(cm_info $cminfo, cm_completion_details $completiondetails, array $activitydates): string {
$activityinfo = new activity_information($cminfo, $completiondetails, $activitydates);
$renderer = $this->page->get_renderer('core', 'course');
return $renderer->render($activityinfo);
}
/**
* Returns standard navigation between activities in a course.
*
+6
View File
@@ -1191,3 +1191,9 @@ span.editinstructions {
white-space: pre-wrap;
}
}
.automatic-completion-conditions {
.badge {
font-size: 100%;
}
}
+9
View File
@@ -9,6 +9,15 @@ select {
width: auto;
}
.path-mod {
div.activity-information {
border-bottom: $border-width solid $table-border-color;
padding-top: map-get($spacers, 2);
padding-bottom: map-get($spacers, 3);
margin-bottom: map-get($spacers, 2);
}
}
// Choice module
.path-mod-choice {
+9
View File
@@ -14120,6 +14120,9 @@ span.editinstructions {
.activity-navigation #next-activity-link {
white-space: pre-wrap; }
.automatic-completion-conditions .badge {
font-size: 100%; }
/* Anchor link offset fix. This makes hash links scroll 60px down to account for the fixed header. */
:target::before {
content: " ";
@@ -16752,6 +16755,12 @@ textarea[data-auto-rows] {
select {
width: auto; }
.path-mod div.activity-information {
border-bottom: 1px solid #dee2e6;
padding-top: 0.5rem;
padding-bottom: 1rem;
margin-bottom: 0.5rem; }
.path-mod-choice .horizontal .choices .option {
display: inline-block; }
+9
View File
@@ -14335,6 +14335,9 @@ span.editinstructions {
.activity-navigation #next-activity-link {
white-space: pre-wrap; }
.automatic-completion-conditions .badge {
font-size: 100%; }
/* Anchor link offset fix. This makes hash links scroll 60px down to account for the fixed header. */
:target::before {
content: " ";
@@ -16979,6 +16982,12 @@ textarea[data-auto-rows] {
select {
width: auto; }
.path-mod div.activity-information {
border-bottom: 1px solid #dee2e6;
padding-top: 0.5rem;
padding-bottom: 1rem;
margin-bottom: 0.5rem; }
.path-mod-choice .horizontal .choices .option {
display: inline-block; }