MDL-63457 block_myoverview: Hide courses from individual overview blocks
authorPeter <peter@moodle.com>
Tue, 16 Oct 2018 07:19:19 +0000 (15:19 +0800)
committerPeter <peter@moodle.com>
Mon, 29 Oct 2018 00:32:05 +0000 (08:32 +0800)
* add new preferences for hidden courses
* Modified course filter to use the user preference
* Behat test
* Regex update
* Toggle hide/show
* Update enrolled_course getter to pass hidden value
* provider
* unit tests

16 files changed:
blocks/myoverview/amd/build/view.min.js
blocks/myoverview/amd/src/view.js
blocks/myoverview/classes/privacy/provider.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/lib.php
blocks/myoverview/templates/course-action-menu.mustache
blocks/myoverview/templates/courses-view.mustache
blocks/myoverview/templates/nav-grouping-selector.mustache
blocks/myoverview/tests/behat/block_myoverview_dashboard.feature
blocks/myoverview/tests/behat/block_myoverview_hidden.feature [new file with mode: 0644]
blocks/myoverview/tests/privacy_test.php
course/classes/external/course_summary_exporter.php
course/externallib.php
course/lib.php
course/tests/courselib_test.php
theme/bootstrapbase/templates/block_myoverview/course-action-menu.mustache

index 5f3fb07..53c65f1 100644 (file)
Binary files a/blocks/myoverview/amd/build/view.min.js and b/blocks/myoverview/amd/build/view.min.js differ
index f4304fa..e799775 100644 (file)
@@ -28,6 +28,7 @@ define(
     'core/paged_content_factory',
     'core/custom_interaction_events',
     'core/notification',
+    'core/ajax',
     'core/templates',
 ],
 function(
@@ -36,10 +37,14 @@ function(
     PagedContentFactory,
     CustomEvents,
     Notification,
+    Ajax,
     Templates
 ) {
 
     var SELECTORS = {
+        COURSE_REGION: '[data-region="course-view-content"]',
+        ACTION_HIDE_COURSE: '[data-action="hide-course"]',
+        ACTION_SHOW_COURSE: '[data-action="show-course"]',
         ACTION_ADD_FAVOURITE: '[data-action="add-favourite"]',
         ACTION_REMOVE_FAVOURITE: '[data-action="remove-favourite"]',
         FAVOURITE_ICON: '[data-region="favourite-icon"]',
@@ -127,7 +132,7 @@ function(
      * @param {Object} root The favourite icon container element.
      * @return {Number} Course id.
      */
-    var getFavouriteCourseId = function(root) {
+    var getCourseId = function(root) {
         return root.attr('data-course-id');
     };
 
@@ -294,20 +299,12 @@ function(
     };
 
     /**
-     * Intialise the courses list and cards views on page load.
+     * Intialise the paged list and cards views on page load.
      *
      * @param {object} root The root element for the courses view.
      * @param {object} content The content element for the courses view.
      */
-    var init = function(root, content) {
-
-        root = $(root);
-
-        if (!root.attr('data-init')) {
-            registerEventListeners(root);
-            root.attr('data-init', true);
-        }
-
+    var initializePagedContent = function(root, content) {
         var filters = getFilterValues(root);
 
         var pagedContentPromise = PagedContentFactory.createWithLimit(
@@ -330,7 +327,7 @@ function(
                         loadedPages[currentPage] = coursesData;
                         return renderCourses(root, coursesData);
                     })
-                    .catch(Notification.exception);
+                        .catch(Notification.exception);
 
                     promises.push(pagePromise);
                 });
@@ -350,21 +347,21 @@ function(
      *
      * @param {Object} root The myoverview block container element.
      */
-    var registerEventListeners = function(root) {
+    var registerEventListeners = function(root, content) {
         CustomEvents.define(root, [
             CustomEvents.events.activate
         ]);
 
         root.on(CustomEvents.events.activate, SELECTORS.ACTION_ADD_FAVOURITE, function(e, data) {
             var favourite = $(e.target).closest(SELECTORS.ACTION_ADD_FAVOURITE);
-            var courseId = getFavouriteCourseId(favourite);
+            var courseId = getCourseId(favourite);
             addToFavourites(root, courseId);
             data.originalEvent.preventDefault();
         });
 
         root.on(CustomEvents.events.activate, SELECTORS.ACTION_REMOVE_FAVOURITE, function(e, data) {
             var favourite = $(e.target).closest(SELECTORS.ACTION_REMOVE_FAVOURITE);
-            var courseId = getFavouriteCourseId(favourite);
+            var courseId = getCourseId(favourite);
             removeFromFavourites(root, courseId);
             data.originalEvent.preventDefault();
         });
@@ -372,9 +369,67 @@ function(
         root.on(CustomEvents.events.activate, SELECTORS.FAVOURITE_ICON, function(e, data) {
             data.originalEvent.preventDefault();
         });
+
+        root.on(CustomEvents.events.activate, SELECTORS.ACTION_HIDE_COURSE, function(e, data) {
+            var target = $(e.target).closest(SELECTORS.ACTION_HIDE_COURSE);
+            var id = getCourseId(target);
+
+            var request = {
+                preferences: [
+                    {
+                        type: 'block_myoverview_hidden_course_' + id,
+                        value: true
+                    }
+                ]
+            };
+            Repository.updateUserPreferences(request);
+
+            // Reload the paged content based on the hidden course
+            initializePagedContent(root, content);
+            data.originalEvent.preventDefault();
+        });
+
+        root.on(CustomEvents.events.activate, SELECTORS.ACTION_SHOW_COURSE, function(e, data) {
+            var target = $(e.target).closest(SELECTORS.ACTION_SHOW_COURSE);
+            var id = getCourseId(target);
+
+            var request = {
+                preferences: [
+                    {
+                        type: 'block_myoverview_hidden_course_' + id,
+                        value: null
+                    }
+                ]
+            };
+
+            Repository.updateUserPreferences(request);
+
+            // Reload the paged content based on the hidden course
+            initializePagedContent(root, content);
+            data.originalEvent.preventDefault();
+        });
     };
 
     /**
+     * Intialise the courses list and cards views on page load.
+     * 
+     * @param {object} root The root element for the courses view.
+     * @param {object} content The content element for the courses view.
+     */
+    var init = function(root, content) {
+
+        root = $(root);
+
+        if (!root.attr('data-init')) {
+            registerEventListeners(root, content);
+            root.attr('data-init', true);
+        }
+
+        initializePagedContent(root, content);
+    };
+
+    /**
+
      * Reset the courses views to their original
      * state on first page load.
      *
index 34463a1..2d10e2b 100644 (file)
@@ -78,5 +78,20 @@ class provider implements \core_privacy\local\metadata\provider, user_preference
                 get_string($preference, 'block_myoverview'),
                 get_string('privacy:metadata:overviewgroupingpreference', 'block_myoverview'));
         }
+
+        $preferences = get_user_preferences(null, null, $userid);
+        foreach ($preferences as $name => $value) {
+            if ((substr($name, 0, 30) == 'block_myoverview_hidden_course')) {
+                writer::export_user_preference(
+                    'block_myoverview',
+                    $name,
+                    $value,
+                    get_string('privacy:request:preference:set', 'block_myoverview', (object) [
+                        'name' => $name,
+                        'value' => $value,
+                    ])
+                );
+            }
+        }
     }
 }
\ No newline at end of file
index 6de059d..1b723ff 100644 (file)
@@ -48,6 +48,7 @@ $string['aria:summary'] = 'Switch to summary view';
 $string['aria:sortingdropdown'] = 'Sorting dropdown';
 $string['card'] = 'Card';
 $string['cards'] = 'Cards';
+$string['courseprogress'] = 'Course progress:';
 $string['complete'] = 'Complete';
 $string['favourite'] = 'Starred course';
 $string['favourites'] = 'Starred';
@@ -67,6 +68,14 @@ $string['privacy:metadata:overviewgroupingpreference'] = 'The myoverview block g
 $string['removefromfavourites'] = 'Unstar this course';
 $string['summary'] = 'Summary';
 $string['title'] = 'Title';
+$string['aria:hidecourse'] = 'Hide {$a} from view';
+$string['aria:showcourse'] = 'Show {$a} in view';
+$string['aria:hiddencourses'] = 'Show hidden courses';
+$string['hidden'] = 'Hidden courses';
+$string['hidecourse'] = 'Hide from view';
+$string['hiddencourses'] = 'Hidden';
+$string['show'] = 'Show this course';
+$string['privacy:request:preference:set'] = 'The value of the setting \'{$a->name}\' was \'{$a->value}\'';
 
 // Deprecated since Moodle 3.6.
 $string['defaulttab'] = 'Default tab';
@@ -84,4 +93,4 @@ $string['sortbydates'] = 'Sort by dates';
 $string['timeline'] = 'Timeline';
 $string['viewcoursename'] = 'View course {$a}';
 $string['privacy:metadata:overviewlasttab'] = 'This stores the last tab selected by the user on the overview block.';
-
+$string['viewcourse'] = 'View course';
index bc0530e..3244e4c 100644 (file)
@@ -32,6 +32,7 @@ define('BLOCK_MYOVERVIEW_GROUPING_INPROGRESS', 'inprogress');
 define('BLOCK_MYOVERVIEW_GROUPING_FUTURE', 'future');
 define('BLOCK_MYOVERVIEW_GROUPING_PAST', 'past');
 define('BLOCK_MYOVERVIEW_GROUPING_FAVOURITES', 'favourites');
+define('BLOCK_MYOVERVIEW_GROUPING_HIDDEN', 'hidden');
 
 /**
  * Constants for the user preferences sorting options
@@ -62,7 +63,8 @@ function block_myoverview_user_preferences() {
             BLOCK_MYOVERVIEW_GROUPING_INPROGRESS,
             BLOCK_MYOVERVIEW_GROUPING_FUTURE,
             BLOCK_MYOVERVIEW_GROUPING_PAST,
-            BLOCK_MYOVERVIEW_GROUPING_FAVOURITES
+            BLOCK_MYOVERVIEW_GROUPING_FAVOURITES,
+            BLOCK_MYOVERVIEW_GROUPING_HIDDEN
         )
     );
     $preferences['block_myoverview_user_sort_preference'] = array(
@@ -84,5 +86,14 @@ function block_myoverview_user_preferences() {
             BLOCK_MYOVERVIEW_VIEW_SUMMARY
         )
     );
+
+    $preferences['/^block_myoverview_hidden_course_(\d)+$/'] = array(
+        'isregex' => true,
+        'choices' => array(0, 1),
+        'type' => PARAM_INT,
+        'null' => NULL_NOT_ALLOWED,
+        'default' => 'none'
+    );
+
     return $preferences;
-}
\ No newline at end of file
+}
index 027bdab..d137472 100644 (file)
                 {{#str}} aria:removefromfavourites, block_myoverview {{/str}} {{{fullname}}}
             </div>
         </a>
+        <a class="dropdown-item {{^hidden}}hidden{{/hidden}}" href="#"
+            data-action="show-course"
+            data-course-id="{{id}}"
+            aria-controls="favorite-icon-{{ id }}"
+            >
+            {{#pix}} i/show, core, {{#str}} hidden, block_myoverview {{/str}} {{/pix}}
+            {{#str}} show, block_myoverview {{/str}}
+            <div class="sr-only">
+                {{#str}} aria:showcourse, block_myoverview, {{fullname}} {{/str}}
+            </div>
+        </a>
+        <a class="dropdown-item {{#hidden}}hidden{{/hidden}}" href="#"
+            data-action="hide-course"
+            data-course-id="{{id}}"
+            aria-controls="favorite-icon-{{ id }}"
+            >
+            {{#pix}} i/hide, core, {{#str}} hidden, block_myoverview {{/str}} {{/pix}}
+            {{#str}} hidecourse, block_myoverview {{/str}}
+            <div class="sr-only">
+                {{#str}} aria:hidecourse, block_myoverview, {{fullname}} {{/str}}
+            </div>
+        </a>
     </div>
 </div>
\ No newline at end of file
index d8f2aa4..c5be1e8 100644 (file)
@@ -32,6 +32,7 @@
     data-display="{{view}}"
     data-grouping="{{grouping}}"
     data-sort="{{sort}}"
+    data-prev-display="{{view}}"
     data-nocoursesimg="{{nocoursesimg}}">
     <div data-region="course-view-content">
         <div data-region="courses-loading-placeholder">
index 9e341a6..687e1a3 100644 (file)
@@ -36,6 +36,7 @@
             {{#future}}{{#str}} future, block_myoverview {{/str}}{{/future}}
             {{#past}}{{#str}} past, block_myoverview {{/str}}{{/past}}
             {{#favourites}}{{#str}} favourites, block_myoverview {{/str}}{{/favourites}}
+            {{#hidden}}{{#str}} hiddencourses, block_myoverview {{/str}}{{/hidden}}
         </span>
     </button>
     <ul class="dropdown-menu" data-show-active-item data-active-item-text aria-labelledby="groupingdropdown">
                 {{#str}} favourites, block_myoverview {{/str}}
             </a>
         </li>
+        <li>
+            <a class="dropdown-item {{#hidden}}active{{/hidden}}" href="#" data-filter="grouping" data-value="hidden"  data-pref="hidden" aria-label="{{#str}} aria:hiddencourses, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
+                {{#str}} hiddencourses, block_myoverview {{/str}}
+            </a>
+        </li>
     </ul>
 </div>
index f63c118..e67576a 100644 (file)
@@ -157,4 +157,46 @@ Feature: The my overview block allows users to easily access their courses
     And I click on "Last accessed" "link" in the "Course overview" "block"
     And I reload the page
     Then I should see "Last accessed" in the "Course overview" "block"
-    And "[data-sort='ul.timeaccess desc']" "css_element" in the "Course overview" "block" should be visible
\ No newline at end of file
+    And "[data-sort='ul.timeaccess desc']" "css_element" in the "Course overview" "block" should be visible
+
+  Scenario: View inprogress courses with hide persistent functionality
+    Given I log in as "student1"
+    And I click on "All" "button" in the "Course overview" "block"
+    When I click on "In progress" "link" in the "Course overview" "block"
+    And I click on ".coursemenubtn" "css_element" in the "//div[@class='card course-card' and contains(.,'Course 2')]" "xpath_element"
+    And I click on "Hide from view" "link" in the "//div[@class='card course-card' and contains(.,'Course 2')]" "xpath_element"
+    And I reload the page
+    Then I should see "Course 3" in the "Course overview" "block"
+    Then I should see "Course 4" in the "Course overview" "block"
+    And I should not see "Course 2" in the "Course overview" "block"
+    And I should not see "Course 1" in the "Course overview" "block"
+    And I should not see "Course 5" in the "Course overview" "block"
+    And I log out
+
+  Scenario: View past courses with hide persistent functionality
+    Given I log in as "student1"
+    And I click on "All" "button" in the "Course overview" "block"
+    When I click on "Past" "link" in the "Course overview" "block"
+    And I click on ".coursemenubtn" "css_element" in the "//div[@class='card course-card' and contains(.,'Course 1')]" "xpath_element"
+    And I click on "Hide from view" "link" in the "//div[@class='card course-card' and contains(.,'Course 1')]" "xpath_element"
+    And I reload the page
+    Then I should not see "Course 1" in the "Course overview" "block"
+    And I should not see "Course 2" in the "Course overview" "block"
+    And I should not see "Course 3" in the "Course overview" "block"
+    And I should not see "Course 4" in the "Course overview" "block"
+    And I should not see "Course 5" in the "Course overview" "block"
+    And I log out
+
+  Scenario: View future courses with hide persistent functionality
+    Given I log in as "student1"
+    And I click on "All" "button" in the "Course overview" "block"
+    When I click on "Future" "link" in the "Course overview" "block"
+    And I click on ".coursemenubtn" "css_element" in the "//div[@class='card course-card' and contains(.,'Course 5')]" "xpath_element"
+    And I click on "Hide from view" "link" in the "//div[@class='card course-card' and contains(.,'Course 5')]" "xpath_element"
+    And I reload the page
+    Then I should not see "Course 5" in the "Course overview" "block"
+    And I should not see "Course 1" in the "Course overview" "block"
+    And I should not see "Course 2" in the "Course overview" "block"
+    And I should not see "Course 3" in the "Course overview" "block"
+    And I should not see "Course 4" in the "Course overview" "block"
+    And I log out
diff --git a/blocks/myoverview/tests/behat/block_myoverview_hidden.feature b/blocks/myoverview/tests/behat/block_myoverview_hidden.feature
new file mode 100644 (file)
index 0000000..9a60437
--- /dev/null
@@ -0,0 +1,83 @@
+@block @block_myoverview @javascript
+Feature: The my overview block allows users to favourite their courses
+  In order to enable the my overview block in a course
+  As a student
+  I can add the my overview block to my dashboard
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                | idnumber |
+      | student1 | Student   | X        | student1@example.com | S1       |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+      | Course 2 | C2        | 0        |
+      | Course 3 | C3        | 0        |
+      | Course 4 | C4        | 0        |
+      | Course 5 | C5        | 0        |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+      | student1 | C3 | student |
+      | student1 | C4 | student |
+      | student1 | C5 | student |
+
+  Scenario: Test hide toggle functionality
+    Given I log in as "student1"
+    When I click on ".coursemenubtn" "css_element" in the "//div[@class='card course-card' and contains(.,'Course 2')]" "xpath_element"
+    And I click on "Hide from view" "link" in the "//div[@class='card course-card' and contains(.,'Course 2')]" "xpath_element"
+    And I reload the page
+    Then I should not see "Course 2" in the "Course overview" "block"
+    And I log out
+
+  Scenario: Test hide toggle functionality w/ favorites
+    Given I log in as "student1"
+    And I click on ".coursemenubtn" "css_element" in the "//div[@class='card course-card' and contains(.,'Course 2')]" "xpath_element"
+    And I click on "Star this course" "link" in the "//div[@class='card course-card' and contains(.,'Course 2')]" "xpath_element"
+    And I click on ".coursemenubtn" "css_element" in the "//div[@class='card course-card' and contains(.,'Course 2')]" "xpath_element"
+    And I click on "Hide from view" "link" in the "//div[@class='card course-card' and contains(.,'Course 2')]" "xpath_element"
+    When I reload the page
+    Then I should not see "Course 2" in the "Course overview" "block"
+    And I click on "All" "button" in the "Course overview" "block"
+    And I click on "Starred" "link" in the "Course overview" "block"
+    Then I should not see "Course 2" in the "Course overview" "block"
+    And I click on "Starred" "button" in the "Course overview" "block"
+    And I click on "Hidden" "link" in the "Course overview" "block"
+    Then I should see "Course 2" in the "Course overview" "block"
+    And I log out
+
+  Scenario: Test show toggle functionality
+    Given I log in as "student1"
+    And I click on ".coursemenubtn" "css_element" in the "//div[@class='card course-card' and contains(.,'Course 2')]" "xpath_element"
+    And I click on "Hide from view" "link" in the "//div[@class='card course-card' and contains(.,'Course 2')]" "xpath_element"
+    When I click on "All" "button" in the "Course overview" "block"
+    And I click on "Hidden" "link" in the "Course overview" "block"
+    When I click on ".coursemenubtn" "css_element" in the "//div[@class='card course-card' and contains(.,'Course 2')]" "xpath_element"
+    And I click on "Show this course" "link" in the "//div[@class='card course-card' and contains(.,'Course 2')]" "xpath_element"
+    And I reload the page
+    And I click on "Hidden" "button" in the "Course overview" "block"
+    When I click on "All" "link" in the "Course overview" "block"
+    Then I should see "Course 2" in the "Course overview" "block"
+    And I log out
+
+  Scenario: Test show toggle functionality w/ favorites
+    Given I log in as "student1"
+    And I click on ".coursemenubtn" "css_element" in the "//div[@class='card course-card' and contains(.,'Course 2')]" "xpath_element"
+    And I click on "Star this course" "link" in the "//div[@class='card course-card' and contains(.,'Course 2')]" "xpath_element"
+    And I click on ".coursemenubtn" "css_element" in the "//div[@class='card course-card' and contains(.,'Course 2')]" "xpath_element"
+    And I click on "Hide from view" "link" in the "//div[@class='card course-card' and contains(.,'Course 2')]" "xpath_element"
+    And I click on "All" "button" in the "Course overview" "block"
+    And I click on "Hidden" "link" in the "Course overview" "block"
+    And I should see "Course 2" in the "Course overview" "block"
+    And I click on ".coursemenubtn" "css_element" in the "//div[@class='card course-card' and contains(.,'Course 2')]" "xpath_element"
+    And I click on "Show this course" "link" in the "//div[@class='card course-card' and contains(.,'Course 2')]" "xpath_element"
+    When I reload the page
+    Then I should not see "Course 2" in the "Course overview" "block"
+    And I click on "Hidden" "button" in the "Course overview" "block"
+    And I click on "All" "link" in the "Course overview" "block"
+    Then I should see "Course 2" in the "Course overview" "block"
+    And I click on "All" "button" in the "Course overview" "block"
+    And I click on "Starred" "link" in the "Course overview" "block"
+    Then I should see "Course 2" in the "Course overview" "block"
+    And I log out
\ No newline at end of file
index 5e2d031..2ccd331 100644 (file)
@@ -78,4 +78,23 @@ class block_myoverview_privacy_testcase extends \core_privacy\tests\provider_tes
             array('block_myoverview_user_view_preference', 'summary')
         );
     }
+
+    public function test_export_user_preferences_with_hidden_courses() {
+        $this->resetAfterTest();
+        $user = $this->getDataGenerator()->create_user();
+        $name = "block_myoverview_hidden_course_1";
+
+        set_user_preference($name, 1, $user);
+        provider::export_user_preferences($user->id);
+        $writer = writer::with_context(\context_system::instance());
+        $blockpreferences = $writer->get_user_preferences('block_myoverview');
+
+        $this->assertEquals(
+            get_string("privacy:request:preference:set", 'block_myoverview', (object) [
+                'name' => $name,
+                'value' => 1,
+            ]),
+            $blockpreferences->{$name}->description
+        );
+    }
 }
\ No newline at end of file
index 9714b2d..7b13d65 100644 (file)
@@ -71,7 +71,8 @@ class course_summary_exporter extends \core\external\exporter {
             'courseimage' => $courseimage,
             'progress' => $progress,
             'hasprogress' => $hasprogress,
-            'isfavourite' => $this->related['isfavourite']
+            'isfavourite' => $this->related['isfavourite'],
+            'hidden' => boolval(get_user_preferences('block_myoverview_hidden_course_' . $this->data->id, 0))
         );
     }
 
@@ -137,6 +138,9 @@ class course_summary_exporter extends \core\external\exporter {
             ),
             'isfavourite' => array(
                 'type' => PARAM_BOOL
+            ),
+            'hidden' => array(
+                'type' => PARAM_BOOL
             )
         );
     }
index 0873f46..f315f8b 100644 (file)
@@ -3665,6 +3665,8 @@ class core_course_external extends external_api {
                 break;
             case COURSE_FAVOURITES:
                 break;
+            case COURSE_TIMELINE_HIDDEN:
+                break;
             default:
                 throw new invalid_parameter_exception('Invalid classification');
         }
@@ -3692,7 +3694,6 @@ class core_course_external extends external_api {
                 $favouritecourseids,
                 $limit
             );
-
         } else {
             list($filteredcourses, $processedcount) = course_filter_courses_by_timeline_classification(
                 $courses,
index f063649..e24ca5f 100644 (file)
@@ -60,6 +60,7 @@ define('COURSE_TIMELINE_PAST', 'past');
 define('COURSE_TIMELINE_INPROGRESS', 'inprogress');
 define('COURSE_TIMELINE_FUTURE', 'future');
 define('COURSE_FAVOURITES', 'favourites');
+define('COURSE_TIMELINE_HIDDEN', 'hidden');
 define('COURSE_DB_QUERY_LIMIT', 1000);
 
 function make_log_url($module, $url) {
@@ -4242,7 +4243,8 @@ function course_filter_courses_by_timeline_classification(
 ) : array {
 
     if (!in_array($classification,
-            [COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, COURSE_TIMELINE_INPROGRESS, COURSE_TIMELINE_FUTURE])) {
+            [COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, COURSE_TIMELINE_INPROGRESS,
+                COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_HIDDEN])) {
         $message = 'Classification must be one of COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, '
             . 'COURSE_TIMELINE_INPROGRESS or COURSE_TIMELINE_FUTURE';
         throw new moodle_exception($message);
@@ -4254,8 +4256,11 @@ function course_filter_courses_by_timeline_classification(
 
     foreach ($courses as $course) {
         $numberofcoursesprocessed++;
+        $pref = get_user_preferences('block_myoverview_hidden_course_' . $course->id, 0);
 
-        if ($classification == COURSE_TIMELINE_ALL || $classification == course_classify_for_timeline($course)) {
+        // Added as of MDL-63457 toggle viewability for each user.
+        if (($classification == COURSE_TIMELINE_HIDDEN && $pref) ||
+            (($classification == COURSE_TIMELINE_ALL || $classification == course_classify_for_timeline($course)) && !$pref)) {
             $filteredcourses[] = $course;
             $filtermatches++;
         }
@@ -4299,8 +4304,9 @@ function course_filter_courses_by_favourites(
 
     foreach ($courses as $course) {
         $numberofcoursesprocessed++;
+        $pref = get_user_preferences('block_myoverview_hidden_course_' . $course->id, 0);
 
-        if (in_array($course->id, $favouritecourseids)) {
+        if (in_array($course->id, $favouritecourseids) && !$pref) {
             $filteredcourses[] = $course;
             $filtermatches++;
         }
index 1bdf3b5..37ca662 100644 (file)
@@ -4718,6 +4718,218 @@ class core_course_courselib_testcase extends advanced_testcase {
         $this->assertEquals($expectedprocessedcount, $processedcount);
     }
 
+    /**
+     * Test cases for the course_filter_courses_by_timeline_classification w/ hidden courses tests.
+     */
+    public function get_course_filter_courses_by_timeline_classification_hidden_courses_test_cases() {
+        $now = time();
+        $day = 86400;
+
+        $coursedata = [
+            [
+                'shortname' => 'apast',
+                'startdate' => $now - ($day * 2),
+                'enddate' => $now - $day
+            ],
+            [
+                'shortname' => 'bpast',
+                'startdate' => $now - ($day * 2),
+                'enddate' => $now - $day
+            ],
+            [
+                'shortname' => 'cpast',
+                'startdate' => $now - ($day * 2),
+                'enddate' => $now - $day
+            ],
+            [
+                'shortname' => 'dpast',
+                'startdate' => $now - ($day * 2),
+                'enddate' => $now - $day
+            ],
+            [
+                'shortname' => 'epast',
+                'startdate' => $now - ($day * 2),
+                'enddate' => $now - $day
+            ],
+            [
+                'shortname' => 'ainprogress',
+                'startdate' => $now - $day,
+                'enddate' => $now + $day
+            ],
+            [
+                'shortname' => 'binprogress',
+                'startdate' => $now - $day,
+                'enddate' => $now + $day
+            ],
+            [
+                'shortname' => 'cinprogress',
+                'startdate' => $now - $day,
+                'enddate' => $now + $day
+            ],
+            [
+                'shortname' => 'dinprogress',
+                'startdate' => $now - $day,
+                'enddate' => $now + $day
+            ],
+            [
+                'shortname' => 'einprogress',
+                'startdate' => $now - $day,
+                'enddate' => $now + $day
+            ],
+            [
+                'shortname' => 'afuture',
+                'startdate' => $now + $day
+            ],
+            [
+                'shortname' => 'bfuture',
+                'startdate' => $now + $day
+            ],
+            [
+                'shortname' => 'cfuture',
+                'startdate' => $now + $day
+            ],
+            [
+                'shortname' => 'dfuture',
+                'startdate' => $now + $day
+            ],
+            [
+                'shortname' => 'efuture',
+                'startdate' => $now + $day
+            ]
+        ];
+
+        // Raw enrolled courses result set should be returned in this order:
+        // afuture, ainprogress, apast, bfuture, binprogress, bpast, cfuture, cinprogress, cpast,
+        // dfuture, dinprogress, dpast, efuture, einprogress, epast
+        //
+        // By classification the offset values for each record should be:
+        // COURSE_TIMELINE_FUTURE
+        // 0 (afuture), 3 (bfuture), 6 (cfuture), 9 (dfuture), 12 (efuture)
+        // COURSE_TIMELINE_INPROGRESS
+        // 1 (ainprogress), 4 (binprogress), 7 (cinprogress), 10 (dinprogress), 13 (einprogress)
+        // COURSE_TIMELINE_PAST
+        // 2 (apast), 5 (bpast), 8 (cpast), 11 (dpast), 14 (epast).
+        return [
+            'empty set' => [
+                'coursedata' => [],
+                'classification' => COURSE_TIMELINE_FUTURE,
+                'limit' => 2,
+                'offset' => 0,
+                'expectedcourses' => [],
+                'expectedprocessedcount' => 0,
+                'hiddencourse' => ''
+            ],
+            // COURSE_TIMELINE_FUTURE.
+            'future not limit no offset' => [
+                'coursedata' => $coursedata,
+                'classification' => COURSE_TIMELINE_FUTURE,
+                'limit' => 0,
+                'offset' => 0,
+                'expectedcourses' => ['afuture', 'cfuture', 'dfuture', 'efuture'],
+                'expectedprocessedcount' => 15,
+                'hiddencourse' => 'bfuture'
+            ],
+            'future no offset' => [
+                'coursedata' => $coursedata,
+                'classification' => COURSE_TIMELINE_FUTURE,
+                'limit' => 2,
+                'offset' => 0,
+                'expectedcourses' => ['afuture', 'cfuture'],
+                'expectedprocessedcount' => 7,
+                'hiddencourse' => 'bfuture'
+            ],
+            'future offset' => [
+                'coursedata' => $coursedata,
+                'classification' => COURSE_TIMELINE_FUTURE,
+                'limit' => 2,
+                'offset' => 2,
+                'expectedcourses' => ['bfuture', 'dfuture'],
+                'expectedprocessedcount' => 8,
+                'hiddencourse' => 'cfuture'
+            ],
+            'future exact limit' => [
+                'coursedata' => $coursedata,
+                'classification' => COURSE_TIMELINE_FUTURE,
+                'limit' => 5,
+                'offset' => 0,
+                'expectedcourses' => ['afuture', 'cfuture', 'dfuture', 'efuture'],
+                'expectedprocessedcount' => 15,
+                'hiddencourse' => 'bfuture'
+            ],
+            'future limit less results' => [
+                'coursedata' => $coursedata,
+                'classification' => COURSE_TIMELINE_FUTURE,
+                'limit' => 10,
+                'offset' => 0,
+                'expectedcourses' => ['afuture', 'cfuture', 'dfuture', 'efuture'],
+                'expectedprocessedcount' => 15,
+                'hiddencourse' => 'bfuture'
+            ],
+            'future limit less results with offset' => [
+                'coursedata' => $coursedata,
+                'classification' => COURSE_TIMELINE_FUTURE,
+                'limit' => 10,
+                'offset' => 5,
+                'expectedcourses' => ['cfuture', 'efuture'],
+                'expectedprocessedcount' => 10,
+                'hiddencourse' => 'dfuture'
+            ],
+        ];
+    }
+
+    /**
+     * Test the course_filter_courses_by_timeline_classification function hidden courses.
+     *
+     * @dataProvider get_course_filter_courses_by_timeline_classification_hidden_courses_test_cases()
+     * @param array $coursedata Course test data to create.
+     * @param string $classification Timeline classification.
+     * @param int $limit Maximum number of results to return.
+     * @param int $offset Results to skip at the start of the result set.
+     * @param string[] $expectedcourses Expected courses in results.
+     * @param int $expectedprocessedcount Expected number of course records to be processed.
+     * @param int $hiddencourse The course to hide as part of this process
+     */
+    public function test_course_filter_courses_by_timeline_classification_with_hidden_courses(
+        $coursedata,
+        $classification,
+        $limit,
+        $offset,
+        $expectedcourses,
+        $expectedprocessedcount,
+        $hiddencourse
+    ) {
+        $this->resetAfterTest();
+        $generator = $this->getDataGenerator();
+        $student = $generator->create_user();
+        $this->setUser($student);
+
+        $courses = array_map(function($coursedata) use ($generator, $hiddencourse) {
+            $course = $generator->create_course($coursedata);
+            if ($course->shortname == $hiddencourse) {
+                set_user_preference('block_myoverview_hidden_course_' . $course->id, true);
+            }
+            return $course;
+        }, $coursedata);
+
+        foreach ($courses as $course) {
+            $generator->enrol_user($student->id, $course->id, 'student');
+        }
+
+        $coursesgenerator = course_get_enrolled_courses_for_logged_in_user(0, $offset, 'shortname ASC', 'shortname');
+        list($result, $processedcount) = course_filter_courses_by_timeline_classification(
+            $coursesgenerator,
+            $classification,
+            $limit
+        );
+
+        $actual = array_map(function($course) {
+            return $course->shortname;
+        }, $result);
+
+        $this->assertEquals($expectedcourses, $actual);
+        $this->assertEquals($expectedprocessedcount, $processedcount);
+    }
+
 
     /**
      * Testing core_course_core_calendar_get_valid_event_timestart_range when the course has no end date.
index ddb9e4b..6d0980a 100644 (file)
@@ -21,7 +21,8 @@
 
     Example context (json):
     {
-        "isfavourite": true
+        "isfavourite": true,
+        "hidden": true
     }
 }}
 <div class="ml-auto dropdown">
                     {{#str}} removefromfavourites, block_myoverview {{/str}}
                 </a>
             </li>
+            <li class="{{^hidden}}hidden{{/hidden}}" data-action="hide-course" data-course-id="{{id}}">
+                <a class="dropdown-item p-a-1" href="#">
+                    {{#str}} aria:hidecourse, block_myoverview, {{fullname}} {{/str}}
+                </a>
+            </li>
+            <li class="{{#hidden}}hidden{{/hidden}}" data-action="show-course" data-course-id="{{id}}">
+                <a class="dropdown-item p-a-1" href="#">
+                    {{#str}} aria:showcourse, block_myoverview, {{fullname}} {{/str}}
+                </a>
+            </li>
         </ul>
     </div>
 </div>
\ No newline at end of file