Merge branch 'MDL-63337-master' of git://github.com/bmbrands/moodle
authorJun Pataleta <jun@moodle.com>
Mon, 15 Oct 2018 13:03:16 +0000 (21:03 +0800)
committerJun Pataleta <jun@moodle.com>
Mon, 15 Oct 2018 13:03:16 +0000 (21:03 +0800)
27 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]
course/externallib.php
course/tests/externallib_test.php
lib/editor/atto/plugins/media/lang/en/atto_media.php
lib/editor/atto/plugins/media/lib.php
lib/editor/atto/plugins/media/tests/behat/media.feature
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js
lib/editor/atto/plugins/media/yui/src/button/js/button.js
theme/boost/scss/moodle/core.scss
theme/boost/style/moodle.css
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 c6042d1..08c35bf 100644 (file)
@@ -58,6 +58,8 @@ class core_course_external extends external_api {
                                                 'The expected keys (value format) are:
                                                 excludemodules (bool) Do not return modules, return only the sections structure
                                                 excludecontents (bool) Do not return module contents (i.e: files inside a resource)
+                                                includestealthmodules (bool) Return stealth modules for students in a special
+                                                    section (with id -1)
                                                 sectionid (int) Return only this section
                                                 sectionnumber (int) Return only this section with number (order)
                                                 cmid (int) Return only this module information (among the whole sections structure)
@@ -98,6 +100,7 @@ class core_course_external extends external_api {
                     switch ($name) {
                         case 'excludemodules':
                         case 'excludecontents':
+                        case 'includestealthmodules':
                             $value = clean_param($option['value'], PARAM_BOOL);
                             $filters[$name] = $value;
                             break;
@@ -163,21 +166,12 @@ class core_course_external extends external_api {
             $modinfo = get_fast_modinfo($course);
             $sections = $modinfo->get_section_info_all();
             $coursenumsections = course_get_format($course)->get_last_section_number();
+            $stealthmodules = array();   // Array to keep all the modules available but not visible in a course section/topic.
 
             //for each sections (first displayed to last displayed)
             $modinfosections = $modinfo->get_sections();
             foreach ($sections as $key => $section) {
 
-                // Show the section if the user is permitted to access it, OR if it's not available
-                // but there is some available info text which explains the reason & should display.
-                $showsection = $section->uservisible ||
-                    ($section->visible && !$section->available &&
-                    !empty($section->availableinfo));
-
-                if (!$showsection) {
-                    continue;
-                }
-
                 // This becomes true when we are filtering and we found the value to filter with.
                 $sectionfound = false;
 
@@ -218,8 +212,8 @@ class core_course_external extends external_api {
 
                 $sectioncontents = array();
 
-                // For each module of the section (if it is visible).
-                if ($section->uservisible and empty($filters['excludemodules']) and !empty($modinfosections[$section->section])) {
+                // For each module of the section.
+                if (empty($filters['excludemodules']) and !empty($modinfosections[$section->section])) {
                     foreach ($modinfosections[$section->section] as $cmid) {
                         $cm = $modinfo->cms[$cmid];
 
@@ -312,8 +306,13 @@ class core_course_external extends external_api {
                             }
                         }
 
-                        //assign result to $sectioncontents
-                        $sectioncontents[] = $module;
+                        // Assign result to $sectioncontents, there is an exception,
+                        // stealth activities in non-visible sections for students go to a special section.
+                        if (!empty($filters['includestealthmodules']) && !$section->uservisible && $cm->is_stealth()) {
+                            $stealthmodules[] = $module;
+                        } else {
+                            $sectioncontents[] = $module;
+                        }
 
                         // If we just did a filtering, break the loop.
                         if ($modfound) {
@@ -325,13 +324,46 @@ class core_course_external extends external_api {
                 $sectionvalues['modules'] = $sectioncontents;
 
                 // assign result to $coursecontents
-                $coursecontents[] = $sectionvalues;
+                $coursecontents[$key] = $sectionvalues;
 
                 // Break the loop if we are filtering.
                 if ($sectionfound) {
                     break;
                 }
             }
+
+            // Now that we have iterated over all the sections and activities, check the visibility.
+            // We didn't this before to be able to retrieve stealth activities.
+            foreach ($coursecontents as $sectionnumber => $sectioncontents) {
+                $section = $sections[$sectionnumber];
+                // Show the section if the user is permitted to access it, OR if it's not available
+                // but there is some available info text which explains the reason & should display.
+                $showsection = $section->uservisible ||
+                    ($section->visible && !$section->available &&
+                    !empty($section->availableinfo));
+
+                if (!$showsection) {
+                    unset($coursecontents[$sectionnumber]);
+                    continue;
+                }
+
+                // Remove modules information if the section is not visible for the user.
+                if (!$section->uservisible) {
+                    $coursecontents[$sectionnumber]['modules'] = array();
+                }
+            }
+
+            // Include stealth modules in special section (without any info).
+            if (!empty($stealthmodules)) {
+                $coursecontents[] = array(
+                    'id' => -1,
+                    'name' => '',
+                    'summary' => '',
+                    'summaryformat' => FORMAT_MOODLE,
+                    'modules' => $stealthmodules
+                );
+            }
+
         }
         return $coursecontents;
     }
index a73d492..e7759b0 100644 (file)
@@ -806,8 +806,11 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
      * @return array A list with the course object and course modules objects
      */
     private function prepare_get_course_contents_test() {
-        global $DB;
-        $course  = self::getDataGenerator()->create_course(['numsections' => 3]);
+        global $DB, $CFG;
+
+        $CFG->allowstealth = 1; // Allow stealth activities.
+
+        $course  = self::getDataGenerator()->create_course(['numsections' => 4]);
         $forumdescription = 'This is the forum description';
         $forum = $this->getDataGenerator()->create_module('forum',
             array('course' => $course->id, 'intro' => $forumdescription),
@@ -817,6 +820,8 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $datacm = get_coursemodule_from_instance('page', $data->id);
         $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
         $pagecm = get_coursemodule_from_instance('page', $page->id);
+        // This is an stealth page (set by visibleoncoursepage).
+        $pagestealth = $this->getDataGenerator()->create_module('page', array('course' => $course->id, 'visibleoncoursepage' => 0));
         $labeldescription = 'This is a very long label to test if more than 50 characters are returned.
                 So bla bla bla bla <b>bold bold bold</b> bla bla bla bla.';
         $label = $this->getDataGenerator()->create_module('label', array('course' => $course->id,
@@ -846,10 +851,18 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $conditions = array('course' => $course->id, 'section' => 2);
         $DB->set_field('course_sections', 'summary', 'Text with iframe <iframe src="https://moodle.org"></iframe>', $conditions);
 
-        // Add date availability condition not met for last section.
+        // Add date availability condition not met for section 3.
         $availability = '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '}],"showc":[true]}';
         $DB->set_field('course_sections', 'availability', $availability,
                 array('course' => $course->id, 'section' => 3));
+
+        // Create resource for last section.
+        $pageinhiddensection = $this->getDataGenerator()->create_module('page',
+            array('course' => $course->id, 'name' => 'Page in hidden section', 'section' => 4));
+        // Set not visible last section.
+        $DB->set_field('course_sections', 'visible', 0,
+                array('course' => $course->id, 'section' => 4));
+
         rebuild_course_cache($course->id, true);
 
         return array($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm);
@@ -863,6 +876,8 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
 
         list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
 
+        // We first run the test as admin.
+        $this->setAdminUser();
         $sections = core_course_external::get_course_contents($course->id, array());
         // We need to execute the return values cleaning process to simulate the web service server.
         $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
@@ -889,21 +904,18 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(2, $testexecuted);
         $this->assertEquals(0, $sections[0]['section']);
 
-        // Check that the only return section has the 5 created modules.
-        $this->assertCount(4, $sections[0]['modules']);
+        $this->assertCount(5, $sections[0]['modules']);
         $this->assertCount(1, $sections[1]['modules']);
         $this->assertCount(1, $sections[2]['modules']);
-        $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
+        $this->assertCount(1, $sections[3]['modules']); // One module for the section with availability restrictions.
+        $this->assertCount(1, $sections[4]['modules']); // One module for the hidden section with a visible activity.
         $this->assertNotEmpty($sections[3]['availabilityinfo']);
         $this->assertEquals(1, $sections[1]['section']);
         $this->assertEquals(2, $sections[2]['section']);
         $this->assertEquals(3, $sections[3]['section']);
+        $this->assertEquals(4, $sections[4]['section']);
         $this->assertContains('<iframe', $sections[2]['summary']);
         $this->assertContains('</iframe>', $sections[2]['summary']);
-        // The module with the availability restriction met is returning contents.
-        $this->assertNotEmpty($sections[1]['modules'][0]['contents']);
-        // The module with the availability restriction not met is not returning contents.
-        $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]);
         $this->assertNotEmpty($sections[2]['modules'][0]['availabilityinfo']);
         try {
             $sections = core_course_external::get_course_contents($course->id,
@@ -915,6 +927,54 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
     }
 
 
+    /**
+     * Test get_course_contents as student
+     */
+    public function test_get_course_contents_student() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
+
+        $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
+        $user = self::getDataGenerator()->create_user();
+        self::getDataGenerator()->enrol_user($user->id, $course->id, $studentroleid);
+        $this->setUser($user);
+
+        $sections = core_course_external::get_course_contents($course->id, array());
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
+
+        $this->assertCount(4, $sections); // Nothing for the not visible section.
+        $this->assertCount(5, $sections[0]['modules']);
+        $this->assertCount(1, $sections[1]['modules']);
+        $this->assertCount(1, $sections[2]['modules']);
+        $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
+
+        $this->assertNotEmpty($sections[3]['availabilityinfo']);
+        $this->assertEquals(1, $sections[1]['section']);
+        $this->assertEquals(2, $sections[2]['section']);
+        $this->assertEquals(3, $sections[3]['section']);
+        // The module with the availability restriction met is returning contents.
+        $this->assertNotEmpty($sections[1]['modules'][0]['contents']);
+        // The module with the availability restriction not met is not returning contents.
+        $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]);
+
+        // Now include flag for returning stealth information (fake section).
+        $sections = core_course_external::get_course_contents($course->id,
+            array(array("name" => "includestealthmodules", "value" => 1)));
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
+
+        $this->assertCount(5, $sections); // Include fake section with stealth activities.
+        $this->assertCount(5, $sections[0]['modules']);
+        $this->assertCount(1, $sections[1]['modules']);
+        $this->assertCount(1, $sections[2]['modules']);
+        $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
+        $this->assertCount(1, $sections[4]['modules']); // One stealh module.
+        $this->assertEquals(-1, $sections[4]['id']);
+    }
+
     /**
      * Test get_course_contents excluding modules
      */
@@ -972,7 +1032,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
 
         $this->assertCount(1, $sections);
-        $this->assertCount(4, $sections[0]['modules']);
+        $this->assertCount(5, $sections[0]['modules']);
     }
 
     /**
index 1d6c1c6..2e14642 100644 (file)
@@ -50,6 +50,7 @@ $string['descriptions_help'] = 'Audio descriptions may be used to provide a narr
 $string['descriptionssourcelabel'] = 'Description track URL';
 $string['displayoptions'] = 'Display options';
 $string['entername'] = 'Enter name';
+$string['entertitle'] = 'Enter title';
 $string['entersource'] = 'Source URL';
 $string['enterurl'] = 'Enter URL';
 $string['height'] = 'Height';
index 86a9d37..3e4b19d 100644 (file)
@@ -53,7 +53,7 @@ function atto_media_strings_for_js() {
                                           'descriptionssourcelabel',
                                           'displayoptions',
                                           'entername',
-                                          'entername',
+                                          'entertitle',
                                           'entersource',
                                           'enterurl',
                                           'height',
@@ -79,7 +79,7 @@ function atto_media_strings_for_js() {
                                           'videosourcelabel',
                                           'videowidth',
                                           'width'),
-                                    'atto_media');
+                                          'atto_media');
 }
 
 /**
index d6e42d3..b600418 100644 (file)
@@ -31,7 +31,7 @@ Feature: Add media to Atto
     When I click on "Save changes" "button"
     Then "//a[. = 'moodle-logo.webm']" "xpath_element" should exist
 
-  @javascript
+  @javascript @atto_media_video
   Scenario: Insert some media as a plain video
     Given I click on "Video" "link"
     And I click on "Browse repositories..." "button" in the "#id_summary_editor_video .atto_media_source.atto_media_media_source" "css_element"
@@ -46,7 +46,7 @@ Feature: Add media to Atto
     When I click on "Insert media" "button"
     Then "//video[descendant::source[contains(@src, 'moodle-logo.webm')]][descendant::source[contains(@src, 'moodle-logo.mp4')]]" "xpath_element" should exist
 
-  @javascript
+  @javascript @atto_media_video
   Scenario: Insert some media as a video with display settings
     Given I click on "Video" "link"
     And I click on "Browse repositories..." "button" in the "#id_summary_editor_video .atto_media_source.atto_media_media_source" "css_element"
@@ -61,11 +61,12 @@ Feature: Add media to Atto
     And I change window size to "large"
     And I set the field with xpath "//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_width_entry ')]" to "420"
     And I set the field with xpath "//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_height_entry ')]" to "69"
+    And I set the field "Enter title" to "VideoTitle"
     And I click on "Display options" "link"
     When I click on "Insert media" "button"
-    Then "//video[descendant::source[contains(@src, 'moodle-logo.webm')]][contains(@poster, 'moodle-logo.png')][@width=420][@height=69]" "xpath_element" should exist
+    Then "//video[descendant::source[contains(@src, 'moodle-logo.webm')]][contains(@poster, 'moodle-logo.png')][@width=420][@height=69][@title='VideoTitle']" "xpath_element" should exist
 
-  @javascript
+  @javascript @atto_media_video
   Scenario: Insert some media as a video with advanced settings
     Given I click on "Video" "link"
     And I click on "Browse repositories..." "button" in the "#id_summary_editor_video .atto_media_source.atto_media_media_source" "css_element"
@@ -80,7 +81,7 @@ Feature: Add media to Atto
     When I click on "Insert media" "button"
     Then "//video[descendant::source[contains(@src, 'moodle-logo.webm')]][@controls='true'][@loop='true'][@autoplay='true'][@autoplay='true']" "xpath_element" should exist
 
-  @javascript
+  @javascript @atto_media_video
   Scenario: Insert some media as a video with tracks
     Given I click on "Video" "link"
     And I change window size to "large"
@@ -164,3 +165,40 @@ Feature: Add media to Atto
     And I set the field with xpath "(//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_track_default ')])[9]" to "1"
     When I click on "Insert media" "button"
     Then "//video[descendant::source[contains(@src, 'moodle-logo.webm')]][descendant::track[contains(@src, 'pretty-good-sv.vtt')][@kind='subtitles'][@label='Swedish'][@srclang='sv'][@default='true']][descendant::track[contains(@src, 'pretty-good-en.vtt')][@kind='subtitles'][@label='English'][@srclang='en'][not(@default)]][descendant::track[contains(@src, 'pretty-good-sv.vtt')][@kind='captions'][@label='Swedish'][@srclang='sv'][not(@default)]][descendant::track[contains(@src, 'pretty-good-en.vtt')][@kind='captions'][@label='English'][@srclang='en'][@default='true']][descendant::track[contains(@src, 'pretty-good-sv.vtt')][@kind='descriptions'][@label='Swedish'][@srclang='sv'][@default='true']][descendant::track[contains(@src, 'pretty-good-en.vtt')][@kind='descriptions'][@label='English'][@srclang='en'][not(@default)]][descendant::track[contains(@src, 'pretty-good-sv.vtt')][@kind='chapters'][@label='Swedish'][@srclang='sv'][not(@default)]][descendant::track[contains(@src, 'pretty-good-en.vtt')][@kind='chapters'][@label='English'][@srclang='en'][@default='true']][descendant::track[contains(@src, 'pretty-good-sv.vtt')][@kind='metadata'][@label='Swedish'][@srclang='sv'][@default='true']][descendant::track[contains(@src, 'pretty-good-en.vtt')][@kind='metadata'][@label='English'][@srclang='en'][not(@default)]]" "xpath_element" should exist
+
+  @javascript @atto_media_audio
+  Scenario: Insert some media as a plain audio
+    Given I click on "Audio" "link"
+    And I click on "Browse repositories..." "button" in the "#id_summary_editor_audio .atto_media_source.atto_media_media_source" "css_element"
+    And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
+    And I click on "moodle-logo.mp4" "link"
+    And I click on "Select this file" "button"
+    When I click on "Insert media" "button"
+    Then "//audio[descendant::source[contains(@src, 'moodle-logo.mp4')]]" "xpath_element" should exist
+
+  @javascript @atto_media_audio
+  Scenario: Insert some media as an audio with display settings
+    Given I click on "Audio" "link"
+    And I click on "Browse repositories..." "button" in the "#id_summary_editor_audio .atto_media_source.atto_media_media_source" "css_element"
+    And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
+    And I click on "moodle-logo.mp4" "link"
+    And I click on "Select this file" "button"
+    And I click on "Display options" "link" in the "#id_summary_editor_audio" "css_element"
+    And I set the field "audio_media-title-entry" to "AudioTitle"
+    When I click on "Insert media" "button"
+    Then "//audio[descendant::source[contains(@src, 'moodle-logo.mp4')]][@title='AudioTitle']" "xpath_element" should exist
+
+  @javascript @atto_media_audio
+  Scenario: Insert some media as an audio with advanced settings
+    Given I click on "Audio" "link"
+    And I click on "Browse repositories..." "button" in the "#id_summary_editor_audio .atto_media_source.atto_media_media_source" "css_element"
+    And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
+    And I click on "moodle-logo.mp4" "link"
+    And I click on "Select this file" "button"
+    And I click on "Advanced settings" "link" in the "#id_summary_editor_audio" "css_element"
+    And the field "audio_media-controls-toggle" matches value "1"
+    And I set the field "audio_media-autoplay-toggle" to "1"
+    And I set the field "audio_media-mute-toggle" to "1"
+    And I set the field "audio_media-loop-toggle" to "1"
+    When I click on "Insert media" "button"
+    Then "//audio[descendant::source[contains(@src, 'moodle-logo.mp4')]][@controls='true'][@loop='true'][@autoplay='true'][@autoplay='true']" "xpath_element" should exist
\ No newline at end of file
index 541685a..895858e 100644 (file)
Binary files a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js and b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js differ
index cfdd7c1..49dc812 100644 (file)
Binary files a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js and b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js differ
index 541685a..895858e 100644 (file)
Binary files a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js and b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js differ
index b406076..042db25 100644 (file)
@@ -49,6 +49,7 @@ var COMPONENTNAME = 'atto_media',
         TRACK_SOURCE: 'atto_media_track_source',
         DISPLAY_OPTIONS: 'atto_media_display_options',
         NAME_INPUT: 'atto_media_name_entry',
+        TITLE_INPUT: 'atto_media_title_entry',
         URL_INPUT: 'atto_media_url_entry',
         POSTER_SIZE: 'atto_media_poster_size',
         LINK_SIZE: 'atto_media_link_size',
@@ -80,6 +81,7 @@ var COMPONENTNAME = 'atto_media',
         TRACK_SOURCE: '.' + CSS.TRACK_SOURCE,
         DISPLAY_OPTIONS: '.' + CSS.DISPLAY_OPTIONS,
         NAME_INPUT: '.' + CSS.NAME_INPUT,
+        TITLE_INPUT: '.' + CSS.TITLE_INPUT,
         URL_INPUT: '.' + CSS.URL_INPUT,
         POSTER_SIZE: '.' + CSS.POSTER_SIZE,
         LINK_SIZE: '.' + CSS.LINK_SIZE,
@@ -160,14 +162,14 @@ var COMPONENTNAME = 'atto_media',
                     '<input name="mform_isexpanded_{{elementid}}_video-display-options" type="hidden">' +
                     '<legend class="ftoggler">{{get_string "displayoptions" component}}</legend>' +
                     '<div class="fcontainer">' +
-                        '{{> form_components.display_options}}' +
+                        '{{renderPartial "form_components.display_options" context=this id=CSS.VIDEO mediatype_video=true}}' +
                     '</div>' +
                 '</fieldset>' +
                 '<fieldset class="collapsible collapsed" id="{{elementid}}_video-advanced-settings">' +
                     '<input name="mform_isexpanded_{{elementid}}_video-advanced-settings" type="hidden">' +
                     '<legend class="ftoggler">{{get_string "advancedsettings" component}}</legend>' +
                     '<div class="fcontainer">' +
-                        '{{> form_components.advanced_settings}}' +
+                        '{{renderPartial "form_components.advanced_settings" context=this id=CSS.VIDEO}}' +
                     '</div>' +
                 '</fieldset>' +
                 '<fieldset class="collapsible collapsed" id="{{elementid}}_video-tracks">' +
@@ -180,11 +182,18 @@ var COMPONENTNAME = 'atto_media',
             AUDIO: '' +
                 '{{renderPartial "form_components.source" context=this id=CSS.MEDIA_SOURCE entersourcelabel="audiosourcelabel"' +
                     ' addcomponentlabel="addsource" multisource="true" addsourcehelp=helpStrings.addsource}}' +
+                '<fieldset class="collapsible collapsed" id="{{elementid}}_audio-display-options">' +
+                    '<input name="mform_isexpanded_{{elementid}}_audio-display-options" type="hidden">' +
+                    '<legend class="ftoggler">{{get_string "displayoptions" component}}</legend>' +
+                    '<div class="fcontainer">' +
+                        '{{renderPartial "form_components.display_options" context=this id=CSS.AUDIO}}' +
+                    '</div>' +
+                '</fieldset>' +
                 '<fieldset class="collapsible collapsed" id="{{elementid}}_audio-advanced-settings">' +
                     '<input name="mform_isexpanded_{{elementid}}_audio-advanced-settings" type="hidden">' +
                     '<legend class="ftoggler">{{get_string "advancedsettings" component}}</legend>' +
                     '<div class="fcontainer">' +
-                        '{{> form_components.advanced_settings}}' +
+                        '{{renderPartial "form_components.advanced_settings" context=this id=CSS.AUDIO}}' +
                     '</div>' +
                 '</fieldset>' +
                 '<fieldset class="collapsible collapsed" id="{{elementid}}_audio-tracks">' +
@@ -233,6 +242,13 @@ var COMPONENTNAME = 'atto_media',
                 '</div>',
             DISPLAY_OPTIONS: '' +
                 '<div class="{{CSS.DISPLAY_OPTIONS}}">' +
+                    '<div class="m-b-1">' +
+                        '<label for="{{id}}_media-title-entry">{{get_string "entertitle" component}}</label>' +
+                        '<input class="form-control fullwidth {{CSS.TITLE_INPUT}}" type="text" id="{{id}}_media-title-entry"' +
+                            'size="32"/>' +
+                    '</div>' +
+                    '<div class="clearfix"></div>' +
+                    '{{#mediatype_video}}' +
                     '<div class="m-b-1">' +
                         '<label>{{get_string "size" component}}</label>' +
                         '<div class="form-inline {{CSS.POSTER_SIZE}}">' +
@@ -245,26 +261,37 @@ var COMPONENTNAME = 'atto_media',
                     '</div>' +
                     '<div class="clearfix"></div>' +
                     '{{renderPartial "form_components.source" context=this id=CSS.POSTER_SOURCE entersourcelabel="poster"}}' +
+                    '{{/mediatype_video}}' +
                 '<div>',
             ADVANCED_SETTINGS: '' +
                 '<div class="{{CSS.ADVANCED_SETTINGS}}">' +
                     '<div class="form-check">' +
                         '<input type="checkbox" checked="true" class="form-check-input {{CSS.MEDIA_CONTROLS_TOGGLE}}"' +
-                        'id="media-controls-toggle"/>' +
-                        '<label class="form-check-label" for="media-controls-toggle">{{get_string "controls" component}}</label>' +
+                        'id="{{id}}_media-controls-toggle"/>' +
+                        '<label class="form-check-label" for="{{id}}_media-controls-toggle">' +
+                        '{{get_string "controls" component}}' +
+                        '</label>' +
                     '</div>' +
                     '<div class="form-check">' +
                         '<input type="checkbox" class="form-check-input {{CSS.MEDIA_AUTOPLAY_TOGGLE}}"' +
-                        'id="media-autoplay-toggle"/>' +
-                        '<label class="form-check-label" for="media-autoplay-toggle">{{get_string "autoplay" component}}</label>' +
+                        'id="{{id}}_media-autoplay-toggle"/>' +
+                        '<label class="form-check-label" for="{{id}}_media-autoplay-toggle">' +
+                        '{{get_string "autoplay" component}}' +
+                        '</label>' +
                     '</div>' +
                     '<div class="form-check">' +
-                        '<input type="checkbox" class="form-check-input {{CSS.MEDIA_MUTE_TOGGLE}}" id="media-mute-toggle"/>' +
-                        '<label class="form-check-label" for="media-mute-toggle">{{get_string "mute" component}}</label>' +
+                        '<input type="checkbox" class="form-check-input {{CSS.MEDIA_MUTE_TOGGLE}}" ' +
+                            'id="{{id}}_media-mute-toggle"/>' +
+                        '<label class="form-check-label" for="{{id}}_media-mute-toggle">' +
+                        '{{get_string "mute" component}}' +
+                        '</label>' +
                     '</div>' +
                     '<div class="form-check">' +
-                        '<input type="checkbox" class="form-check-input {{CSS.MEDIA_LOOP_TOGGLE}}" id="media-loop-toggle"/>' +
-                        '<label class="form-check-label" for="media-loop-toggle">{{get_string "loop" component}}</label>' +
+                        '<input type="checkbox" class="form-check-input {{CSS.MEDIA_LOOP_TOGGLE}}" ' +
+                            'id="{{id}}_media-loop-toggle"/>' +
+                        '<label class="form-check-label" for="{{id}}_media-loop-toggle">' +
+                        '{{get_string "loop" component}}' +
+                        '</label>' +
                     '</div>' +
                 '</div>',
             TRACK_TABS: '' +
@@ -366,6 +393,7 @@ var COMPONENTNAME = 'atto_media',
                     '{{#loop}}loop="true" {{/loop}}' +
                     '{{#muted}}muted="true" {{/muted}}' +
                     '{{#autoplay}}autoplay="true" {{/autoplay}}' +
+                    '{{#title}}title="{{../title}}" {{/title}}' +
                 '>' +
                     '{{#sources}}<source src="{{source}}">{{/sources}}' +
                     '{{#tracks}}' +
@@ -380,6 +408,7 @@ var COMPONENTNAME = 'atto_media',
                     '{{#loop}}loop="true" {{/loop}}' +
                     '{{#muted}}muted="true" {{/muted}}' +
                     '{{#autoplay}}autoplay="true" {{/autoplay}}' +
+                    '{{#title}}title="{{../title}}" {{/title}}' +
                 '>' +
                     '{{#sources}}<source src="{{source}}">{{/sources}}' +
                     '{{#tracks}}' +
@@ -645,17 +674,23 @@ Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.Edi
         }, this);
 
         // Populate values.
-        tabPane.one(SELECTORS.POSTER_SOURCE + ' ' + SELECTORS.URL_INPUT).setAttribute('value', properties.poster);
-        tabPane.one(SELECTORS.WIDTH_INPUT).set('value', properties.width);
-        tabPane.one(SELECTORS.HEIGHT_INPUT).set('value', properties.height);
+        tabPane.one(SELECTORS.TITLE_INPUT).set('value', properties.title);
         tabPane.one(SELECTORS.MEDIA_CONTROLS_TOGGLE).set('checked', properties.controls);
         tabPane.one(SELECTORS.MEDIA_AUTOPLAY_TOGGLE).set('checked', properties.autoplay);
         tabPane.one(SELECTORS.MEDIA_MUTE_TOGGLE).set('checked', properties.muted);
         tabPane.one(SELECTORS.MEDIA_LOOP_TOGGLE).set('checked', properties.loop);
 
-        // Switch to the correct tab.
+        // Determine medium type.
         var mediumType = this._getMediumTypeFromTabPane(tabPane);
 
+        if (mediumType === 'video') {
+            // Populate values unique for video.
+            tabPane.one(SELECTORS.POSTER_SOURCE + ' ' + SELECTORS.URL_INPUT).setAttribute('value', properties.poster);
+            tabPane.one(SELECTORS.WIDTH_INPUT).set('value', properties.width);
+            tabPane.one(SELECTORS.HEIGHT_INPUT).set('value', properties.height);
+        }
+
+        // Switch to the correct tab.
         // Remove active class from all tabs + tab panes.
         tabPane.siblings('.active').removeClass('active');
         content.all('.root.nav-tabs .nav-item a').removeClass('active');
@@ -701,6 +736,7 @@ Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.Edi
             type: medium.test('video') ? MEDIA_TYPES.VIDEO : MEDIA_TYPES.AUDIO,
             sources: medium.all('source').get('src'),
             poster: medium.getAttribute('poster'),
+            title: medium.getAttribute('title'),
             width: medium.getAttribute('width'),
             height: medium.getAttribute('height'),
             autoplay: boolAttr(medium, 'autoplay'),
@@ -945,7 +981,8 @@ Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.Edi
             showControls: tab.one(SELECTORS.MEDIA_CONTROLS_TOGGLE).get('checked'),
             autoplay: tab.one(SELECTORS.MEDIA_AUTOPLAY_TOGGLE).get('checked'),
             muted: tab.one(SELECTORS.MEDIA_MUTE_TOGGLE).get('checked'),
-            loop: tab.one(SELECTORS.MEDIA_LOOP_TOGGLE).get('checked')
+            loop: tab.one(SELECTORS.MEDIA_LOOP_TOGGLE).get('checked'),
+            title: tab.one(SELECTORS.TITLE_INPUT).get('value') || false
         };
     }
 }, {
index b22908e..c3e9c48 100644 (file)
@@ -148,6 +148,13 @@ a.autolink.glossary:hover {
     cursor: pointer;
 }
 
+.pagelayout-mydashboard #region-main {
+    border: 0;
+    padding: 0;
+    background-color: transparent;
+    margin-top: -1px;
+}
+
 .collapsibleregioncaption img {
     vertical-align: middle;
 }
index ed92399..6e3e54f 100644 (file)
@@ -8882,6 +8882,12 @@ a.autolink.glossary:hover {
 .pagelayout-mydashboard.jsenabled .collapsibleregioncaption {
   cursor: pointer; }
 
+.pagelayout-mydashboard #region-main {
+  border: 0;
+  padding: 0;
+  background-color: transparent;
+  margin-top: -1px; }
+
 .collapsibleregioncaption img {
   vertical-align: middle; }
 
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"