grunt ;
# Add all files to the git index and then run diff --cached to see all changes.
# This ensures that we get the status of all files, including new files.
+ # We ignore npm-shrinkwrap.json to make the tasks immune to npm changes.
git add . ;
+ git reset -- npm-shrinkwrap.json ;
git diff --cached --exit-code ;
fi
if (optional_param('submit', false, PARAM_BOOL) && data_submitted() && confirm_sesskey()) {
$controller->process_submission();
- $syscontext->mark_dirty();
$event = null;
// Create event depending on mode.
switch ($mode) {
assign_capability($changedcap, $this->permissions[$changedcap],
$this->roleid, $this->context->id, true);
}
-
- // Force accessinfo refresh for users visiting this context.
- $this->context->mark_dirty();
}
public function display() {
die;
}
if (!delete_role($roleid)) {
- // The delete failed, but mark the context dirty in case.
- $systemcontext->mark_dirty();
+ // The delete failed.
print_error('cannotdeleterolewithid', 'error', $baseurl, $roleid);
}
// Deleted a role sitewide...
- $systemcontext->mark_dirty();
redirect($baseurl);
break;
'customusermenuitems',
new lang_string('customusermenuitems', 'admin'),
new lang_string('configcustomusermenuitems', 'admin'),
- 'grades,grades|/grade/report/mygrades.php|grades
-messages,message|/message/index.php|message
-preferences,moodle|/user/preferences.php|preferences',
+ 'grades,grades|/grade/report/mygrades.php|t/grades
+messages,message|/message/index.php|t/message
+preferences,moodle|/user/preferences.php|t/preferences',
PARAM_RAW,
'50',
'10'
new lang_string('configcourseoverviewfileslimit', 'admin'), 1, PARAM_INT));
$temp->add(new admin_setting_configtext('courseoverviewfilesext', new lang_string('courseoverviewfilesext'),
new lang_string('configcourseoverviewfilesext', 'admin'), '.jpg,.gif,.png'));
+ $temp->add(new admin_setting_configtext('coursegraceperiodbefore', new lang_string('coursegraceperiodbefore', 'admin'),
+ new lang_string('configcoursegraceperiodbefore', 'admin'), 0, PARAM_INT));
+ $temp->add(new admin_setting_configtext('coursegraceperiodafter', new lang_string('coursegraceperiodafter', 'admin'),
+ new lang_string('configcoursegraceperiodafter', 'admin'), 0, PARAM_INT));
$ADMIN->add('appearance', $temp);
$temp = new admin_settingpage('ajax', new lang_string('ajaxuse'));
$ADMIN->add('appearance', $temp);
} // end of speedup
-
array('moodle/category:manage')
)
);
+ $ADMIN->add('courses',
+ new admin_externalpage('addnewcourse', new lang_string('addnewcourse'),
+ new moodle_url('/course/edit.php', array('category' => 0)),
+ array('moodle/category:manage')
+ )
+ );
$ADMIN->add('courses',
new admin_externalpage('restorecourse', new lang_string('restorecourse', 'admin'),
new moodle_url('/backup/restorefile.php', array('contextid' => context_system::instance()->id)),
Context variables required for this template:
* actionurl - Url to post to
* hasresults - True if there are results
- * results - List of results containing url, title, settings (array of raw html)
+ * results - List of results containing url, title, path (array of strings), settings (array of raw html)
* showsave - Show save buttons
Example context (json):
"actionurl": "/",
"hasresults": true,
"results": [
- { "url": "/", "title": "Match!", "settings": [ "blah blah blah" ] }
+ { "url": "/", "title": "Match!", "path": ["Administration", "Match!"], "settings": [ "blah blah blah" ] }
]
}
}}
</div>
<fieldset>
<div class="clearer"></div>
+ <h2 class="main">{{#str}}searchresults, admin{{/str}}</h2>
{{#hasresults}}
{{#results}}
- <h2 class="main">{{#str}}searchresults, admin{{/str}} - <a href="{{url}}">{{{title}}}</a></h2>
+ <h3 class="adminpagetitle"><a href="{{url}}">{{{title}}}</a></h3>
+ <ul class="adminpagepath" aria-label="{{#str}} pagepath, core {{/str}}">
+ {{#path}}
+ <li>{{.}}</li>
+ {{/path}}
+ </ul>
<fieldset class="adminsettings">
{{#settings}}
<div class="clearer"></div>
api::policy_content_field_options());
$mform->addRule('content_editor', null, 'required', null, 'client');
+ $mform->addElement('selectyesno', 'agreementstyle', get_string('policypriorityagreement', 'tool_policy'));
+
if (!$formdata->id || $formdata->status == policy_version::STATUS_DRAFT) {
// Creating a new version or editing a draft/archived version.
$mform->addElement('hidden', 'minorchange');
/** @var array $policies List of public policies objects with information about the user acceptance. */
protected $policies = null;
+ /** @var array List of policy version ids that were displayed to the user to agree with. */
+ protected $listdocs = null;
+
/** @var array $agreedocs List of policy identifiers which the user has agreed using the form. */
protected $agreedocs = null;
/**
* Prepare the page for rendering.
*
- * @param array $agreedocs Array with the policy identifiers which the user has agreed using the form.
+ * @param array $listdocs List of policy version ids that were displayed to the user to agree with.
+ * @param array $agreedocs List of policy version ids that the user actually agreed with.
* @param int $behalfid The userid to accept the policy versions as (such as child's id).
* @param string $action Form action to identify when user agreeds policies.
*/
- public function __construct($agreedocs = null, $behalfid = 0, $action = null) {
+ public function __construct(array $listdocs, array $agreedocs = [], $behalfid = 0, $action = null) {
global $USER;
$realuser = manager::get_realuser();
+ $this->listdocs = $listdocs;
$this->agreedocs = $agreedocs;
- if (empty($this->agreedocs)) {
- $this->agreedocs = [];
- }
-
$this->action = $action;
-
$this->isexistinguser = isloggedin() && !isguestuser();
+
$behalfid = $behalfid ?: $USER->id;
if ($realuser->id != $behalfid) {
$this->behalfuser = core_user::get_user($behalfid, '*', MUST_EXIST);
// Accept / revoke policies.
$acceptversionids = array();
foreach ($this->policies as $policy) {
- if (in_array($policy->id, $this->agreedocs)) {
- // Save policy version doc to accept it.
- $acceptversionids[] = $policy->id;
- } else {
- // Revoke policy doc.
- api::revoke_acceptance($policy->id, $this->behalfid);
+ if (in_array($policy->id, $this->listdocs)) {
+ if (in_array($policy->id, $this->agreedocs)) {
+ // Save policy version doc to accept it.
+ $acceptversionids[] = $policy->id;
+ } else {
+ // If the policy was displayed but not agreed, revoke the eventually given acceptance.
+ api::revoke_acceptance($policy->id, $this->behalfid);
+ }
}
}
// Accept all policy docs saved in $acceptversionids.
} else {
// New user.
if (!empty($this->action) && confirm_sesskey()) {
- // The form has been sent.
$currentpolicyversionids = [];
+ $presignupcache = \cache::make('core', 'presignup');
+ $acceptances = $presignupcache->get('tool_policy_policyversionidsagreed');
+ if (!$acceptances) {
+ $acceptances = [];
+ }
foreach ($this->policies as $policy) {
$currentpolicyversionids[] = $policy->id;
+ if (in_array($policy->id, $this->listdocs)) {
+ if (in_array($policy->id, $this->agreedocs)) {
+ $acceptances[] = $policy->id;
+ } else {
+ $acceptances = array_values(array_diff($acceptances, [$policy->id]));
+ }
+ }
}
// If the user has accepted all the policies, add it to the session to let continue with the signup process.
- $this->signupuserpolicyagreed = empty(array_diff($currentpolicyversionids, $this->agreedocs));
- \cache::make('core', 'presignup')->set('tool_policy_userpolicyagreed',
- $this->signupuserpolicyagreed);
+ $this->signupuserpolicyagreed = empty(array_diff($currentpolicyversionids, $acceptances));
+ $presignupcache->set('tool_policy_userpolicyagreed', $this->signupuserpolicyagreed);
+ $presignupcache->set('tool_policy_policyversionidsagreed', $acceptances);
} else if (empty($this->policies)) {
// There are no policies to agree to. Update the policyagreed value to avoid show empty consent page.
\cache::make('core', 'presignup')->set('tool_policy_userpolicyagreed', 1);
* @param moodle_url $returnurl URL to return after shown the policy docs.
*/
protected function redirect_to_policies($userid, $returnurl = null) {
+
+ // Make a list of all policies that the user has not accepted yet.
$allpolicies = $this->policies;
+
if ($this->isexistinguser) {
$acceptances = api::get_user_acceptances($userid);
- foreach ($allpolicies as $policy) {
+ foreach ($allpolicies as $ix => $policy) {
if (api::is_user_version_accepted($userid, $policy->id, $acceptances)) {
- // If this version is accepted by the user, remove from the pending policies list.
- unset($allpolicies[array_search($policy, $allpolicies)]);
+ unset($allpolicies[$ix]);
+ }
+ }
+ } else {
+ $presignupcache = \cache::make('core', 'presignup');
+ $acceptances = $presignupcache->get('tool_policy_policyversionidsagreed');
+ if ($acceptances) {
+ foreach ($allpolicies as $ix => $policy) {
+ if (in_array($policy->id, $acceptances)) {
+ unset($allpolicies[$ix]);
+ }
}
}
}
if (!empty($allpolicies)) {
+ // Check if some of the to-be-accepted policies should be agreed on their own page.
+ foreach ($allpolicies as $policy) {
+ if ($policy->agreementstyle == policy_version::AGREEMENTSTYLE_OWNPAGE) {
+ if (empty($returnurl)) {
+ $returnurl = (new moodle_url('/admin/tool/policy/index.php'))->out_as_local_url(false);
+ }
+ $urlparams = ['versionid' => $policy->id, 'returnurl' => $returnurl];
+ redirect(new moodle_url('/admin/tool/policy/view.php', $urlparams));
+ }
+ }
+
$currentpolicyversionids = [];
foreach ($allpolicies as $policy) {
$currentpolicyversionids[] = $policy->id;
];
redirect(new moodle_url('/admin/tool/policy/view.php', $urlparams));
}
+ } else {
+ $this->redirect_to_previous_url();
}
}
}
}
- $data->policies = array_values($this->policies);
+ // Filter out policies already shown on their own page, keep just policies to be shown here on the consent page.
+ $data->policies = array_values(array_filter($this->policies, function ($policy) {
+ return $policy->agreementstyle == policy_version::AGREEMENTSTYLE_CONSENTPAGE;
+ }));
// If viewing docs in behalf of other user, get his/her full name and profile link.
if (!empty($this->behalfuser)) {
* @return stdClass
*/
public function export_for_template(renderer_base $output) {
+ global $USER;
$data = (object) [
'pluginbaseurl' => (new moodle_url('/admin/tool/policy'))->out(false),
$data->editurl = (new moodle_url('/admin/tool/policy/editpolicydoc.php', $paramsurl))->out(false);
}
+ if ($this->policy->agreementstyle == policy_version::AGREEMENTSTYLE_OWNPAGE) {
+ if (!api::is_user_version_accepted($USER->id, $this->policy->id)) {
+ unset($data->returnurl);
+ $data->accepturl = (new moodle_url('/admin/tool/policy/index.php', [
+ 'listdoc[]' => $this->policy->id,
+ 'agreedoc[]' => $this->policy->id,
+ 'submit' => 'accept',
+ 'sesskey' => sesskey(),
+ ]))->out(false);
+ }
+ }
+
$data->policy = clone($this->policy);
return $data;
/** @var int Policy version has been archived. */
const STATUS_ARCHIVED = 2;
+ /** @var int Policy to be accepted together with others on the consent page. */
+ const AGREEMENTSTYLE_CONSENTPAGE = 0;
+
+ /** @var int Policy to be accepted on its own page before reaching the consent page. */
+ const AGREEMENTSTYLE_OWNPAGE = 1;
+
/**
* Return the definition of the properties of this model.
*
'policyid' => [
'type' => PARAM_INT,
],
+ 'agreementstyle' => [
+ 'type' => PARAM_INT,
+ 'choices' => [
+ self::AGREEMENTSTYLE_CONSENTPAGE,
+ self::AGREEMENTSTYLE_OWNPAGE,
+ ],
+ 'default' => self::AGREEMENTSTYLE_CONSENTPAGE,
+ ],
'revision' => [
'type' => PARAM_TEXT,
'default' => '',
<?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="admin/tool/policy/db" VERSION="20180307" COMMENT="The plugin allows to manage various policy documents that users have to accept to use the site."
+<XMLDB PATH="admin/tool/policy/db" VERSION="20180829" COMMENT="The plugin allows to manage various policy documents that users have to accept to use the site."
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Timestamp of when the policy version was created."/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Timestamp of when the policy version was last modified."/>
<FIELD NAME="policyid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="ID of the policy document we are version of."/>
+ <FIELD NAME="agreementstyle" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="How this agreement should flow: 0 - on the consent page, 1 - on a separate page before reaching the consent page."/>
<FIELD NAME="revision" TYPE="char" LENGTH="1333" NOTNULL="true" SEQUENCE="false" COMMENT="Human readable version of the policy document"/>
<FIELD NAME="summary" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Policy text summary"/>
<FIELD NAME="summaryformat" TYPE="int" LENGTH="3" NOTNULL="true" SEQUENCE="false" COMMENT="Format of the summary field"/>
--- /dev/null
+<?php
+// This file is part of Moodle - https://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/>.
+
+/**
+ * Plugin upgrade steps are defined here.
+ *
+ * @package tool_policy
+ * @category upgrade
+ * @copyright 2018 David Mudrák <david@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Execute the plugin upgrade steps from the given old version.
+ *
+ * @param int $oldversion
+ * @return bool
+ */
+function xmldb_tool_policy_upgrade($oldversion) {
+ global $DB;
+
+ $dbman = $DB->get_manager();
+
+ if ($oldversion < 2018082900) {
+ // Add field agreementstyle to the table tool_policy_versions.
+ $table = new xmldb_table('tool_policy_versions');
+ $field = new xmldb_field('agreementstyle', XMLDB_TYPE_INTEGER, '3', null, XMLDB_NOTNULL, null, '0', 'policyid');
+
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ upgrade_plugin_savepoint(true, 2018082900, 'tool', 'policy');
+ }
+
+ return true;
+}
+
* Show a user the policy documents to be agreed to.
*
* Script parameters:
- * agreedoc=<array> Policy version id which have been accepted by the user.
+ * listdoc=<array> List of policy version ids that were displayed to the user to accept.
+ * agreedoc=<array> List of policy version ids that were accepted by the user.
* behalfid=<id> The user id to view the policy version as (such as child's id).
*
* @package tool_policy
$submit = optional_param('submit', null, PARAM_NOTAGS);
$cancel = optional_param('cancel', null, PARAM_NOTAGS);
-$agreedocs = optional_param_array('agreedoc', null, PARAM_INT);
+$listdocs = optional_param_array('listdoc', [], PARAM_INT);
+$agreedocs = optional_param_array('agreedoc', [], PARAM_INT);
$behalfid = optional_param('userid', null, PARAM_INT);
$PAGE->set_context(context_system::instance());
$PAGE->set_url('/admin/tool/policy/index.php');
$PAGE->set_popup_notification_allowed(false);
+if (array_diff($agreedocs, $listdocs)) {
+ throw new moodle_exception('invalidaccessparameter');
+}
+
if (isloggedin() && !isguestuser()) {
// Existing user.
$haspermissionagreedocs = api::can_accept_policies($behalfid);
if (!$behalfid && \core\session\manager::is_loggedinas()) {
$behalfid = $USER->id;
}
- $outputpage = new \tool_policy\output\page_agreedocs($agreedocs, $behalfid, $submit);
+ $outputpage = new \tool_policy\output\page_agreedocs($listdocs, $agreedocs, $behalfid, $submit);
}
$output = $PAGE->get_renderer('tool_policy');
$string['policydoctype99'] = 'Other policy';
$string['policydocuments'] = 'Policy documents';
$string['policynamedversion'] = 'Policy {$a->name} (version {$a->revision} - {$a->id})';
+$string['policypriorityagreement'] = 'Show policy before showing other policies';
$string['policyversionacceptedinbehalf'] = 'Consent for this policy has been given on your behalf.';
$string['policyversionacceptedinotherlang'] = 'Consent for this policy version has been given in a different language.';
$string['previousversions'] = '{$a} previous versions';
&& empty($USER->policyagreed)
&& (isguestuser() || !isloggedin())) {
$output = $PAGE->get_renderer('tool_policy');
- $page = new \tool_policy\output\guestconsent();
-
- $message = $output->render($page);
+ try {
+ $page = new \tool_policy\output\guestconsent();
+ $message = $output->render($page);
+ } catch (dml_read_exception $e) {
+ // During upgrades, the new plugin code with new SQL could be in place but the DB not upgraded yet.
+ $message = null;
+ }
}
return $message;
<div class="agreedoc-form m-t-1">
<div class="agreedoc-checkbox">
<label>
+ <input value="{{id}}" name="listdoc[]" type="hidden" >
<input value="{{id}}" name="agreedoc[]" {{#versionagreed}}checked="{{.}}"{{/versionagreed}} type="checkbox">
<strong>{{# str }}iagree, tool_policy, {{{name}}} {{/ str }}</strong>
<i class="icon fa fa-exclamation-circle text-danger fa-fw" title="{{# str }} required {{/ str }}" ></i>
"content": "Policy <em>content</em>"
},
"returnurl": "#",
- "editurl": "#"
+ "editurl": "#",
+ "accepturl": "#"
}
}}
{{#editurl}}
<a role="button" href="{{editurl}}" class="btn">{{#str}} edit {{/str}}</a>
{{/editurl}}
+ {{#accepturl}}
+ <a role="button" href="{{accepturl}}" class="btn btn-primary">{{#str}} iagree, tool_policy, {{{policy.name}}} {{/str}}</a>
+ {{/accepturl}}
<div class="pull-right">
<a href="#top">
* - Type: 0 - site policy, 1 - privacy policy, 2 - third party policy, 99 - other.
* - Summary: Policy summary text.
* - Content: Policy full text.
+ * - Agreement style (agreementstyle): 0 - On the consent page, 1 - On its own page
*
* @param TableNode $data
*/
'type',
'content',
'summary',
+ 'agreementstyle',
];
// Associative array "policy identifier" => id in the database .
And I follow "Policies and agreements"
And "Agreed" "icon" should exist in the "This site policy" "table_row"
And I log out
+
+ Scenario: Accepting policies on sign up, multiple policies with different style of giving ageement.
+ Given the following config values are set as admin:
+ | registerauth | email |
+ | passwordpolicy | 0 |
+ | sitepolicyhandler | tool_policy |
+ And the following policies exist:
+ | name | summary | content | agreementstyle |
+ | Privacy policy | We scan your thoughts | Here goes content. | 0 |
+ | Digital maturity declaration | You declare be old enough | Here goes content. | 1 |
+ | Cookies policy | We eat cookies, srsly | Here goes content. | 0 |
+ | Terms of Service | We teach, you learn | Here goes content. | 1 |
+ And I am on site homepage
+ And I follow "Log in"
+ When I press "Create new account"
+ # The first policy with the agreement style "on its own page" must be accepted first.
+ Then I should see "Digital maturity declaration" in the "region-main" "region"
+ And I should see "You declare be old enough"
+ And I should see "Here goes content."
+ And I press "I agree to the Digital maturity declaration"
+ # The second policy with the agreement style "on its own page" must be accepted now.
+ And I should see "Terms of Service" in the "region-main" "region"
+ And I should see "We teach, you learn"
+ And I should see "Here goes content."
+ And I press "I agree to the Terms of Service"
+ # Only now we see the remaining consent page policies.
+ And I should see "Policy 1 out of 2"
+ And I should see "Privacy policy" in the "region-main" "region"
+ And I should see "We scan your thoughts"
+ And I should see "Here goes content."
+ And I press "Next"
+ And I should see "Policy 2 out of 2"
+ And I should see "Cookies policy" in the "region-main" "region"
+ And I should see "We eat cookies, srsly"
+ And I should see "Here goes content."
+ And I press "Next"
+ And I should see "Please agree to the following policies"
+ And I should see "Privacy policy"
+ And I should see "Cookies policy"
+ And I should not see "Digital maturity declaration" in the "region-main" "region"
+ And I should not see "Terms of Service" in the "region-main" "region"
+ And I should not see "Here goes content."
+ And I set the field "I agree to the Privacy policy" to "1"
+ And I set the field "I agree to the Cookies policy" to "1"
+ And I press "Next"
+ And I should see "New account"
+ And I set the following fields to these values:
+ | Username | user1 |
+ | Password | user1 |
+ | Email address | user1@address.invalid |
+ | Email (again) | user1@address.invalid |
+ | First name | User1 |
+ | Surname | L1 |
+ And I press "Create my new account"
+ And I should see "Confirm your account"
+ And I should see "An email should have been sent to your address at user1@address.invalid"
+ And I confirm email for "user1"
+ And I should see "Thanks, User1 L1"
+ And I should see "Your registration has been confirmed"
+ And I open my profile in edit mode
+ And the field "First name" matches value "User1"
+ And I log out
+ # Confirm that user can login and browse the site.
+ And I log in as "user1"
+ And I follow "Profile" in the user menu
+ # User can see his own agreements in the profile.
+ And I follow "Policies and agreements"
+ And "Agreed" "icon" should exist in the "Privacy policy" "table_row"
+ And "Agreed" "icon" should exist in the "Cookies policy" "table_row"
+ And "Agreed" "icon" should exist in the "Terms of Service" "table_row"
+ And "Agreed" "icon" should exist in the "Digital maturity declaration" "table_row"
+ And I log out
+
+ Scenario: Accepting policies on login, multiple policies with different style of giving ageement.
+ Given the following config values are set as admin:
+ | sitepolicyhandler | tool_policy |
+ And the following policies exist:
+ | name | summary | content | agreementstyle |
+ | Digital maturity declaration | You declare be old enough | Here goes content. | 1 |
+ | Privacy policy | We scan your thoughts | Here goes content. | 0 |
+ | Terms of Service | We teach, you learn | Here goes content. | 1 |
+ | Cookies policy | We eat cookies, srsly | Here goes content. | 0 |
+ And the following "users" exist:
+ | username | firstname | lastname | email |
+ | user1 | User | One | user1@example.com |
+ And I log in as "user1"
+ # The first policy with the agreement style "on its own page" must be accepted first.
+ Then I should see "Digital maturity declaration" in the "region-main" "region"
+ And I should see "You declare be old enough"
+ And I should see "Here goes content."
+ And I press "I agree to the Digital maturity declaration"
+ # The second policy with the agreement style "on its own page" must be accepted now.
+ And I should see "Terms of Service" in the "region-main" "region"
+ And I should see "We teach, you learn"
+ And I should see "Here goes content."
+ # If the user logs out now, only the first policy is accepted and we return to the same page.
+ And I log out
+ And I log in as "user1"
+ And I should see "Terms of Service" in the "region-main" "region"
+ And I should see "We teach, you learn"
+ And I should see "Here goes content."
+ And I press "I agree to the Terms of Service"
+ # Only now we see the remaining consent page policies.
+ And I should see "Policy 1 out of 2"
+ And I should see "Privacy policy" in the "region-main" "region"
+ And I should see "We scan your thoughts"
+ And I should see "Here goes content."
+ And I press "Next"
+ And I should see "Policy 2 out of 2"
+ And I should see "Cookies policy" in the "region-main" "region"
+ And I should see "We eat cookies, srsly"
+ And I should see "Here goes content."
+ And I press "Next"
+ And I should see "Please agree to the following policies"
+ And I should see "Privacy policy"
+ And I should see "Cookies policy"
+ And I should not see "Digital maturity declaration" in the "region-main" "region"
+ And I should not see "Terms of Service" in the "region-main" "region"
+ And I should not see "Here goes content."
+ And I set the field "I agree to the Privacy policy" to "1"
+ And I set the field "I agree to the Cookies policy" to "1"
+ And I press "Next"
+ And I follow "Profile" in the user menu
+ # User can see his own agreements in the profile.
+ And I follow "Policies and agreements"
+ And "Agreed" "icon" should exist in the "Privacy policy" "table_row"
+ And "Agreed" "icon" should exist in the "Cookies policy" "table_row"
+ And "Agreed" "icon" should exist in the "Terms of Service" "table_row"
+ And "Agreed" "icon" should exist in the "Digital maturity declaration" "table_row"
+ And I log out
+
+ Scenario: Accepting policies on login, all and loggedin policies to be accepted on their own page.
+ Given the following config values are set as admin:
+ | sitepolicyhandler | tool_policy |
+ And the following policies exist:
+ | name | summary | content | agreementstyle | audience |
+ | Privacy policy | We scan your thoughts | Here goes content. | 1 | all |
+ | Digital maturity declaration | You declare be old enough | Here goes content. | 1 | loggedin |
+ | Cookies policy | We eat cookies, srsly | Here goes content. | 1 | guest |
+ | Terms of Service | We teach, you learn | Here goes content. | 1 | all |
+ And the following "users" exist:
+ | username | firstname | lastname | email |
+ | user1 | User | One | user1@example.com |
+ And I log in as "user1"
+ # All the policies to be displayed one by one with a button to accept each of them prior seeing the next.
+ Then I should see "Privacy policy" in the "region-main" "region"
+ And I should see "We scan your thoughts"
+ And I should see "Here goes content."
+ And I press "I agree to the Privacy policy"
+ And I should see "Digital maturity declaration" in the "region-main" "region"
+ And I should see "You declare be old enough"
+ And I should see "Here goes content."
+ And I press "I agree to the Digital maturity declaration"
+ And I should see "Terms of Service" in the "region-main" "region"
+ And I should see "We teach, you learn"
+ And I should see "Here goes content."
+ And I press "I agree to the Terms of Service"
+ And I follow "Profile" in the user menu
+ And I follow "Policies and agreements"
+ And "Agreed" "icon" should exist in the "Privacy policy" "table_row"
+ And "Agreed" "icon" should exist in the "Terms of Service" "table_row"
+ And "Agreed" "icon" should exist in the "Digital maturity declaration" "table_row"
+ And "Cookies policy" "table_row" should not exist
+ And I log out
+
+ Scenario: Accepting policies on sign up, policies to be accepted on their own page.
+ Given the following config values are set as admin:
+ | registerauth | email |
+ | passwordpolicy | 0 |
+ | sitepolicyhandler | tool_policy |
+ And the following policies exist:
+ | name | summary | content | agreementstyle | audience |
+ | Privacy policy | We scan your thoughts | Here goes content. | 1 | guest |
+ | Digital maturity declaration | You declare be old enough | Here goes content. | 1 | all |
+ | Cookies policy | We eat cookies, srsly | Here goes content. | 1 | loggedin |
+ | Terms of Service | We teach, you learn | Here goes content. | 1 | guest |
+ And I am on site homepage
+ And I follow "Log in"
+ When I press "Create new account"
+ # All the policies to be displayed one by one with a button to accept each of them prior seeing the next.
+ Then I should see "Digital maturity declaration" in the "region-main" "region"
+ And I should see "You declare be old enough"
+ And I should see "Here goes content."
+ And I press "I agree to the Digital maturity declaration"
+ And I should see "Cookies policy" in the "region-main" "region"
+ And I should see "We eat cookies, srsly"
+ And I press "I agree to the Cookies policy"
+ And I should see "New account"
+ And I set the following fields to these values:
+ | Username | user1 |
+ | Password | user1 |
+ | Email address | user1@address.invalid |
+ | Email (again) | user1@address.invalid |
+ | First name | User1 |
+ | Surname | L1 |
+ And I press "Create my new account"
+ And I should see "Confirm your account"
+ And I should see "An email should have been sent to your address at user1@address.invalid"
+ And I confirm email for "user1"
+ And I should see "Thanks, User1 L1"
+ And I should see "Your registration has been confirmed"
+ And I open my profile in edit mode
+ And the field "First name" matches value "User1"
+ And I log out
+ # Confirm that user can login and browse the site.
+ And I log in as "user1"
+ And I follow "Profile" in the user menu
+ # User can see his own agreements in the profile.
+ And I follow "Policies and agreements"
+ And "Agreed" "icon" should exist in the "Digital maturity declaration" "table_row"
+ And "Agreed" "icon" should exist in the "Cookies policy" "table_row"
+ And "Privacy policy" "table_row" should not exist
+ And "Terms of Service" "table_row" should not exist
+ And I log out
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2018082900; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2018050800; // Requires this Moodle version.
$plugin->component = 'tool_policy'; // Full name of the plugin (used for diagnostics).
| Selector | .usermenu | User menu | This is your personal user menu. You'll find your personal preferences and your user profile here. |
When I am on homepage
Then I should see "Welcome to your personal learning space. We'd like to give you a quick tour to show you some of the areas you may find helpful"
- And I press "Next"
+ And I click on "Next" "button" in the "[data-role='flexitour-step']" "css_element"
And I should see "This area shows you what's happening in some of your courses"
And I should not see "This is the Calendar. All of your assignments and due dates can be found here"
- And I press "Next"
+ And I click on "Next" "button" in the "[data-role='flexitour-step']" "css_element"
And I should see "This is the Calendar. All of your assignments and due dates can be found here"
And I should not see "This area shows you what's happening in some of your courses"
- And I press "Prev"
+ And I click on "Prev" "button" in the "[data-role='flexitour-step']" "css_element"
And I should not see "This is the Calendar. All of your assignments and due dates can be found here"
And I should see "This area shows you what's happening in some of your courses"
- And I press "End tour"
+ And I click on "End tour" "button" in the "[data-role='flexitour-step']" "css_element"
And I should not see "This area shows you what's happening in some of your courses"
And I am on homepage
And I should not see "Welcome to your personal learning space. We'd like to give you a quick tour to show you some of the areas you may find helpful"
$qcategory = new backup_nested_element('question_category', array('id'), array(
'name', 'contextid', 'contextlevel', 'contextinstanceid',
'info', 'infoformat', 'stamp', 'parent',
- 'sortorder'));
+ 'sortorder', 'idnumber'));
$questions = new backup_nested_element('questions');
'parent', 'name', 'questiontext', 'questiontextformat',
'generalfeedback', 'generalfeedbackformat', 'defaultmark', 'penalty',
'qtype', 'length', 'stamp', 'version',
- 'hidden', 'timecreated', 'timemodified', 'createdby', 'modifiedby'));
+ 'hidden', 'timecreated', 'timemodified', 'createdby', 'modifiedby', 'idnumber'));
// attach qtype plugin structure to $question element, only one allowed
$this->add_plugin_structure('qtype', $question, false);
$data->stamp = make_unique_id_code();
}
+ // The idnumber if it exists also needs to be unique within a context or reset it to null.
+ if (!empty($data->idnumber) && $DB->record_exists('question_categories',
+ ['idnumber' => $data->idnumber, 'contextid' => $data->contextid])) {
+ unset($data->idnumber);
+ }
+
// Let's create the question_category and save mapping.
$newitemid = $DB->insert_record('question_categories', $data);
$this->set_mapping('question_category', $oldid, $newitemid);
// With newitemid = 0, let's create the question
if (!$questionmapping->newitemid) {
+
+ // The idnumber if it exists also needs to be unique within a category or reset it to null.
+ if (!empty($data->idnumber) && $DB->record_exists('question',
+ ['idnumber' => $data->idnumber, 'category' => $data->category])) {
+ unset($data->idnumber);
+ }
+
$newitemid = $DB->insert_record('question', $data);
$this->set_mapping('question', $oldid, $newitemid);
// Also annotate them as question_created, we need
+++ /dev/null
-// 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/>.
-
-/**
- * Javascript to load and render the list of calendar events for a
- * given day range.
- *
- * @module block_myoverview/event_list
- * @package block_myoverview
- * @copyright 2016 Ryan Wyllie <ryan@moodle.com>
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-define(['jquery', 'core/notification', 'core/templates',
- 'core/custom_interaction_events',
- 'block_myoverview/calendar_events_repository'],
- function($, Notification, Templates, CustomEvents, CalendarEventsRepository) {
-
- var SECONDS_IN_DAY = 60 * 60 * 24;
-
- var SELECTORS = {
- EMPTY_MESSAGE: '[data-region="empty-message"]',
- ROOT: '[data-region="event-list-container"]',
- EVENT_LIST: '[data-region="event-list"]',
- EVENT_LIST_CONTENT: '[data-region="event-list-content"]',
- EVENT_LIST_GROUP_CONTAINER: '[data-region="event-list-group-container"]',
- LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]',
- VIEW_MORE_BUTTON: '[data-action="view-more"]'
- };
-
- var TEMPLATES = {
- EVENT_LIST_ITEMS: 'block_myoverview/event-list-items',
- COURSE_EVENT_LIST_ITEMS: 'block_myoverview/course-event-list-items'
- };
-
- /**
- * Set a flag on the element to indicate that it has completed
- * loading all event data.
- *
- * @method setLoadedAll
- * @private
- * @param {object} root The container element
- */
- var setLoadedAll = function(root) {
- root.attr('data-loaded-all', true);
- };
-
- /**
- * Check if all event data has finished loading.
- *
- * @method hasLoadedAll
- * @private
- * @param {object} root The container element
- * @return {bool} if the element has completed all loading
- */
- var hasLoadedAll = function(root) {
- return !!root.attr('data-loaded-all');
- };
-
- /**
- * Set the element state to loading.
- *
- * @method startLoading
- * @private
- * @param {object} root The container element
- */
- var startLoading = function(root) {
- var loadingIcon = root.find(SELECTORS.LOADING_ICON_CONTAINER),
- viewMoreButton = root.find(SELECTORS.VIEW_MORE_BUTTON);
-
- root.addClass('loading');
- loadingIcon.removeClass('hidden');
- viewMoreButton.prop('disabled', true);
- };
-
- /**
- * Remove the loading state from the element.
- *
- * @method stopLoading
- * @private
- * @param {object} root The container element
- */
- var stopLoading = function(root) {
- var loadingIcon = root.find(SELECTORS.LOADING_ICON_CONTAINER),
- viewMoreButton = root.find(SELECTORS.VIEW_MORE_BUTTON);
-
- root.removeClass('loading');
- loadingIcon.addClass('hidden');
-
- if (!hasLoadedAll(root)) {
- // Only enable the button if we've got more events to load.
- viewMoreButton.prop('disabled', false);
- } else {
- viewMoreButton.addClass('hidden');
- }
- };
-
- /**
- * Check if the element is currently loading some event data.
- *
- * @method isLoading
- * @private
- * @param {object} root The container element
- * @returns {Boolean}
- */
- var isLoading = function(root) {
- return root.hasClass('loading');
- };
-
- /**
- * Flag the root element to remember that it contains events.
- *
- * @method setHasContent
- * @private
- * @param {object} root The container element
- */
- var setHasContent = function(root) {
- root.attr('data-has-events', true);
- };
-
- /**
- * Check if the root element has had events loaded.
- *
- * @method hasContent
- * @private
- * @param {object} root The container element
- * @return {bool}
- */
- var hasContent = function(root) {
- return root.attr('data-has-events') ? true : false;
- };
-
- /**
- * Update the visibility of the content area. The content area
- * is hidden if we have no events.
- *
- * @method updateContentVisibility
- * @private
- * @param {object} root The container element
- * @param {int} eventCount A count of the events we just received.
- */
- var updateContentVisibility = function(root, eventCount) {
- if (eventCount) {
- // We've rendered some events, let's remember that.
- setHasContent(root);
- } else {
- // If this is the first time trying to load events and
- // we don't have any then there isn't any so let's show
- // the empty message.
- if (!hasContent(root)) {
- hideContent(root);
- }
- }
- };
-
- /**
- * Hide the content area and display the empty content message.
- *
- * @method hideContent
- * @private
- * @param {object} root The container element
- */
- var hideContent = function(root) {
- root.find(SELECTORS.EVENT_LIST_CONTENT).addClass('hidden');
- root.find(SELECTORS.EMPTY_MESSAGE).removeClass('hidden');
- };
-
- /**
- * Render a group of calendar events and add them to the event
- * list.
- *
- * @method renderGroup
- * @private
- * @param {object} group The group container element
- * @param {array} calendarEvents The list of calendar events
- * @param {string} templateName The template name
- * @return {promise} Resolved when the elements are attached to the DOM
- */
- var renderGroup = function(group, calendarEvents, templateName) {
-
- group.removeClass('hidden');
-
- return Templates.render(
- templateName,
- {events: calendarEvents}
- ).done(function(html, js) {
- Templates.appendNodeContents(group.find(SELECTORS.EVENT_LIST), html, js);
- });
- };
-
- /**
- * Determine the time (in seconds) from the given timestamp until the calendar
- * event will need actioning.
- *
- * @method timeUntilEvent
- * @private
- * @param {int} timestamp The time to compare with
- * @param {object} event The calendar event
- * @return {int}
- */
- var timeUntilEvent = function(timestamp, event) {
- var orderTime = event.timesort || 0;
- return orderTime - timestamp;
- };
-
- /**
- * Check if the given calendar event should be added to the given event
- * list group container. The event list group container will specify a
- * day range for the time boundary it is interested in.
- *
- * If only a start day is specified for the container then it will be treated
- * as an open catchment for all events that begin after that time.
- *
- * @method eventBelongsInContainer
- * @private
- * @param {object} root The root element
- * @param {object} event The calendar event
- * @param {object} container The group event list container
- * @return {bool}
- */
- var eventBelongsInContainer = function(root, event, container) {
- var todayTime = root.attr('data-midnight'),
- timeUntilContainerStart = +container.attr('data-start-day') * SECONDS_IN_DAY,
- timeUntilContainerEnd = +container.attr('data-end-day') * SECONDS_IN_DAY,
- timeUntilEventNeedsAction = timeUntilEvent(todayTime, event);
-
- if (container.attr('data-end-day') === '') {
- return timeUntilContainerStart <= timeUntilEventNeedsAction;
- } else {
- return timeUntilContainerStart <= timeUntilEventNeedsAction &&
- timeUntilEventNeedsAction < timeUntilContainerEnd;
- }
- };
-
- /**
- * Return a function that can be used to filter a list of events based on the day
- * range specified on the given event list group container.
- *
- * @method getFilterCallbackForContainer
- * @private
- * @param {object} root The root element
- * @param {object} container Event list group container
- * @return {function}
- */
- var getFilterCallbackForContainer = function(root, container) {
- return function(event) {
- return eventBelongsInContainer(root, event, $(container));
- };
- };
-
- /**
- * Render the given calendar events in the container element. The container
- * elements must have a day range defined using data attributes that will be
- * used to group the calendar events according to their order time.
- *
- * @method render
- * @private
- * @param {object} root The container element
- * @param {array} calendarEvents A list of calendar events
- * @return {promise} Resolved with a count of the number of rendered events
- */
- var render = function(root, calendarEvents) {
- var renderCount = 0;
- var templateName = TEMPLATES.EVENT_LIST_ITEMS;
-
- if (root.attr('data-course-id')) {
- templateName = TEMPLATES.COURSE_EVENT_LIST_ITEMS;
- }
-
- // Loop over each of the element list groups and find the set of calendar events
- // that belong to that group (as defined by the group's day range). The matching
- // list of calendar events are rendered and added to the DOM within that group.
- return $.when.apply($, $.map(root.find(SELECTORS.EVENT_LIST_GROUP_CONTAINER), function(container) {
- var events = calendarEvents.filter(getFilterCallbackForContainer(root, container));
-
- if (events.length) {
- renderCount += events.length;
- return renderGroup($(container), events, templateName);
- } else {
- return null;
- }
- })).then(function() {
- return renderCount;
- });
- };
-
- /**
- * Retrieve a list of calendar events, render and append them to the end of the
- * existing list. The events will be loaded based on the set of data attributes
- * on the root element.
- *
- * This function can be provided with a jQuery promise. If it is then it won't
- * attempt to load data by itself, instead it will use the given promise.
- *
- * The provided promise must resolve with an an object that has an events key
- * and value is an array of calendar events.
- * E.g.
- * { events: ['event 1', 'event 2'] }
- *
- * @method load
- * @param {object} root The root element of the event list
- * @param {object} promise A jQuery promise resolved with events
- * @return {promise} A jquery promise
- */
- var load = function(root, promise) {
- root = $(root);
- var limit = +root.attr('data-limit'),
- courseId = +root.attr('data-course-id'),
- lastId = root.attr('data-last-id'),
- midnight = root.attr('data-midnight'),
- startTime = midnight - (14 * SECONDS_IN_DAY);
-
- // Don't load twice.
- if (isLoading(root)) {
- return $.Deferred().resolve();
- }
-
- startLoading(root);
-
- // If we haven't been provided a promise to resolve the
- // data then we will load our own.
- if (typeof promise == 'undefined') {
- var args = {
- starttime: startTime,
- limit: limit,
- };
-
- if (lastId) {
- args.aftereventid = lastId;
- }
-
- // If we have a course id then we only want events from that course.
- if (courseId) {
- args.courseid = courseId;
- promise = CalendarEventsRepository.queryByCourse(args);
- } else {
- // Otherwise we want events from any course.
- promise = CalendarEventsRepository.queryByTime(args);
- }
- }
-
- // Request data from the server.
- return promise.then(function(result) {
- if (!result.events.length) {
- // No events, nothing to do.
- setLoadedAll(root);
- return 0;
- }
-
- var calendarEvents = result.events;
-
- // Remember the last id we've seen.
- root.attr('data-last-id', calendarEvents[calendarEvents.length - 1].id);
-
- if (calendarEvents.length < limit) {
- // No more events to load, disable loading button.
- setLoadedAll(root);
- }
-
- // Render the events.
- return render(root, calendarEvents).then(function(renderCount) {
- if (renderCount < calendarEvents.length) {
- // If the number of events that was rendered is less than
- // the number we sent for rendering we can assume that there
- // are no groups to add them in. Since the ordering of the
- // events is guaranteed it means that any future requests will
- // also yield events that can't be rendered, so let's not bother
- // sending any more requests.
- setLoadedAll(root);
- }
- return calendarEvents.length;
- });
- }).then(function(eventCount) {
- return updateContentVisibility(root, eventCount);
- }).fail(
- Notification.exception
- ).always(function() {
- stopLoading(root);
- });
- };
-
- /**
- * Register the event listeners for the container element.
- *
- * @method registerEventListeners
- * @param {object} root The root element of the event list
- */
- var registerEventListeners = function(root) {
- CustomEvents.define(root, [CustomEvents.events.activate]);
- root.on(CustomEvents.events.activate, SELECTORS.VIEW_MORE_BUTTON, function() {
- load(root);
- });
- };
-
- return {
- init: function(root) {
- root = $(root);
- load(root);
- registerEventListeners(root);
- },
- registerEventListeners: registerEventListeners,
- load: load,
- rootSelector: SELECTORS.ROOT,
- };
-});
+++ /dev/null
-// 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/>.
-
-/**
- * Javascript to load and render the list of calendar events grouping by course.
- *
- * @module block_myoverview/events_by_course_list
- * @package block_myoverview
- * @copyright 2016 Simey Lameze <simey@moodle.com>
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-define(
-[
- 'jquery',
- 'block_myoverview/event_list',
- 'block_myoverview/calendar_events_repository'
-],
-function($, EventList, EventsRepository) {
-
- var SECONDS_IN_DAY = 60 * 60 * 24;
-
- var SELECTORS = {
- EVENTS_BY_COURSE_CONTAINER: '[data-region="course-events-container"]',
- EVENT_LIST_CONTAINER: '[data-region="event-list-container"]',
- };
-
- /**
- * Loop through course events containers and load calendar events for that course.
- *
- * @method load
- * @param {Object} root The root element of sort by course list.
- */
- var load = function(root) {
- var courseBlocks = root.find(SELECTORS.EVENTS_BY_COURSE_CONTAINER);
-
- if (!courseBlocks.length) {
- return;
- }
-
- var eventList = courseBlocks.find(SELECTORS.EVENT_LIST_CONTAINER).first();
- var midnight = eventList.attr('data-midnight');
- var startTime = midnight - (14 * SECONDS_IN_DAY);
- var limit = eventList.attr('data-limit');
- var courseIds = courseBlocks.map(function() {
- return $(this).attr('data-course-id');
- }).get();
-
- // Load the first set of events for each course in a single request.
- // We want to avoid sending an individual request for each course because
- // there could be lots of them.
- var coursesPromise = EventsRepository.queryByCourses({
- courseids: courseIds,
- starttime: startTime,
- limit: limit
- });
-
- // Load the events into each course block.
- courseBlocks.each(function(index, container) {
- container = $(container);
- var courseId = container.attr('data-course-id');
- var eventListContainer = container.find(EventList.rootSelector);
- var promise = $.Deferred();
-
- // Once all of the course events have been loaded then we need
- // to extract just the ones relevant to this course block and
- // hand them to the event list to render.
- coursesPromise.done(function(result) {
- var events = [];
- // Get this course block's events from the collection returned
- // from the server.
- var courseGroup = result.groupedbycourse.filter(function(group) {
- return group.courseid == courseId;
- });
-
- if (courseGroup.length) {
- events = courseGroup[0].events;
- }
-
- promise.resolve({events: events});
- }).fail(function(e) {
- promise.reject(e);
- });
-
- // Provide the event list with a promise that will be resolved
- // when we have received the events from the server.
- EventList.load(eventListContainer, promise);
- });
- };
-
- return {
- init: function(root) {
- root = $(root);
- load(root);
- }
- };
-});
+++ /dev/null
-// 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/>.
-
-/**
- * Javascript used to save the user's tab preference.
- *
- * @package block_myoverview
- * @copyright 2017 Mark Nelson <markn@moodle.com>
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-define(['jquery', 'core/ajax', 'core/custom_interaction_events',
- 'core/notification'], function($, Ajax, CustomEvents, Notification) {
-
- /**
- * Registers an event that saves the user's tab preference when switching between them.
- *
- * @param {object} root The container element
- */
- var registerEventListeners = function(root) {
- CustomEvents.define(root, [CustomEvents.events.activate]);
- root.on(CustomEvents.events.activate, "[data-toggle='tab']", function(e) {
- var tabname = $(e.currentTarget).data('tabname');
- // Bootstrap does not change the URL when using BS tabs, so need to do this here.
- // Also check to make sure the browser supports the history API.
- if (typeof window.history.pushState === "function") {
- window.history.pushState(null, null, '?myoverviewtab=' + tabname);
- }
- var request = {
- methodname: 'core_user_update_user_preferences',
- args: {
- preferences: [
- {
- type: 'block_myoverview_last_tab',
- value: tabname
- }
- ]
- }
- };
-
- Ajax.call([request])[0]
- .fail(Notification.exception);
- });
- };
-
- return {
- registerEventListeners: registerEventListeners
- };
-});
return $this->content;
}
- // Check if the tab to select wasn't passed in the URL, if so see if the user has any preference.
- if (!$tab = optional_param('myoverviewtab', null, PARAM_ALPHA)) {
- // Check if the user has no preference, if so get the site setting.
- if (!$tab = get_user_preferences('block_myoverview_last_tab')) {
- $config = get_config('block_myoverview');
- $tab = $config->defaulttab;
- }
- }
-
- $renderable = new \block_myoverview\output\main($tab);
+ $renderable = new \block_myoverview\output\main();
$renderer = $this->page->get_renderer('block_myoverview');
$this->content = new stdClass();
public function applicable_formats() {
return array('my' => true);
}
-
- /**
- * This block does contain a configuration settings.
- *
- * @return boolean
- */
- public function has_config() {
- return true;
- }
}
use templatable;
use core_completion\progress;
-require_once($CFG->dirroot . '/blocks/myoverview/lib.php');
require_once($CFG->libdir . '/completionlib.php');
/**
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class main implements renderable, templatable {
-
- /**
- * @var string The tab to display.
- */
- public $tab;
-
- /**
- * Constructor.
- *
- * @param string $tab The tab to display.
- */
- public function __construct($tab) {
- $this->tab = $tab;
- }
-
/**
* Export this data so it can be used as the context for a mustache template.
*
$coursesview = new courses_view($courses, $coursesprogress);
$nocoursesurl = $output->image_url('courses', 'block_myoverview')->out();
- $noeventsurl = $output->image_url('activities', 'block_myoverview')->out();
-
- // Now, set the tab we are going to be viewing.
- $viewingtimeline = false;
- $viewingcourses = false;
- if ($this->tab == BLOCK_MYOVERVIEW_TIMELINE_VIEW) {
- $viewingtimeline = true;
- } else {
- $viewingcourses = true;
- }
return [
'midnight' => usergetmidnight(time()),
'coursesview' => $coursesview->export_for_template($output),
'urls' => [
'nocourses' => $nocoursesurl,
- 'noevents' => $noeventsurl
],
- 'viewingtimeline' => $viewingtimeline,
- 'viewingcourses' => $viewingcourses
];
}
}
* @copyright 2018 Zig Tan <zig@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\user_preference_provider {
+class provider implements \core_privacy\local\metadata\null_provider {
/**
- * Returns meta-data information about the myoverview block.
+ * Get the language string identifier with the component's language
+ * file to explain why this plugin stores no data.
*
- * @param \core_privacy\local\metadata\collection $collection A collection of meta-data.
- * @return \core_privacy\local\metadata\collection Return the collection of meta-data.
+ * @return string
*/
- public static function get_metadata(\core_privacy\local\metadata\collection $collection) :
- \core_privacy\local\metadata\collection {
- $collection->add_user_preference('block_myoverview_last_tab', 'privacy:metadata:overviewlasttab');
- return $collection;
- }
-
- /**
- * Export all user preferences for the myoverview block
- *
- * @param int $userid The userid of the user whose data is to be exported.
- */
- public static function export_user_preferences(int $userid) {
- $preference = get_user_preferences('block_myoverview_last_tab', null, $userid);
- if (isset($preference)) {
- \core_privacy\local\request\writer::export_user_preference('block_myoverview', 'block_myoverview_last_tab',
- $preference, get_string('privacy:metadata:overviewlasttab', 'block_myoverview'));
- }
+ public static function get_reason() : string {
+ return 'privacy:metadata';
}
}
--- /dev/null
+<?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/>.
+
+/**
+ * This file keeps track of upgrades to the myoverview block
+ *
+ * @package block_myoverview
+ * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Upgrade code for the myoverview block.
+ *
+ * @param int $oldversion
+ */
+function xmldb_block_myoverview_upgrade($oldversion) {
+ global $DB;
+
+ if ($oldversion < 2018092700) {
+ $DB->delete_records('user_preferences', ['name' => 'block_myoverview_last_tab']);
+ upgrade_block_savepoint(true, 2018092700, 'myoverview');
+ }
+
+ return true;
+}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-$string['defaulttab'] = 'Default tab';
-$string['defaulttab_desc'] = 'The tab that will be displayed when a user first views their course overview. When returning to their course overview, the user\'s active tab is remembered.';
$string['future'] = 'Future';
$string['inprogress'] = 'In progress';
$string['morecourses'] = 'More courses';
$string['nocoursesinprogress'] = 'No in progress courses';
$string['nocoursesfuture'] = 'No future courses';
$string['nocoursespast'] = 'No past courses';
-$string['noevents'] = 'No upcoming activities due';
-$string['next30days'] = 'Next 30 days';
-$string['next7days'] = 'Next 7 days';
$string['past'] = 'Past';
$string['pluginname'] = 'Course overview';
-$string['recentlyoverdue'] = 'Recently overdue';
-$string['sortbycourses'] = 'Sort by courses';
-$string['sortbydates'] = 'Sort by dates';
-$string['timeline'] = 'Timeline';
$string['viewcourse'] = 'View course';
$string['viewcoursename'] = 'View course {$a}';
-$string['privacy:metadata:overviewlasttab'] = 'This stores the last tab selected by the user on the overview block.';
+$string['privacy:metadata'] = 'The myoverview block does not store any personal data.';
+++ /dev/null
-{{!
- 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 block_myoverview/course-event-list-item
-
- This template renders an event list item for the myoverview block
- in the courses view.
-
- Example context (json):
- {
- "name": "Assignment due 1",
- "url": "https://www.google.com",
- "timesort": 1490320388,
- "action": {
- "name": "Submit assignment",
- "url": "https://www.google.com",
- "itemcount": 1,
- "showitemcount": true,
- "actionable": true
- },
- "icon": {
- "key": "icon",
- "component": "mod_assign",
- "alttext": "Assignment icon"
- }
- }
-}}
-<li class="list-group-item event-list-item" data-region="event-list-item">
- <div class="row">
- <div class="col-lg-7 col-xl-8">
- <div class="d-inline-block icon-large event-icon">
- {{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}}
- </div>
- <div class="d-inline-block event-name-container">
- <a class="event-name text-truncate" href="{{{url}}}">{{{name}}}</a>
- <p class="small text-muted text-truncate m-b-0">
- {{#userdate}} {{timesort}}, {{#str}} strftimerecent {{/str}} {{/userdate}}
- </p>
- </div>
- </div>
- <div class="hidden-md-down d-none d-md-block col-lg-5 col-xl-4 text-truncate">
- {{#action.actionable}}
- <a href="{{{action.url}}}">{{action.name}}</a>
- {{#action.itemcount}}
- {{#action.showitemcount}}
- <span class="tag tag-pill tag-default">{{.}}</span>
- {{/action.showitemcount}}
- {{/action.itemcount}}
- {{/action.actionable}}
- {{^action.actionable}}
- <div class="text-muted">{{action.name}}</div>
- {{/action.actionable}}
- </div>
- </div>
-</li>
+++ /dev/null
-{{!
- 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 block_myoverview/course-event-list
-
- This template renders a list of events for the myoverview block
- sort by courses view.
-
- Example context (json):
- {
- "urls": {
- "noevents": "#"
- }
- }
-}}
-<div data-region="event-list-container"
- data-limit="{{$limit}}20{{/limit}}"
- data-course-id="{{$courseid}}{{/courseid}}"
- data-last-id="{{$lastid}}{{/lastid}}"
- data-midnight="{{midnight}}"
- id="event-list-container-{{$courseid}}{{/courseid}}">
-
- <div data-region="event-list-content">
- {{< block_myoverview/event-list-group }}
- {{$title}}{{#str}} recentlyoverdue, block_myoverview {{/str}}{{/title}}
- {{$extratitleclasses}}text-danger{{/extratitleclasses}}
- {{$startday}}-14{{/startday}}
- {{$endday}}0{{/endday}}
- {{$eventlistitems}}
- {{> block_myoverview/course-event-list-items }}
- {{/eventlistitems}}
- {{/ block_myoverview/event-list-group }}
- {{< block_myoverview/event-list-group }}
- {{$title}}{{#str}} today {{/str}}{{/title}}
- {{$extratitleclasses}}{{/extratitleclasses}}
- {{$startday}}0{{/startday}}
- {{$endday}}1{{/endday}}
- {{$eventlistitems}}
- {{> block_myoverview/course-event-list-items }}
- {{/eventlistitems}}
- {{/ block_myoverview/event-list-group }}
- {{< block_myoverview/event-list-group }}
- {{$title}}{{#str}} next7days, block_myoverview {{/str}}{{/title}}
- {{$extratitleclasses}}{{/extratitleclasses}}
- {{$startday}}1{{/startday}}
- {{$endday}}7{{/endday}}
- {{$eventlistitems}}
- {{> block_myoverview/course-event-list-items }}
- {{/eventlistitems}}
- {{/ block_myoverview/event-list-group }}
- {{< block_myoverview/event-list-group }}
- {{$title}}{{#str}} next30days, block_myoverview {{/str}}{{/title}}
- {{$extratitleclasses}}{{/extratitleclasses}}
- {{$startday}}7{{/startday}}
- {{$endday}}30{{/endday}}
- {{$eventlistitems}}
- {{> block_myoverview/course-event-list-items }}
- {{/eventlistitems}}
- {{/ block_myoverview/event-list-group }}
- {{< block_myoverview/event-list-group }}
- {{$title}}{{#str}} future, block_myoverview {{/str}}{{/title}}
- {{$extratitleclasses}}{{/extratitleclasses}}
- {{$startday}}30{{/startday}}
- {{$endday}}{{/endday}}
- {{$eventlistitems}}
- {{> block_myoverview/course-event-list-items }}
- {{/eventlistitems}}
- {{/ block_myoverview/event-list-group }}
-
- <div class="text-xs-center text-center m-y-2">
- <button type="button" class="btn btn-secondary" data-action="view-more">
- {{#str}} viewmore {{/str}}
- <span class="hidden" data-region="loading-icon-container">
- {{> core/loading }}
- </span>
- </button>
- </div>
- </div>
- <div class="hidden text-xs-center text-center m-y-3" data-region="empty-message">
- <img class="empty-placeholder-image-sm"
- src="{{urls.noevents}}"
- alt="{{#str}} noevents, block_myoverview {{/str}}"
- role="presentation">
- <p class="text-muted m-t-1">{{#str}} noevents, block_myoverview {{/str}}</p>
- <a href="{{viewurl}}" class="btn btn-secondary {{#visible}}text-primary{{/visible}}"
- aria-label="{{#str}} viewcoursename, block_myoverview, {{{fullnamedisplay}}} {{/str}}">
- {{#str}} viewcourse, block_myoverview {{/str}}
- </a>
- </div>
-</div>
-{{#js}}
-require(['jquery', 'block_myoverview/event_list'], function($, EventList) {
- var root = $("#event-list-container-{{$courseid}}{{/courseid}}");
- EventList.registerEventListeners(root);
-});
-{{/js}}
+++ /dev/null
-{{!
- 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 block_myoverview/course-summary
-
- This template renders the course summary (view by courses) for the myoverview block.
-
- Example context (json):
- {
- "fullnamedisplay": "course 3",
- "viewurl": "https://www.google.com",
- "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
- }
-}}
-<div class="course-info-container" id="course-info-container-{{id}}">
- <div class="d-sm-none d-lg-block">
- {{> block_myoverview/progress-chart}}
- <h4 class="h5"><a href="{{viewurl}}" class="{{^visible}}dimmed{{/visible}}">{{{fullnamedisplay}}}</a></h4>
- </div>
- <div class="d-none d-sm-block d-lg-none visible-tablet">
- <div class="media">
- <div class="media-left pr-3">
- <div class="media-object">
- {{> block_myoverview/progress-chart}}
- </div>
- </div>
- <div class="media-body">
- <h4 class="h5"><a href="{{viewurl}}" class="{{^visible}}dimmed{{/visible}}">{{{fullnamedisplay}}}</a></h4>
- </div>
- </div>
- </div>
- <p class="text-muted">
- {{#shortentext}} 140, {{{summary}}}{{/shortentext}}
- </p>
-</div>
--- /dev/null
+{{!
+ 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 block_myoverview/courses-view-nav-grouping-display-filter
+
+ This template renders the main content area for the myoverview block.
+
+ Example context (json):
+ {}
+}}
+<div data-region="courses-grouping-display-filter" class="btn-group">
+ <button type="button" class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ {{#str}} inprogress, block_myoverview {{/str}}
+ </button>
+ <div class="dropdown-menu list-group hidden" data-show-active-item data-skip-active-class="true">
+ <a class="dropdown-item active" href="#myoverview_courses_view_in_progress" data-toggle="tab">
+ {{#str}} inprogress, block_myoverview {{/str}}
+ </a>
+ <a class="dropdown-item" href="#myoverview_courses_view_future" data-toggle="tab">
+ {{#str}} future, block_myoverview {{/str}}
+ </a>
+ <a class="dropdown-item" href="#myoverview_courses_view_past" data-toggle="tab">
+ {{#str}} past, block_myoverview {{/str}}
+ </a>
+ </div>
+</div>
}}
<div id="courses-view-{{uniqid}}" data-region="courses-view">
{{#hascourses}}
- <div class="d-flex justify-content-center">
- <ul class="nav nav-pills my-5">
- <li class="nav-item">
- <a class="nav-link active" href="#myoverview_courses_view_in_progress" data-toggle="tab">
- {{#str}} inprogress, block_myoverview {{/str}}
- </a>
- </li>
- <li class="nav-item">
- <a class="nav-link" href="#myoverview_courses_view_future" data-toggle="tab">
- {{#str}} future, block_myoverview {{/str}}
- </a>
- </li>
- <li class="nav-item">
- <a class="nav-link" href="#myoverview_courses_view_past" data-toggle="tab">
- {{#str}} past, block_myoverview {{/str}}
- </a>
- </li>
- </ul>
- </div>
<div class="tab-content">
<div class="tab-pane active fade show" id="myoverview_courses_view_in_progress">
{{#inprogress}}
+++ /dev/null
-{{!
- 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 block_myoverview/event-list-group
-
- This template renders a list of events for the myoverview block.
-
- Example context (json):
- {
- "events": [
- {
- "enddate": "Nov 4th, 10am",
- "name": "Assignment due 1",
- "url": "https://www.google.com",
- "course": {
- "fullname": "Course 1"
- },
- "action": {
- "name": "Submit assignment",
- "url": "https://www.google.com",
- "itemcount": 1
- },
- "icon": {
- "key": "icon",
- "component": "mod_assign",
- "alttext": "Assignment icon"
- }
- },
- {
- "enddate": "Nov 4th, 10am",
- "name": "Assignment due 2",
- "url": "https://www.google.com",
- "course": {
- "fullname": "Course 1"
- },
- "action": {
- "name": "Submit assignment",
- "url": "https://www.google.com",
- "itemcount": 1
- },
- "icon": {
- "key": "icon",
- "component": "mod_assign",
- "alttext": "Assignment icon"
- }
- }
- ]
- }
-}}
-<div data-region="event-list-group-container"
- data-start-day="{{$startday}}0{{/startday}}"
- data-end-day="{{$endday}}{{/endday}}"
- class="hidden">
-
- <h5 class="h6 m-t-1 {{$extratitleclasses}}{{/extratitleclasses}}" id="event-list-title-{{uniqid}}"><strong>{{$title}}{{/title}}</strong></h5>
- <ul class="list-group unstyled" data-region="event-list" aria-describedby="event-list-title-{{uniqid}}">
- {{$eventlistitems}}
- {{> block_myoverview/event-list-items }}
- {{/eventlistitems}}
- </ul>
-</div>
+++ /dev/null
-{{!
- 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 block_myoverview/event-list-item
-
- This template renders an event list item for the myoverview block.
-
- Example context (json):
- {
- "name": "Assignment due 1",
- "url": "https://www.google.com",
- "timesort": 1490320388,
- "course": {
- "fullnamedisplay": "Course 1"
- },
- "action": {
- "name": "Submit assignment",
- "url": "https://www.google.com",
- "itemcount": 1,
- "showitemcount": true,
- "actionable": true
- },
- "icon": {
- "key": "icon",
- "component": "mod_assign",
- "alttext": "Assignment icon"
- }
- }
-}}
-<li class="list-group-item event-list-item" data-region="event-list-item">
- <div class="row">
- <div class="col-sm-8 col-lg-6 col-xl-7">
- <div class="d-inline-block icon-large event-icon">
- {{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}}
- </div>
- <div class="d-inline-block event-name-container">
- <a class="event-name text-truncate" href="{{{url}}}">{{{name}}}</a>
- <p class="small text-muted text-truncate m-b-0">{{{course.fullnamedisplay}}}</p>
- </div>
- </div>
- <div class="col-sm-4 col-lg-6 col-xl-5">
- <div class="row">
- <div class="col-lg-5 text-xs-right text-lg-left text-truncate">
- {{#userdate}} {{timesort}}, {{#str}} strftimerecent {{/str}} {{/userdate}}
- </div>
- <div class="hidden-md-down d-none d-md-block col-lg-7 text-truncate">
- {{#action.actionable}}
- <a href="{{{action.url}}}">{{action.name}}</a>
- {{#action.itemcount}}
- {{#action.showitemcount}}
- <span class="tag tag-pill tag-default">{{.}}</span>
- {{/action.showitemcount}}
- {{/action.itemcount}}
- {{/action.actionable}}
- {{^action.actionable}}
- <div class="text-muted">{{action.name}}</div>
- {{/action.actionable}}
- </div>
- </div>
- </div>
- </div>
-</li>
+++ /dev/null
-{{!
- 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 block_myoverview/event-list
-
- This template renders a list of events for the myoverview block.
-
- Example context (json):
- {
- }
-}}
-<div data-region="event-list-container"
- data-limit="{{$limit}}20{{/limit}}"
- data-course-id="{{$courseid}}{{/courseid}}"
- data-last-id="{{$lastid}}{{/lastid}}"
- data-midnight="{{midnight}}"
- id="event-list-container-{{$courseid}}{{/courseid}}">
-
- <div data-region="event-list-content">
- {{< block_myoverview/event-list-group }}
- {{$title}}{{#str}} recentlyoverdue, block_myoverview {{/str}}{{/title}}
- {{$extratitleclasses}}text-danger{{/extratitleclasses}}
- {{$startday}}-14{{/startday}}
- {{$endday}}0{{/endday}}
- {{/ block_myoverview/event-list-group }}
- {{< block_myoverview/event-list-group }}
- {{$title}}{{#str}} today {{/str}}{{/title}}
- {{$extratitleclasses}}{{/extratitleclasses}}
- {{$startday}}0{{/startday}}
- {{$endday}}1{{/endday}}
- {{/ block_myoverview/event-list-group }}
- {{< block_myoverview/event-list-group }}
- {{$title}}{{#str}} next7days, block_myoverview {{/str}}{{/title}}
- {{$extratitleclasses}}{{/extratitleclasses}}
- {{$startday}}1{{/startday}}
- {{$endday}}7{{/endday}}
- {{/ block_myoverview/event-list-group }}
- {{< block_myoverview/event-list-group }}
- {{$title}}{{#str}} next30days, block_myoverview {{/str}}{{/title}}
- {{$extratitleclasses}}{{/extratitleclasses}}
- {{$startday}}7{{/startday}}
- {{$endday}}30{{/endday}}
- {{/ block_myoverview/event-list-group }}
- {{< block_myoverview/event-list-group }}
- {{$title}}{{#str}} future, block_myoverview {{/str}}{{/title}}
- {{$extratitleclasses}}{{/extratitleclasses}}
- {{$startday}}30{{/startday}}
- {{$endday}}{{/endday}}
- {{/ block_myoverview/event-list-group }}
-
- <div class="text-xs-center text-center m-y-2">
- <button type="button" class="btn btn-secondary" data-action="view-more">
- {{#str}} viewmore {{/str}}
- <span class="hidden" data-region="loading-icon-container">
- {{> core/loading }}
- </span>
- </button>
- </div>
- </div>
- <div class="hidden text-xs-center text-center m-t-3" data-region="empty-message">
- <img class="empty-placeholder-image-lg"
- src="{{urls.noevents}}"
- alt="{{#str}} noevents, block_myoverview {{/str}}"
- role="presentation">
- <p class="text-muted m-t-1">{{#str}} noevents, block_myoverview {{/str}}</p>
- </div>
-</div>
-{{#js}}
-require(['jquery', 'block_myoverview/event_list'], function($, EventList) {
- var root = $("#event-list-container-{{$courseid}}{{/courseid}}");
- EventList.registerEventListeners(root);
-});
-{{/js}}
}}
<div id="block-myoverview-{{uniqid}}" class="block-myoverview" data-region="myoverview">
- <ul id="block-myoverview-view-choices-{{uniqid}}" class="nav nav-tabs" role="tablist">
- <li class="nav-item">
- <a class="nav-link {{#viewingtimeline}}active{{/viewingtimeline}}" href="#myoverview_timeline_view" role="tab" data-toggle="tab" data-tabname="timeline">
- {{#str}} timeline, block_myoverview {{/str}}
- </a>
- </li>
- <li class="nav-item">
- <a class="nav-link {{#viewingcourses}}active{{/viewingcourses}}" href="#myoverview_courses_view" role="tab" data-toggle="tab" data-tabname="courses">
- {{#str}} courses {{/str}}
- </a>
- </li>
- </ul>
- <div class="tab-content content-centred">
- <div role="tabpanel" class="tab-pane fade {{#viewingtimeline}}in active{{/viewingtimeline}}" id="myoverview_timeline_view">
- {{> block_myoverview/timeline-view }}
- </div>
- <div role="tabpanel" class="tab-pane fade {{#viewingcourses}}in active{{/viewingcourses}}" id="myoverview_courses_view">
+ <div class="container p-0 m-b-1">
+ <div class="row no-gutters">
{{#coursesview}}
- {{> block_myoverview/courses-view }}
+ {{#hascourses}}
+ <div class="{{#viewingtimeline}}d-none{{/viewingtimeline}}" data-tab-content="courses">
+ {{> block_myoverview/courses-view-nav-grouping-display-filter }}
+ </div>
+ {{/hascourses}}
{{/coursesview}}
</div>
</div>
+ <div class="container p-0">
+ {{#coursesview}}
+ {{> block_myoverview/courses-view }}
+ {{/coursesview}}
+ </div>
</div>
-{{#js}}
-require(['jquery', 'block_myoverview/tab_preferences'], function($, TabPreferences) {
- var root = $('#block-myoverview-view-choices-{{uniqid}}');
- TabPreferences.registerEventListeners(root);
-});
-{{/js}}
+++ /dev/null
-{{!
- 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 block_myoverview/timeline-view-courses
-
- This template renders the timeline view by courses for the myoverview block.
-
- Example context (json):
- {}
-}}
-<div id="sort-by-courses-view-{{uniqid}}">
- {{#coursesview}}
- {{#inprogress}}
- {{#haspages}}
- {{#pages}}
- <ul class="list-group unstyled hidden" data-region="course-block">
- {{#courses}} {{> block_myoverview/course-item }} {{/courses}}
- </ul>
- {{/pages}}
- <div class="text-xs-center text-center m-t-1">
- <button type="button" class="btn btn-secondary" data-action="more-courses">
- {{#str}} morecourses, block_myoverview {{/str}}
- <span class="hidden" data-region="loading-icon-container">
- {{> core/loading }}
- </span>
- </button>
- </div>
- {{/haspages}}
- {{^haspages}}
- <div class="text-xs-center text-center m-t-3">
- <img class="empty-placeholder-image-lg"
- src="{{urls.noevents}}"
- alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}"
- role="presentation">
- <p class="text-muted m-t-1">{{#str}} nocoursesinprogress, block_myoverview {{/str}}</p>
- </div>
- {{/haspages}}
- {{/inprogress}}
- {{^inprogress}}
- <div class="text-xs-center text-center m-t-3">
- <img class="empty-placeholder-image-lg"
- src="{{urls.noevents}}"
- alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}"
- role="presentation">
- <p class="text-muted m-t-1">{{#str}} nocoursesinprogress, block_myoverview {{/str}}</p>
- </div>
- {{/inprogress}}
- {{/coursesview}}
-</div>
-{{#js}}
- require(['jquery', 'core/custom_interaction_events', 'block_myoverview/event_list_by_course'],
- function($, CustomEvents, EventListByCourse) {
-
- var root = $("#sort-by-courses-view-{{uniqid}}");
- // This flag is used so that we can delay the loading of the events until the tab
- // is toggled by the user.
- var seen = false;
-
- CustomEvents.define(root, [CustomEvents.events.activate]);
- // Show more courses and load their events when the user clicks the "more courses"
- // button.
- root.on(CustomEvents.events.activate, '[data-action="more-courses"]', function(e, data) {
- var button = $(e.target);
- var blocks = root.find('[data-region="course-block"].hidden');
-
- if (blocks && blocks.length) {
- var block = blocks.first();
- EventListByCourse.init(block);
- block.removeClass('hidden');
- }
-
- // If there was only one hidden block then we have no more to show now
- // so we can disable the button.
- if (blocks && blocks.length == 1) {
- button.addClass('hidden');
- }
-
- if (data) {
- data.originalEvent.preventDefault();
- data.originalEvent.stopPropagation();
- }
- e.stopPropagation();
- });
-
- // Listen for when the user changes tab so that we can show the first set of courses
- // and load their events when they request the sort by courses view for the first time.
- root.closest('[data-region="timeline-view"]').on('shown shown.bs.tab', function(e) {
- if (seen) {
- return;
- }
-
- var tab = $(e.target);
- var tabTarget = $(tab.attr('href'));
-
- if (!tabTarget || !tabTarget.length) {
- return;
- }
-
- var viewCourses = tabTarget.find('#sort-by-courses-view-{{uniqid}}');
-
- if (viewCourses && viewCourses.length && !seen) {
- seen = true;
- viewCourses.find('[data-action="more-courses"]').trigger(CustomEvents.events.activate);
- }
- });
- });
-{{/js}}
+++ /dev/null
-{{!
- 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 block_myoverview/timeline-view
-
- This template renders the timeline view for the myoverview block.
-
- Example context (json):
- {}
-}}
-<div id="timeline-view-{{uniqid}}" data-region="timeline-view">
- <div class="d-flex justify-content-center">
- <ul class="nav nav-pills my-5">
- <li class="nav-item">
- <a class="nav-link active" href="#myoverview_timeline_dates" data-toggle="tab">
- {{#str}} sortbydates, block_myoverview {{/str}}
- </a>
- </li>
- <li class="nav-item">
- <a class="nav-link" href="#myoverview_timeline_courses" data-toggle="tab">
- {{#str}} sortbycourses, block_myoverview {{/str}}
- </a>
- </li>
- </ul>
- </div>
-
- <div class="tab-content">
- <div class="tab-pane active fade show" id="myoverview_timeline_dates">
- {{> block_myoverview/timeline-view-dates }}
- </div>
- <div class="tab-pane fade" id="myoverview_timeline_courses">
- {{> block_myoverview/timeline-view-courses }}
- </div>
- </div>
-</div>
\ No newline at end of file
@block @block_myoverview @javascript
-Feature: The my overview block allows users to easily access their courses and see upcoming activities
+Feature: The my overview block allows users to easily access their courses
In order to enable the my overview block in a course
As a student
I can add the my overview block to my dashboard
| Course 1 | C1 | 0 | ##1 month ago## | ##15 days ago## |
| Course 2 | C2 | 0 | ##yesterday## | ##tomorrow## |
| Course 3 | C3 | 0 | ##first day of next month## | ##last day of next month## |
- And the following "activities" exist:
- | activity | course | idnumber | name | intro | timeopen | timeclose |
- | choice | C2 | choice1 | Test choice 1 | Test choice description | ##yesterday## | ##tomorrow## |
- | choice | C1 | choice2 | Test choice 2 | Test choice description | ##1 month ago## | ##15 days ago## |
- | choice | C3 | choice3 | Test choice 3 | Test choice description | ##first day of +5 months## | ##last day of +5 months## |
- | feedback | C2 | feedback1 | Test feedback 1 | Test feedback description | ##yesterday## | ##tomorrow## |
- | feedback | C3 | feedback3 | Test feedback 3 | Test feedback description | ##first day of +5 months## | ##last day of +5 months## |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| student1 | C2 | student |
| student1 | C3 | student |
- Scenario: View courses and upcoming activities on timeline view
- Given I log in as "student1"
- And I click on "Timeline" "link" in the "Course overview" "block"
- When I click on "Sort by dates" "link" in the "Course overview" "block"
- Then I should see "Next 7 days" in the "Course overview" "block"
- And I should see "Test choice 1 closes" in the "Course overview" "block"
- And I should see "View choices" in the "Course overview" "block"
- And I should see "Test feedback 1 closes" in the "Course overview" "block"
- And I should see "Answer the questions" in the "Course overview" "block"
- And I should see "Future" in the "Course overview" "block"
- And I should see "Test choice 3 closes" in the "Course overview" "block"
- And I should see "Test feedback 3 closes" in the "Course overview" "block"
- And I log out
-
- Scenario: Past activities should not be displayed on the timeline view
- Given I log in as "student1"
- And I click on "Timeline" "link" in the "Course overview" "block"
- When I click on "Sort by dates" "link" in the "Course overview" "block"
- And I should not see "Test choice 2 closes" in the "Course overview" "block"
- And I log out
-
Scenario: See the courses I am enrolled by their status on courses view
Given I log in as "student1"
- And I click on "Courses" "link" in the "Course overview" "block"
- And I click on "In progress" "link" in the "Course overview" "block"
And I should see "Course 2" in the "Course overview" "block"
And I should not see "Course 1" in the "Course overview" "block"
+ And I click on "In progress" "button" in the "Course overview" "block"
And I click on "Future" "link" in the "Course overview" "block"
And I should see "Course 3" in the "Course overview" "block"
And I should not see "Course 1" in the "Course overview" "block"
+ And I click on "Future" "button" in the "Course overview" "block"
When I click on "Past" "link" in the "Course overview" "block"
Then I should see "Course 1" in the "Course overview" "block"
And I should not see "Course 2" in the "Course overview" "block"
And I should not see "Course 3" in the "Course overview" "block"
And I log out
-
- Scenario: No activities should be displayed if the user is not enrolled
- Given I log in as "student2"
- And I click on "Timeline" "link" in the "Course overview" "block"
- And I should see "No upcoming activities" in the "Course overview" "block"
- When I click on "Courses" "link" in the "Course overview" "block"
- Then I should see "No courses" in the "Course overview" "block"
- And I log out
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
- Scenario: Course progress percentage should not be displayed if completion is not enabled
- Given I log in as "student1"
- And I click on "Timeline" "link" in the "Course overview" "block"
- When I click on "Sort by courses" "link" in the "Course overview" "block"
- Then I should see "Test choice 1 closes" in the "#myoverview_timeline_courses" "css_element"
- And I should not see "0%" in the "Course overview" "block"
- And I click on "Courses" "link" in the "Course overview" "block"
- And I click on "In progress" "link" in the "Course overview" "block"
- And I should see "Course 1" in the "Course overview" "block"
- And I should not see "0%" in the "Course overview" "block"
- And I log out
-
Scenario: User complete activity and verify his progress
Given I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I press "Save and return to course"
And I log out
And I log in as "student1"
- And I click on "Sort by courses" "link" in the "Course overview" "block"
- And I should see "Test choice 1 closes" in the "#myoverview_timeline_courses" "css_element"
- And I should see "0%" in the "Course overview" "block"
- And I click on "Courses" "link" in the "Course overview" "block"
- When I click on "In progress" "link" in the "Course overview" "block"
Then I should see "Course 1" in the "Course overview" "block"
And I should see "0%" in the "Course overview" "block"
And I am on "Course 1" course homepage
And I follow "Test choice 1"
And I follow "Dashboard" in the user menu
- And I click on "Timeline" "link" in the "Course overview" "block"
- And I click on "Sort by courses" "link" in the "Course overview" "block"
- And I should see "100%" in the "Course overview" "block"
- And I click on "Courses" "link" in the "Course overview" "block"
- And I click on "In progress" "link" in the "Course overview" "block"
And I should see "Course 1" in the "Course overview" "block"
And I should see "100%" in the "Course overview" "block"
And I log out
+++ /dev/null
-<?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/>.
-
-/**
- * Unit tests for the block_myoverview implementation of the privacy API.
- *
- * @package block_myoverview
- * @category test
- * @copyright 2018 Adrian Greeve <adriangreeve.com>
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-use \core_privacy\local\request\writer;
-use \block_myoverview\privacy\provider;
-
-/**
- * Unit tests for the block_myoverview implementation of the privacy API.
- *
- * @copyright 2018 Adrian Greeve <adriangreeve.com>
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class block_myoverview_privacy_testcase extends \core_privacy\tests\provider_testcase {
-
- /**
- * Ensure that export_user_preferences returns no data if the user has not visited the myoverview block.
- */
- public function test_export_user_preferences_no_pref() {
- $this->resetAfterTest();
-
- $user = $this->getDataGenerator()->create_user();
- provider::export_user_preferences($user->id);
- $writer = writer::with_context(\context_system::instance());
- $this->assertFalse($writer->has_any_data());
- }
-
- /**
- * Test that the preference courses is exported properly.
- */
- public function test_export_user_preferences_course_preference() {
- $this->resetAfterTest();
-
- $user = $this->getDataGenerator()->create_user();
- set_user_preference('block_myoverview_last_tab', 'courses', $user);
-
- provider::export_user_preferences($user->id);
- $writer = writer::with_context(\context_system::instance());
- $blockpreferences = $writer->get_user_preferences('block_myoverview');
- $this->assertEquals('courses', $blockpreferences->block_myoverview_last_tab->value);
- }
-
- /**
- * Test that the preference timeline is exported properly.
- */
- public function test_export_user_preferences_timeline_preference() {
- $this->resetAfterTest();
-
- $user = $this->getDataGenerator()->create_user();
- set_user_preference('block_myoverview_last_tab', 'timeline', $user);
-
- provider::export_user_preferences($user->id);
- $writer = writer::with_context(\context_system::instance());
- $blockpreferences = $writer->get_user_preferences('block_myoverview');
- $this->assertEquals('timeline', $blockpreferences->block_myoverview_last_tab->value);
- }
-}
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2018092700; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2018050800; // Requires this Moodle version.
$plugin->component = 'block_myoverview'; // Full name of the plugin (used for diagnostics).
/**
* A javascript module to retrieve calendar events from the server.
*
- * @module block_myoverview/calendar_events_repository
- * @class repository
- * @package block_myoverview
- * @copyright 2016 Ryan Wyllie <ryan@moodle.com>
+ * @module block_timeline/calendar_events_repository
+ * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notification) {
--- /dev/null
+// 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/>.
+
+/**
+ * Javascript to load and render the list of calendar events for a
+ * given day range.
+ *
+ * @module block_timeline/event_list
+ * @package block_timeline
+ * @copyright 2016 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(
+[
+ 'jquery',
+ 'core/notification',
+ 'core/templates',
+ 'core/paged_content_factory',
+ 'core/str',
+ 'core/user_date',
+ 'block_timeline/calendar_events_repository'
+],
+function(
+ $,
+ Notification,
+ Templates,
+ PagedContentFactory,
+ Str,
+ UserDate,
+ CalendarEventsRepository
+) {
+
+ var SECONDS_IN_DAY = 60 * 60 * 24;
+
+ var SELECTORS = {
+ EMPTY_MESSAGE: '[data-region="empty-message"]',
+ ROOT: '[data-region="event-list-container"]',
+ EVENT_LIST_CONTENT: '[data-region="event-list-content"]',
+ EVENT_LIST_LOADING_PLACEHOLDER: '[data-region="event-list-loading-placeholder"]',
+ };
+
+ var TEMPLATES = {
+ EVENT_LIST_CONTENT: 'block_timeline/event-list-content'
+ };
+
+ // We want the paged content controls below the paged content area
+ // and the controls should be ignored while data is loading.
+ var DEFAULT_PAGED_CONTENT_CONFIG = {
+ ignoreControlWhileLoading: true,
+ controlPlacementBottom: true,
+ ariaLabels: {
+ itemsperpagecomponents: 'ariaeventlistpagelimit, block_timeline',
+ }
+ };
+
+ /**
+ * Hide the content area and display the empty content message.
+ *
+ * @param {object} root The container element
+ */
+ var hideContent = function(root) {
+ root.find(SELECTORS.EVENT_LIST_CONTENT).addClass('hidden');
+ root.find(SELECTORS.EMPTY_MESSAGE).removeClass('hidden');
+ };
+
+ /**
+ * Show the content area and hide the empty content message.
+ *
+ * @param {object} root The container element
+ */
+ var showContent = function(root) {
+ root.find(SELECTORS.EVENT_LIST_CONTENT).removeClass('hidden');
+ root.find(SELECTORS.EMPTY_MESSAGE).addClass('hidden');
+ };
+
+ /**
+ * Empty the content area.
+ *
+ * @param {object} root The container element
+ */
+ var emptyContent = function(root) {
+ root.find(SELECTORS.EVENT_LIST_CONTENT).empty();
+ };
+
+ /**
+ * Construct the template context from a list of calendar events. The events
+ * are grouped by which day they are on. The day is calculated from the user's
+ * midnight timestamp to ensure that the calculation is timezone agnostic.
+ *
+ * The return data structure will look like:
+ * {
+ * eventsbyday: [
+ * {
+ * dayTimestamp: 1533744000,
+ * events: [
+ * { ...event 1 data... },
+ * { ...event 2 data... }
+ * ]
+ * },
+ * {
+ * dayTimestamp: 1533830400,
+ * events: [
+ * { ...event 3 data... },
+ * { ...event 4 data... }
+ * ]
+ * }
+ * ]
+ * }
+ *
+ * Each day timestamp is the day's midnight in the user's timezone.
+ *
+ * @param {array} calendarEvents List of calendar events
+ * @param {Number} midnight A timestamp representing midnight in the user's timezone
+ * @return {object}
+ */
+ var buildTemplateContext = function(calendarEvents, midnight) {
+ var eventsByDay = {};
+ var templateContext = {
+ eventsbyday: []
+ };
+
+ calendarEvents.forEach(function(calendarEvent) {
+ var dayTimestamp = UserDate.getUserMidnightForTimestamp(calendarEvent.timesort, midnight);
+ if (eventsByDay[dayTimestamp]) {
+ eventsByDay[dayTimestamp].push(calendarEvent);
+ } else {
+ eventsByDay[dayTimestamp] = [calendarEvent];
+ }
+ });
+
+ Object.keys(eventsByDay).forEach(function(dayTimestamp) {
+ var events = eventsByDay[dayTimestamp];
+ templateContext.eventsbyday.push({
+ past: dayTimestamp < midnight,
+ dayTimestamp: dayTimestamp,
+ events: events
+ });
+ });
+
+ return templateContext;
+ };
+
+ /**
+ * Render the HTML for the given calendar events.
+ *
+ * @param {array} calendarEvents A list of calendar events
+ * @param {Number} midnight A timestamp representing midnight for the user
+ * @return {promise} Resolved with HTML and JS strings.
+ */
+ var render = function(calendarEvents, midnight) {
+ var templateContext = buildTemplateContext(calendarEvents, midnight);
+ var templateName = TEMPLATES.EVENT_LIST_CONTENT;
+
+ return Templates.render(templateName, templateContext);
+ };
+
+ /**
+ * Retrieve a list of calendar events from the server for the given
+ * constraints.
+ *
+ * @param {Number} midnight The user's midnight time in unix timestamp.
+ * @param {Number} limit Limit the result set to this number of items
+ * @param {Number} daysOffset How many days (from midnight) to offset the results from
+ * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to
+ * @param {int|falsey} lastId The ID of the last seen event (if any)
+ * @param {int|undefined} courseId Course ID to restrict events to
+ * @return {promise} A jquery promise
+ */
+ var load = function(midnight, limit, daysOffset, daysLimit, lastId, courseId) {
+ var startTime = midnight + (daysOffset * SECONDS_IN_DAY);
+ var endTime = daysLimit != undefined ? midnight + (daysLimit * SECONDS_IN_DAY) : false;
+
+ var args = {
+ starttime: startTime,
+ limit: limit,
+ };
+
+ if (lastId) {
+ args.aftereventid = lastId;
+ }
+
+ if (endTime) {
+ args.endtime = endTime;
+ }
+
+ if (courseId) {
+ // If we have a course id then we only want events from that course.
+ args.courseid = courseId;
+ return CalendarEventsRepository.queryByCourse(args);
+ } else {
+ // Otherwise we want events from any course.
+ return CalendarEventsRepository.queryByTime(args);
+ }
+ };
+
+ /**
+ * Handle a single page request from the paged content. Uses the given page data to request
+ * the events from the server.
+ *
+ * Checks the given preloadedPages before sending a request to the server to make sure we
+ * don't load data unnecessarily.
+ *
+ * @param {object} pageData A single page data (see core/paged_content_pages for more info).
+ * @param {object} actions Paged content actions (see core/paged_content_pages for more info).
+ * @param {Number} midnight The user's midnight time in unix timestamp.
+ * @param {object} lastIds The last event ID for each loaded page. Page number is key, id is value.
+ * @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value.
+ * @param {int|undefined} courseId Course ID to restrict events to
+ * @param {Number} daysOffset How many days (from midnight) to offset the results from
+ * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to
+ * @return {object} jQuery promise resolved with calendar events.
+ */
+ var loadEventsFromPageData = function(
+ pageData,
+ actions,
+ midnight,
+ lastIds,
+ preloadedPages,
+ courseId,
+ daysOffset,
+ daysLimit
+ ) {
+ var pageNumber = pageData.pageNumber;
+ var limit = pageData.limit;
+ var lastPageNumber = pageNumber;
+
+ // This is here to protect us if, for some reason, the pages
+ // are loaded out of order somehow and we don't have a reference
+ // to the previous page. In that case, scan back to find the most
+ // recent page we've seen.
+ while (!lastIds.hasOwnProperty(lastPageNumber)) {
+ lastPageNumber--;
+ }
+ // Use the last id of the most recent page.
+ var lastId = lastIds[lastPageNumber];
+ var eventsPromise = null;
+
+ if (preloadedPages && preloadedPages.hasOwnProperty(pageNumber)) {
+ // This page has been preloaded so use that rather than load the values
+ // again.
+ eventsPromise = preloadedPages[pageNumber];
+ } else {
+ // Load one more than the given limit so that we can tell if there
+ // is more content to load after this.
+ eventsPromise = load(midnight, limit + 1, daysOffset, daysLimit, lastId, courseId);
+ }
+
+ return eventsPromise.then(function(result) {
+ if (!result.events.length) {
+ // If we didn't get any events back then tell the paged content
+ // that we're done loading.
+ actions.allItemsLoaded(pageNumber);
+ return [];
+ }
+
+ var calendarEvents = result.events;
+ // We expect to receive limit + 1 events back from the server.
+ // Any less means there are no more events to load.
+ var loadedAll = calendarEvents.length <= limit;
+
+ if (loadedAll) {
+ // Tell the pagination that everything is loaded.
+ actions.allItemsLoaded(pageNumber);
+ } else {
+ // Remove the last element from the array because it isn't
+ // needed in this result set.
+ calendarEvents.pop();
+ }
+
+ return calendarEvents;
+ });
+ };
+
+ /**
+ * Use the paged content factory to create a paged content element for showing
+ * the event list. We only provide a page limit to the factory because we don't
+ * know exactly how many pages we'll need. This creates a paging bar with just
+ * next/previous buttons.
+ *
+ * This function specifies the callback for loading the event data that the user
+ * is requesting.
+ *
+ * @param {int|array} pageLimit A single limit or list of limits as options for the paged content
+ * @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value.
+ * @param {Number} midnight The user's midnight time in unix timestamp.
+ * @param {object} firstLoad A jQuery promise to be resolved after the first set of data is loaded.
+ * @param {int|undefined} courseId Course ID to restrict events to
+ * @param {Number} daysOffset How many days (from midnight) to offset the results from
+ * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to
+ * @param {string} paginationAriaLabel String to set as the aria label for the pagination bar.
+ * @return {object} jQuery promise.
+ */
+ var createPagedContent = function(
+ pageLimit,
+ preloadedPages,
+ midnight,
+ firstLoad,
+ courseId,
+ daysOffset,
+ daysLimit,
+ paginationAriaLabel
+ ) {
+ // Remember the last event id we loaded on each page because we can't
+ // use the offset value since the backend can skip events if the user doesn't
+ // have the capability to see them. Instead we load the next page of events
+ // based on the last seen event id.
+ var lastIds = {'1': 0};
+ var hasContent = false;
+ var config = $.extend({}, DEFAULT_PAGED_CONTENT_CONFIG);
+
+ return Str.get_string(
+ 'ariaeventlistpagelimit',
+ 'block_timeline',
+ $.isArray(pageLimit) ? pageLimit[0] : pageLimit
+ )
+ .then(function(string) {
+ config.ariaLabels.itemsperpage = string;
+ config.ariaLabels.paginationnav = paginationAriaLabel;
+ return string;
+ })
+ .then(function() {
+ return PagedContentFactory.createWithLimit(
+ pageLimit,
+ function(pagesData, actions) {
+ var promises = [];
+
+ pagesData.forEach(function(pageData) {
+ var pageNumber = pageData.pageNumber;
+ // Load the page data.
+ var pagePromise = loadEventsFromPageData(
+ pageData,
+ actions,
+ midnight,
+ lastIds,
+ preloadedPages,
+ courseId,
+ daysOffset,
+ daysLimit
+ ).then(function(calendarEvents) {
+ if (calendarEvents.length) {
+ // Remember that we've loaded content.
+ hasContent = true;
+ // Remember the last id we've seen.
+ var lastEventId = calendarEvents[calendarEvents.length - 1].id;
+ // Record the id that the next page will need to start from.
+ lastIds[pageNumber + 1] = lastEventId;
+ // Get the HTML and JS for these calendar events.
+ return render(calendarEvents, midnight);
+ } else {
+ return calendarEvents;
+ }
+ })
+ .catch(Notification.exception);
+
+ promises.push(pagePromise);
+ });
+
+ $.when.apply($, promises).then(function() {
+ // Tell the calling code that the first page has been loaded
+ // and whether it contains any content.
+ firstLoad.resolve(hasContent);
+ return;
+ })
+ .catch(function() {
+ firstLoad.resolve(hasContent);
+ });
+
+ return promises;
+ },
+ config
+ );
+ });
+ };
+
+ /**
+ * Create a paged content region for the calendar events in the given root element.
+ * The content of the root element are replaced with a new paged content section
+ * each time this function is called.
+ *
+ * This function will be called each time the offset or limit values are changed to
+ * reload the event list region.
+ *
+ * @param {object} root The event list container element
+ * @param {int|array} pageLimit A single limit or list of limits as options for the paged content
+ * @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value.
+ * @param {string} paginationAriaLabel String to set as the aria label for the pagination bar.
+ */
+ var init = function(root, pageLimit, preloadedPages, paginationAriaLabel) {
+ root = $(root);
+
+ // Create a promise that will be resolved once the first set of page
+ // data has been loaded. This ensures that the loading placeholder isn't
+ // hidden until we have all of the data back to prevent the page elements
+ // jumping around.
+ var firstLoad = $.Deferred();
+ var eventListContent = root.find(SELECTORS.EVENT_LIST_CONTENT);
+ var loadingPlaceholder = root.find(SELECTORS.EVENT_LIST_LOADING_PLACEHOLDER);
+ var courseId = root.attr('data-course-id');
+ var daysOffset = parseInt(root.attr('data-days-offset'), 10);
+ var daysLimit = root.attr('data-days-limit');
+ var midnight = parseInt(root.attr('data-midnight'), 10);
+
+ // Make sure the content area and loading placeholder is visible.
+ // This is because the init function can be called to re-initialise
+ // an existing event list area.
+ emptyContent(root);
+ showContent(root);
+ loadingPlaceholder.removeClass('hidden');
+
+ // Days limit isn't mandatory.
+ if (daysLimit != undefined) {
+ daysLimit = parseInt(daysLimit, 10);
+ }
+
+ // Created the paged content element.
+ createPagedContent(pageLimit, preloadedPages, midnight, firstLoad, courseId, daysOffset, daysLimit, paginationAriaLabel)
+ .then(function(html, js) {
+ html = $(html);
+ // Hide the content for now.
+ html.addClass('hidden');
+ // Replace existing elements with the newly created paged content.
+ // If we're reinitialising an existing event list this will replace
+ // the old event list (including removing any event handlers).
+ Templates.replaceNodeContents(eventListContent, html, js);
+
+ firstLoad.then(function(hasContent) {
+ // Prevent changing page elements too much by only showing the content
+ // once we've loaded some data for the first time. This allows our
+ // fancy loading placeholder to shine.
+ html.removeClass('hidden');
+ loadingPlaceholder.addClass('hidden');
+
+ if (!hasContent) {
+ // If we didn't get any data then show the empty data message.
+ hideContent(root);
+ }
+
+ return hasContent;
+ })
+ .catch(function() {
+ return false;
+ });
+
+ return html;
+ })
+ .catch(Notification.exception);
+ };
+
+ return {
+ init: init,
+ rootSelector: SELECTORS.ROOT,
+ };
+});
--- /dev/null
+// 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/>.
+
+/**
+ * Javascript to initialise the timeline block.
+ *
+ * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(
+[
+ 'jquery',
+ 'block_timeline/view_nav',
+ 'block_timeline/view'
+],
+function(
+ $,
+ ViewNav,
+ View
+) {
+
+ var SELECTORS = {
+ TIMELINE_VIEW: '[data-region="timeline-view"]'
+ };
+
+ /**
+ * Initialise all of the modules for the timeline block.
+ *
+ * @param {object} root The root element for the timeline block.
+ */
+ var init = function(root) {
+ root = $(root);
+ var viewRoot = root.find(SELECTORS.TIMELINE_VIEW);
+
+ // Initialise the timeline navigation elements.
+ ViewNav.init(root, viewRoot);
+ // Initialise the timeline view modules.
+ View.init(viewRoot);
+ };
+
+ return {
+ init: init
+ };
+});
--- /dev/null
+// 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/>.
+
+/**
+ * Manage the timeline view for the timeline block.
+ *
+ * @package block_timeline
+ * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(
+[
+ 'jquery',
+ 'block_timeline/view_dates',
+ 'block_timeline/view_courses',
+],
+function(
+ $,
+ ViewDates,
+ ViewCourses
+) {
+
+ var SELECTORS = {
+ TIMELINE_DATES_VIEW: '[data-region="view-dates"]',
+ TIMELINE_COURSES_VIEW: '[data-region="view-courses"]',
+ };
+
+ /**
+ * Intialise the timeline dates and courses views on page load.
+ * This function should only be called once per page load because
+ * it can cause event listeners to be added to the page.
+ *
+ * @param {object} root The root element for the timeline view.
+ */
+ var init = function(root) {
+ root = $(root);
+ var datesViewRoot = root.find(SELECTORS.TIMELINE_DATES_VIEW);
+ var coursesViewRoot = root.find(SELECTORS.TIMELINE_COURSES_VIEW);
+
+ ViewDates.init(datesViewRoot);
+ ViewCourses.init(coursesViewRoot);
+ };
+
+ /**
+ * Reset the timeline dates and courses views to their original
+ * state on first page load.
+ *
+ * This is called when configuration has changed for the event lists
+ * to cause them to reload their data.
+ *
+ * @param {object} root The root element for the timeline view.
+ */
+ var reset = function(root) {
+ var datesViewRoot = root.find(SELECTORS.TIMELINE_DATES_VIEW);
+ var coursesViewRoot = root.find(SELECTORS.TIMELINE_COURSES_VIEW);
+ ViewDates.reset(datesViewRoot);
+ ViewCourses.reset(coursesViewRoot);
+ };
+
+ /**
+ * Tell the timeline dates or courses view that it has been displayed.
+ *
+ * This is called each time one of the views is displayed and is used to
+ * lazy load the data within it on first load.
+ *
+ * @param {object} root The root element for the timeline view.
+ */
+ var shown = function(root) {
+ var datesViewRoot = root.find(SELECTORS.TIMELINE_DATES_VIEW);
+ var coursesViewRoot = root.find(SELECTORS.TIMELINE_COURSES_VIEW);
+
+ if (datesViewRoot.hasClass('active')) {
+ ViewDates.shown(datesViewRoot);
+ } else {
+ ViewCourses.shown(coursesViewRoot);
+ }
+ };
+
+ return {
+ init: init,
+ reset: reset,
+ shown: shown,
+ };
+});
--- /dev/null
+// 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/>.
+
+/**
+ * Manage the timeline courses view for the timeline block.
+ *
+ * @package block_timeline
+ * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(
+[
+ 'jquery',
+ 'core/notification',
+ 'core/custom_interaction_events',
+ 'core/str',
+ 'core/templates',
+ 'block_timeline/event_list',
+ 'core_course/repository',
+ 'block_timeline/calendar_events_repository'
+],
+function(
+ $,
+ Notification,
+ CustomEvents,
+ Str,
+ Templates,
+ EventList,
+ CourseRepository,
+ EventsRepository
+) {
+
+ var SELECTORS = {
+ MORE_COURSES_BUTTON: '[data-action="more-courses"]',
+ MORE_COURSES_BUTTON_CONTAINER: '[data-region="more-courses-button-container"]',
+ NO_COURSES_EMPTY_MESSAGE: '[data-region="no-courses-empty-message"]',
+ COURSES_LIST: '[data-region="courses-list"]',
+ COURSE_ITEMS_LOADING_PLACEHOLDER: '[data-region="course-items-loading-placeholder"]',
+ COURSE_EVENTS_CONTAINER: '[data-region="course-events-container"]',
+ COURSE_NAME: '[data-region="course-name"]',
+ LOADING_ICON: '.loading-icon'
+ };
+
+ var TEMPLATES = {
+ COURSE_ITEMS: 'block_timeline/course-items',
+ LOADING_ICON: 'core/loading'
+ };
+
+ var COURSE_CLASSIFICATION = 'inprogress';
+ var COURSE_SORT = 'fullname asc';
+ var COURSE_EVENT_LIMIT = 5;
+ var COURSE_LIMIT = 2;
+ var SECONDS_IN_DAY = 60 * 60 * 24;
+
+ /**
+ * Hide the loading placeholder elements.
+ *
+ * @param {object} root The rool element.
+ */
+ var hideLoadingPlaceholder = function(root) {
+ root.find(SELECTORS.COURSE_ITEMS_LOADING_PLACEHOLDER).addClass('hidden');
+ };
+
+ /**
+ * Hide the "more courses" button.
+ *
+ * @param {object} root The rool element.
+ */
+ var hideMoreCoursesButton = function(root) {
+ root.find(SELECTORS.MORE_COURSES_BUTTON_CONTAINER).addClass('hidden');
+ };
+
+ /**
+ * Show the "more courses" button.
+ *
+ * @param {object} root The rool element.
+ */
+ var showMoreCoursesButton = function(root) {
+ root.find(SELECTORS.MORE_COURSES_BUTTON_CONTAINER).removeClass('hidden');
+ };
+
+ /**
+ * Disable the "more courses" button and show the loading spinner.
+ *
+ * @param {object} root The rool element.
+ */
+ var enableMoreCoursesButtonLoading = function(root) {
+ var button = root.find(SELECTORS.MORE_COURSES_BUTTON);
+ button.prop('disabled', true);
+ Templates.render(TEMPLATES.LOADING_ICON, {})
+ .then(function(html) {
+ button.append(html);
+ return html;
+ })
+ .catch(function() {
+ // It's not important if this false so just do so silently.
+ return false;
+ });
+ };
+
+ /**
+ * Enable the "more courses" button and remove the loading spinner.
+ *
+ * @param {object} root The rool element.
+ */
+ var disableMoreCoursesButtonLoading = function(root) {
+ var button = root.find(SELECTORS.MORE_COURSES_BUTTON);
+ button.prop('disabled', false);
+ button.find(SELECTORS.LOADING_ICON).remove();
+ };
+
+ /**
+ * Display the message for when there are no courses available.
+ *
+ * @param {object} root The rool element.
+ */
+ var showNoCoursesEmptyMessage = function(root) {
+ root.find(SELECTORS.NO_COURSES_EMPTY_MESSAGE).removeClass('hidden');
+ };
+
+ /**
+ * Render the course items HTML to the page.
+ *
+ * @param {object} root The rool element.
+ * @param {string} html The course items HTML to render.
+ */
+ var renderCourseItemsHTML = function(root, html) {
+ var container = root.find(SELECTORS.COURSES_LIST);
+ Templates.appendNodeContents(container, html, '');
+ };
+
+ /**
+ * Check if any courses have been loaded.
+ *
+ * @param {object} root The rool element.
+ * @return {bool}
+ */
+ var hasLoadedCourses = function(root) {
+ return root.find(SELECTORS.COURSE_EVENTS_CONTAINER).length > 0;
+ };
+
+ /**
+ * Return the offset value for fetching courses.
+ *
+ * @param {object} root The rool element.
+ * @return {Number}
+ */
+ var getOffset = function(root) {
+ return parseInt(root.attr('data-offset'), 10);
+ };
+
+ /**
+ * Set the offset value for fetching courses.
+ *
+ * @param {object} root The rool element.
+ * @param {Number} offset Offset value.
+ */
+ var setOffset = function(root, offset) {
+ root.attr('data-offset', offset);
+ };
+
+ /**
+ * Return the limit value for fetching courses.
+ *
+ * @param {object} root The rool element.
+ * @return {Number}
+ */
+ var getLimit = function(root) {
+ return parseInt(root.attr('data-limit'), 10);
+ };
+
+ /**
+ * Return the days offset value for fetching events.
+ *
+ * @param {object} root The rool element.
+ * @return {Number}
+ */
+ var getDaysOffset = function(root) {
+ return parseInt(root.attr('data-days-offset'), 10);
+ };
+
+ /**
+ * Return the days limit value for fetching events. The days
+ * limit is optional so undefined will be returned if it isn't
+ * set.
+ *
+ * @param {object} root The rool element.
+ * @return {int|undefined}
+ */
+ var getDaysLimit = function(root) {
+ var daysLimit = root.attr('data-days-limit');
+ return daysLimit != undefined ? parseInt(daysLimit, 10) : undefined;
+ };
+
+ /**
+ * Return the timestamp for the user's midnight.
+ *
+ * @param {object} root The rool element.
+ * @return {Number}
+ */
+ var getMidnight = function(root) {
+ return parseInt(root.attr('data-midnight'), 10);
+ };
+
+ /**
+ * Return the start time for fetching events. This is calculated
+ * based on the user's midnight value so that timezones are
+ * preserved.
+ *
+ * @param {object} root The rool element.
+ * @return {Number}
+ */
+ var getStartTime = function(root) {
+ var midnight = getMidnight(root);
+ var daysOffset = getDaysOffset(root);
+ return midnight + (daysOffset * SECONDS_IN_DAY);
+ };
+
+ /**
+ * Return the end time for fetching events. This is calculated
+ * based on the user's midnight value so that timezones are
+ * preserved.
+ *
+ * @param {object} root The rool element.
+ * @return {Number}
+ */
+ var getEndTime = function(root) {
+ var midnight = getMidnight(root);
+ var daysLimit = getDaysLimit(root);
+ return daysLimit != undefined ? midnight + (daysLimit * SECONDS_IN_DAY) : false;
+ };
+
+ /**
+ * Get a list of events for the given course ids. Returns a promise that will
+ * be resolved with the events.
+ *
+ * @param {array} courseIds The list of course ids to fetch events for.
+ * @param {Number} startTime Timestamp to fetch events from.
+ * @param {Number} limit Limit to the number of events (this applies per course, not total)
+ * @param {Number} endTime Timestamp to fetch events to.
+ * @return {object} jQuery promise.
+ */
+ var getEventsForCourseIds = function(courseIds, startTime, limit, endTime) {
+ var args = {
+ courseids: courseIds,
+ starttime: startTime,
+ limit: limit
+ };
+
+ if (endTime) {
+ args.endtime = endTime;
+ }
+
+ return EventsRepository.queryByCourses(args);
+ };
+
+ /**
+ * Get the last time the events were reloaded.
+ *
+ * @param {object} root The rool element.
+ * @return {Number}
+ */
+ var getEventReloadTime = function(root) {
+ return root.data('last-event-load-time');
+ };
+
+ /**
+ * Set the last time the events were reloaded.
+ *
+ * @param {object} root The rool element.
+ * @param {Number} time Timestamp in milliseconds.
+ */
+ var setEventReloadTime = function(root, time) {
+ root.data('last-event-load-time', time);
+ };
+
+ /**
+ * Check if events have begun reloading since the given
+ * time.
+ *
+ * @param {object} root The rool element.
+ * @param {Number} time Timestamp in milliseconds.
+ * @return {bool}
+ */
+ var hasReloadedEventsSince = function(root, time) {
+ return getEventReloadTime(root) > time;
+ };
+
+ /**
+ * Send a request to the server to load the events for the courses.
+ *
+ * @param {array} courses List of course objects.
+ * @param {Number} startTime Timestamp to load events after.
+ * @param {int|undefined} endTime Timestamp to load events up until.
+ * @return {object} jQuery promise resolved with the events.
+ */
+ var loadEventsForCourses = function(courses, startTime, endTime) {
+ var courseIds = courses.map(function(course) {
+ return course.id;
+ });
+
+ return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime);
+ };
+
+ /**
+ * Render the courses in the DOM once the server has returned the courses.
+ *
+ * @param {array} courses List of course objects.
+ * @param {object} root The root element
+ * @param {Number} midnight The midnight timestamp in the user's timezone.
+ * @param {Number} daysOffset Number of days from today to offset the events.
+ * @param {Number} daysLimit Number of days from today to limit the events to.
+ * @param {string} noEventsURL URL for the image to display for no events.
+ * @return {object} jQuery promise resolved after rendering is complete.
+ */
+ var updateDisplayFromCourses = function(courses, root, midnight, daysOffset, daysLimit, noEventsURL) {
+ // Render the courses template.
+ return Templates.render(TEMPLATES.COURSE_ITEMS, {
+ courses: courses,
+ midnight: midnight,
+ hasdaysoffset: true,
+ hasdayslimit: daysLimit != undefined,
+ daysoffset: daysOffset,
+ dayslimit: daysLimit,
+ nodayslimit: daysLimit == undefined,
+ urls: {
+ noevents: noEventsURL
+ }
+ }).then(function(html) {
+ hideLoadingPlaceholder(root);
+
+ if (html) {
+ // Template rendering is complete and we have the HTML so we can
+ // add it to the DOM.
+ renderCourseItemsHTML(root, html);
+ } else {
+ if (!hasLoadedCourses(root)) {
+ // There were no courses to render so show the empty placeholder
+ // message for the user to tell them.
+ showNoCoursesEmptyMessage(root);
+ }
+ }
+
+ return html;
+ })
+ .then(function(html) {
+ if (courses.length < COURSE_LIMIT) {
+ // We know there aren't any more courses because we got back less
+ // than we asked for so hide the button to request more.
+ hideMoreCoursesButton(root);
+ } else {
+ // Make sure the button is visible if there are more courses to load.
+ showMoreCoursesButton(root);
+ }
+
+ return html;
+ })
+ .catch(function() {
+ hideLoadingPlaceholder(root);
+ });
+ };
+
+ /**
+ * Find all of the visible course blocks and initialise the event
+ * list module to being loading the events for the course block.
+ *
+ * @param {object} root The root element for the timeline courses view.
+ * @return {object} jQuery promise resolved with courses and events.
+ */
+ var loadMoreCourses = function(root) {
+ var offset = getOffset(root);
+ var limit = getLimit(root);
+
+ // Start loading the next set of courses.
+ return CourseRepository.getEnrolledCoursesByTimelineClassification(
+ COURSE_CLASSIFICATION,
+ limit,
+ offset,
+ COURSE_SORT
+ ).then(function(result) {
+ var startEventLoadingTime = Date.now();
+ var courses = result.courses;
+ var nextOffset = result.nextoffset;
+ var daysOffset = getDaysOffset(root);
+ var daysLimit = getDaysLimit(root);
+ var midnight = getMidnight(root);
+ var startTime = getStartTime(root);
+ var endTime = getEndTime(root);
+ var noEventsURL = root.attr('data-no-events-url');
+ // Record the next offset if we want to request more courses.
+ setOffset(root, nextOffset);
+ // Load the events for these courses.
+ var eventsPromise = loadEventsForCourses(courses, startTime, endTime);
+ // Render the courses in the DOM.
+ var renderPromise = updateDisplayFromCourses(courses, root, midnight, daysOffset, daysLimit, noEventsURL);
+
+ return $.when(eventsPromise, renderPromise)
+ .then(function(eventsByCourse) {
+ if (hasReloadedEventsSince(root, startEventLoadingTime)) {
+ // All of the events are being reloaded so ignore our results.
+ return eventsByCourse;
+ }
+
+ // When we've got all of the courses and events we can render the events in the
+ // correct course event list.
+ courses.forEach(function(course) {
+ var courseId = course.id;
+ var events = [];
+ var containerSelector = '[data-region="course-events-container"][data-course-id="' + courseId + '"]';
+ var courseEventsContainer = root.find(containerSelector);
+ var eventListRoot = courseEventsContainer.find(EventList.rootSelector);
+ var courseGroups = eventsByCourse.groupedbycourse.filter(function(group) {
+ return group.courseid == courseId;
+ });
+
+ if (courseGroups.length) {
+ // Get the events for this course.
+ events = courseGroups[0].events;
+ }
+
+ // Create a preloaded page to pass to the event list because we've already
+ // loaded the first page of events.
+ var pageOnePreload = $.Deferred().resolve({events: events}).promise();
+ // Initialise the event list pagination area for this course.
+ Str.get_string('ariaeventlistpaginationnavcourses', 'block_timeline', course.fullnamedisplay)
+ .then(function(string) {
+ EventList.init(eventListRoot, COURSE_EVENT_LIMIT, {'1': pageOnePreload}, string);
+ return string;
+ })
+ .catch(function() {
+ // An error is ok, just render with the default string.
+ EventList.init(eventListRoot, COURSE_EVENT_LIMIT, {'1': pageOnePreload});
+ });
+ });
+
+ return eventsByCourse;
+ });
+ }).catch(Notification.exception);
+ };
+
+ /**
+ * Reload the events for all of the visible courses. These events will be loaded
+ * in a single request to the server.
+ *
+ * @param {object} root The root element.
+ * @return {object} jQuery promise resolved with courses and events.
+ */
+ var reloadCourseEvents = function(root) {
+ var startReloadTime = Date.now();
+ var startTime = getStartTime(root);
+ var endTime = getEndTime(root);
+ var courseEventsContainers = root.find(SELECTORS.COURSE_EVENTS_CONTAINER);
+ var courseIds = courseEventsContainers.map(function() {
+ return $(this).attr('data-course-id');
+ }).get();
+
+ // Record when we started our request.
+ setEventReloadTime(root, startReloadTime);
+
+ // Load all of the events for the given courses.
+ return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime)
+ .then(function(eventsByCourse) {
+ if (hasReloadedEventsSince(root, startReloadTime)) {
+ // A new reload has begun so ignore our results.
+ return eventsByCourse;
+ }
+
+ courseEventsContainers.each(function(index, container) {
+ container = $(container);
+ var courseId = container.attr('data-course-id');
+ var courseName = container.find(SELECTORS.COURSE_NAME).text();
+ var eventListContainer = container.find(EventList.rootSelector);
+ var pageDeferred = $.Deferred();
+ var events = [];
+ var courseGroups = eventsByCourse.groupedbycourse.filter(function(group) {
+ return group.courseid == courseId;
+ });
+
+ if (courseGroups.length) {
+ // Get the events just for this course.
+ events = courseGroups[0].events;
+ }
+
+ pageDeferred.resolve({events: events});
+
+ // Re-initialise the events list with the preloaded events we just got from
+ // the server.
+ Str.get_string('ariaeventlistpaginationnavcourses', 'block_timeline', courseName)
+ .then(function(string) {
+ EventList.init(eventListContainer, COURSE_EVENT_LIMIT, {'1': pageDeferred.promise()}, string);
+ return string;
+ })
+ .catch(function() {
+ // Ignore a failure to load the string. Just render with the default string.
+ EventList.init(eventListContainer, COURSE_EVENT_LIMIT, {'1': pageDeferred.promise()});
+ });
+ });
+
+ return eventsByCourse;
+ }).catch(Notification.exception);
+ };
+
+ /**
+ * Add event listeners to load more courses for the courses view.
+ *
+ * @param {object} root The root element for the timeline courses view.
+ */
+ var registerEventListeners = function(root) {
+ CustomEvents.define(root, [CustomEvents.events.activate]);
+ // Show more courses and load their events when the user clicks the "more courses"
+ // button.
+ root.on(CustomEvents.events.activate, SELECTORS.MORE_COURSES_BUTTON, function(e, data) {
+ enableMoreCoursesButtonLoading(root);
+ loadMoreCourses(root)
+ .then(function() {
+ disableMoreCoursesButtonLoading(root);
+ return;
+ })
+ .catch(function() {
+ disableMoreCoursesButtonLoading(root);
+ });
+
+ if (data) {
+ data.originalEvent.preventDefault();
+ data.originalEvent.stopPropagation();
+ }
+ e.stopPropagation();
+ });
+ };
+
+ /**
+ * Initialise the timeline courses view. Begin loading the events
+ * if this view is active. Add the relevant event listeners.
+ *
+ * This function should only be called once per page load because it
+ * is adding event listeners to the page.
+ *
+ * @param {object} root The root element for the timeline courses view.
+ */
+ var init = function(root) {
+ root = $(root);
+
+ setEventReloadTime(root, Date.now());
+
+ if (root.hasClass('active')) {
+ // Only load if this is active otherwise it will be lazy loaded later.
+ loadMoreCourses(root);
+ root.attr('data-seen', true);
+ }
+
+ registerEventListeners(root);
+ };
+
+ /**
+ * Reset the element back to it's initial state. Begin loading the events again
+ * if this view is active.
+ *
+ * @param {object} root The root element for the timeline courses view.
+ */
+ var reset = function(root) {
+ root.removeAttr('data-seen');
+ if (root.hasClass('active')) {
+ shown(root);
+ }
+ };
+
+ /**
+ * If this is the first time this view has been displayed then begin loading
+ * the events.
+ *
+ * @param {object} root The root element for the timeline courses view.
+ */
+ var shown = function(root) {
+ if (!root.attr('data-seen')) {
+ if (hasLoadedCourses(root)) {
+ // This isn't the first time this view is shown so just reload the
+ // events for the courses we've already loaded.
+ reloadCourseEvents(root);
+ } else {
+ // We haven't loaded any courses yet so do that now.
+ loadMoreCourses(root);
+ }
+
+ root.attr('data-seen', true);
+ }
+ };
+
+ return {
+ init: init,
+ reset: reset,
+ shown: shown
+ };
+});
--- /dev/null
+// 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/>.
+
+/**
+ * Manage the timeline dates view for the timeline block.
+ *
+ * @package block_timeline
+ * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(
+[
+ 'jquery',
+ 'core/str',
+ 'block_timeline/event_list'
+],
+function(
+ $,
+ Str,
+ EventList
+) {
+
+ var SELECTORS = {
+ EVENT_LIST_CONTAINER: '[data-region="event-list-container"]',
+ };
+
+ /**
+ * Initialise the event list and being loading the events.
+ *
+ * @param {object} root The root element for the timeline dates view.
+ */
+ var load = function(root) {
+ var eventListContainer = root.find(SELECTORS.EVENT_LIST_CONTAINER);
+ Str.get_string('ariaeventlistpaginationnavdates', 'block_timeline')
+ .then(function(string) {
+ EventList.init(eventListContainer, [5, 10, 25], {}, string);
+ return string;
+ })
+ .catch(function() {
+ // Ignore if we can't load the string. Still init the event list.
+ EventList.init(eventListContainer, [5, 10, 25]);
+ });
+ };
+
+ /**
+ * Initialise the timeline dates view. Begin loading the events
+ * if this view is active.
+ *
+ * @param {object} root The root element for the timeline courses view.
+ */
+ var init = function(root) {
+ root = $(root);
+ if (root.hasClass('active')) {
+ load(root);
+ root.attr('data-seen', true);
+ }
+ };
+
+ /**
+ * Reset the view back to it's initial state. If this view is active then
+ * beging loading the events.
+ *
+ * @param {object} root The root element for the timeline courses view.
+ */
+ var reset = function(root) {
+ root.removeAttr('data-seen');
+ if (root.hasClass('active')) {
+ load(root);
+ root.attr('data-seen', true);
+ }
+ };
+
+ /**
+ * Load the events if this is the first time the view is displayed.
+ *
+ * @param {object} root The root element for the timeline courses view.
+ */
+ var shown = function(root) {
+ if (!root.attr('data-seen')) {
+ load(root);
+ root.attr('data-seen', true);
+ }
+ };
+
+ return {
+ init: init,
+ reset: reset,
+ shown: shown
+ };
+});
--- /dev/null
+// 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/>.
+
+/**
+ * Manage the timeline view navigation for the timeline block.
+ *
+ * @package block_timeline
+ * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(
+[
+ 'jquery',
+ 'core/custom_interaction_events',
+ 'block_timeline/view'
+],
+function(
+ $,
+ CustomEvents,
+ View
+) {
+
+ var SELECTORS = {
+ TIMELINE_DAY_FILTER: '[data-region="day-filter"]',
+ TIMELINE_DAY_FILTER_OPTION: '[data-from]',
+ TIMELINE_VIEW_SELECTOR: '[data-region="view-selector"]',
+ DATA_DAYS_OFFSET: '[data-days-offset]',
+ DATA_DAYS_LIMIT: '[data-days-limit]',
+ };
+
+ /**
+ * Event listener for the day selector ("Next 7 days", "Next 30 days", etc).
+ *
+ * @param {object} root The root element for the timeline block
+ * @param {object} timelineViewRoot The root element for the timeline view
+ */
+ var registerTimelineDaySelector = function(root, timelineViewRoot) {
+ var timelineDaySelectorContainer = root.find(SELECTORS.TIMELINE_DAY_FILTER);
+
+ CustomEvents.define(timelineDaySelectorContainer, [CustomEvents.events.activate]);
+ timelineDaySelectorContainer.on(
+ CustomEvents.events.activate,
+ SELECTORS.TIMELINE_DAY_FILTER_OPTION,
+ function(e, data) {
+ var option = $(e.target).closest(SELECTORS.TIMELINE_DAY_FILTER_OPTION);
+
+ if (option.hasClass('active')) {
+ // If it's already active then we don't need to do anything.
+ return;
+ }
+
+ var daysOffset = option.attr('data-from');
+ var daysLimit = option.attr('data-to');
+ var elementsWithDaysOffset = root.find(SELECTORS.DATA_DAYS_OFFSET);
+
+ elementsWithDaysOffset.attr('data-days-offset', daysOffset);
+
+ if (daysLimit != undefined) {
+ elementsWithDaysOffset.attr('data-days-limit', daysLimit);
+ } else {
+ elementsWithDaysOffset.removeAttr('data-days-limit');
+ }
+
+ // Reset the views to reinitialise the event lists now that we've
+ // updated the day limits.
+ View.reset(timelineViewRoot);
+
+ data.originalEvent.preventDefault();
+ }
+ );
+ };
+
+ /**
+ * Event listener for the "sort" button in the timeline navigation that allows for
+ * changing between the timeline dates and courses views.
+ *
+ * On a view change we tell the timeline view module that the view has been shown
+ * so that it can handle how to display the appropriate view.
+ *
+ * @param {object} root The root element for the timeline block
+ * @param {object} timelineViewRoot The root element for the timeline view
+ */
+ var registerViewSelector = function(root, timelineViewRoot) {
+ // Listen for when the user changes tab so that we can show the first set of courses
+ // and load their events when they request the sort by courses view for the first time.
+ root.find(SELECTORS.TIMELINE_VIEW_SELECTOR).on('shown shown.bs.tab', function() {
+ View.shown(timelineViewRoot);
+ });
+ };
+
+ /**
+ * Initialise the timeline view navigation by adding event listeners to
+ * the navigation elements.
+ *
+ * @param {object} root The root element for the timeline block
+ * @param {object} timelineViewRoot The root element for the timeline view
+ */
+ var init = function(root, timelineViewRoot) {
+ root = $(root);
+ registerTimelineDaySelector(root, timelineViewRoot);
+ registerViewSelector(root, timelineViewRoot);
+ };
+
+ return {
+ init: init
+ };
+});
--- /dev/null
+<?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/>.
+
+/**
+ * Contains the class for the Timeline block.
+ *
+ * @package block_timeline
+ * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Timeline block class.
+ *
+ * @package block_timeline
+ * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block_timeline extends block_base {
+
+ /**
+ * Init.
+ */
+ public function init() {
+ $this->title = get_string('pluginname', 'block_timeline');
+ }
+
+ /**
+ * Returns the contents.
+ *
+ * @return stdClass contents of block
+ */
+ public function get_content() {
+ if (isset($this->content)) {
+ return $this->content;
+ }
+
+ $renderable = new \block_timeline\output\main();
+ $renderer = $this->page->get_renderer('block_timeline');
+
+ $this->content = (object) [
+ 'text' => $renderer->render($renderable),
+ 'footer' => ''
+ ];
+
+ return $this->content;
+ }
+
+ /**
+ * Locations where block can be displayed.
+ *
+ * @return array
+ */
+ public function applicable_formats() {
+ return array('my' => true);
+ }
+}
--- /dev/null
+<?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/>.
+
+/**
+ * Class containing data for timeline block.
+ *
+ * @package block_timeline
+ * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace block_timeline\output;
+defined('MOODLE_INTERNAL') || die();
+
+use renderable;
+use renderer_base;
+use templatable;
+use core_course\external\course_summary_exporter;
+
+require_once($CFG->dirroot . '/course/lib.php');
+require_once($CFG->libdir . '/completionlib.php');
+
+/**
+ * Class containing data for timeline block.
+ *
+ * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class main implements renderable, templatable {
+
+ /** Number of courses to load per page */
+ const COURSES_PER_PAGE = 2;
+
+ /**
+ * Export this data so it can be used as the context for a mustache template.
+ *
+ * @param \renderer_base $output
+ * @return stdClass
+ */
+ public function export_for_template(renderer_base $output) {
+
+ $nocoursesurl = $output->image_url('courses', 'block_timeline')->out();
+ $noeventsurl = $output->image_url('activities', 'block_timeline')->out();
+
+ $requiredproperties = course_summary_exporter::define_properties();
+ $fields = join(',', array_keys($requiredproperties));
+ $courses = course_get_enrolled_courses_for_logged_in_user(0, 0, null, $fields);
+ list($inprogresscourses, $processedcount) = course_filter_courses_by_timeline_classification(
+ $courses,
+ COURSE_TIMELINE_INPROGRESS,
+ self::COURSES_PER_PAGE
+ );
+ $formattedcourses = array_map(function($course) use ($output) {
+ \context_helper::preload_from_record($course);
+ $context = \context_course::instance($course->id);
+ $exporter = new course_summary_exporter($course, ['context' => $context]);
+ return $exporter->export($output);
+ }, $inprogresscourses);
+
+ return [
+ 'midnight' => usergetmidnight(time()),
+ 'coursepages' => [$formattedcourses],
+ 'urls' => [
+ 'nocourses' => $nocoursesurl,
+ 'noevents' => $noeventsurl
+ ]
+ ];
+ }
+}
--- /dev/null
+<?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/>.
+
+/**
+ * Timeline block rendrer.
+ *
+ * @package block_timeline
+ * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace block_timeline\output;
+defined('MOODLE_INTERNAL') || die;
+
+use plugin_renderer_base;
+use renderable;
+
+/**
+ * Timeline block renderer.
+ *
+ * @package block_timeline
+ * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends plugin_renderer_base {
+
+ /**
+ * Return the main content for the block timeline.
+ *
+ * @param main $main The main renderable
+ * @return string HTML string
+ */
+ public function render_main(main $main) {
+ return $this->render_from_template('block_timeline/main', $main->export_for_template($this));
+ }
+}
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
- * Contains functions called by core.
+ * Privacy Subsystem implementation for block_timeline.
*
- * @package block_myoverview
- * @copyright 2017 Mark Nelson <markn@moodle.com>
+ * @package block_timeline
+ * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-defined('MOODLE_INTERNAL') || die();
-
-/**
- * The timeline view.
- */
-define('BLOCK_MYOVERVIEW_TIMELINE_VIEW', 'timeline');
+namespace block_timeline\privacy;
-/**
- * The courses view.
- */
-define('BLOCK_MYOVERVIEW_COURSES_VIEW', 'courses');
+defined('MOODLE_INTERNAL') || die();
/**
- * Returns the name of the user preferences as well as the details this plugin uses.
+ * Privacy Subsystem for block_timeline.
*
- * @return array
+ * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-function block_myoverview_user_preferences() {
- $preferences = array();
- $preferences['block_myoverview_last_tab'] = array(
- 'type' => PARAM_ALPHA,
- 'null' => NULL_NOT_ALLOWED,
- 'default' => BLOCK_MYOVERVIEW_TIMELINE_VIEW,
- 'choices' => array(BLOCK_MYOVERVIEW_TIMELINE_VIEW, BLOCK_MYOVERVIEW_COURSES_VIEW)
- );
+class provider implements \core_privacy\local\metadata\null_provider {
- return $preferences;
+ /**
+ * Get the language string identifier with the component's language
+ * file to explain why this plugin stores no data.
+ *
+ * @return string
+ */
+ public static function get_reason() : string {
+ return 'privacy:metadata';
+ }
}
--- /dev/null
+<?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/>.
+
+/**
+ * Capabilities for the timeline block.
+ *
+ * @package block_timeline
+ * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$capabilities = array(
+
+ 'block/timeline:myaddinstance' => array(
+ 'captype' => 'write',
+ 'contextlevel' => CONTEXT_SYSTEM,
+ 'archetypes' => array(
+ 'user' => CAP_ALLOW
+ ),
+
+ 'clonepermissionsfrom' => 'moodle/my:manageblocks'
+ ),
+
+ 'block/timeline:addinstance' => array(
+ 'riskbitmask' => RISK_SPAM | RISK_XSS,
+
+ 'captype' => 'write',
+ 'contextlevel' => CONTEXT_BLOCK,
+ 'archetypes' => array(
+ 'manager' => CAP_ALLOW
+ ),
+
+ 'clonepermissionsfrom' => 'moodle/site:manageblocks'
+ )
+);
--- /dev/null
+<?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/>.
+
+/**
+ * Timeline block installation.
+ *
+ * @package block_timeline
+ * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+ /**
+ * Add the timeline block to the dashboard for all users by default
+ * when it is installed.
+ */
+function xmldb_block_timeline_install() {
+ global $DB;
+
+ if ($DB->count_records('block_instances') < 1) {
+ // Only add the timeline block if it's being installed on an existing site.
+ // For new sites it will be added by blocks_add_default_system_blocks().
+ return;
+ }
+
+ if ($defaultmypage = $DB->get_record('my_pages', array('userid' => null, 'name' => '__default', 'private' => 1))) {
+ $subpagepattern = $defaultmypage->id;
+ } else {
+ $subpagepattern = null;
+ }
+
+ $page = new moodle_page();
+ $systemcontext = context_system::instance();
+ $page->set_context($systemcontext);
+ // Add the block to the default /my.
+ $page->blocks->add_region(BLOCK_POS_RIGHT);
+ $page->blocks->add_block('timeline', BLOCK_POS_RIGHT, 0, false, 'my-index', $subpagepattern);
+
+ // Now we need to find all users that have viewed their dashboard because it'll have
+ // made duplicates of the default block_instances for them so they won't see the new
+ // timeline block without the admin resetting all of the dashboards.
+ //
+ // Instead we'll just add the timeline block to their dashboards here. We will only
+ // add the timeline block if they still have the myoverview block.
+ $sql = "SELECT parentcontextid, subpagepattern
+ FROM {block_instances}
+ WHERE pagetypepattern = 'my-index'
+ AND blockname = 'myoverview'
+ AND parentcontextid != ?";
+ $params = [$systemcontext->id];
+ $existingrecords = $DB->get_recordset_sql($sql, $params);
+ $blockinstances = [];
+ $seencontexts = [];
+ $now = time();
+
+ foreach ($existingrecords as $existingrecord) {
+ $parentcontextid = $existingrecord->parentcontextid;
+ if (isset($seencontexts[$parentcontextid])) {
+ // If we've seen this context already then skip it because we don't want
+ // to add duplicate timeline blocks to the same context. This happens
+ // if something funny is going on with the subpagepattern.
+ continue;
+ } else {
+ $seencontexts[$parentcontextid] = true;
+ }
+
+ $blockinstances[] = [
+ 'blockname' => 'timeline',
+ 'parentcontextid' => $parentcontextid,
+ 'showinsubcontexts' => false,
+ 'pagetypepattern' => 'my-index',
+ 'subpagepattern' => $existingrecord->subpagepattern,
+ 'defaultregion' => BLOCK_POS_RIGHT,
+ 'defaultweight' => 0,
+ 'configdata' => '',
+ 'timecreated' => $now,
+ 'timemodified' => $now,
+ ];
+
+ if (count($blockinstances) >= 1000) {
+ // Insert after every 1000 records so that the memory usage doesn't
+ // get out of control.
+ $DB->insert_records('block_instances', $blockinstances);
+ $blockinstances = [];
+ }
+ }
+
+ $existingrecords->close();
+
+ if (!empty($blockinstances)) {
+ // Insert what ever is left over.
+ $DB->insert_records('block_instances', $blockinstances);
+ }
+}
--- /dev/null
+<?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/>.
+
+/**
+ * Lang strings for the timeline block.
+ *
+ * @package block_timeline
+ * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['ariadayfilter'] = 'Filter timeline items';
+$string['ariadayfilteroption'] = '{$a} filter option';
+$string['ariaeventlistitem'] = '{$a->name} activity in {$a->course} is due on {$a->date}';
+$string['ariaeventlistpagelimit'] = 'Show {$a} activities per page';
+$string['ariaeventlistpaginationnavdates'] = 'Timeline activities pagination';
+$string['ariaeventlistpaginationnavcourses'] = 'Timeline activities for course {$a} pagination';
+$string['ariaviewselector'] = 'Sort timeline items';
+$string['ariaviewselectoroption'] = '{$a} sort option';
+$string['duedate'] = 'Due date';
+$string['morecourses'] = 'More courses';
+$string['timeline:addinstance'] = 'Add a new timeline block';
+$string['timeline:myaddinstance'] = 'Add a new timeline block to Dashboard';
+$string['nocoursesinprogress'] = 'No in progress courses';
+$string['noevents'] = 'No upcoming activities due';
+$string['next30days'] = 'Next 30 days';
+$string['next7days'] = 'Next 7 days';
+$string['next3months'] = 'Next 3 months';
+$string['next6months'] = 'Next 6 months';
+$string['overdue'] = 'Overdue';
+$string['pluginname'] = 'Timeline';
+$string['sortbycourses'] = 'Sort by courses';
+$string['sortbydates'] = 'Sort by dates';
+$string['timeline'] = 'Timeline';
+$string['viewcourse'] = 'View course';
+$string['privacy:metadata'] = 'The timeline block does not store any personal data.';
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="157 -1305 148 125" preserveAspectRatio="xMinYMid meet">
+ <defs>
+ <style>
+ .cls-1 {
+ clip-path: url(#clip-Courses);
+ }
+
+ .cls-2 {
+ fill: #eee;
+ }
+
+ .cls-3 {
+ fill: #c4c8cc;
+ }
+
+ .cls-4 {
+ fill: #fff;
+ }
+ </style>
+ <clipPath id="clip-Courses">
+ <rect x="157" y="-1305" width="148" height="125"/>
+ </clipPath>
+ </defs>
+ <g id="Courses" class="cls-1">
+ <g id="Group_44" data-name="Group 44" transform="translate(-268 -1781)">
+ <ellipse id="Ellipse_41" data-name="Ellipse 41" class="cls-2" cx="74" cy="14.785" rx="74" ry="14.785" transform="translate(425 571.43)"/>
+ <rect id="Rectangle_87" data-name="Rectangle 87" class="cls-3" width="95.097" height="110.215" transform="translate(451.909 476)"/>
+ <g id="Group_43" data-name="Group 43" transform="translate(464.04 494)">
+ <rect id="Rectangle_88" data-name="Rectangle 88" class="cls-4" width="31.043" height="34" transform="translate(0)"/>
+ <rect id="Rectangle_89" data-name="Rectangle 89" class="cls-4" width="31.043" height="34" transform="translate(0 42)"/>
+ <rect id="Rectangle_90" data-name="Rectangle 90" class="cls-4" width="31.067" height="34" transform="translate(39.005)"/>
+ <rect id="Rectangle_91" data-name="Rectangle 91" class="cls-4" width="31.067" height="34" transform="translate(39.005 42)"/>
+ <rect id="Rectangle_92" data-name="Rectangle 92" class="cls-3" width="23.023" height="3.18" transform="translate(3.081 16.549)"/>
+ <rect id="Rectangle_93" data-name="Rectangle 93" class="cls-3" width="23.023" height="3.18" transform="translate(3.081 58.549)"/>
+ <rect id="Rectangle_94" data-name="Rectangle 94" class="cls-3" width="23.023" height="3.18" transform="translate(43.122 16.549)"/>
+ <rect id="Rectangle_95" data-name="Rectangle 95" class="cls-3" width="23.023" height="3.18" transform="translate(43.122 58.549)"/>
+ <rect id="Rectangle_96" data-name="Rectangle 96" class="cls-3" width="14.014" height="3.18" transform="translate(3.081 21.825)"/>
+ <rect id="Rectangle_97" data-name="Rectangle 97" class="cls-3" width="18.845" height="3.18" transform="translate(3.081 26.825)"/>
+ <rect id="Rectangle_98" data-name="Rectangle 98" class="cls-3" width="14.014" height="3.18" transform="translate(3.081 63.825)"/>
+ <rect id="Rectangle_99" data-name="Rectangle 99" class="cls-3" width="18.845" height="3.18" transform="translate(3.081 68.825)"/>
+ <rect id="Rectangle_100" data-name="Rectangle 100" class="cls-3" width="14.014" height="3.18" transform="translate(43.122 21.825)"/>
+ <rect id="Rectangle_101" data-name="Rectangle 101" class="cls-3" width="18.845" height="3.18" transform="translate(43.122 26.825)"/>
+ <rect id="Rectangle_102" data-name="Rectangle 102" class="cls-3" width="14.014" height="3.18" transform="translate(43.122 63.825)"/>
+ <rect id="Rectangle_103" data-name="Rectangle 103" class="cls-3" width="18.845" height="3.18" transform="translate(43.122 68.825)"/>
+ <ellipse id="Ellipse_42" data-name="Ellipse 42" class="cls-3" cx="5.658" cy="5.652" rx="5.658" ry="5.652" transform="translate(3.003 3.55)"/>
+ <ellipse id="Ellipse_43" data-name="Ellipse 43" class="cls-3" cx="5.658" cy="5.652" rx="5.658" ry="5.652" transform="translate(3.003 45.55)"/>
+ <ellipse id="Ellipse_44" data-name="Ellipse 44" class="cls-3" cx="5.658" cy="5.652" rx="5.658" ry="5.652" transform="translate(43.044 3.55)"/>
+ <ellipse id="Ellipse_45" data-name="Ellipse 45" class="cls-3" cx="5.658" cy="5.652" rx="5.658" ry="5.652" transform="translate(43.044 45.55)"/>
+ </g>
+ </g>
+ </g>
+</svg>
--- /dev/null
+{{!
+ 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 block_timeline/course-item-loading-placeholder
+
+ This template renders the each course block containing a summary and calendar events.
+
+ Example context (json):
+ {}
+}}
+<li class="list-group-item mt-3 p-0 border-0">
+ <div class="w-50 bg-pulse-grey mb-2" style="height: 20px"></div>
+ <div>
+ <ul class="pl-0 list-group list-group-flush">
+ {{> block_timeline/placeholder-event-list-item }}
+ {{> block_timeline/placeholder-event-list-item }}
+ {{> block_timeline/placeholder-event-list-item }}
+ {{> block_timeline/placeholder-event-list-item }}
+ {{> block_timeline/placeholder-event-list-item }}
+ </ul>
+ <div class="pt-3 d-flex justify-content-between">
+ <div class="w-25 bg-pulse-grey" style="height: 35px"></div>
+ <div class="w-25 bg-pulse-grey" style="height: 35px"></div>
+ </div>
+ </div>
+</li>
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
- @template block_myoverview/course-item
+ @template block_timeline/course-item
This template renders the each course block containing a summary and calendar events.
"summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
}
}}
-<li class="list-group-item m-y-1">
-<div data-region="course-events-container" id="course-events-container-{{id}}" data-course-id="{{id}}">
- <div class="row">
- <div class="col-lg-3">
- {{> block_myoverview/course-summary }}
- </div>
- <div class="col-lg-9">
- {{< block_myoverview/course-event-list }}
- {{$limit}}10{{/limit}}
- {{$offset}}0{{/offset}}
- {{$courseid}}{{id}}{{/courseid}}
- {{/ block_myoverview/course-event-list }}
- </div>
+<li class="list-group-item mt-3 p-0 border-0">
+ <div data-region="course-events-container" id="course-events-container-{{id}}" data-course-id="{{id}}">
+ <h4 class="h5"><a href="{{viewurl}}" data-region="course-name">{{{fullnamedisplay}}}</a></h4>
+ {{< block_timeline/event-list }}
+ {{$courseid}}{{id}}{{/courseid}}
+ {{/ block_timeline/event-list }}
</div>
-</div>
</li>
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
- @template block_myoverview/course-item
+ @template block_timeline/course-items
This template renders the each course block containing a summary and calendar events.
"summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
}
}}
-<li class="list-group-item well well-small">
-<div data-region="course-events-container" id="course-events-container-{{id}}" data-course-id="{{id}}">
- <div class="row-fluid">
- <div class="span3">
- {{> block_myoverview/course-summary }}
- </div>
- <div class="span9">
- {{< block_myoverview/course-event-list }}
- {{$limit}}10{{/limit}}
- {{$offset}}0{{/offset}}
- {{$courseid}}{{id}}{{/courseid}}
- {{/ block_myoverview/course-event-list }}
- </div>
- </div>
-</div>
-</li>
+{{#courses}}
+ {{> block_timeline/course-item }}
+{{/courses}}
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
- @template block_myoverview/event-list-group
+ @template block_timeline/event-list-content
- This template renders a list of events for the myoverview block.
+ This template renders a group of event list items for the timeline block.
Example context (json):
{
"events": [
{
- "enddate": "Nov 4th, 10am",
"name": "Assignment due 1",
"url": "https://www.google.com",
+ "timesort": 1490320388,
"course": {
- "fullname": "Course 1"
+ "fullnamedisplay": "Course 1"
},
"action": {
"name": "Submit assignment",
"url": "https://www.google.com",
- "itemcount": 1
+ "itemcount": 1,
+ "actionable": true
},
"icon": {
"key": "icon",
}
},
{
- "enddate": "Nov 4th, 10am",
"name": "Assignment due 2",
"url": "https://www.google.com",
+ "timesort": 1490320388,
"course": {
- "fullname": "Course 1"
+ "fullnamedisplay": "Course 1"
},
"action": {
"name": "Submit assignment",
"url": "https://www.google.com",
- "itemcount": 1
+ "itemcount": 1,
+ "actionable": true
},
"icon": {
"key": "icon",
]
}
}}
-<div data-region="event-list-group-container"
- data-start-day="{{$startday}}0{{/startday}}"
- data-end-day="{{$endday}}{{/endday}}"
- class="hidden">
-
- <h5 class="{{$extratitleclasses}}{{/extratitleclasses}}" id="event-list-title-{{uniqid}}"><strong>{{$title}}{{/title}}</strong></h5>
- <ul class="unstyled well well-small" data-region="event-list" aria-describedby="event-list-title-{{uniqid}}">
- {{$eventlistitems}}
- {{> block_myoverview/event-list-items }}
- {{/eventlistitems}}
- </ul>
-</div>
+<div class="border-bottom pb-2">
+ {{#eventsbyday}}
+ <h5 class="h6 mt-3 mb-0 {{#past}}text-danger{{/past}}">{{#userdate}} {{dayTimestamp}}, {{#str}} strftimedayshort, core_langconfig {{/str}} {{/userdate}}</h5>
+ {{> block_timeline/event-list-items }}
+ {{/eventsbyday}}
+</div>
\ No newline at end of file
--- /dev/null
+{{!
+ 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 block_timeline/event-list-item
+
+ This template renders an event list item for the timeline block.
+
+ Example context (json):
+ {
+ "name": "Assignment due 1",
+ "url": "https://www.google.com",
+ "timesort": 1490320388,
+ "course": {
+ "fullnamedisplay": "Course 1"
+ },
+ "action": {
+ "name": "Submit assignment",
+ "url": "https://www.google.com",
+ "itemcount": 1,
+ "showitemcount": true,
+ "actionable": true
+ },
+ "icon": {
+ "key": "icon",
+ "component": "mod_assign",
+ "alttext": "Assignment icon"
+ }
+ }
+}}
+<a
+ class="list-group-item list-group-item-action flex-column py-2 pl-0 pr-0 border-0"
+ href="{{{action.url}}}"
+ title="{{name}}"
+ data-region="event-list-item"
+ aria-label='{{#str}} ariaeventlistitem, block_timeline, { "name": "{{name}}", "course": "{{course.fullnamedisplay}}", "date": "{{#userdate}} {{timesort}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}}" } {{/str}}'
+>
+ <div class="d-flex">
+ <div class="icon-size-4 d-flex align-self-center">
+ {{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}}
+ </div>
+ <div class="w-100 event-name-container text-truncate line-height-3">
+ <h6 class="event-name text-truncate mb-0">{{{name}}}</h6>
+ <small class="text-muted text-truncate m-b-0">{{{course.fullnamedisplay}}}</small>
+ </div>
+ <small class="text-right text-nowrap ml-1">
+ {{#userdate}} {{timesort}}, {{#str}} strftimetime24, core_langconfig {{/str}} {{/userdate}}
+ </small>
+ </div>
+</a>
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
- @template block_myoverview/course-event-list-items
+ @template block_timeline/event-list-items
- This template renders a group of event list items for the myoverview block
- sort by courses view.
+ This template renders a group of event list items for the timeline block.
Example context (json):
{
"name": "Assignment due 1",
"url": "https://www.google.com",
"timesort": 1490320388,
+ "course": {
+ "fullnamedisplay": "Course 1"
+ },
"action": {
"name": "Submit assignment",
"url": "https://www.google.com",
"name": "Assignment due 2",
"url": "https://www.google.com",
"timesort": 1490320388,
+ "course": {
+ "fullnamedisplay": "Course 1"
+ },
"action": {
"name": "Submit assignment",
"url": "https://www.google.com",
]
}
}}
+<div class="pl-0 list-group list-group-flush">
{{#events}}
- {{> block_myoverview/course-event-list-item }}
+ {{> block_timeline/event-list-item }}
{{/events}}
+</div>
--- /dev/null
+{{!
+ 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 block_timeline/event-list
+
+ This template renders a list of events for the timeline block.
+
+ Example context (json):
+ {
+ }
+}}
+<div data-region="event-list-container"
+ data-days-offset="{{$daysoffset}}{{#hasdaysoffset}}{{daysoffset}}{{/hasdaysoffset}}{{^hasdaysoffset}}0{{/hasdaysoffset}}{{/daysoffset}}"
+ {{^nodayslimit}}data-days-limit="{{$dayslimit}}{{#hasdayslimit}}{{dayslimit}}{{/hasdayslimit}}{{^hasdayslimit}}30{{/hasdayslimit}}{{/dayslimit}}"{{/nodayslimit}}
+ data-course-id="{{$courseid}}{{/courseid}}"
+ data-midnight="{{midnight}}"
+>
+ <div data-region="event-list-loading-placeholder">
+ <ul class="pl-0 list-group list-group-flush">
+ {{> block_timeline/placeholder-event-list-item }}
+ {{> block_timeline/placeholder-event-list-item }}
+ {{> block_timeline/placeholder-event-list-item }}
+ {{> block_timeline/placeholder-event-list-item }}
+ {{> block_timeline/placeholder-event-list-item }}
+ </ul>
+ <div class="pt-3 d-flex justify-content-between">
+ <div class="w-25 bg-pulse-grey" style="height: 35px"></div>
+ <div class="w-25 bg-pulse-grey" style="height: 35px"></div>
+ </div>
+ </div>
+ <div data-region="event-list-content"></div>
+ <div class="hidden text-xs-center text-center mt-3" data-region="empty-message">
+ <img
+ src="{{urls.noevents}}"
+ alt="{{#str}} noevents, block_timeline {{/str}}"
+ role="presentation"
+ style="height: 70px; width: 70px"
+ >
+ <p class="text-muted m-t-1">{{#str}} noevents, block_timeline {{/str}}</p>
+ </div>
+</div>
--- /dev/null
+{{!
+ 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 block_timeline/main
+
+ This template renders the main content area for the timeline block.
+
+ Example context (json):
+ {}
+}}
+
+<div id="block-timeline-{{uniqid}}" class="block-timeline" data-region="timeline">
+ <div class="container p-0 pb-3 border-bottom">
+ <div class="row no-gutters">
+ <div class="col-sm d-flex justify-content-start">
+ {{> block_timeline/nav-day-filter }}
+ </div>
+ <div class="col-sm d-flex justify-content-end">
+ {{> block_timeline/nav-view-selector }}
+ </div>
+ </div>
+ </div>
+ <div class="container p-0">
+ {{> block_timeline/view }}
+ </div>
+</div>
+{{#js}}
+require(
+[
+ 'jquery',
+ 'block_timeline/main',
+],
+function(
+ $,
+ Main
+) {
+ var root = $('#block-timeline-{{uniqid}}');
+ Main.init(root);
+});
+{{/js}}
--- /dev/null
+{{!
+ 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 block_timeline/nav-day-filter
+
+ This template renders the day range selector for the timeline view.
+
+ Example context (json):
+ {}
+}}
+<div data-region="day-filter" class="dropdown">
+ <button type="button" class="btn btn-outline-secondary dropdown-toggle icon-no-margin" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ {{#pix}} i/duration {{/pix}}
+ <span class="sr-only">
+ {{#str}} ariadayfilter, block_timeline {{/str}}
+ <span data-active-item-text>{{#str}} next30days, block_timeline {{/str}}</span>
+ </span>
+ </button>
+ <div role="menu" class="dropdown-menu" data-show-active-item>
+ <a
+ class="dropdown-item"
+ href="#"
+ data-from="-14"
+ aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} all, core {{/str}}{{/str}}"
+ >
+ {{#str}} all, core {{/str}}
+ </a>
+ <a
+ class="dropdown-item"
+ href="#"
+ data-from="-14"
+ data-to="0"
+ aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} overdue, block_timeline {{/str}}{{/str}}"
+ >
+ {{#str}} overdue, block_timeline {{/str}}
+ </a>
+ <div class="dropdown-divider"></div>
+ <h6 class="dropdown-header">{{#str}} duedate, block_timeline {{/str}}</h6>
+ <a
+ class="dropdown-item"
+ href="#"
+ data-from="0"
+ data-to="7"
+ aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next7days, block_timeline {{/str}}{{/str}}"
+ >
+ {{#str}} next7days, block_timeline {{/str}}
+ </a>
+ <a
+ class="dropdown-item active"
+ href="#"
+ data-from="0"
+ data-to="30"
+ aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next30days, block_timeline {{/str}}{{/str}}"
+ >
+ {{#str}} next30days, block_timeline {{/str}}
+ </a>
+ <a
+ class="dropdown-item"
+ href="#"
+ data-from="0"
+ data-to="90"
+ aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next3months, block_timeline {{/str}}{{/str}}"
+ >
+ {{#str}} next3months, block_timeline {{/str}}
+ </a>
+ <a
+ class="dropdown-item"
+ href="#"
+ data-from="0"
+ data-to="180"
+ aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next6months, block_timeline {{/str}}{{/str}}"
+ >
+ {{#str}} next6months, block_timeline {{/str}}
+ </a>
+ </div>
+</div>
--- /dev/null
+{{!
+ 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 block_timeline/nav-view-selector
+
+ This template renders the timeline sort selector.
+
+ Example context (json):
+ {}
+}}
+<div data-region="view-selector" class="btn-group">
+ <button type="button" class="btn btn-outline-secondary dropdown-toggle icon-no-margin" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ {{#pix}} i/filter {{/pix}}
+ <span class="sr-only">
+ {{#str}} ariaviewselector, block_timeline{{/str}}
+ <span data-active-item-text>{{#str}} sortbydates, block_timeline {{/str}}</span>
+ </span>
+ </button>
+ <div role="menu" class="dropdown-menu dropdown-menu-right list-group hidden" data-show-active-item data-skip-active-class="true">
+ <a
+ class="dropdown-item active"
+ href="#view_dates_{{uniqid}}"
+ data-toggle="tab"
+ aria-label="{{#str}} ariaviewselectoroption, block_timeline, {{#str}} sortbydates, block_timeline {{/str}}{{/str}}"
+ >
+ {{#str}} sortbydates, block_timeline {{/str}}
+ </a>
+ <a
+ class="dropdown-item"
+ href="#view_courses_{{uniqid}}"
+ data-toggle="tab"
+ aria-label="{{#str}} ariaviewselectoroption, block_timeline, {{#str}} sortbycourses, block_timeline {{/str}}{{/str}}"
+ >
+ {{#str}} sortbycourses, block_timeline {{/str}}
+ </a>
+ </div>
+</div>
--- /dev/null
+{{!
+ 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 block_timeline/event-list-item
+
+ This template renders an event list item loading placeholder for the timeline block.
+
+ Example context (json):
+ {}
+}}
+<li class="list-group-item pl-0 pr-0">
+ <div class="row">
+ <div class="col-8 pr-0">
+ <div class="d-flex flex-row align-items-center" style="height: 32px">
+ <div class="bg-pulse-grey rounded-circle" style="height: 32px; width: 32px;"></div>
+ <div style="flex: 1" class="pl-2">
+ <div class="bg-pulse-grey w-100" style="height: 15px;"></div>
+ <div class="bg-pulse-grey w-75 mt-1" style="height: 10px;"></div>
+ </div>
+ </div>
+ </div>
+ <div class="col-4 pr-3">
+ <div class="d-flex flex-row justify-content-end" style="height: 32px; padding-top: 2px">
+ <div class="bg-pulse-grey w-75" style="height: 15px;"></div>
+ </div>
+ </div>
+ </div>
+</li>
--- /dev/null
+{{!
+ 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 block_timeline/view-courses
+
+ This template renders the timeline view by courses for the timeline block.
+
+ Example context (json):
+ {}
+}}
+<div data-region="course-items-loading-placeholder">
+ <ul class="list-group unstyled">
+ {{> block_timeline/course-item-loading-placeholder }}
+ {{> block_timeline/course-item-loading-placeholder }}
+ </ul>
+ <div class="bg-pulse-grey m-t-1" style="width: 100px; height: 30px; margin-left: auto; margin-right: auto"></div>
+</div>
+<ul class="list-group unstyled" data-region="courses-list"></ul>
+<div class="hidden text-xs-center text-center pt-3" data-region="more-courses-button-container">
+ <button type="button" class="btn btn-secondary" data-action="more-courses">
+ {{#str}} morecourses, block_timeline {{/str}}
+ <span class="hidden" data-region="loading-icon-container">
+ {{> core/loading }}
+ </span>
+ </button>
+</div>
+<div class="hidden text-xs-center text-center mt-3" data-region="no-courses-empty-message">
+ <img
+ src="{{urls.noevents}}"
+ alt="{{#str}} nocoursesinprogress, block_timeline {{/str}}"
+ role="presentation"
+ style="height: 70px; width: 70px"
+ >
+ <p class="text-muted m-t-1">{{#str}} nocoursesinprogress, block_timeline {{/str}}</p>
+</div>
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
- @template block_myoverview/timeline-view-dates
+ @template block_timeline/view-dates
- This template renders the timeline view by dates for the myoverview block.
+ This template renders the timeline view by dates for the timeline block.
Example context (json):
{}
}}
-<div data-region="timeline-view-dates" id="timeline-view-dates-{{uniqid}}">
- {{< block_myoverview/event-list }}
- {{$limit}}20{{/limit}}
- {{/ block_myoverview/event-list }}
+<div data-region="timeline-view-dates">
+ {{> block_timeline/event-list }}
</div>
-{{#js}}
- require(['jquery', 'block_myoverview/event_list'], function($, EventList) {
- var root = $("#timeline-view-dates-{{uniqid}}").find('[data-region="event-list-container"]');
- EventList.load(root);
- });
-{{/js}}
--- /dev/null
+{{!
+ 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 block_timeline/view
+
+ This template renders the timeline view for the timeline block.
+
+ Example context (json):
+ {}
+}}
+<div data-region="timeline-view">
+ <div class="tab-content">
+ <div class="tab-pane active fade show" data-region="view-dates" id="view_dates_{{uniqid}}">
+ {{> block_timeline/view-dates }}
+ </div>
+ <div
+ class="tab-pane fade"
+ data-region="view-courses"
+ data-midnight="{{midnight}}"
+ data-limit="2"
+ data-offset="0"
+ data-days-limit="30"
+ data-days-offset="0"
+ data-no-events-url="{{urls.noevents}}"
+ id="view_courses_{{uniqid}}"
+ >
+ {{> block_timeline/view-courses }}
+ </div>
+ </div>
+</div>
\ No newline at end of file
--- /dev/null
+@block @block_timeline @javascript
+Feature: The timeline block allows users to see upcoming courses
+ In order to enable the timeline block
+ As a student
+ I can add the timeline block to my dashboard
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email | idnumber |
+ | student1 | Student | 1 | student1@example.com | S1 |
+ | student2 | Student | 2 | student2@example.com | S2 |
+ And the following "courses" exist:
+ | fullname | shortname | category | startdate | enddate |
+ | Course 1 | C1 | 0 | ##yesterday## | ##tomorrow## |
+ | Course 2 | C2 | 0 | ##yesterday## | ##tomorrow## |
+ | Course 3 | C3 | 0 | ##yesterday## | ##tomorrow## |
+ | Course 4 | C4 | 0 | ##first day of next month## | ##last day of next month## |
+ And the following "activities" exist:
+ | activity | course | idnumber | name | intro | timeopen | timeclose |
+ | choice | C2 | choice1 | Test choice 1 | Test choice description | ##yesterday## | ##tomorrow## |
+ | choice | C1 | choice2 | Test choice 2 | Test choice description | ##1 month ago## | ##15 days ago## |
+ | choice | C3 | choice3 | Test choice 3 | Test choice description | ##first day of +5 months## | ##last day of +5 months## |
+ | feedback | C2 | feedback1 | Test feedback 1 | Test feedback description | ##yesterday## | ##tomorrow## |
+ | feedback | C1 | feedback2 | Test feedback 2 | Test feedback description | ##first day of +10 months## | ##last day of +10 months## |
+ | feedback | C3 | feedback3 | Test feedback 3 | Test feedback description | ##first day of +5 months## | ##last day of +5 months## |
+ | feedback | C4 | feedback4 | Test feedback 4 | Test feedback description | ##yesterday## | ##tomorrow## |
+ And the following "activities" exist:
+ | activity | course | idnumber | name | intro | timeopen | duedate |
+ | assign | C1 | assign1 | Test assign 1 | Test assign description | ##1 month ago## | ##yesterday## |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | student1 | C1 | student |
+ | student1 | C2 | student |
+ | student1 | C3 | student |
+ | student1 | C4 | student |
+
+ Scenario: Next 30 days in course view
+ Given I log in as "student1"
+ And I click on "Sort" "button" in the "Timeline" "block"
+ When I click on "Sort by courses" "link" in the "Timeline" "block"
+ Then I should see "Course 1" in the "Timeline" "block"
+ And I should see "Course 2" in the "Timeline" "block"
+ And I should see "More courses" in the "Timeline" "block"
+ And I should see "Test choice 1 closes" in the "Timeline" "block"
+ And I should see "Test feedback 1 closes" in the "Timeline" "block"
+ And I should not see "Course 3" in the "Timeline" "block"
+ And I should not see "Test choice 2 closes" in the "Timeline" "block"
+ And I should not see "Test choice 3 closes" in the "Timeline" "block"
+ And I should not see "Test feedback 2 closes" in the "Timeline" "block"
+ And I should not see "Test feedback 3 closes" in the "Timeline" "block"
+ And I should not see "Test assign 1 is due" in the "Timeline" "block"
+
+ Scenario: All in course view
+ Given I log in as "student1"
+ And I click on "Next 30 days" "button" in the "Timeline" "block"
+ And I click on "All" "link" in the "Timeline" "block"
+ And I click on "Sort" "button" in the "Timeline" "block"
+ And I click on "Sort by courses" "link" in the "Timeline" "block"
+ When I click on "More courses" "button" in the "Timeline" "block"
+ Then I should see "Course 3" in the "Timeline" "block"
+ And I should see "Course 2" in the "Timeline" "block"
+ And I should see "Course 1" in the "Timeline" "block"
+ And I should see "Test choice 1 closes" in the "Timeline" "block"
+ And I should see "Test choice 3 closes" in the "Timeline" "block"
+ And I should see "Test feedback 1 closes" in the "Timeline" "block"
+ And I should see "Test feedback 2 closes" in the "Timeline" "block"
+ And I should see "Test feedback 3 closes" in the "Timeline" "block"
+ And I should see "Test assign 1 is due" in the "Timeline" "block"
+ And I should not see "More courses" in the "Timeline" "block"
+ And I should not see "Course 4" in the "Timeline" "block"
+ And I should not see "Test choice 2 closes" in the "Timeline" "block"
+ And I should not see "Test feedback 4 closes" in the "Timeline" "block"
--- /dev/null
+@block @block_timeline @javascript
+Feature: The timeline block allows users to see upcoming activities
+ In order to enable the timeline block
+ As a student
+ I can add the timeline block to my dashboard
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email | idnumber |
+ | student1 | Student | 1 | student1@example.com | S1 |
+ | student2 | Student | 2 | student2@example.com | S2 |
+ And the following "courses" exist:
+ | fullname | shortname | category | startdate | enddate |
+ | Course 1 | C1 | 0 | ##1 month ago## | ##15 days ago## |
+ | Course 2 | C2 | 0 | ##yesterday## | ##tomorrow## |
+ | Course 3 | C3 | 0 | ##first day of next month## | ##last day of next month## |
+ And the following "activities" exist:
+ | activity | course | idnumber | name | intro | timeopen | timeclose |
+ | choice | C2 | choice1 | Test choice 1 | Test choice description | ##yesterday## | ##tomorrow## |
+ | choice | C1 | choice2 | Test choice 2 | Test choice description | ##1 month ago## | ##15 days ago## |
+ | choice | C3 | choice3 | Test choice 3 | Test choice description | ##first day of +5 months## | ##last day of +5 months## |
+ | feedback | C2 | feedback1 | Test feedback 1 | Test feedback description | ##yesterday## | ##tomorrow## |
+ | feedback | C1 | feedback2 | Test feedback 2 | Test feedback description | ##first day of +10 months## | ##last day of +10 months## |
+ | feedback | C3 | feedback3 | Test feedback 3 | Test feedback description | ##first day of +5 months## | ##last day of +5 months## |
+ And the following "activities" exist:
+ | activity | course | idnumber | name | intro | timeopen | duedate |
+ | assign | C1 | assign1 | Test assign 1 | Test assign description | ##1 month ago## | ##yesterday## |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | student1 | C1 | student |
+ | student1 | C2 | student |
+ | student1 | C3 | student |
+
+ Scenario: Next 7 days in date view
+ Given I log in as "student1"
+ And I click on "Next 30 days" "button" in the "Timeline" "block"
+ When I click on "Next 7 days" "link" in the "Timeline" "block"
+ Then I should see "Test choice 1 closes" in the "Timeline" "block"
+ And I should see "Test feedback 1 closes" in the "Timeline" "block"
+ And I should not see "Test choice 2 closes" in the "Timeline" "block"
+ And I should not see "Test choice 3 closes" in the "Timeline" "block"
+ And I should not see "Test feedback 3 closes" in the "Timeline" "block"
+ And I should not see "Test assign 1 is due" in the "Timeline" "block"
+
+ Scenario: Overdue in date view
+ Given I log in as "student1"
+ And I click on "Next 30 days" "button" in the "Timeline" "block"
+ When I click on "Overdue" "link" in the "Timeline" "block"
+ Then I should see "Test assign 1 is due" in the "Timeline" "block"
+ And I should not see "Test choice 2 closes" in the "Timeline" "block"
+ And I should not see "Test feedback 1 closes" in the "Timeline" "block"
+ And I should not see "Test choice 1 closes" in the "Timeline" "block"
+ And I should not see "Test choice 3 closes" in the "Timeline" "block"
+ And I should not see "Test feedback 3 closes" in the "Timeline" "block"
+
+ Scenario: All in date view
+ Given I log in as "student1"
+ And I click on "Next 30 days" "button" in the "Timeline" "block"
+ When I click on "All" "link" in the "Timeline" "block"
+ Then I should see "Test assign 1 is due" in the "Timeline" "block"
+ And I should see "Test feedback 1 closes" in the "Timeline" "block"
+ And I should see "Test choice 1 closes" in the "Timeline" "block"
+ And I should see "Test choice 3 closes" in the "Timeline" "block"
+ And I should see "Test feedback 3 closes" in the "Timeline" "block"
+ And I should not see "Test choice 2 closes" in the "Timeline" "block"
+ And I should not see "Test feedback 2 closes" in the "Timeline" "block"
+ And I click on "[data-region='paging-bar'] [data-control='next'] [data-region='page-link']" "css_element" in the "Timeline" "block"
+ And I should see "Test feedback 2 closes" in the "Timeline" "block"
+ And I should not see "Test assign 1 is due" in the "Timeline" "block"
+ And I should not see "Test feedback 1 closes" in the "Timeline" "block"
+ And I should not see "Test choice 1 closes" in the "Timeline" "block"
+ And I should not see "Test choice 3 closes" in the "Timeline" "block"
+ And I should not see "Test feedback 3 closes" in the "Timeline" "block"
+ And I should not see "Test choice 2 closes" in the "Timeline" "block"
+
+ Scenario: All in date view no next
+ Given I log in as "student1"
+ And I click on "Next 30 days" "button" in the "Timeline" "block"
+ And I click on "All" "link" in the "Timeline" "block"
+ And I click on "5" "button" in the "Timeline" "block"
+ When I click on "25" "link" in the "Timeline" "block"
+ Then I should see "Test assign 1 is due" in the "Timeline" "block"
+ And I should see "Test feedback 1 closes" in the "Timeline" "block"
+ And I should see "Test choice 1 closes" in the "Timeline" "block"
+ And I should see "Test choice 3 closes" in the "Timeline" "block"
+ And I should see "Test feedback 3 closes" in the "Timeline" "block"
+ And I should see "Test feedback 2 closes" in the "Timeline" "block"
+ And I should not see "Test choice 2 closes" in the "Timeline" "block"