Merge branch 'MDL-58000-master' of https://github.com/mackensen/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 2 Oct 2018 21:39:27 +0000 (23:39 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 2 Oct 2018 21:39:27 +0000 (23:39 +0200)
242 files changed:
.travis.yml
admin/roles/allow.php
admin/roles/classes/capability_table_with_risks.php
admin/roles/manage.php
admin/settings/appearance.php
admin/settings/courses.php
admin/templates/settings_search_results.mustache
admin/tool/policy/classes/form/policydoc.php
admin/tool/policy/classes/output/page_agreedocs.php
admin/tool/policy/classes/output/page_viewdoc.php
admin/tool/policy/classes/policy_version.php
admin/tool/policy/db/install.xml
admin/tool/policy/db/upgrade.php [new file with mode: 0644]
admin/tool/policy/index.php
admin/tool/policy/lang/en/tool_policy.php
admin/tool/policy/lib.php
admin/tool/policy/templates/page_agreedocs.mustache
admin/tool/policy/templates/page_viewdoc.mustache
admin/tool/policy/tests/behat/behat_tool_policy.php
admin/tool/policy/tests/behat/consent.feature
admin/tool/policy/version.php
admin/tool/usertours/tests/behat/create_tour.feature
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
blocks/myoverview/amd/build/event_list.min.js [deleted file]
blocks/myoverview/amd/build/event_list_by_course.min.js [deleted file]
blocks/myoverview/amd/build/tab_preferences.min.js [deleted file]
blocks/myoverview/amd/src/event_list.js [deleted file]
blocks/myoverview/amd/src/event_list_by_course.js [deleted file]
blocks/myoverview/amd/src/tab_preferences.js [deleted file]
blocks/myoverview/block_myoverview.php
blocks/myoverview/classes/output/main.php
blocks/myoverview/classes/privacy/provider.php
blocks/myoverview/db/upgrade.php [new file with mode: 0644]
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/templates/course-event-list-item.mustache [deleted file]
blocks/myoverview/templates/course-event-list.mustache [deleted file]
blocks/myoverview/templates/course-summary.mustache [deleted file]
blocks/myoverview/templates/courses-view-nav-grouping-display-filter.mustache [new file with mode: 0644]
blocks/myoverview/templates/courses-view.mustache
blocks/myoverview/templates/event-list-group.mustache [deleted file]
blocks/myoverview/templates/event-list-item.mustache [deleted file]
blocks/myoverview/templates/event-list.mustache [deleted file]
blocks/myoverview/templates/main.mustache
blocks/myoverview/templates/timeline-view-courses.mustache [deleted file]
blocks/myoverview/templates/timeline-view.mustache [deleted file]
blocks/myoverview/tests/behat/block_myoverview_dashboard.feature
blocks/myoverview/tests/behat/block_myoverview_progress.feature
blocks/myoverview/tests/privacy_test.php [deleted file]
blocks/myoverview/version.php
blocks/timeline/amd/build/calendar_events_repository.min.js [moved from blocks/myoverview/amd/build/calendar_events_repository.min.js with 100% similarity]
blocks/timeline/amd/build/event_list.min.js [new file with mode: 0644]
blocks/timeline/amd/build/main.min.js [new file with mode: 0644]
blocks/timeline/amd/build/view.min.js [new file with mode: 0644]
blocks/timeline/amd/build/view_courses.min.js [new file with mode: 0644]
blocks/timeline/amd/build/view_dates.min.js [new file with mode: 0644]
blocks/timeline/amd/build/view_nav.min.js [new file with mode: 0644]
blocks/timeline/amd/src/calendar_events_repository.js [moved from blocks/myoverview/amd/src/calendar_events_repository.js with 96% similarity]
blocks/timeline/amd/src/event_list.js [new file with mode: 0644]
blocks/timeline/amd/src/main.js [new file with mode: 0644]
blocks/timeline/amd/src/view.js [new file with mode: 0644]
blocks/timeline/amd/src/view_courses.js [new file with mode: 0644]
blocks/timeline/amd/src/view_dates.js [new file with mode: 0644]
blocks/timeline/amd/src/view_nav.js [new file with mode: 0644]
blocks/timeline/block_timeline.php [new file with mode: 0644]
blocks/timeline/classes/output/main.php [new file with mode: 0644]
blocks/timeline/classes/output/renderer.php [new file with mode: 0644]
blocks/timeline/classes/privacy/provider.php [moved from blocks/myoverview/lib.php with 52% similarity]
blocks/timeline/db/access.php [new file with mode: 0644]
blocks/timeline/db/install.php [new file with mode: 0644]
blocks/timeline/lang/en/block_timeline.php [new file with mode: 0644]
blocks/timeline/pix/activities.svg [moved from blocks/myoverview/pix/activities.svg with 100% similarity]
blocks/timeline/pix/courses.svg [new file with mode: 0644]
blocks/timeline/templates/course-item-loading-placeholder.mustache [new file with mode: 0644]
blocks/timeline/templates/course-item.mustache [moved from blocks/myoverview/templates/course-item.mustache with 64% similarity]
blocks/timeline/templates/course-items.mustache [moved from theme/bootstrapbase/templates/block_myoverview/course-item.mustache with 63% similarity]
blocks/timeline/templates/event-list-content.mustache [moved from theme/bootstrapbase/templates/block_myoverview/event-list-group.mustache with 66% similarity]
blocks/timeline/templates/event-list-item.mustache [new file with mode: 0644]
blocks/timeline/templates/event-list-items.mustache [moved from blocks/myoverview/templates/course-event-list-items.mustache with 82% similarity]
blocks/timeline/templates/event-list.mustache [new file with mode: 0644]
blocks/timeline/templates/main.mustache [new file with mode: 0644]
blocks/timeline/templates/nav-day-filter.mustache [new file with mode: 0644]
blocks/timeline/templates/nav-view-selector.mustache [new file with mode: 0644]
blocks/timeline/templates/placeholder-event-list-item.mustache [new file with mode: 0644]
blocks/timeline/templates/view-courses.mustache [new file with mode: 0644]
blocks/timeline/templates/view-dates.mustache [moved from blocks/myoverview/templates/timeline-view-dates.mustache with 57% similarity]
blocks/timeline/templates/view.mustache [new file with mode: 0644]
blocks/timeline/tests/behat/block_timeline_courses.feature [new file with mode: 0644]
blocks/timeline/tests/behat/block_timeline_dates.feature [new file with mode: 0644]
blocks/timeline/version.php [moved from blocks/myoverview/settings.php with 55% similarity]
blocks/upgrade.txt
calendar/classes/external/event_exporter.php
calendar/templates/event_summary_body.mustache
config-dist.php
course/amd/build/repository.min.js [new file with mode: 0644]
course/amd/src/repository.js [new file with mode: 0644]
course/classes/category.php
course/edit.php
course/externallib.php
course/lib.php
course/tests/courselib_test.php
course/tests/externallib_test.php
enrol/category/tests/plugin_test.php
enrol/tests/enrollib_test.php
filter/glossary/filter.php
filter/glossary/lang/en/filter_glossary.php
filter/glossary/tests/filter_test.php
filter/mathjaxloader/filter.php
filter/mathjaxloader/tests/filtermath_test.php [new file with mode: 0644]
filter/upgrade.txt
grade/tests/report_graderlib_test.php
lang/en/admin.php
lang/en/moodle.php
lang/en/question.php
lib/accesslib.php
lib/adminlib.php
lib/amd/build/page_global.min.js [new file with mode: 0644]
lib/amd/build/paged_content.min.js [new file with mode: 0644]
lib/amd/build/paged_content_events.min.js
lib/amd/build/paged_content_factory.min.js
lib/amd/build/paged_content_pages.min.js
lib/amd/build/paged_content_paging_bar.min.js
lib/amd/build/paged_content_paging_bar_limit_selector.min.js [new file with mode: 0644]
lib/amd/build/paged_content_paging_dropdown.min.js
lib/amd/build/pubsub.min.js [new file with mode: 0644]
lib/amd/build/user_date.min.js
lib/amd/src/page_global.js [new file with mode: 0644]
lib/amd/src/paged_content.js [new file with mode: 0644]
lib/amd/src/paged_content_events.js
lib/amd/src/paged_content_factory.js
lib/amd/src/paged_content_pages.js
lib/amd/src/paged_content_paging_bar.js
lib/amd/src/paged_content_paging_bar_limit_selector.js [new file with mode: 0644]
lib/amd/src/paged_content_paging_dropdown.js
lib/amd/src/pubsub.js [new file with mode: 0644]
lib/amd/src/user_date.js
lib/behat/classes/util.php
lib/blocklib.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/plugin_manager.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/ddl/mssql_sql_generator.php
lib/ddl/oracle_sql_generator.php
lib/ddl/sql_generator.php
lib/ddl/tests/ddl_test.php
lib/dml/moodle_database.php
lib/dml/tests/dml_test.php
lib/enrollib.php
lib/filterlib.php
lib/grouplib.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputrequirementslib.php
lib/phpunit/classes/util.php
lib/questionlib.php
lib/templates/paged_content.mustache
lib/templates/paged_content_pages.mustache
lib/templates/paged_content_paging_bar.mustache
lib/templates/paged_content_paging_dropdown.mustache
lib/tests/accesslib_test.php
lib/tests/grouplib_test.php
lib/tests/questionlib_test.php
lib/upgrade.txt
lib/xmldb/xmldb_index.php
lib/xmldb/xmldb_table.php
message/tests/api_test.php
message/tests/privacy_provider_test.php
mod/assign/feedback/editpdf/classes/event/observer.php
mod/assign/feedback/editpdf/classes/task/convert_submissions.php
mod/assign/feedback/editpdf/db/install.xml
mod/assign/feedback/editpdf/db/upgrade.php
mod/assign/feedback/editpdf/tests/editpdf_test.php
mod/assign/feedback/editpdf/version.php
mod/assign/submission/file/locallib.php
mod/feedback/tests/lib_test.php
mod/forum/lib.php
mod/forum/tests/subscriptions_test.php
npm-shrinkwrap.json
question/category.php
question/category_class.php
question/category_form.php
question/engine/tests/helpers.php
question/format.php
question/format/xml/format.php
question/format/xml/tests/fixtures/export_category.xml
question/format/xml/tests/fixtures/nested_categories.xml
question/format/xml/tests/fixtures/nested_categories_with_questions.xml
question/format/xml/tests/qformat_xml_import_export_test.php
question/format/xml/tests/xmlformat_test.php
question/tests/backup_test.php
question/tests/behat/question_categories_idnumber.feature [new file with mode: 0644]
question/tests/generator/lib.php
question/tests/generator_test.php
question/type/ddwtos/tests/questiontype_test.php
question/type/edit_question_form.php
question/type/gapselect/tests/questiontype_test.php
question/type/match/tests/questiontype_test.php
question/type/missingtype/tests/missingtype_test.php
question/type/questiontypebase.php
tag/tests/external_test.php
theme/boost/scss/moodle/admin.scss
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/bs4alphacompat.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/icons.scss
theme/boost/scss/preset/default.scss
theme/boost/style/moodle.css
theme/boost/templates/core/initials_bar.mustache
theme/boost/templates/core/navbar.mustache
theme/boost/templates/core_admin/settings_search_results.mustache
theme/bootstrapbase/less/moodle/admin.less
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/block_myoverview/course-event-list-item.mustache [deleted file]
theme/bootstrapbase/templates/block_myoverview/course-event-list.mustache [deleted file]
theme/bootstrapbase/templates/block_myoverview/course-summary.mustache [deleted file]
theme/bootstrapbase/templates/block_myoverview/courses-view-nav-grouping-display-filter.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_myoverview/courses-view.mustache
theme/bootstrapbase/templates/block_myoverview/event-list-item.mustache [deleted file]
theme/bootstrapbase/templates/block_myoverview/main.mustache
theme/bootstrapbase/templates/block_myoverview/timeline-view.mustache [deleted file]
theme/bootstrapbase/templates/block_timeline/course-item-loading-placeholder.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/event-list-item.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/event-list-items.mustache [moved from blocks/myoverview/templates/event-list-items.mustache with 91% similarity]
theme/bootstrapbase/templates/block_timeline/event-list.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/main.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/nav-day-filter.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/nav-view-selector.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/placeholder-event-list-item.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/view.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/core/paged_content_paging_bar.mustache
theme/bootstrapbase/templates/core/paged_content_paging_dropdown.mustache
user/classes/participants_table.php
user/index.php
user/lib.php
user/renderer.php
user/tests/behat/filter_participants.feature
user/tests/userlib_test.php
version.php

index 0b6690a..aa0db16 100644 (file)
@@ -263,7 +263,9 @@ script:
         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
 
index 71e9bcb..88609cf 100644 (file)
@@ -46,7 +46,6 @@ $controller = new $classformode[$mode]();
 
 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) {
index db7e16b..4e597f4 100644 (file)
@@ -123,9 +123,6 @@ abstract class core_role_capability_table_with_risks extends core_role_capabilit
             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() {
index fb390ea..d2e703f 100644 (file)
@@ -85,12 +85,10 @@ switch ($action) {
             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;
 
index db05c1d..b971e30 100644 (file)
@@ -32,9 +32,9 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) { // sp
         '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'
@@ -227,6 +227,10 @@ preferences,moodle|/user/preferences.php|preferences',
             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'));
@@ -252,4 +256,3 @@ preferences,moodle|/user/preferences.php|preferences',
     $ADMIN->add('appearance', $temp);
 
 } // end of speedup
-
index 64ef150..cf9b286 100644 (file)
@@ -43,6 +43,12 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
             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)),
index 78b6205..df82b0f 100644 (file)
@@ -22,7 +22,7 @@
     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):
@@ -30,7 +30,7 @@
         "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>
index 91e4ce0..9c16180 100644 (file)
@@ -89,6 +89,8 @@ class policydoc extends moodleform {
             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');
index af6ebd6..771ac05 100644 (file)
@@ -51,6 +51,9 @@ class page_agreedocs implements renderable, templatable {
     /** @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;
 
@@ -75,22 +78,20 @@ class page_agreedocs implements renderable, templatable {
     /**
      * 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);
@@ -124,12 +125,14 @@ class page_agreedocs implements renderable, templatable {
                 // 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.
@@ -158,15 +161,26 @@ class page_agreedocs implements renderable, templatable {
         } 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);
@@ -190,18 +204,41 @@ class page_agreedocs implements renderable, templatable {
      * @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;
@@ -232,6 +269,8 @@ class page_agreedocs implements renderable, templatable {
                 ];
                 redirect(new moodle_url('/admin/tool/policy/view.php', $urlparams));
             }
+        } else {
+            $this->redirect_to_previous_url();
         }
     }
 
@@ -401,7 +440,10 @@ class page_agreedocs implements renderable, templatable {
             }
         }
 
-        $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)) {
index 95613a7..73c216f 100644 (file)
@@ -151,6 +151,7 @@ class page_viewdoc implements renderable, templatable {
      * @return stdClass
      */
     public function export_for_template(renderer_base $output) {
+        global $USER;
 
         $data = (object) [
             'pluginbaseurl' => (new moodle_url('/admin/tool/policy'))->out(false),
@@ -163,6 +164,18 @@ class page_viewdoc implements renderable, templatable {
             $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;
index b3bbd29..f675ac9 100644 (file)
@@ -69,6 +69,12 @@ class policy_version extends persistent {
     /** @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.
      *
@@ -106,6 +112,14 @@ class policy_version extends persistent {
             'policyid' => [
                 'type' => PARAM_INT,
             ],
+            'agreementstyle' => [
+                'type' => PARAM_INT,
+                'choices' => [
+                    self::AGREEMENTSTYLE_CONSENTPAGE,
+                    self::AGREEMENTSTYLE_OWNPAGE,
+                ],
+                'default' => self::AGREEMENTSTYLE_CONSENTPAGE,
+            ],
             'revision' => [
                 'type' => PARAM_TEXT,
                 'default' => '',
index c1601f6..0ab0d63 100644 (file)
@@ -1,5 +1,5 @@
 <?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"
 >
@@ -26,6 +26,7 @@
         <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"/>
diff --git a/admin/tool/policy/db/upgrade.php b/admin/tool/policy/db/upgrade.php
new file mode 100644 (file)
index 0000000..7aabeef
--- /dev/null
@@ -0,0 +1,53 @@
+<?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;
+}
+
index 067d8fa..9f03344 100644 (file)
@@ -18,7 +18,8 @@
  * 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
@@ -37,7 +38,8 @@ require(__DIR__.'/../../../config.php');
 
 $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());
@@ -45,6 +47,10 @@ $PAGE->set_pagelayout('standard');
 $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);
@@ -61,7 +67,7 @@ if (!$haspermissionagreedocs) {
     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');
index 3ede09b..e3250cb 100644 (file)
@@ -129,6 +129,7 @@ $string['policydoctype2'] = 'Third parties 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';
index 563254d..4f36d31 100644 (file)
@@ -85,9 +85,13 @@ function tool_policy_before_standard_html_head() {
             && 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;
index 31674de..8773b4b 100644 (file)
@@ -92,6 +92,7 @@
         <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>
index 8eb290a..b351f2d 100644 (file)
@@ -38,7 +38,8 @@
             "content": "Policy <em>content</em>"
         },
         "returnurl": "#",
-        "editurl": "#"
+        "editurl": "#",
+        "accepturl": "#"
     }
 }}
 
@@ -88,6 +89,9 @@
     {{#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">
index 11b73cb..0183be7 100644 (file)
@@ -51,6 +51,7 @@ class behat_tool_policy extends behat_base {
      * - 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
      */
@@ -69,6 +70,7 @@ class behat_tool_policy extends behat_base {
             'type',
             'content',
             'summary',
+            'agreementstyle',
         ];
 
         // Associative array "policy identifier" => id in the database .
index 047ac5e..75f1a8f 100644 (file)
@@ -663,3 +663,217 @@ Feature: User must accept policy managed by this plugin when logging in and sign
     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
index e87145d..9f09c60 100644 (file)
@@ -24,6 +24,6 @@
 
 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).
index 33c7db7..09f99ff 100644 (file)
@@ -27,16 +27,16 @@ Feature: Add a new user tour
       | 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"
index 3793d9b..b90490a 100644 (file)
@@ -2211,7 +2211,7 @@ class backup_questions_structure_step extends backup_structure_step {
         $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');
 
@@ -2219,7 +2219,7 @@ class backup_questions_structure_step extends backup_structure_step {
             '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);
index 8443174..250aeb2 100644 (file)
@@ -4434,6 +4434,12 @@ class restore_create_categories_and_questions extends restore_structure_step {
                 $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);
@@ -4479,6 +4485,13 @@ class restore_create_categories_and_questions extends restore_structure_step {
 
         // 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
diff --git a/blocks/myoverview/amd/build/event_list.min.js b/blocks/myoverview/amd/build/event_list.min.js
deleted file mode 100644 (file)
index 343b46e..0000000
Binary files a/blocks/myoverview/amd/build/event_list.min.js and /dev/null differ
diff --git a/blocks/myoverview/amd/build/event_list_by_course.min.js b/blocks/myoverview/amd/build/event_list_by_course.min.js
deleted file mode 100644 (file)
index 055a8b3..0000000
Binary files a/blocks/myoverview/amd/build/event_list_by_course.min.js and /dev/null differ
diff --git a/blocks/myoverview/amd/build/tab_preferences.min.js b/blocks/myoverview/amd/build/tab_preferences.min.js
deleted file mode 100644 (file)
index da5bd97..0000000
Binary files a/blocks/myoverview/amd/build/tab_preferences.min.js and /dev/null differ
diff --git a/blocks/myoverview/amd/src/event_list.js b/blocks/myoverview/amd/src/event_list.js
deleted file mode 100644 (file)
index 8a1bd91..0000000
+++ /dev/null
@@ -1,416 +0,0 @@
-// 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,
-    };
-});
diff --git a/blocks/myoverview/amd/src/event_list_by_course.js b/blocks/myoverview/amd/src/event_list_by_course.js
deleted file mode 100644 (file)
index 32d52cc..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-// 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);
-        }
-    };
-});
diff --git a/blocks/myoverview/amd/src/tab_preferences.js b/blocks/myoverview/amd/src/tab_preferences.js
deleted file mode 100644 (file)
index 25ac2ee..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-// 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
-    };
-});
index 8afd4a1..f22ce15 100644 (file)
@@ -50,16 +50,7 @@ class block_myoverview extends block_base {
             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();
@@ -77,13 +68,4 @@ class block_myoverview extends block_base {
     public function applicable_formats() {
         return array('my' => true);
     }
-
-    /**
-     * This block does contain a configuration settings.
-     *
-     * @return boolean
-     */
-    public function has_config() {
-        return true;
-    }
 }
index 2850637..46834f3 100644 (file)
@@ -29,7 +29,6 @@ use renderer_base;
 use templatable;
 use core_completion\progress;
 
-require_once($CFG->dirroot . '/blocks/myoverview/lib.php');
 require_once($CFG->libdir . '/completionlib.php');
 
 /**
@@ -39,21 +38,6 @@ 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.
      *
@@ -86,26 +70,13 @@ class main implements renderable, templatable {
 
         $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
         ];
     }
 }
index d0ee9e8..b3cf042 100644 (file)
@@ -32,30 +32,15 @@ defined('MOODLE_INTERNAL') || die();
  * @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';
     }
 }
diff --git a/blocks/myoverview/db/upgrade.php b/blocks/myoverview/db/upgrade.php
new file mode 100644 (file)
index 0000000..b91cb96
--- /dev/null
@@ -0,0 +1,41 @@
+<?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;
+}
index a3ca64e..df3ae66 100644 (file)
@@ -22,8 +22,6 @@
  * @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';
@@ -33,15 +31,8 @@ $string['nocourses'] = 'No 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.';
diff --git a/blocks/myoverview/templates/course-event-list-item.mustache b/blocks/myoverview/templates/course-event-list-item.mustache
deleted file mode 100644 (file)
index 55c0e46..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-{{!
-    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>
diff --git a/blocks/myoverview/templates/course-event-list.mustache b/blocks/myoverview/templates/course-event-list.mustache
deleted file mode 100644 (file)
index d7f9fb2..0000000
+++ /dev/null
@@ -1,110 +0,0 @@
-{{!
-    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}}
diff --git a/blocks/myoverview/templates/course-summary.mustache b/blocks/myoverview/templates/course-summary.mustache
deleted file mode 100644 (file)
index 53f40a0..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-{{!
-    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>
diff --git a/blocks/myoverview/templates/courses-view-nav-grouping-display-filter.mustache b/blocks/myoverview/templates/courses-view-nav-grouping-display-filter.mustache
new file mode 100644 (file)
index 0000000..f7aa275
--- /dev/null
@@ -0,0 +1,40 @@
+{{!
+    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>
index 14ffa49..cb5b926 100644 (file)
 }}
 <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}}
diff --git a/blocks/myoverview/templates/event-list-group.mustache b/blocks/myoverview/templates/event-list-group.mustache
deleted file mode 100644 (file)
index 340fdcb..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-{{!
-    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>
diff --git a/blocks/myoverview/templates/event-list-item.mustache b/blocks/myoverview/templates/event-list-item.mustache
deleted file mode 100644 (file)
index a269b5c..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-{{!
-    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>
diff --git a/blocks/myoverview/templates/event-list.mustache b/blocks/myoverview/templates/event-list.mustache
deleted file mode 100644 (file)
index dbe3d25..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-{{!
-    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}}
index e9b21bd..fef8e9f 100644 (file)
 }}
 
 <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}}
diff --git a/blocks/myoverview/templates/timeline-view-courses.mustache b/blocks/myoverview/templates/timeline-view-courses.mustache
deleted file mode 100644 (file)
index 6f35022..0000000
+++ /dev/null
@@ -1,121 +0,0 @@
-{{!
-    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}}
diff --git a/blocks/myoverview/templates/timeline-view.mustache b/blocks/myoverview/templates/timeline-view.mustache
deleted file mode 100644 (file)
index 9d57cd2..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-{{!
-    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
index bf6f356..de37359 100644 (file)
@@ -1,5 +1,5 @@
 @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
@@ -14,59 +14,23 @@ Feature: The my overview block allows users to easily access their courses and s
       | 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
index 44cd539..5ed5656 100644 (file)
@@ -20,18 +20,6 @@ Feature: Course overview block show users their progress on courses
       | 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
@@ -43,21 +31,11 @@ Feature: Course overview block show users their progress on courses
     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
diff --git a/blocks/myoverview/tests/privacy_test.php b/blocks/myoverview/tests/privacy_test.php
deleted file mode 100644 (file)
index 875dd03..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-<?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);
-    }
-}
index 26ccc6d..6b8960c 100644 (file)
@@ -24,6 +24,6 @@
 
 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).
diff --git a/blocks/timeline/amd/build/event_list.min.js b/blocks/timeline/amd/build/event_list.min.js
new file mode 100644 (file)
index 0000000..cd209c5
Binary files /dev/null and b/blocks/timeline/amd/build/event_list.min.js differ
diff --git a/blocks/timeline/amd/build/main.min.js b/blocks/timeline/amd/build/main.min.js
new file mode 100644 (file)
index 0000000..4b8ded9
Binary files /dev/null and b/blocks/timeline/amd/build/main.min.js differ
diff --git a/blocks/timeline/amd/build/view.min.js b/blocks/timeline/amd/build/view.min.js
new file mode 100644 (file)
index 0000000..e374dc3
Binary files /dev/null and b/blocks/timeline/amd/build/view.min.js differ
diff --git a/blocks/timeline/amd/build/view_courses.min.js b/blocks/timeline/amd/build/view_courses.min.js
new file mode 100644 (file)
index 0000000..918fdd1
Binary files /dev/null and b/blocks/timeline/amd/build/view_courses.min.js differ
diff --git a/blocks/timeline/amd/build/view_dates.min.js b/blocks/timeline/amd/build/view_dates.min.js
new file mode 100644 (file)
index 0000000..b29cb60
Binary files /dev/null and b/blocks/timeline/amd/build/view_dates.min.js differ
diff --git a/blocks/timeline/amd/build/view_nav.min.js b/blocks/timeline/amd/build/view_nav.min.js
new file mode 100644 (file)
index 0000000..c66ad2f
Binary files /dev/null and b/blocks/timeline/amd/build/view_nav.min.js differ
 /**
  * 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) {
diff --git a/blocks/timeline/amd/src/event_list.js b/blocks/timeline/amd/src/event_list.js
new file mode 100644 (file)
index 0000000..dfd1517
--- /dev/null
@@ -0,0 +1,465 @@
+// 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,
+    };
+});
diff --git a/blocks/timeline/amd/src/main.js b/blocks/timeline/amd/src/main.js
new file mode 100644 (file)
index 0000000..334248b
--- /dev/null
@@ -0,0 +1,57 @@
+// 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
+    };
+});
diff --git a/blocks/timeline/amd/src/view.js b/blocks/timeline/amd/src/view.js
new file mode 100644 (file)
index 0000000..89430a7
--- /dev/null
@@ -0,0 +1,97 @@
+// 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,
+    };
+});
diff --git a/blocks/timeline/amd/src/view_courses.js b/blocks/timeline/amd/src/view_courses.js
new file mode 100644 (file)
index 0000000..4749c92
--- /dev/null
@@ -0,0 +1,606 @@
+// 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
+    };
+});
diff --git a/blocks/timeline/amd/src/view_dates.js b/blocks/timeline/amd/src/view_dates.js
new file mode 100644 (file)
index 0000000..b18d21a
--- /dev/null
@@ -0,0 +1,103 @@
+// 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
+    };
+});
diff --git a/blocks/timeline/amd/src/view_nav.js b/blocks/timeline/amd/src/view_nav.js
new file mode 100644 (file)
index 0000000..092dcbd
--- /dev/null
@@ -0,0 +1,120 @@
+// 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
+    };
+});
diff --git a/blocks/timeline/block_timeline.php b/blocks/timeline/block_timeline.php
new file mode 100644 (file)
index 0000000..5a24f9c
--- /dev/null
@@ -0,0 +1,72 @@
+<?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);
+    }
+}
diff --git a/blocks/timeline/classes/output/main.php b/blocks/timeline/classes/output/main.php
new file mode 100644 (file)
index 0000000..b5e9609
--- /dev/null
@@ -0,0 +1,81 @@
+<?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
+            ]
+        ];
+    }
+}
diff --git a/blocks/timeline/classes/output/renderer.php b/blocks/timeline/classes/output/renderer.php
new file mode 100644 (file)
index 0000000..dbe6bb7
--- /dev/null
@@ -0,0 +1,48 @@
+<?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));
+    }
+}
similarity index 52%
rename from blocks/myoverview/lib.php
rename to blocks/timeline/classes/privacy/provider.php
index a73db25..02ae526 100644 (file)
 // 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';
+    }
 }
diff --git a/blocks/timeline/db/access.php b/blocks/timeline/db/access.php
new file mode 100644 (file)
index 0000000..6bd70a2
--- /dev/null
@@ -0,0 +1,50 @@
+<?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'
+    )
+);
diff --git a/blocks/timeline/db/install.php b/blocks/timeline/db/install.php
new file mode 100644 (file)
index 0000000..c5a3d65
--- /dev/null
@@ -0,0 +1,108 @@
+<?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);
+    }
+}
diff --git a/blocks/timeline/lang/en/block_timeline.php b/blocks/timeline/lang/en/block_timeline.php
new file mode 100644 (file)
index 0000000..70f961c
--- /dev/null
@@ -0,0 +1,49 @@
+<?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.';
diff --git a/blocks/timeline/pix/courses.svg b/blocks/timeline/pix/courses.svg
new file mode 100644 (file)
index 0000000..75e59fc
--- /dev/null
@@ -0,0 +1,52 @@
+<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>
diff --git a/blocks/timeline/templates/course-item-loading-placeholder.mustache b/blocks/timeline/templates/course-item-loading-placeholder.mustache
new file mode 100644 (file)
index 0000000..d2c8168
--- /dev/null
@@ -0,0 +1,40 @@
+{{!
+    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>
@@ -15,7 +15,7 @@
     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>
@@ -15,7 +15,7 @@
     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
diff --git a/blocks/timeline/templates/event-list-item.mustache b/blocks/timeline/templates/event-list-item.mustache
new file mode 100644 (file)
index 0000000..40fe0dc
--- /dev/null
@@ -0,0 +1,63 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template 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):
     {
@@ -27,6 +26,9 @@
                 "name": "Assignment due 1",
                 "url": "https://www.google.com",
                 "timesort": 1490320388,
+                "course": {
+                    "fullnamedisplay": "Course 1"
+                },
                 "action": {
                     "name": "Submit assignment",
                     "url": "https://www.google.com",
@@ -43,6 +45,9 @@
                 "name": "Assignment due 2",
                 "url": "https://www.google.com",
                 "timesort": 1490320388,
+                "course": {
+                    "fullnamedisplay": "Course 1"
+                },
                 "action": {
                     "name": "Submit assignment",
                     "url": "https://www.google.com",
@@ -58,6 +63,8 @@
         ]
     }
 }}
+<div class="pl-0 list-group list-group-flush">
 {{#events}}
-    {{> block_myoverview/course-event-list-item }}
+    {{> block_timeline/event-list-item }}
 {{/events}}
+</div>
diff --git a/blocks/timeline/templates/event-list.mustache b/blocks/timeline/templates/event-list.mustache
new file mode 100644 (file)
index 0000000..9a640c4
--- /dev/null
@@ -0,0 +1,55 @@
+{{!
+    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>
diff --git a/blocks/timeline/templates/main.mustache b/blocks/timeline/templates/main.mustache
new file mode 100644 (file)
index 0000000..021cf47
--- /dev/null
@@ -0,0 +1,54 @@
+{{!
+    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}}
diff --git a/blocks/timeline/templates/nav-day-filter.mustache b/blocks/timeline/templates/nav-day-filter.mustache
new file mode 100644 (file)
index 0000000..90afd62
--- /dev/null
@@ -0,0 +1,90 @@
+{{!
+    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>
diff --git a/blocks/timeline/templates/nav-view-selector.mustache b/blocks/timeline/templates/nav-view-selector.mustache
new file mode 100644 (file)
index 0000000..50c2add
--- /dev/null
@@ -0,0 +1,51 @@
+{{!
+    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>
diff --git a/blocks/timeline/templates/placeholder-event-list-item.mustache b/blocks/timeline/templates/placeholder-event-list-item.mustache
new file mode 100644 (file)
index 0000000..ad01fc0
--- /dev/null
@@ -0,0 +1,42 @@
+{{!
+    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>
diff --git a/blocks/timeline/templates/view-courses.mustache b/blocks/timeline/templates/view-courses.mustache
new file mode 100644 (file)
index 0000000..d4d1799
--- /dev/null
@@ -0,0 +1,49 @@
+{{!
+    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}}
diff --git a/blocks/timeline/templates/view.mustache b/blocks/timeline/templates/view.mustache
new file mode 100644 (file)
index 0000000..73decee
--- /dev/null
@@ -0,0 +1,44 @@
+{{!
+    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
diff --git a/blocks/timeline/tests/behat/block_timeline_courses.feature b/blocks/timeline/tests/behat/block_timeline_courses.feature
new file mode 100644 (file)
index 0000000..2da3916
--- /dev/null
@@ -0,0 +1,72 @@
+@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"
diff --git a/blocks/timeline/tests/behat/block_timeline_dates.feature b/blocks/timeline/tests/behat/block_timeline_dates.feature
new file mode 100644 (file)
index 0000000..22c04ec
--- /dev/null
@@ -0,0 +1,88 @@
+@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"