MDL-59392 calendar: Switch to templates in the cal block
authorAndrew Nicols <andrew@nicols.co.uk>
Fri, 1 Sep 2017 06:58:14 +0000 (14:58 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Mon, 11 Sep 2017 04:45:46 +0000 (12:45 +0800)
This commit adds a set of templates for the calendar month block, and
the events filter. The same data exporters used in the main calendar
view are utilised and the same logic.

Some additional data was added to the exporters, and in the long term
this will be used in the main monthly view so I did not feel it was
prudent to strip it out into a different view of the model.

AMOS BEGIN
 MOV [monthnext,access],[monthnext,calendar]
AMOS END

32 files changed:
blocks/calendar_month/block_calendar_month.php
blocks/calendar_month/tests/behat/block_calendar_month.feature
calendar/amd/build/calendar_filter.min.js [new file with mode: 0644]
calendar/amd/build/calendar_mini.min.js [new file with mode: 0644]
calendar/amd/build/events.min.js
calendar/amd/build/selectors.min.js [new file with mode: 0644]
calendar/amd/build/view_manager.min.js
calendar/amd/src/calendar_filter.js [new file with mode: 0644]
calendar/amd/src/calendar_mini.js [new file with mode: 0644]
calendar/amd/src/events.js
calendar/amd/src/selectors.js [new file with mode: 0644]
calendar/amd/src/view_manager.js
calendar/classes/external/calendar_event_exporter.php
calendar/classes/external/day_exporter.php
calendar/classes/external/month_exporter.php
calendar/classes/external/week_exporter.php
calendar/lib.php
calendar/renderer.php
calendar/templates/calendar_mini.mustache [new file with mode: 0644]
calendar/templates/event_filter.mustache [new file with mode: 0644]
calendar/templates/event_filter_key.mustache [new file with mode: 0644]
calendar/templates/minicalendar_day_link.mustache
calendar/templates/month_mini.mustache [new file with mode: 0644]
calendar/tests/behat/behat_calendar.php
lang/en/access.php
lang/en/calendar.php
lang/en/deprecated.txt
theme/boost/scss/moodle/calendar.scss
theme/boost/templates/core_calendar/minicalendar_day_link.mustache
theme/bootstrapbase/less/moodle/calendar.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/core_calendar/minicalendar_day_link.mustache

index cc3d6f6..18a57d7 100644 (file)
@@ -70,29 +70,26 @@ class block_calendar_month extends block_base {
         if ($issite) {
             // Being displayed at site level. This will cause the filter to fall back to auto-detecting
             // the list of courses it will be grabbing events from.
-            $filtercourse = calendar_get_default_courses();
+            $course = get_site();
+            $courses = calendar_get_default_courses();
         } else {
             // Forcibly filter events to include only those from the particular course we are in.
-            $filtercourse = array($courseid => $this->page->course);
+            $course = $this->page->course;
+            $courses = [$course->id => $course];
         }
 
-        list($courses, $group, $user) = calendar_set_filters($filtercourse);
-        if ($issite) {
-            // For the front page.
-            $this->content->text .= calendar_get_mini($courses, $group, $user, false, false,
-                'frontpage', $courseid, $time);
-            // No filters for now.
-        } else {
-            // For any other course.
-            $this->content->text .= calendar_get_mini($courses, $group, $user, false, false,
-                'course', $courseid, $time);
-            $this->content->text .= '<h3 class="eventskey">'.get_string('eventskey', 'calendar').'</h3>';
-            $this->content->text .= '<div class="filters calendar_filters">' .
-                calendar_filter_controls($this->page->url) . '</div>';
+        $renderer = $this->page->get_renderer('core_calendar');
+
+        $calendar = new calendar_information(0, 0, 0, $time);
+        $calendar->prepare_for_view($course, $courses);
+
+        list($data, $template) = calendar_get_view($calendar, 'mini');
+        $this->content->text .= $renderer->render_from_template($template, $data);
+
+        if (!$issite) {
+            $this->content->text .= $renderer->event_filter();
         }
 
         return $this->content;
     }
 }
-
-
index 8753167..86baea3 100644 (file)
@@ -23,7 +23,7 @@ Feature: Enable the calendar block in a course and test it's functionality
     Given I log in as "teacher1"
     And I am on "Course 1" course homepage with editing mode on
     When I add the "Calendar" block
-    Then I should see "Events key" in the "Calendar" "block"
+    Then "Calendar" "block" should exist
 
   @javascript
   Scenario: View a global event in the calendar block
@@ -136,9 +136,14 @@ Feature: Enable the calendar block in a course and test it's functionality
     And I press "Save and display"
     And I turn editing mode on
     And I add the "Calendar" block
-    And I create a calendar event with form data:
+    And I click on "This month" "link"
+    And I click on "New event" "button"
+    And I set the following fields to these values:
       | id_eventtype | Group |
       | id_name | Group Event |
+    And I set the following fields to these values:
+      | Group | Group 1 |
+    And I press "Save"
     And I log out
     Then I log in as "student1"
     And I am on "Course 1" course homepage
@@ -150,7 +155,7 @@ Feature: Enable the calendar block in a course and test it's functionality
     And I hover over today in the calendar
     And I should not see "Group Event"
 
-  @javascript
+  @javascript @arn
   Scenario: Filter group events in the calendar block
     Given the following "groups" exist:
       | name    | course | idnumber |
@@ -173,9 +178,14 @@ Feature: Enable the calendar block in a course and test it's functionality
       | id_eventtype | Course |
       | id_name | Course Event 1 |
     And I am on "Course 1" course homepage
-    And I create a calendar event with form data:
+    And I click on "This month" "link"
+    And I click on "New event" "button"
+    And I set the following fields to these values:
       | id_eventtype | Group |
       | id_name | Group Event 1 |
+    And I set the following fields to these values:
+      | Group | Group 1 |
+    And I press "Save"
     And I log out
     Then I log in as "student1"
     And I am on "Course 1" course homepage
diff --git a/calendar/amd/build/calendar_filter.min.js b/calendar/amd/build/calendar_filter.min.js
new file mode 100644 (file)
index 0000000..ef94d15
Binary files /dev/null and b/calendar/amd/build/calendar_filter.min.js differ
diff --git a/calendar/amd/build/calendar_mini.min.js b/calendar/amd/build/calendar_mini.min.js
new file mode 100644 (file)
index 0000000..979e92e
Binary files /dev/null and b/calendar/amd/build/calendar_mini.min.js differ
index 762a824..77b2153 100644 (file)
Binary files a/calendar/amd/build/events.min.js and b/calendar/amd/build/events.min.js differ
diff --git a/calendar/amd/build/selectors.min.js b/calendar/amd/build/selectors.min.js
new file mode 100644 (file)
index 0000000..8dbeb18
Binary files /dev/null and b/calendar/amd/build/selectors.min.js differ
index 88812a5..301ef42 100644 (file)
Binary files a/calendar/amd/build/view_manager.min.js and b/calendar/amd/build/view_manager.min.js differ
diff --git a/calendar/amd/src/calendar_filter.js b/calendar/amd/src/calendar_filter.js
new file mode 100644 (file)
index 0000000..5352a7c
--- /dev/null
@@ -0,0 +1,119 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This module is responsible for the calendar filter.
+ *
+ * @module     core_calendar/calendar_filter
+ * @package    core_calendar
+ * @copyright  2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([
+    'jquery',
+    'core_calendar/selectors',
+    'core_calendar/events',
+    'core/str',
+    'core/templates',
+],
+function(
+    $,
+    CalendarSelectors,
+    CalendarEvents,
+    Str,
+    Templates
+) {
+
+    var registerEventListeners = function(root) {
+        root.on('click', CalendarSelectors.eventFilterItem, function(e) {
+            var target = $(e.currentTarget);
+
+            toggleFilter(target);
+
+            e.preventDefault();
+        });
+
+        $('body').on(CalendarEvents.viewUpdated, function() {
+            var filters = root.find(CalendarSelectors.eventFilterItem);
+
+            filters.each(function(i, filter) {
+                filter = $(filter);
+                if (filter.data('eventtype-hidden')) {
+                    var data = getFilterData(filter);
+                    fireFilterChangedEvent(data);
+                }
+            });
+        });
+    };
+
+    var toggleFilter = function(target) {
+        var data = getFilterData(target);
+
+        // Toggle the hidden. We need to render the template before we change the value.
+        data.hidden = !data.hidden;
+
+        return Str.get_string('eventtype' + data.type, 'calendar')
+        .then(function(nameStr) {
+            data.name = nameStr;
+
+            return data;
+        })
+        .then(function(context) {
+            return Templates.render('core_calendar/event_filter_key', context);
+        })
+        .then(function(html, js) {
+            return Templates.replaceNode(target, html, js);
+        })
+        .then(function() {
+            fireFilterChangedEvent(data);
+            return;
+        });
+    };
+
+    /**
+     * Fire the filterChanged event for the specified data.
+     *
+     * @param   {object} data The data to include
+     */
+    var fireFilterChangedEvent = function(data) {
+        M.util.js_pending("month-mini-filterChanged");
+        $('body').trigger(CalendarEvents.filterChanged, {
+            type: data.type,
+            hidden: data.hidden,
+        });
+        M.util.js_complete("month-mini-filterChanged");
+    };
+
+    /**
+     * Get the filter data for the specified target.
+     *
+     * @param   {jQuery} target The target node
+     * @return  {Object}
+     */
+    var getFilterData = function(target) {
+        return {
+            type: target.data('eventtype'),
+            hidden: target.data('eventtype-hidden'),
+        };
+    };
+
+    return {
+        init: function(root) {
+            root = $(root);
+
+            registerEventListeners(root);
+        }
+    };
+});
diff --git a/calendar/amd/src/calendar_mini.js b/calendar/amd/src/calendar_mini.js
new file mode 100644 (file)
index 0000000..48a4f44
--- /dev/null
@@ -0,0 +1,57 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This module is the highest level module for the calendar. It is
+ * responsible for initialising all of the components required for
+ * the calendar to run. It also coordinates the interaction between
+ * components by listening for and responding to different events
+ * triggered within the calendar UI.
+ *
+ * @module     core_calendar/calendar
+ * @package    core_calendar
+ * @copyright  2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([
+    'jquery',
+    'core_calendar/selectors',
+    'core_calendar/events',
+    'core_calendar/view_manager',
+],
+function(
+    $,
+    CalendarSelectors,
+    CalendarEvents,
+    CalendarViewManager
+) {
+
+    var registerEventListeners = function(root) {
+        $('body').on(CalendarEvents.filterChanged, function(e, data) {
+            var daysWithEvent = root.find(CalendarSelectors.eventType[data.type]);
+
+            daysWithEvent.toggleClass('calendar_event_' + data.type, !data.hidden);
+        });
+    };
+
+    return {
+        init: function(root) {
+            root = $(root);
+
+            CalendarViewManager.init(root);
+            registerEventListeners(root);
+        }
+    };
+});
index 95e0968..29b5fd2 100644 (file)
@@ -31,6 +31,8 @@ define([], function() {
         editActionEvent: 'calendar-events:edit_action_event',
         eventMoved: 'calendar-events:event_moved',
         monthChanged: 'calendar-events:month_changed',
-        moveEvent: 'calendar-events:move_event'
+        moveEvent: 'calendar-events:move_event',
+        filterChanged: 'calendar-events:filter_changed',
+        viewUpdated: 'calendar-events:view_updated',
     };
 });
diff --git a/calendar/amd/src/selectors.js b/calendar/amd/src/selectors.js
new file mode 100644 (file)
index 0000000..89bb65a
--- /dev/null
@@ -0,0 +1,40 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This module is responsible for the calendar filter.
+ *
+ * @module     core_calendar/calendar_selectors
+ * @package    core_calendar
+ * @copyright  2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([], function() {
+    return {
+        eventFilterItem: "[data-action='filter-event-type']",
+        eventType: {
+            site: "[data-eventtype-site]",
+            course: "[data-eventtype-course]",
+            group: "[data-eventtype-group]",
+            user: "[data-eventtype-user]",
+        },
+        popoverType: {
+            site: "[data-popover-eventtype-site]",
+            course: "[data-popover-eventtype-course]",
+            group: "[data-popover-eventtype-group]",
+            user: "[data-popover-eventtype-user]",
+        },
+    };
+});
index 2c9791b..f8636ef 100644 (file)
@@ -65,6 +65,10 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
                 .then(function(html, js) {
                     return Templates.replaceNode(root.find(SELECTORS.CALENDAR_MONTH_WRAPPER), html, js);
                 })
+                .then(function() {
+                    $('body').trigger(CalendarEvents.viewUpdated);
+                    return;
+                })
                 .always(function() {
                     return stopLoading(root);
                 })
index 7ba5805..f83f273 100644 (file)
@@ -47,6 +47,16 @@ class calendar_event_exporter extends event_exporter_base {
 
         $values = parent::define_other_properties();
         $values['url'] = ['type' => PARAM_URL];
+        $values['islastday'] = [
+            'type' => PARAM_BOOL,
+            'default' => false,
+        ];
+        $values['calendareventtype'] = [
+            'type' => PARAM_TEXT,
+        ];
+        $values['popupname'] = [
+            'type' => PARAM_RAW,
+        ];
 
         return $values;
     }
@@ -58,6 +68,8 @@ class calendar_event_exporter extends event_exporter_base {
      * @return array Keys are the property names, values are their values.
      */
     protected function get_other_values(renderer_base $output) {
+        global $CFG;
+
         $values = parent::get_other_values($output);
 
         $eventid = $this->event->get_id();
@@ -65,6 +77,55 @@ class calendar_event_exporter extends event_exporter_base {
         $url = new \moodle_url($this->related['daylink'], [], "event_{$eventid}");
         $values['url'] = $url->out(false);
 
+        $values['islastday'] = false;
+        $today = $this->related['type']->timestamp_to_date_array($this->related['today']);
+
+        $values['popupname'] = $this->event->get_name();
+
+        $times = $this->event->get_times();
+        if ($duration = $times->get_duration()) {
+            $enddate = $this->related['type']->timestamp_to_date_array($times->get_end_time()->getTimestamp());
+            $values['islastday'] = true;
+            $values['islastday'] = $values['islastday'] && $enddate['year'] == $today['year'];
+            $values['islastday'] = $values['islastday'] && $enddate['mon'] == $today['mon'];
+            $values['islastday'] = $values['islastday'] && $enddate['mday'] == $today['mday'];
+        }
+
+        $subscription = $this->event->get_subscription();
+        if ($subscription && !empty($subscription->get('id')) && $CFG->calendar_showicalsource) {
+            $a = (object) [
+                'name' => $values['popupname'],
+                'source' => $subscription->get('name'),
+            ];
+            $values['popupname'] = get_string('namewithsource', 'calendar', $a);
+        } else {
+            if ($values['islastday']) {
+                $startdate = $this->related['type']->timestamp_to_date_array($times->get_start_time()->getTimestamp());
+                $samedate = true;
+                $samedate = $samedate && $startdate['mon'] == $enddate['mon'];
+                $samedate = $samedate && $startdate['year'] == $enddate['year'];
+                $samedate = $samedate && $startdate['mday'] == $enddate['mday'];
+
+                if (!$samedate) {
+                    $values['popupname'] = get_string('eventendtimewrapped', 'calendar', $values['popupname']);
+                }
+            }
+        }
+
+        // Include course's shortname into the event name, if applicable.
+        $course = $this->event->get_course();
+        if ($course && $course->get('id') && $course->get('id') !== SITEID) {
+            $eventnameparams = (object) [
+                'name' => $values['popupname'],
+                'course' => format_string($course->get('shortname'), true, [
+                        'context' => $this->related['context'],
+                    ])
+            ];
+            $values['popupname'] = get_string('eventnameandcourse', 'calendar', $eventnameparams);
+        }
+
+        $values['calendareventtype'] = $this->get_calendar_event_type();
+
         return $values;
     }
 
@@ -76,7 +137,24 @@ class calendar_event_exporter extends event_exporter_base {
     protected static function define_related() {
         $related = parent::define_related();
         $related['daylink'] = \moodle_url::class;
+        $related['type'] = '\core_calendar\type_base';
+        $related['today'] = 'int';
 
         return $related;
     }
+
+    /**
+     * Return the normalised event type.
+     * Activity events are normalised to be course events.
+     *
+     * @return string
+     */
+    public function get_calendar_event_type() {
+        $type = $this->event->get_type();
+        if ($type == 'open' || $type == 'close') {
+            $type = 'course';
+        }
+
+        return $type;
+    }
 }
index 90b5c73..0f5b92b 100644 (file)
@@ -87,6 +87,15 @@ class day_exporter extends exporter {
             'yday' => [
                 'type' => PARAM_INT,
             ],
+            // These are additional params.
+            'istoday' => [
+                'type' => PARAM_BOOL,
+                'default' => false,
+            ],
+            'isweekend' => [
+                'type' => PARAM_BOOL,
+                'default' => false,
+            ],
         ];
     }
 
@@ -110,7 +119,19 @@ class day_exporter extends exporter {
             'events' => [
                 'type' => calendar_event_exporter::read_properties_definition(),
                 'multiple' => true,
-            ]
+            ],
+            'calendareventtypes' => [
+                'type' => PARAM_RAW,
+                'multiple' => true,
+            ],
+            'popovertitle' => [
+                'type' => PARAM_RAW,
+                'default' => '',
+            ],
+            'haslastdayofevent' => [
+                'type' => PARAM_BOOL,
+                'default' => false,
+            ],
         ];
     }
 
@@ -141,22 +162,46 @@ class day_exporter extends exporter {
         $url = new moodle_url('/calendar/view.php', [
                 'view' => 'day',
                 'time' => $timestamp,
+                'course' => $this->calendar->course->id,
             ]);
         $return['viewdaylink'] = $url->out(false);
 
         $cache = $this->related['cache'];
-        $return['events'] = array_map(function($event) use ($cache, $output, $url) {
+        $eventexporters = array_map(function($event) use ($cache, $output, $url) {
             $context = $cache->get_context($event);
             $course = $cache->get_course($event);
             $exporter = new calendar_event_exporter($event, [
                 'context' => $context,
                 'course' => $course,
                 'daylink' => $url,
+                'type' => $this->related['type'],
+                'today' => $this->data[0],
             ]);
 
-            return $exporter->export($output);
+            return $exporter;
         }, $this->related['events']);
 
+        $return['events'] = array_map(function($exporter) use ($output) {
+            return $exporter->export($output);
+        }, $eventexporters);
+
+        if ($popovertitle = $this->get_popover_title()) {
+            $return['popovertitle'] = $popovertitle;
+        }
+
+        $return['calendareventtypes'] = array_map(function($exporter) {
+            return $exporter->get_calendar_event_type();
+        }, $eventexporters);
+        $return['calendareventtypes'] = array_values(array_unique($return['calendareventtypes']));
+
+        $return['haslastdayofevent'] = false;
+        foreach ($return['events'] as $event) {
+            if ($event->islastday) {
+                $return['haslastdayofevent'] = true;
+                break;
+            }
+        }
+
         return $return;
     }
 
@@ -172,4 +217,26 @@ class day_exporter extends exporter {
             'type' => '\core_calendar\type_base',
         ];
     }
+
+    /**
+     * Get the title for this popover.
+     *
+     * @return string
+     */
+    protected function get_popover_title() {
+        $title = null;
+
+        $userdate = userdate($this->data[0], get_string('strftimedayshort'));
+        if (count($this->related['events'])) {
+            $title = get_string('eventsfor', 'calendar', $userdate);
+        } else if ($this->data['istoday']) {
+            $title = $userdate;
+        }
+
+        if ($this->data['istoday']) {
+            $title = get_string('todayplustitle', 'calendar', $userdate);
+        }
+
+        return $title;
+    }
 }
index 2a1988f..6d180aa 100644 (file)
@@ -76,7 +76,19 @@ class month_exporter extends exporter {
 
         $related['type'] = $type;
 
-        parent::__construct([], $related);
+        $data = [
+            'url' => $this->url->out(false),
+        ];
+
+        parent::__construct($data, $related);
+    }
+
+    protected static function define_properties() {
+        return [
+            'url' => [
+                'type' => PARAM_URL,
+            ],
+        ];
     }
 
     /**
@@ -106,15 +118,38 @@ class month_exporter extends exporter {
             'view' => [
                 'type' => PARAM_ALPHA,
             ],
+            'time' => [
+                'type' => PARAM_INT,
+            ],
+            'periodname' => [
+                // Note: We must use RAW here because the calendar type returns the formatted month name based on a
+                // calendar format.
+                'type' => PARAM_RAW,
+            ],
             'previousperiod' => [
                 'type' => PARAM_INT,
             ],
+            'previousperiodname' => [
+                // Note: We must use RAW here because the calendar type returns the formatted month name based on a
+                // calendar format.
+                'type' => PARAM_RAW,
+            ],
             'nextperiod' => [
                 'type' => PARAM_INT,
             ],
-            'time' => [
-                'type' => PARAM_INT,
-            ]
+            'nextperiodname' => [
+                // Note: We must use RAW here because the calendar type returns the formatted month name based on a
+                // calendar format.
+                'type' => PARAM_RAW,
+            ],
+            'larrow' => [
+                // The left arrow defined by the theme.
+                'type' => PARAM_RAW,
+            ],
+            'rarrow' => [
+                // The right arrow defined by the theme.
+                'type' => PARAM_RAW,
+            ],
         ];
     }
 
@@ -125,16 +160,24 @@ class month_exporter extends exporter {
      * @return array Keys are the property names, values are their values.
      */
     protected function get_other_values(renderer_base $output) {
+        $previousperiod = $this->get_previous_month_timestamp();
+        $nextperiod = $this->get_next_month_timestamp();
+
         return [
             'courseid' => $this->calendar->courseid,
-            'view' => 'month',
-            'previousperiod' => $this->get_previous_month_timestamp(),
-            'nextperiod' => $this->get_next_month_timestamp(),
             'filter_selector' => $this->get_course_filter_selector($output),
             'navigation' => $this->get_navigation($output),
             'weeks' => $this->get_weeks($output),
             'daynames' => $this->get_day_names($output),
-            'time' => $this->calendar->time
+            'view' => 'month',
+            'time' => $this->calendar->time,
+            'periodname' => userdate($this->calendar->time, get_string('strftimemonthyear')),
+            'previousperiod' => $previousperiod,
+            'previousperiodname' => userdate($previousperiod, get_string('strftimemonthyear')),
+            'nextperiod' => $nextperiod,
+            'nextperiodname' => userdate($nextperiod, get_string('strftimemonthyear')),
+            'larrow' => $output->larrow(),
+            'rarrow' => $output->rarrow(),
         ];
     }
 
index f0b6c4d..ff75a35 100644 (file)
@@ -53,7 +53,6 @@ class week_exporter extends exporter {
      */
     protected $postpadding = 0;
 
-
     /**
      * @var \calendar_information $calendar The calendar being displayed.
      */
@@ -106,6 +105,7 @@ class week_exporter extends exporter {
      * @return array Keys are the property names, values are their values.
      */
     protected function get_other_values(renderer_base $output) {
+        global $CFG;
         $return = [
             'prepadding' => [],
             'postpadding' => [],
@@ -120,6 +120,14 @@ class week_exporter extends exporter {
         }
 
         $return['days'] = [];
+        $today = $this->related['type']->timestamp_to_date_array(time());
+
+        $weekend = CALENDAR_DEFAULT_WEEKEND;
+        if (isset($CFG->calendar_weekend)) {
+            $weekend = intval($CFG->calendar_weekend);
+        }
+        $numberofdaysinweek = $this->related['type']->get_num_weekdays();
+
         foreach ($this->days as $daydata) {
             $events = [];
             foreach ($this->related['events'] as $event) {
@@ -140,7 +148,14 @@ class week_exporter extends exporter {
                 $events[] = $event;
             }
 
-            $day = new week_day_exporter($daydata, [
+            $istoday = true;
+            $istoday = $istoday && $today['year'] == $daydata['year'];
+            $istoday = $istoday && $today['yday'] == $daydata['yday'];
+            $daydata['istoday'] = $istoday;
+
+            $daydata['isweekend'] = !!($weekend & (1 << ($daydata['wday'] % $numberofdaysinweek)));
+
+            $day = new week_day_exporter($this->calendar, $daydata, [
                 'events' => $events,
                 'cache' => $this->related['cache'],
                 'type' => $this->related['type'],
index 8df62f0..34bd7b4 100644 (file)
@@ -1478,7 +1478,7 @@ function calendar_get_mini($courses, $groups, $users, $calmonth = false, $calyea
 
             // Class and cell content.
             if (isset($typesbyday[$day]['startglobal'])) {
-                $class .= ' calendar_event_global';
+                $class .= ' calendar_event_site';
             } else if (isset($typesbyday[$day]['startcourse'])) {
                 $class .= ' calendar_event_course';
             } else if (isset($typesbyday[$day]['startgroup'])) {
@@ -3464,6 +3464,11 @@ function calendar_get_view(\calendar_information $calendar, $view) {
         $monthdays = $type->get_num_days_in_month($date['year'], $date['mon']);
         $tend = $tstart + ($monthdays * DAYSECS) - 1;
         $selectortitle = get_string('detailedmonthviewfor', 'calendar');
+        if ($view === 'mini') {
+            $template = 'core_calendar/calendar_mini';
+        } else {
+            $template = 'core_calendar/calendar_month';
+        }
     }
 
     list($userparam, $groupparam, $courseparam) = array_map(function($param) {
@@ -3518,7 +3523,6 @@ function calendar_get_view(\calendar_information $calendar, $view) {
 
     $month = new \core_calendar\external\month_exporter($calendar, $type, $related);
     $data = $month->export($renderer);
-    $template = 'core_calendar/calendar_month';
 
     return [$data, $template];
 }
@@ -3654,3 +3658,25 @@ function calendar_get_footer_options($calendar) {
 
     return [$data, $template];
 }
+
+/**
+ * Get the list of potential calendar filter types as a type => name
+ * combination.
+ *
+ * @return array
+ */
+function calendar_get_filter_types() {
+    $types = [
+        'site',
+        'course',
+        'group',
+        'user',
+    ];
+
+    return array_map(function($type) {
+        return [
+            'type' => $type,
+            'name' => get_string("eventtype{$type}", "calendar"),
+        ];
+    }, $types);
+}
index 1818234..d0fdec6 100644 (file)
@@ -502,4 +502,16 @@ class core_calendar_renderer extends plugin_renderer_base {
         $html .= html_writer::end_tag('form');
         return $html;
     }
+
+    /**
+     * Render the event filter region.
+     *
+     * @return  string
+     */
+    public function event_filter() {
+        $data = [
+            'eventtypes' => calendar_get_filter_types(),
+        ];
+        return $this->render_from_template('core_calendar/event_filter', $data);
+    }
 }
diff --git a/calendar/templates/calendar_mini.mustache b/calendar/templates/calendar_mini.mustache
new file mode 100644 (file)
index 0000000..6e28cbf
--- /dev/null
@@ -0,0 +1,45 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template calendar/calendar_mini
+
+    Calendar month view for blocks.
+
+    The purpose of this template is to render the month view for blocks.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<div{{!
+  }} id="calendar-month-{{uniqid}}-{{time}}" {{!
+    }} data-template="core_calendar/month_mini" {{!
+    }} data-includenavigation="{{#includenavigation}}true{{/includenavigation}}{{^includenavigation}}false{{/includenavigation}}"{{!
+    }}>
+    {{> core_calendar/month_mini}}
+</div>
+{{#js}}
+require(['jquery', 'core_calendar/calendar_mini'], function($, CalendarMini) {
+    CalendarMini.init($("#calendar-month-{{uniqid}}-{{time}}"));
+});
+{{/js}}
diff --git a/calendar/templates/event_filter.mustache b/calendar/templates/event_filter.mustache
new file mode 100644 (file)
index 0000000..4e6e789
--- /dev/null
@@ -0,0 +1,45 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template calendar/event_filter
+
+    Filter to select which event types displayed on this page.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<div id="event-filter-{{uniqid}}" class="calendar_filters m-t-2">
+    <ul>
+        {{#eventtypes}}
+            <li>
+                {{> core_calendar/event_filter_key }}
+            </li>
+        {{/eventtypes}}
+    </ul>
+</div>
+{{#js}}
+require(['jquery', 'core_calendar/calendar_filter'], function($, CalendarFilter) {
+    CalendarFilter.init($("#event-filter-{{uniqid}}"));
+});
+{{/js}}
diff --git a/calendar/templates/event_filter_key.mustache b/calendar/templates/event_filter_key.mustache
new file mode 100644 (file)
index 0000000..be1154b
--- /dev/null
@@ -0,0 +1,45 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template calendar/event_filter_key
+
+    Key item for the event filter.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<a href="#" data-action="filter-event-type" data-eventtype="{{type}}" data-eventtype-hidden="{{hidden}}">
+    {{^hidden}}
+        <span class="calendar_event_{{type}}">
+            {{#pix}}i/hide, core, {{#str}}hideeventtype, calendar, {{name}}{{/str}}{{/pix}}
+        </span>
+        <span class="eventname">{{#str}}hideeventtype, calendar, {{name}}{{/str}}</span>
+    {{/hidden}}
+    {{#hidden}}
+        <span class="calendar_event_{{type}}">
+            {{#pix}}i/show, core, {{#str}}showeventtype, calendar, {{name}}{{/str}}{{/pix}}
+        </span>
+        <span class="eventname">{{#str}}showeventtype, calendar, {{name}}{{/str}}</span>
+    {{/hidden}}
+</a>
index bdb5e67..d6c91af 100644 (file)
 }}
 {{< core/hover_tooltip }}
     {{$anchor}}
-        <a href="{{url}}">{{day}}</a>
+        <a href="{{url}}">{{$day}}{{day}}{{/day}}</a>
     {{/anchor}}
     {{$tooltip}}
-        <b>{{title}}</b>
-        {{{content}}}
+        <b>{{$title}}{{title}}{{/title}}</b>
+        {{$content}}{{{content}}}{{/content}}
     {{/tooltip}}
 {{/ core/hover_tooltip }}
diff --git a/calendar/templates/month_mini.mustache b/calendar/templates/month_mini.mustache
new file mode 100644 (file)
index 0000000..b14afc9
--- /dev/null
@@ -0,0 +1,154 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template calendar/month_mini
+
+    Calendar month view for blocks.
+
+    The purpose of this template is to render the month view for blocks.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<div id="month-mini-{{uniqid}}-{{time}}" class="calendarwrapper" data-courseid="{{courseid}}" data-current-time="{{time}}">
+    {{> core/overlay_loading}}
+    <table class="minicalendar calendartable">
+        <caption class="calendar-controls">
+            <a href="#" class="arrow_link previous" title="{{#str}}monthprev, calendar{{/str}}" data-time="{{previousperiod}}"><span class="arrow">{{{larrow}}}</span></a>
+            <span class="hide"> | </span>
+            <span class="current">
+                <a href="{{{url}}}" title="{{#str}}monththis, calendar{{/str}}" data-time="">{{periodname}}</a>
+            </span>
+            <span class="hide"> | </span>
+            <a href="#" class="arrow_link next" title="{{#str}}monthnext, calendar{{/str}}" data-time="{{nextperiod}}"><span class="arrow">{{{rarrow}}}</span></a>
+        </caption>
+        <thead>
+          <tr>
+                {{# daynames }}
+                <th class="header text-xs-center" scope="col">
+                    <abbr title="{{fullname}}">{{shortname}}</abbr>
+                </th>
+                {{/ daynames }}
+            </tr>
+        </thead>
+        <tbody>
+{{#weeks}}
+            <tr data-region="month-view-week">
+                {{#prepadding}}
+                    <td class="dayblank">&nbsp;</td>
+                {{/prepadding}}
+                {{#days}}
+                    <td class="day text-center{{!
+                            This is the list of additional classes to display.
+
+                            This cell is for today.
+                            }}{{#istoday}} today{{/istoday}}{{!
+
+                            This day falls on a weekend.
+                            }}{{#isweekend}} weekend{{/isweekend}}{{!
+
+                            There is at least one event.
+                            }}{{#events.0}} hasevent{{/events.0}}{{!
+
+                            There are events on this day which span multiple days.
+                            }}{{#durationevents.0}} duration{{/durationevents.0}}{{!
+                            }}{{#durationevents}} duration_{{.}}{{/durationevents}}{{!
+                            }}{{#islastday}} duraction_finish{{/islastday}}{{!
+
+                            These event types fall on this day.
+                            }}{{#calendareventtypes}}{{!
+                            }} calendar_event_{{.}}{{!
+                            }}{{/calendareventtypes}}{{!
+
+                            This day contains the last day of an event.
+                            }}{{#haslastdayofevent}}{{!
+                            }} duration_finish{{!
+                            }}{{/haslastdayofevent}}{{!
+                        }}"{{!
+
+                        Add data-eventtype-[eventtype] data attributes.
+                        These are used to show and hide events using the filter.
+                        }}{{#calendareventtypes}}{{!
+                        }} data-eventtype-{{.}}="1"{{!
+                        }}{{/calendareventtypes}}{{!
+
+                        This is the timestamp for this month.
+                        }} data-day-timestamp="{{timestamp}}"{{!
+                    }}>{{!
+                        }}{{#popovertitle}}
+                            {{< core_calendar/minicalendar_day_link }}
+                                {{$day}}{{mday}}{{/day}}
+                                {{$url}}{{viewdaylink}}{{/url}}
+                                {{$title}}{{popovertitle}}{{/title}}
+                                {{$nocontent}}{{#str}}eventnone, calendar{{/str}}{{/nocontent}}
+                                {{$content}}
+                                    {{#events}}
+                                        <div data-popover-eventtype-{{calendareventtype}}="1">
+                                            {{#modulename}}
+                                                {{#pix}} icon, {{modulename}} {{/pix}}
+                                            {{/modulename}}
+                                            {{^modulename}}
+                                                {{#pix}} i/{{eventtype}}event, core {{/pix}}
+                                            {{/modulename}}
+                                            {{{popupname}}}
+                                        </div>
+                                    {{/events}}
+                                {{/content}}
+                            {{/ core_calendar/minicalendar_day_link }}
+                        {{/popovertitle}}{{!
+                        }}{{^popovertitle}}
+                            {{mday}}
+                        {{/popovertitle}}{{!
+                    }}</td>
+                {{/days}}
+                {{#postpadding}}
+                    <td class="dayblank">&nbsp;</td>
+                {{/postpadding}}
+            </tr>
+{{/weeks}}
+        </tbody>
+    </table>
+</div>
+{{#js}}
+require([
+    'jquery',
+    'core_calendar/selectors',
+    'core_calendar/events',
+], function(
+    $,
+    CalendarSelectors,
+    CalendarEvents
+) {
+
+    $('body').on(CalendarEvents.filterChanged, function(e, data) {
+        M.util.js_pending("month-mini-{{uniqid}}-filterChanged");
+        // A filter value has been changed.
+        // Find all matching cells in the popover data, and hide them.
+        $("#month-mini-{{uniqid}}-{{time}}")
+            .find(CalendarSelectors.popoverType[data.type])
+            .toggleClass('hidden', !!data.hidden);
+        M.util.js_complete("month-mini-{{uniqid}}-filterChanged");
+    });
+});
+{{/js}}
index ef8fa66..0bd835c 100644 (file)
@@ -84,9 +84,9 @@ class behat_calendar extends behat_base {
      * @param int $day The day of the current month
      */
     public function i_hover_over_day_of_this_month_in_calendar($day) {
-        $summarytitle = get_string('calendarheading', 'calendar', userdate(time(), get_string('strftimemonthyear')));
+        $summarytitle = userdate(time(), get_string('strftimemonthyear'));
         // The current month table.
-        $currentmonth = "table[contains(concat(' ', normalize-space(@summary), ' '), ' {$summarytitle} ')]";
+        $currentmonth = "table[descendant::*[self::caption[contains(concat(' ', normalize-space(.), ' '), ' {$summarytitle} ')]]]";
 
         // Strings for the class cell match.
         $cellclasses  = "contains(concat(' ', normalize-space(@class), ' '), ' day ')";
@@ -95,6 +95,7 @@ class behat_calendar extends behat_base {
         $dayofmonth   = "a[{$daycontains}]";
 
         $xpath = '//' . $currentmonth . '/descendant::' . $daycell . '/' . $dayofmonth;
+        $this->execute("behat_general::wait_until_the_page_is_ready");
         $this->execute("behat_general::i_hover", array($xpath, "xpath_element"));
 
     }
index 6e8789e..9eb6d21 100644 (file)
@@ -30,8 +30,6 @@ $string['activitynext'] = 'Next activity';
 $string['activityprev'] = 'Previous activity';
 $string['breadcrumb'] = 'Breadcrumb trail';
 $string['hideblocka'] = 'Hide {$a} block';
-$string['monthnext'] = 'Next month';
-$string['monthprev'] = 'Previous month';
 $string['showblocka'] = 'Show {$a} block';
 $string['sitemap'] = 'Site map';
 $string['skipa'] = 'Skip {$a}';
@@ -41,3 +39,7 @@ $string['skipto'] = 'Skip to {$a}';
 $string['tocontent'] = 'Skip to main content';
 $string['tonavigation'] = 'Go to navigation';
 $string['youarehere'] = 'You are here';
+
+// Deprecated since Moodle 3.4.
+$string['monthnext'] = 'Next month';
+$string['monthprev'] = 'Previous month';
index 405046d..aad2d91 100644 (file)
@@ -90,6 +90,7 @@ $string['eventdate'] = 'Date';
 $string['eventdescription'] = 'Description';
 $string['eventduration'] = 'Duration';
 $string['eventendtime'] = 'End time';
+$string['eventendtimewrapped'] = '{$a} (End time)';
 $string['eventinstanttime'] = 'Time';
 $string['eventkind'] = 'Type of event';
 $string['eventname'] = 'Event title';
@@ -138,7 +139,16 @@ $string['groupevents'] = 'Group events';
 $string['hidecourseevents'] = 'Hide course events';
 $string['hideglobalevents'] = 'Hide global events';
 $string['hidegroupsevents'] = 'Hide group events';
+$string['hidemoduleevents'] = 'Hide module events';
 $string['hideuserevents'] = 'Hide user events';
+$string['eventtypeglobal'] = 'global';
+$string['eventtypesite'] = 'global';
+$string['eventtypecourse'] = 'course';
+$string['eventtypemodule'] = 'module';
+$string['eventtypegroup'] = 'group';
+$string['eventtypeuser'] = 'user';
+$string['hideeventtype'] = 'Hide {$a} events';
+$string['showeventtype'] = 'Show {$a} events';
 $string['hourly'] = 'Hourly';
 $string['ical'] = 'iCal';
 $string['importcalendar'] = 'Import calendar';
@@ -159,9 +169,10 @@ $string['monday'] = 'Monday';
 $string['monthly'] = 'Monthly';
 $string['monthlyview'] = 'Monthly view';
 $string['monthnext'] = 'Next month';
+$string['monthprev'] = 'Previous month';
 $string['monththis'] = 'This month';
 $string['more'] = 'More';
-$string['namewithsource'] = '{$a->name}({$a->source})';
+$string['namewithsource'] = '{$a->name} ({$a->source})';
 $string['never'] = 'Never';
 $string['newevent'] = 'New event';
 $string['notitle'] = 'no title';
@@ -214,6 +225,7 @@ $string['timeformat_12'] = '12-hour (am/pm)';
 $string['timeformat_24'] = '24-hour';
 $string['timeperiod'] = 'Time period';
 $string['today'] = 'Today';
+$string['todayplustitle'] = 'Today {$a}';
 $string['tomorrow'] = 'Tomorrow';
 $string['tt_deleteevent'] = 'Delete event';
 $string['tt_editevent'] = 'Edit event';
index 55a0e70..995f16b 100644 (file)
@@ -61,3 +61,5 @@ error:backpackloginfailed,core_badges
 signinwithyouremail,core_badges
 sectionusedefaultname,core
 registermoochtips,core_hub
+monthnext,core_access
+monthprev,core_access
index 2c04705..b13af94 100644 (file)
@@ -355,3 +355,13 @@ table.calendartable caption {
         }
     }
 }
+
+.calendarwrapper {
+    position: relative;
+}
+
+.day-popover-content {
+    &:empty + .day-popover-alternate {
+        display: block;
+    }
+}
index d4ce07c..072345d 100644 (file)
         "content": "<img class='icon smallicon' src='../../../pix/i/siteevent.svg'>Test site event"
     }
 }}
-<a href="{{url}}"
-    data-container="body"
-    data-toggle="popover"
-    data-html="true"
-    data-trigger="hover"
-    data-placement="top"
-    data-title="{{title}}"
-    data-content="{{content}}"
->{{day}}</a>
+<a {{!
+    }} id="calendar-day-popover-link-{{courseid}}-{{year}}-{{yday}}-{{uniqid}}"{{!
+    }} href="{{$url}}{{url}}{{/url}}"{{!
+    }} data-container="body"{{!
+    }} data-toggle="popover"{{!
+    }} data-html="true"{{!
+    }} data-trigger="hover"{{!
+    }} data-placement="top"{{!
+    }} data-title="{{$title}}{{title}}{{/title}}"{{!
+    }} data-alternate="{{$nocontent}}{{/nocontent}}"{{!
+}}>{{$day}}{{day}}{{/day}}</a>
+<div class="hidden">
+    {{$content}}{{/content}}
+</div>
+{{#js}}
+require(['jquery'], function($) {
+    require(['theme_boost/popover'], function() {
+        var target = $("#calendar-day-popover-link-{{courseid}}-{{year}}-{{yday}}-{{uniqid}}");
+        target.popover({
+            content: function() {
+                var source = target.next().find("> *:not('.hidden')");
+                var content = $('<div>');
+
+                if (source.length) {
+                    content.html(source.clone(false));
+                } else {
+                    content.html(target.data('alternate'));
+                }
+
+                return content.html();
+            }
+        });
+    });
+});
+{{/js}}
index 0e84a08..72fb4a4 100644 (file)
         }
     }
 }
+
+
+.calendarwrapper {
+    position: relative;
+}
index b6f4cfd..2e34106 100644 (file)
@@ -5852,6 +5852,9 @@ img.iconsmall {
     min-width: 1024px;
   }
 }
+.calendarwrapper {
+  position: relative;
+}
 /* course.less */
 /* COURSE CONTENT */
 /* stylelint-disable unit-blacklist */
index c3a0034..56f0e7b 100644 (file)
         "content": "<img class='icon smallicon' src='../../../pix/i/siteevent.svg'>Test site event"
     }
 }}
-<a id="{{uniqid}}" href="{{url}}"
-    data-container="body"
-    data-toggle="popover"
-    data-html="true"
-    data-trigger="hover"
-    data-placement="top"
-    data-title="{{title}}"
-    data-content="{{content}}">{{day}}</a>
+<a {{!
+    }} href="{{url}}"{{!
+    }} id="calendar-day-popover-link-{{courseid}}-{{year}}-{{yday}}-{{uniqid}}"{{!
+    }} data-container="body"{{!
+    }} data-toggle="popover"{{!
+    }} data-html="true"{{!
+    }} data-trigger="hover"{{!
+    }} data-placement="top"{{!
+    }} data-title="{{$title}}{{title}}{{/title}}"{{!
+    }} data-alternate="{{$nocontent}}{{/nocontent}}"{{!
+}}>{{$day}}{{day}}{{/day}}</a>
+<div class="hidden">
+    {{$content}}{{/content}}
+</div>
 {{#js}}
-require(['jquery'], function($) {
+require(['jquery', 'theme_bootstrapbase/bootstrap'], function($) {
     require(['theme_bootstrapbase/bootstrap'], function() {
-        $('#{{uniqid}}').popover();
+        var target = $("#calendar-day-popover-link-{{courseid}}-{{year}}-{{yday}}-{{uniqid}}");
+        target.popover({
+            content: function() {
+                var source = target.next().find("> *:not('.hidden')");
+                var content = $('<div>');
+
+                if (source.length) {
+                    content.html(source.clone(false));
+                } else {
+                    content.html(target.data('alternate'));
+                }
+
+                return content.html();
+            }
+        });
     });
 });
 {{/js}}