MDL-63352 block_timeline: Persist user preference on timeline
authorPeter <peter@moodle.com>
Thu, 4 Oct 2018 07:57:04 +0000 (15:57 +0800)
committerPeter <peter@moodle.com>
Mon, 15 Oct 2018 06:46:12 +0000 (14:46 +0800)
* Persist sort preference to the user table
* Persist filter preferences to the user table
* Add behat tests to test persistence
* Add the gdpr exports for the user preferences as well as unit tests for the same
* Updated view_nav.min

16 files changed:
blocks/timeline/amd/build/view_nav.min.js
blocks/timeline/amd/src/view_nav.js
blocks/timeline/block_timeline.php
blocks/timeline/classes/output/main.php
blocks/timeline/classes/privacy/provider.php
blocks/timeline/lang/en/block_timeline.php
blocks/timeline/lib.php [new file with mode: 0644]
blocks/timeline/templates/nav-day-filter.mustache
blocks/timeline/templates/nav-view-selector.mustache
blocks/timeline/templates/view.mustache
blocks/timeline/tests/behat/block_timeline_courses.feature
blocks/timeline/tests/behat/block_timeline_dates.feature
blocks/timeline/tests/privacy_test.php [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/nav-day-filter.mustache
theme/bootstrapbase/templates/block_timeline/nav-view-selector.mustache
theme/bootstrapbase/templates/block_timeline/view.mustache

index c66ad2f..09fbfd9 100644 (file)
Binary files a/blocks/timeline/amd/build/view_nav.min.js and b/blocks/timeline/amd/build/view_nav.min.js differ
index 092dcbd..6fd32a6 100644 (file)
@@ -25,12 +25,16 @@ define(
 [
     'jquery',
     'core/custom_interaction_events',
-    'block_timeline/view'
+    'block_timeline/view',
+    'core/ajax',
+    'core/notification'
 ],
 function(
     $,
     CustomEvents,
-    View
+    View,
+    Ajax,
+    Notification
 ) {
 
     var SELECTORS = {
@@ -41,6 +45,29 @@ function(
         DATA_DAYS_LIMIT: '[data-days-limit]',
     };
 
+    /**
+     * Generic handler to persist user preferences
+     *
+     * @param {string} type The name of the attribute you're updating
+     * @param {string} value The value of the attribute you're updating
+     */
+    var updateUserPreferences = function(type, value) {
+        var request = {
+            methodname: 'core_user_update_user_preferences',
+            args: {
+                preferences: [
+                    {
+                        type: type,
+                        value: value
+                    }
+                ]
+            }
+        };
+
+        Ajax.call([request])[0]
+            .fail(Notification.exception);
+    };
+
     /**
      * Event listener for the day selector ("Next 7 days", "Next 30 days", etc).
      *
@@ -55,6 +82,11 @@ function(
             CustomEvents.events.activate,
             SELECTORS.TIMELINE_DAY_FILTER_OPTION,
             function(e, data) {
+                // Update the user preference
+                var filtername = $(e.currentTarget).data('filtername');
+                var type = 'block_timeline_user_filter_preference';
+                updateUserPreferences(type, filtername);
+
                 var option = $(e.target).closest(SELECTORS.TIMELINE_DAY_FILTER_OPTION);
 
                 if (option.hasClass('active')) {
@@ -94,11 +126,21 @@ function(
      * @param {object} timelineViewRoot The root element for the timeline view
      */
     var registerViewSelector = function(root, timelineViewRoot) {
+        var viewSelector = root.find(SELECTORS.TIMELINE_VIEW_SELECTOR);
+
         // 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() {
+        viewSelector.on('shown shown.bs.tab', function() {
             View.shown(timelineViewRoot);
         });
+
+        // Event selector for user_sort
+        CustomEvents.define(viewSelector, [CustomEvents.events.activate]);
+        viewSelector.on(CustomEvents.events.activate, "[data-toggle='tab']", function(e) {
+            var filtername = $(e.currentTarget).data('filtername');
+            var type = 'block_timeline_user_sort_preference';
+            updateUserPreferences(type, filtername);
+        });
     };
 
     /**
index 5a24f9c..03e7230 100644 (file)
@@ -50,7 +50,10 @@ class block_timeline extends block_base {
             return $this->content;
         }
 
-        $renderable = new \block_timeline\output\main();
+        $sort = get_user_preferences('block_timeline_user_sort_preference');
+        $filter = get_user_preferences('block_timeline_user_filter_preference');
+
+        $renderable = new \block_timeline\output\main($sort, $filter);
         $renderer = $this->page->get_renderer('block_timeline');
 
         $this->content = (object) [
index b5e9609..218eb46 100644 (file)
@@ -30,6 +30,7 @@ use templatable;
 use core_course\external\course_summary_exporter;
 
 require_once($CFG->dirroot . '/course/lib.php');
+require_once($CFG->dirroot . '/blocks/timeline/lib.php');
 require_once($CFG->libdir . '/completionlib.php');
 
 /**
@@ -43,6 +44,86 @@ class main implements renderable, templatable {
     /** Number of courses to load per page */
     const COURSES_PER_PAGE = 2;
 
+    /**
+     * @var string The current filter preference
+     */
+    public $filter;
+
+    /**
+     * @var string The current sort/order preference
+     */
+    public $order;
+
+    /**
+     * main constructor.
+     *
+     * @param string $order Constant sort value from ../timeline/lib.php
+     * @param string $filter Constant sort value from ../timeline/lib.php
+     */
+    public function __construct($order, $filter) {
+        $this->order = $order ? $order : BLOCK_TIMELINE_SORT_BY_DATES;
+        $this->filter = $filter ? $filter : BLOCK_TIMELINE_FILTER_BY_7_DAYS;
+    }
+
+    /**
+     * Test the available filters with the current user preference and return an array with
+     * bool flags corresponding to which is active
+     *
+     * @return array
+     */
+    protected function get_filters_as_booleans() {
+        $filters = [
+            BLOCK_TIMELINE_FILTER_BY_NONE => false,
+            BLOCK_TIMELINE_FILTER_BY_OVERDUE => false,
+            BLOCK_TIMELINE_FILTER_BY_7_DAYS => false,
+            BLOCK_TIMELINE_FILTER_BY_30_DAYS => false,
+            BLOCK_TIMELINE_FILTER_BY_3_MONTHS => false,
+            BLOCK_TIMELINE_FILTER_BY_6_MONTHS => false
+        ];
+
+        // Set the selected filter to true.
+        $filters[$this->filter] = true;
+
+        return $filters;
+    }
+
+    /**
+     * Get the offset/limit values corresponding to $this->filter
+     * which are used to send through to the context as default values
+     *
+     * @return array
+     */
+    private function get_filter_offsets() {
+
+        $limit = false;
+        if (in_array($this->filter, [BLOCK_TIMELINE_FILTER_BY_NONE, BLOCK_TIMELINE_FILTER_BY_OVERDUE])) {
+            $offset = -14;
+            if ($this->filter == BLOCK_TIMELINE_FILTER_BY_OVERDUE) {
+                $limit = 0;
+            }
+        } else {
+            $offset = 0;
+            $limit = 7;
+
+            switch($this->filter) {
+                case BLOCK_TIMELINE_FILTER_BY_30_DAYS:
+                    $limit = 30;
+                    break;
+                case BLOCK_TIMELINE_FILTER_BY_3_MONTHS:
+                    $limit = 90;
+                    break;
+                case BLOCK_TIMELINE_FILTER_BY_6_MONTHS:
+                    $limit = 180;
+                    break;
+            }
+        }
+
+        return [
+            'daysoffset' => $offset,
+            'dayslimit' => $limit
+        ];
+    }
+
     /**
      * Export this data so it can be used as the context for a mustache template.
      *
@@ -69,13 +150,22 @@ class main implements renderable, templatable {
             return $exporter->export($output);
         }, $inprogresscourses);
 
-        return [
+        $filters = $this->get_filters_as_booleans();
+        $offsets = $this->get_filter_offsets();
+        $contextvariables = [
             'midnight' => usergetmidnight(time()),
             'coursepages' => [$formattedcourses],
             'urls' => [
                 'nocourses' => $nocoursesurl,
                 'noevents' => $noeventsurl
-            ]
+            ],
+            'sorttimelinedates' => $this->order == BLOCK_TIMELINE_SORT_BY_DATES,
+            'sorttimelinecourses' => $this->order == BLOCK_TIMELINE_SORT_BY_COURSES,
+            'selectedfilter' => $this->filter,
+            'hasdaysoffset' => true,
+            'hasdayslimit' => $offsets['dayslimit'] !== false ,
+            'nodayslimit' => $offsets['dayslimit'] === false ,
         ];
+        return array_merge($contextvariables, $filters, $offsets);
     }
 }
index 02ae526..3be0998 100644 (file)
@@ -25,6 +25,7 @@
 namespace block_timeline\privacy;
 
 defined('MOODLE_INTERNAL') || die();
+use \core_privacy\local\metadata\collection;
 
 /**
  * Privacy Subsystem for block_timeline.
@@ -32,15 +33,40 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class provider implements \core_privacy\local\metadata\null_provider {
+class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\user_preference_provider {
 
     /**
-     * Get the language string identifier with the component's language
-     * file to explain why this plugin stores no data.
+     * Returns meta-data information about the myoverview block.
      *
-     * @return  string
+     * @param  \core_privacy\local\metadata\collection $collection A collection of meta-data.
+     * @return \core_privacy\local\metadata\collection Return the collection of meta-data.
      */
-    public static function get_reason() : string {
-        return 'privacy:metadata';
+    public static function get_metadata(collection $collection) : collection {
+        $collection->add_user_preference('block_timeline_user_sort_preference', 'privacy:metadata:timelinesortpreference');
+        $collection->add_user_preference('block_timeline_user_filter_preference', 'privacy:metadata:timelinefilterpreference');
+        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_timeline_user_sort_preference', null, $userid);
+        if (isset($preference)) {
+            \core_privacy\local\request\writer::export_user_preference('block_timeline', 'block_timeline_user_sort_preference',
+                    get_string($preference, 'block_timeline'),
+                    get_string('privacy:metadata:timelinesortpreference', 'block_timeline')
+            );
+        }
+
+        $preference = get_user_preferences('block_timeline_user_filter_preference', null, $userid);
+        if (isset($preference)) {
+            \core_privacy\local\request\writer::export_user_preference('block_timeline', 'block_timeline_user_filter_preference',
+                    get_string($preference, 'block_timeline'),
+                    get_string('privacy:metadata:timelinefilterpreference', 'block_timeline')
+            );
+        }
     }
 }
index 70f961c..d474cc9 100644 (file)
@@ -41,9 +41,11 @@ $string['next7days'] = 'Next 7 days';
 $string['next3months'] = 'Next 3 months';
 $string['next6months'] = 'Next 6 months';
 $string['overdue'] = 'Overdue';
+$string['all'] = 'All';
 $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.';
+$string['privacy:metadata:timelinesortpreference'] = 'The user sort preference for the timeline block.';
+$string['privacy:metadata:timelinefilterpreference'] = 'The user day filter preference for the timeline block.';
diff --git a/blocks/timeline/lib.php b/blocks/timeline/lib.php
new file mode 100644 (file)
index 0000000..26e8783
--- /dev/null
@@ -0,0 +1,70 @@
+<?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/>.
+
+/**
+ * Library functions for timeline
+ *
+ * @package   block_timeline
+ * @copyright 2018 Peter Dias
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Define constants to store the SORT user preference
+ */
+define('BLOCK_TIMELINE_SORT_BY_DATES', 'sortbydates');
+define('BLOCK_TIMELINE_SORT_BY_COURSES', 'sortbycourses');
+
+/**
+ * Define constants to store the FILTER user preference
+ */
+define('BLOCK_TIMELINE_FILTER_BY_NONE', 'all');
+define('BLOCK_TIMELINE_FILTER_BY_OVERDUE', 'overdue');
+define('BLOCK_TIMELINE_FILTER_BY_7_DAYS', 'next7days');
+define('BLOCK_TIMELINE_FILTER_BY_30_DAYS', 'next30days');
+define('BLOCK_TIMELINE_FILTER_BY_3_MONTHS', 'next3months');
+define('BLOCK_TIMELINE_FILTER_BY_6_MONTHS', 'next6months');
+
+/**
+ * Returns the name of the user preferences as well as the details this plugin uses.
+ *
+ * @return array
+ */
+function block_timeline_user_preferences() {
+    $preferences['block_timeline_user_sort_preference'] = array(
+        'null' => NULL_NOT_ALLOWED,
+        'default' => BLOCK_TIMELINE_SORT_BY_DATES,
+        'type' => PARAM_ALPHA,
+        'choices' => array(BLOCK_TIMELINE_SORT_BY_DATES, BLOCK_TIMELINE_SORT_BY_COURSES)
+    );
+
+    $preferences['block_timeline_user_filter_preference'] = array(
+        'null' => NULL_NOT_ALLOWED,
+        'default' => BLOCK_TIMELINE_FILTER_BY_30_DAYS,
+        'type' => PARAM_ALPHANUM,
+        'choices' => array(
+                BLOCK_TIMELINE_FILTER_BY_NONE,
+                BLOCK_TIMELINE_FILTER_BY_OVERDUE,
+                BLOCK_TIMELINE_FILTER_BY_7_DAYS,
+                BLOCK_TIMELINE_FILTER_BY_30_DAYS,
+                BLOCK_TIMELINE_FILTER_BY_3_MONTHS,
+                BLOCK_TIMELINE_FILTER_BY_6_MONTHS
+        )
+    );
+
+    return $preferences;
+}
index 90afd62..938763b 100644 (file)
     </button>
     <div role="menu" class="dropdown-menu" data-show-active-item>
         <a
-            class="dropdown-item"
+            class="dropdown-item {{#all}} active {{/all}}"
             href="#"
             data-from="-14"
+            data-filtername="all"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} all, core {{/str}}{{/str}}"
         >
             {{#str}} all, core {{/str}}
         </a>
         <a
-            class="dropdown-item"
+            class="dropdown-item {{#overdue}} active {{/overdue}}"
             href="#"
             data-from="-14"
             data-to="0"
+            data-filtername="overdue"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} overdue, block_timeline {{/str}}{{/str}}"
         >
             {{#str}} overdue, block_timeline {{/str}}
         <div class="dropdown-divider"></div>
         <h6 class="dropdown-header">{{#str}} duedate, block_timeline {{/str}}</h6>
         <a
-            class="dropdown-item"
+            class="dropdown-item {{#next7days}} active {{/next7days}}"
             href="#"
             data-from="0"
             data-to="7"
+            data-filtername="next7days"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next7days, block_timeline {{/str}}{{/str}}"
         >
             {{#str}} next7days, block_timeline {{/str}}
         </a>
         <a
-            class="dropdown-item active"
+            class="dropdown-item {{#next30days}} active {{/next30days}}"
             href="#"
             data-from="0"
             data-to="30"
+            data-filtername="next30days"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next30days, block_timeline {{/str}}{{/str}}"
         >
             {{#str}} next30days, block_timeline {{/str}}
         </a>
         <a
-            class="dropdown-item"
+            class="dropdown-item {{#next3months}} active {{/next3months}}"
             href="#"
             data-from="0"
             data-to="90"
+            data-filtername="next3months"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next3months, block_timeline {{/str}}{{/str}}"
         >
             {{#str}} next3months, block_timeline {{/str}}
         </a>
         <a
-            class="dropdown-item"
+            class="dropdown-item {{#next6months}} active {{/next6months}}"
             href="#"
             data-from="0"
             data-to="180"
+            data-filtername="next6months"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next6months, block_timeline {{/str}}{{/str}}"
         >
             {{#str}} next6months, block_timeline {{/str}}
index 50c2add..c7cbc1c 100644 (file)
     <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>
+            {{#sorttimelinecourses}}<span data-active-item-text>{{/sorttimelinecourses}}{{#str}} ariaviewselector, block_timeline{{/str}}{{#sorttimelinecourses}}</span>{{/sorttimelinecourses}}
+            {{#sorttimelinedates}}<span data-active-item-text>{{/sorttimelinedates}}{{#str}} sortbydates, block_timeline {{/str}}{{#sorttimelinedates}}</span>{{/sorttimelinedates}}
         </span>
     </button>
-    <div role="menu" class="dropdown-menu dropdown-menu-right list-group hidden" data-show-active-item data-skip-active-class="true">
+    <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"
+            class="dropdown-item {{#sorttimelinedates}}active{{/sorttimelinedates}}"
             href="#view_dates_{{uniqid}}"
             data-toggle="tab"
+            data-filtername="sortbydates"
             aria-label="{{#str}} ariaviewselectoroption, block_timeline, {{#str}} sortbydates, block_timeline {{/str}}{{/str}}"
         >
             {{#str}} sortbydates, block_timeline {{/str}}
         </a>
         <a
-            class="dropdown-item"
+            class="dropdown-item {{#sorttimelinecourses}}active{{/sorttimelinecourses}}"
             href="#view_courses_{{uniqid}}"
             data-toggle="tab"
+            data-filtername="sortbycourses"
             aria-label="{{#str}} ariaviewselectoroption, block_timeline, {{#str}} sortbycourses, block_timeline {{/str}}{{/str}}"
         >
             {{#str}} sortbycourses, block_timeline {{/str}}
index 73decee..acb5f78 100644 (file)
     This template renders the timeline view for the timeline block.
 
     Example context (json):
-    {}
+    {
+        "midnight": 1538954668,
+        "coursepages": [
+            {}
+        ],
+        "urls": {
+            "nocourses": "#",
+            "noevents": "#"
+        },
+        "sorttimelinedates": true,
+        "sorttimelinecourses": false,
+        "selectedfilter": "all",
+        "hasdaysoffset": true,
+        "hasdayslimit": false,
+        "nodayslimit": true,
+        "all": true,
+        "overdue": false,
+        "next7days": false,
+        "next30days": false,
+        "next3months": false,
+        "next6months": false,
+        "daysoffset": -14,
+        "dayslimit": false
+    }
 }}
 <div data-region="timeline-view">
     <div class="tab-content">
-        <div class="tab-pane active fade show" data-region="view-dates" id="view_dates_{{uniqid}}">
+        <div class="tab-pane {{#sorttimelinedates}}active show{{/sorttimelinedates}} fade" data-region="view-dates" id="view_dates_{{uniqid}}">
             {{> block_timeline/view-dates }}
         </div>
         <div
-            class="tab-pane fade"
+            class="tab-pane {{#sorttimelinecourses}}active show{{/sorttimelinecourses}} fade"
             data-region="view-courses"
             data-midnight="{{midnight}}"
             data-limit="2"
index 2da3916..d9a8a20 100644 (file)
@@ -70,3 +70,22 @@ Feature: The timeline block allows users to see upcoming courses
     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"
+
+  Scenario: Persistent sort filter
+    Given I log in as "student1"
+    And I click on "Sort" "button" in the "Timeline" "block"
+    And I click on "Sort by dates" "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"
+    And I reload the page
+    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"
index 22c04ec..29548a0 100644 (file)
@@ -86,3 +86,36 @@ Feature: The timeline block allows users to see upcoming activities
     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"
+
+  Scenario: Persistent 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"
+    And I reload the page
+    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']" "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: Persistent 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"
+    And I reload the page
+    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"
diff --git a/blocks/timeline/tests/privacy_test.php b/blocks/timeline/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..0aacf3e
--- /dev/null
@@ -0,0 +1,110 @@
+<?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_timeline implementation of the privacy API.
+ *
+ * @package    block_timeline
+ * @category   test
+ * @copyright  2018 Peter Dias <peter@moodle.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_timeline\privacy\provider;
+
+/**
+ * Unit tests for the block_timeline implementation of the privacy API.
+ *
+ * @copyright  2018 Peter Dias <peter@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block_timeline_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_date_sort_preference() {
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        set_user_preference('block_timeline_user_sort_preference', 'sortbydates', $user);
+
+        provider::export_user_preferences($user->id);
+        $writer = writer::with_context(\context_system::instance());
+        $blockpreferences = $writer->get_user_preferences('block_timeline');
+        $this->assertEquals('Sort by dates', $blockpreferences->block_timeline_user_sort_preference->value);
+    }
+
+    /**
+     * Test that the preference timeline is exported properly.
+     */
+    public function test_export_user_preferences_course_sort_preference() {
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        set_user_preference('block_timeline_user_sort_preference', 'sortbycourses', $user);
+
+        provider::export_user_preferences($user->id);
+        $writer = writer::with_context(\context_system::instance());
+        $blockpreferences = $writer->get_user_preferences('block_timeline');
+        $this->assertEquals('Sort by courses', $blockpreferences->block_timeline_user_sort_preference->value);
+    }
+
+    /**
+     * Test that the preference timeline is exported properly.
+     */
+    public function test_export_user_preferences_7day_filter_preference() {
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        set_user_preference('block_timeline_user_filter_preference', 'next7days', $user);
+
+        provider::export_user_preferences($user->id);
+        $writer = writer::with_context(\context_system::instance());
+        $blockpreferences = $writer->get_user_preferences('block_timeline');
+        $this->assertEquals('Next 7 days', $blockpreferences->block_timeline_user_filter_preference->value);
+    }
+
+    /**
+     * Test that the preference timeline is exported properly.
+     */
+    public function test_export_user_preferences_all_filter_preference() {
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        set_user_preference('block_timeline_user_filter_preference', 'all', $user);
+
+        provider::export_user_preferences($user->id);
+        $writer = writer::with_context(\context_system::instance());
+        $blockpreferences = $writer->get_user_preferences('block_timeline');
+        $this->assertEquals('All', $blockpreferences->block_timeline_user_filter_preference->value);
+    }
+}
index 85a1183..e792b0d 100644 (file)
         <span data-region="caret" class="caret"></span>
     </button>
     <ul role="menu" class="dropdown-menu" data-show-active-item>
-        <li class="dropdown-item" data-from="-14">
+        <li class="dropdown-item {{#all}} active {{/all}}" data-from="-14" data-filtername="all">
             <a href="#" aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} all, core {{/str}}{{/str}}">
                 {{#str}} all, core {{/str}}
             </a>
         </li>
-        <li class="dropdown-item" data-from="-14" data-to="0">
+        <li class="dropdown-item {{#overdue}} active {{/overdue}}" data-from="-14" data-to="0" data-filtername="overdue">
             <a href="#" aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} overdue, block_timeline {{/str}}{{/str}}">
                 {{#str}} overdue, block_timeline {{/str}}
             </a>
         </li>
         <li class="divider"></li>
-        <li class="dropdown-item" data-from="0" data-to="7">
+        <li class="dropdown-item {{#next7days}} active {{/next7days}}" data-from="0" data-to="7" data-filtername="next7days">
             <a href="#" aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next7days, block_timeline {{/str}}{{/str}}">
                 {{#str}} next7days, block_timeline {{/str}}
             </a>
         </li>
-        <li class="dropdown-item active" data-from="0" data-to="30">
+        <li class="dropdown-item {{#next30days}} active {{/next30days}}" data-from="0" data-to="30" data-filtername="next30days">
             <a href="#" aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next30days, block_timeline {{/str}}{{/str}}">
                 {{#str}} next30days, block_timeline {{/str}}
             </a>
         </li>
-        <li class="dropdown-item" data-from="0" data-to="90">
+        <li class="dropdown-item {{#next3months}} active {{/next3months}}" data-from="0" data-to="90" data-filtername="next3months">
             <a href="#" aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next3months, block_timeline {{/str}}{{/str}}">
                 {{#str}} next3months, block_timeline {{/str}}
             </a>
         </li>
-        <li class="dropdown-item" data-from="0" data-to="180">
+        <li class="dropdown-item {{#next6months}} active {{/next6months}}" data-from="0" data-to="180" data-filtername="next6months">
             <a href="#" aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next6months, block_timeline {{/str}}{{/str}}">
                 {{#str}} next6months, block_timeline {{/str}}
             </a>
index 4caeda9..9339cb4 100644 (file)
         <span data-region="caret" class="caret"></span>
     </button>
     <ul role="menu" class="dropdown-menu dropdown-menu-right" data-show-active-item>
-        <li class="dropdown-item active" data-target="#view_dates_{{uniqid}}" data-toggle="tab">
+        <li class="dropdown-item {{#sorttimelinedates}}active{{/sorttimelinedates}}" data-target="#view_dates_{{uniqid}}" data-toggle="tab" data-filtername="sortbydates">
             <a href="#" aria-label="{{#str}} ariaviewselectoroption, block_timeline, {{#str}} sortbydates, block_timeline {{/str}}{{/str}}">
                 {{#str}} sortbydates, block_timeline {{/str}}
             </a>
         </li>
-        <li class="dropdown-item" data-target="#view_courses_{{uniqid}}" data-toggle="tab">
+        <li class="dropdown-item {{#sorttimelinecourses}}active{{/sorttimelinecourses}}" data-target="#view_courses_{{uniqid}}" data-toggle="tab" data-filtername="sortbycourses">
             <a href="#" aria-label="{{#str}} ariaviewselectoroption, block_timeline, {{#str}} sortbycourses, block_timeline {{/str}}{{/str}}">
                 {{#str}} sortbycourses, block_timeline {{/str}}
             </a>
index 7d67b03..352631d 100644 (file)
     This template renders the timeline view for the timeline block.
 
     Example context (json):
-    {}
+    {
+        "midnight": 1538954668,
+        "coursepages": [
+            {}
+        ],
+        "urls": {
+            "nocourses": "#",
+            "noevents": "#"
+        },
+        "sorttimelinedates": true,
+        "sorttimelinecourses": false,
+        "selectedfilter": "all",
+        "hasdaysoffset": true,
+        "hasdayslimit": false,
+        "nodayslimit": true,
+        "all": true,
+        "overdue": false,
+        "next7days": false,
+        "next30days": false,
+        "next3months": false,
+        "next6months": false,
+        "daysoffset": -14,
+        "dayslimit": false
+    }
 }}
 <div data-region="timeline-view">
     <div class="tab-content" style="overflow: visible">
-        <div class="tab-pane active fade in" data-region="view-dates" id="view_dates_{{uniqid}}">
+        <div class="tab-pane {{#sorttimelinedates}}active in{{/sorttimelinedates}} fade" data-region="view-dates" id="view_dates_{{uniqid}}">
             {{> block_timeline/view-dates }}
         </div>
         <div
-            class="tab-pane fade"
+            class="tab-pane {{#sorttimelinecourses}}active in{{/sorttimelinecourses}} fade"
             data-region="view-courses"
             data-midnight="{{midnight}}"
             data-limit="2"