Merge branch 'MDL-59505-master-gdrive' of git://github.com/mudrd8mz/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 25 Sep 2017 23:29:35 +0000 (01:29 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 25 Sep 2017 23:29:35 +0000 (01:29 +0200)
113 files changed:
admin/cli/kill_all_sessions.php [new file with mode: 0644]
admin/cli/purge_caches.php
admin/settings/analytics.php
admin/settings/server.php
admin/settings/top.php
admin/tool/analytics/settings.php
admin/tool/mobile/classes/api.php
blocks/messages/block_messages.php [deleted file]
blocks/messages/db/access.php [deleted file]
blocks/messages/styles.css [deleted file]
blocks/messages/tests/behat/block_messages_course.feature [deleted file]
blocks/messages/tests/behat/block_messages_dashboard.feature [deleted file]
blocks/messages/tests/behat/block_messages_frontpage.feature [deleted file]
blocks/messages/version.php [deleted file]
blocks/upgrade.txt
calendar/amd/build/calendar.min.js
calendar/amd/build/calendar_mini.min.js
calendar/amd/build/calendar_threemonth.min.js [new file with mode: 0644]
calendar/amd/build/events.min.js
calendar/amd/build/modal_delete.min.js [new file with mode: 0644]
calendar/amd/build/repository.min.js
calendar/amd/build/selectors.min.js
calendar/amd/build/summary_modal.min.js
calendar/amd/build/view_manager.min.js
calendar/amd/src/calendar.js
calendar/amd/src/calendar_mini.js
calendar/amd/src/calendar_threemonth.js [new file with mode: 0644]
calendar/amd/src/events.js
calendar/amd/src/modal_delete.js [new file with mode: 0644]
calendar/amd/src/repository.js
calendar/amd/src/selectors.js
calendar/amd/src/summary_modal.js
calendar/amd/src/view_manager.js
calendar/classes/external/calendar_event_exporter.php
calendar/classes/external/date_exporter.php [new file with mode: 0644]
calendar/classes/external/day_exporter.php
calendar/classes/external/event_exporter.php
calendar/classes/external/event_exporter_base.php
calendar/classes/external/event_subscription_exporter.php [new file with mode: 0644]
calendar/classes/external/footer_options_exporter.php
calendar/classes/external/month_exporter.php
calendar/classes/external/week_day_exporter.php
calendar/classes/local/api.php
calendar/classes/type_base.php
calendar/externallib.php
calendar/lib.php
calendar/renderer.php
calendar/templates/calendar_mini.mustache
calendar/templates/calendar_threemonth.mustache [new file with mode: 0644]
calendar/templates/day_detailed.mustache [new file with mode: 0644]
calendar/templates/day_navigation.mustache [new file with mode: 0644]
calendar/templates/event_delete_modal.mustache [new file with mode: 0644]
calendar/templates/event_item.mustache [new file with mode: 0644]
calendar/templates/event_list.mustache [new file with mode: 0644]
calendar/templates/event_subscription.mustache [new file with mode: 0644]
calendar/templates/event_summary_body.mustache
calendar/templates/event_summary_modal.mustache
calendar/templates/footer_options.mustache
calendar/templates/header.mustache [moved from calendar/templates/month_header.mustache with 94% similarity]
calendar/templates/month_detailed.mustache
calendar/templates/month_mini.mustache
calendar/templates/month_navigation.mustache
calendar/templates/threemonth_month.mustache [new file with mode: 0644]
calendar/upgrade.txt
calendar/view.php
competency/classes/api.php
competency/tests/api_test.php
enrol/meta/locallib.php
enrol/tests/enrollib_test.php
install/lang/mi/langconfig.php [moved from blocks/messages/lang/en/block_messages.php with 60% similarity]
install/lang/oc_lnc/admin.php
install/lang/sv_fi/install.php
lang/en/admin.php
lang/en/analytics.php
lang/en/calendar.php
lib/accesslib.php
lib/classes/plugin_manager.php
lib/db/upgrade.php
lib/deprecatedlib.php
lib/enrollib.php
lib/filestorage/file_system.php
lib/filestorage/stored_file.php
lib/filestorage/tests/file_system_test.php
lib/form/classes/filetypes_util.php
lib/form/tests/filetypes_util_test.php
lib/mlbackend/python/classes/processor.php
lib/mlbackend/python/lang/en/mlbackend_python.php
lib/navigationlib.php
lib/tests/accesslib_test.php
lib/tests/navigationlib_test.php
login/signup_form.php
mod/assign/submission/onlinetext/locallib.php
mod/chat/classes/external.php
mod/chat/tests/externallib_test.php
mod/choice/lang/en/choice.php
mod/choice/lib.php
mod/choice/tests/lib_test.php
mod/choice/tests/restore_date_test.php
mod/folder/edit.php
mod/workshop/classes/external.php
mod/workshop/db/services.php
mod/workshop/locallib.php
mod/workshop/submission.php
mod/workshop/tests/external_test.php
mod/workshop/version.php
plagiarism/upgrade.txt
question/behaviour/manualgraded/tests/walkthrough_test.php
question/behaviour/rendererbase.php
search/classes/document.php
theme/boost/scss/preset/default.scss
user/index.php
user/renderer.php
version.php

diff --git a/admin/cli/kill_all_sessions.php b/admin/cli/kill_all_sessions.php
new file mode 100644 (file)
index 0000000..9cf301d
--- /dev/null
@@ -0,0 +1,55 @@
+<?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/>.
+
+/**
+ * CLI script to kill all user sessions without asking for confirmation.
+ *
+ * @package    core
+ * @subpackage cli
+ * @copyright  2017 Alexander Bias <alexander.bias@uni-ulm.de>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require(__DIR__.'/../../config.php');
+require_once($CFG->libdir.'/clilib.php');
+
+list($options, $unrecognized) = cli_get_params(array('help' => false), array('h' => 'help'));
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized), 2);
+}
+
+if ($options['help']) {
+    $help =
+"Kill all Moodle sessions
+
+Options:
+-h, --help            Print out this help
+
+Example:
+\$sudo -u www-data /usr/bin/php admin/cli/kill_all_sessions.php
+";
+
+    echo $help;
+    exit(0);
+}
+
+\core\session\manager::kill_all_sessions();
+
+exit(0);
index 7c9d344..64f29bd 100644 (file)
@@ -15,6 +15,8 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
+ * CLI script to purge caches without asking for confirmation.
+ *
  * @package    core
  * @subpackage cli
  * @copyright  2011 David Mudrak <david@moodle.com>
@@ -50,4 +52,4 @@ Example:
 
 purge_all_caches();
 
-exit(0);
\ No newline at end of file
+exit(0);
index 3d28719..e8247c8 100644 (file)
@@ -26,7 +26,7 @@ defined('MOODLE_INTERNAL') || die();
 
 if ($hassiteconfig) {
     $settings = new admin_settingpage('analyticssettings', new lang_string('analyticssettings', 'analytics'));
-    $ADMIN->add('appearance', $settings);
+    $ADMIN->add('analytics', $settings);
 
     if ($ADMIN->fulltree) {
         // Select the site prediction's processor.
index 32af104..e847077 100644 (file)
@@ -12,6 +12,8 @@ $temp->add(new admin_setting_configexecutable('pathtodu', new lang_string('patht
 $temp->add(new admin_setting_configexecutable('aspellpath', new lang_string('aspellpath', 'admin'), new lang_string('edhelpaspellpath'), ''));
 $temp->add(new admin_setting_configexecutable('pathtodot', new lang_string('pathtodot', 'admin'), new lang_string('pathtodot_help', 'admin'), ''));
 $temp->add(new admin_setting_configexecutable('pathtogs', new lang_string('pathtogs', 'admin'), new lang_string('pathtogs_help', 'admin'), '/usr/bin/gs'));
+$temp->add(new admin_setting_configexecutable('pathtopython', new lang_string('pathtopython', 'admin'),
+    new lang_string('pathtopythondesc', 'admin'), ''));
 $ADMIN->add('server', $temp);
 
 
index c78c909..be45ef6 100644 (file)
@@ -30,6 +30,7 @@ if ($hassiteconfig) {
 $ADMIN->add('root', new admin_category('users', new lang_string('users','admin')));
 $ADMIN->add('root', new admin_category('courses', new lang_string('courses','admin')));
 $ADMIN->add('root', new admin_category('grades', new lang_string('grades')));
+$ADMIN->add('root', new admin_category('analytics', new lang_string('analytics', 'analytics')));
 $ADMIN->add('root', new admin_category('competencies', new lang_string('competencies', 'core_competency')));
 $ADMIN->add('root', new admin_category('badges', new lang_string('badges'), empty($CFG->enablebadges)));
 $ADMIN->add('root', new admin_category('location', new lang_string('location','admin')));
index 76a1bde..aad459a 100644 (file)
@@ -24,5 +24,5 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$ADMIN->add('reports', new admin_externalpage('analyticmodels', get_string('analyticmodels', 'tool_analytics'),
+$ADMIN->add('analytics', new admin_externalpage('analyticmodels', get_string('analyticmodels', 'tool_analytics'),
     "$CFG->wwwroot/$CFG->admin/tool/analytics/index.php", 'moodle/analytics:managemodels'));
index 2fc0bfe..3c247e3 100644 (file)
@@ -152,10 +152,11 @@ class api {
         $url = new moodle_url("/$CFG->admin/tool/mobile/launch.php");
         $settings['launchurl'] = $url->out(false);
 
-        if ($logourl = $OUTPUT->get_logo_url()) {
+        // Check that we are receiving a moodle_url object, themes can override get_logo_url and may return incorrect values.
+        if (($logourl = $OUTPUT->get_logo_url()) && $logourl instanceof moodle_url) {
             $settings['logourl'] = $logourl->out(false);
         }
-        if ($compactlogourl = $OUTPUT->get_compact_logo_url()) {
+        if (($compactlogourl = $OUTPUT->get_compact_logo_url()) && $compactlogourl instanceof moodle_url) {
             $settings['compactlogourl'] = $compactlogourl->out(false);
         }
 
@@ -215,7 +216,11 @@ class api {
 
         if (empty($section) or $section == 'gradessettings') {
             require_once($CFG->dirroot . '/user/lib.php');
-            $settings->mygradesurl = user_mygrades_url()->out(false);
+            $settings->mygradesurl = user_mygrades_url();
+            // The previous function may return moodle_url instances or plain string URLs.
+            if ($settings->mygradesurl instanceof moodle_url) {
+                $settings->mygradesurl = $settings->mygradesurl->out(false);
+            }
         }
 
         if (empty($section) or $section == 'mobileapp') {
diff --git a/blocks/messages/block_messages.php b/blocks/messages/block_messages.php
deleted file mode 100644 (file)
index 79d74b8..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Mentees block.
- *
- * @package    block_messages
- * @copyright  1999 onwards Martin Dougiamas (http://dougiamas.com)
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-class block_messages extends block_base {
-    function init() {
-        $this->title = get_string('pluginname', 'block_messages');
-    }
-
-    function get_content() {
-        global $USER, $CFG, $DB, $OUTPUT;
-
-        if (!$CFG->messaging) {
-            $this->content = new stdClass;
-            $this->content->text = '';
-            $this->content->footer = '';
-            if ($this->page->user_is_editing()) {
-                $this->content->text = get_string('disabled', 'message');
-            }
-            return $this->content;
-        }
-
-        if ($this->content !== NULL) {
-            return $this->content;
-        }
-
-        $this->content = new stdClass;
-        $this->content->text = '';
-        $this->content->footer = '';
-
-        if (empty($this->instance) or !isloggedin() or isguestuser() or empty($CFG->messaging)) {
-            return $this->content;
-        }
-
-        $link = '/message/index.php';
-        $action = null; //this was using popup_action() but popping up a fullsize window seems wrong
-        $this->content->footer = $OUTPUT->action_link($link, get_string('messages', 'message'), $action);
-
-        $ufields = user_picture::fields('u', array('lastaccess'));
-        $users = $DB->get_records_sql("SELECT $ufields, COUNT(m.useridfrom) AS count
-                                         FROM {user} u, {message} m
-                                        WHERE m.useridto = ? AND u.id = m.useridfrom AND m.notification = 0
-                                     GROUP BY $ufields", array($USER->id));
-
-
-        //Now, we have in users, the list of users to show
-        //Because they are online
-        if (!empty($users)) {
-            $this->content->text .= '<ul class="list">';
-            foreach ($users as $user) {
-                $timeago = format_time(time() - $user->lastaccess);
-                $this->content->text .= '<li class="listentry"><div class="user"><a href="'.$CFG->wwwroot.'/user/view.php?id='.$user->id.'&amp;course='.SITEID.'" title="'.$timeago.'">';
-                $this->content->text .= $OUTPUT->user_picture($user, array('courseid'=>SITEID)); //TODO: user might not have capability to view frontpage profile :-(
-                $this->content->text .= fullname($user).'</a></div>';
-
-                $link = '/message/index.php?usergroup=unread&id='.$user->id;
-                $anchortagcontents = $OUTPUT->pix_icon('t/message', fullname($user)) . '&nbsp;' . $user->count;
-
-                $action = null; // popup is gone now
-                $anchortag = $OUTPUT->action_link($link, $anchortagcontents, $action);
-
-                $this->content->text .= '<div class="message">'.$anchortag.'</div></li>';
-            }
-            $this->content->text .= '</ul>';
-        } else {
-            $this->content->text .= '<div class="info">';
-            $this->content->text .= get_string('nomessages', 'message');
-            $this->content->text .= '</div>';
-        }
-
-        return $this->content;
-    }
-}
-
-
diff --git a/blocks/messages/db/access.php b/blocks/messages/db/access.php
deleted file mode 100644 (file)
index 4ea589d..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Messages block caps.
- *
- * @package    block_messages
- * @copyright  Mark Nelson <markn@moodle.com>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-$capabilities = array(
-
-    'block/messages:myaddinstance' => array(
-        'captype' => 'write',
-        'contextlevel' => CONTEXT_SYSTEM,
-        'archetypes' => array(
-            'user' => CAP_ALLOW
-        ),
-
-        'clonepermissionsfrom' => 'moodle/my:manageblocks'
-    ),
-
-    'block/messages:addinstance' => array(
-        'riskbitmask' => RISK_SPAM | RISK_XSS,
-
-        'captype' => 'write',
-        'contextlevel' => CONTEXT_BLOCK,
-        'archetypes' => array(
-            'editingteacher' => CAP_ALLOW,
-            'manager' => CAP_ALLOW
-        ),
-
-        'clonepermissionsfrom' => 'moodle/site:manageblocks'
-    ),
-);
diff --git a/blocks/messages/styles.css b/blocks/messages/styles.css
deleted file mode 100644 (file)
index 176f756..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-.block_messages .content {
-    text-align: left;
-    padding-top: 5px;
-}
-
-.block_messages .content .list li.listentry {
-    clear: both;
-}
-
-.block_messages .content .list li.listentry .user {
-    float: left;
-    position: relative;
-}
-
-.block_messages .content .list li.listentry .message {
-    float: right;
-}
-
-.block_messages .content .info {
-    text-align: center;
-}
-
-.block_messages .content .footer {
-    clear: both;
-}
diff --git a/blocks/messages/tests/behat/block_messages_course.feature b/blocks/messages/tests/behat/block_messages_course.feature
deleted file mode 100644 (file)
index 63dd4a5..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-@block @block_messages
-Feature: The messages block allows users to list new messages an a course
-  In order to enable the messages block in a course
-  As a teacher
-  I can add the messages block to a course and view my messages
-
-  Background:
-    Given the following "users" exist:
-      | username | firstname | lastname | email | idnumber |
-      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
-      | student1 | Student | 1 | student1@example.com | S1 |
-    And the following "courses" exist:
-      | fullname | shortname | category |
-      | Course 1 | C1 | 0 |
-    And the following "course enrolments" exist:
-      | user | course | role |
-      | teacher1 | C1 | editingteacher |
-      | student1 | C1 | student |
-
-  Scenario: View the block by a user with messaging disabled.
-    Given the following config values are set as admin:
-      | messaging       | 0 |
-    And I log in as "teacher1"
-    And I am on "Course 1" course homepage with editing mode on
-    And I add the "Messages" block
-    Then I should see "Messaging is disabled on this site" in the "Messages" "block"
-
-  Scenario: View the block by a user who does not have any messages.
-    Given I log in as "teacher1"
-    And I am on "Course 1" course homepage with editing mode on
-    And I add the "Messages" block
-    Then I should see "No messages" in the "Messages" "block"
-
-  @javascript
-  Scenario: View the block by a user who has messages.
-    Given I log in as "student1"
-    And I follow "Messages" in the user menu
-    And I send "This is message 1" message to "Teacher 1" user
-    And I send "This is message 2" message to "Teacher 1" user
-    And I log out
-    And I log in as "teacher1"
-    And I am on "Course 1" course homepage with editing mode on
-    And I add the "Messages" block
-    Then I should see "Student 1" in the "Messages" "block"
-
-  @javascript
-  Scenario: Use the block to send a message to a user.
-    Given I log in as "teacher1"
-    And I am on "Course 1" course homepage with editing mode on
-    And I add the "Messages" block
-    And I click on "//a[normalize-space(.) = 'Messages']" "xpath_element" in the "Messages" "block"
-    And I send "This is message 1" message to "Student 1" user
-    And I log out
-    When I log in as "student1"
-    And I am on "Course 1" course homepage
-    Then I should see "Teacher 1" in the "Messages" "block"
diff --git a/blocks/messages/tests/behat/block_messages_dashboard.feature b/blocks/messages/tests/behat/block_messages_dashboard.feature
deleted file mode 100644 (file)
index 509057e..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-@block @block_messages
-Feature: The messages block allows users to list new messages on the dashboard
-  In order to enable the messages block on the dashboard
-  As a user
-  I can add the messages block to a my dashboard and view my messages
-
-  Background:
-    Given the following "users" exist:
-      | username | firstname | lastname | email | idnumber |
-      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
-      | student1 | Student | 1 | student1@example.com | S1 |
-
-  Scenario: View the block by a user with messaging disabled.
-    Given the following config values are set as admin:
-      | messaging       | 0 |
-    And I log in as "teacher1"
-    And I press "Customise this page"
-    When I add the "Messages" block
-    Then I should see "Messaging is disabled on this site" in the "Messages" "block"
-
-  Scenario: View the block by a user who does not have any messages.
-    Given I log in as "teacher1"
-    And I press "Customise this page"
-    When I add the "Messages" block
-    Then I should see "No messages" in the "Messages" "block"
-
-  @javascript
-  Scenario: View the block by a user who has messages.
-    Given I log in as "student1"
-    And I follow "Messages" in the user menu
-    And I send "This is message 1" message to "Teacher 1" user
-    And I send "This is message 2" message to "Teacher 1" user
-    And I log out
-    When I log in as "teacher1"
-    And I press "Customise this page"
-    And I add the "Messages" block
-    Then I should see "Student 1" in the "Messages" "block"
-
-  @javascript
-  Scenario: Use the block to send a message to a user.
-    Given I log in as "teacher1"
-    And I press "Customise this page"
-    And I add the "Messages" block
-    And I click on "//a[normalize-space(.) = 'Messages']" "xpath_element" in the "Messages" "block"
-    And I send "This is message 1" message to "Student 1" user
-    And I log out
-    When I log in as "student1"
-    And I press "Customise this page"
-    And I add the "Messages" block
-    Then I should see "Teacher 1" in the "Messages" "block"
diff --git a/blocks/messages/tests/behat/block_messages_frontpage.feature b/blocks/messages/tests/behat/block_messages_frontpage.feature
deleted file mode 100644 (file)
index 2c71001..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-@block @block_messages
-Feature: The messages block allows users to list new messages on the frontpage
-  In order to enable the messages block on the frontpage
-  As an admin
-  I can add the messages block to a the frontpage and view my messages
-
-  Background:
-    Given the following "users" exist:
-      | username | firstname | lastname | email | idnumber |
-      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
-      | student1 | Student | 1 | student1@example.com | S1 |
-    And I log in as "admin"
-    And I am on site homepage
-    And I navigate to "Turn editing on" node in "Front page settings"
-    And I add the "Messages" block
-    And I log out
-
-  Scenario: View the block by a user with messaging disabled.
-    Given the following config values are set as admin:
-      | messaging       | 0 |
-    And I log in as "admin"
-    And I am on site homepage
-    When I navigate to "Turn editing on" node in "Front page settings"
-    And I should see "Messaging is disabled on this site" in the "Messages" "block"
-    Then I navigate to "Turn editing off" node in "Front page settings"
-    And I should not see "Messaging is disabled on this site"
-
-  Scenario: View the block by a user who does not have any messages.
-    Given I log in as "teacher1"
-    When I am on site homepage
-    Then I should see "No messages" in the "Messages" "block"
-
-  Scenario: Try to view the block as a guest user.
-    Given I log in as "guest"
-    When I am on site homepage
-    Then I should not see "Messages"
-
-  @javascript
-  Scenario: View the block by a user who has messages.
-    Given I log in as "student1"
-    And I follow "Messages" in the user menu
-    And I send "This is message 1" message to "Teacher 1" user
-    And I send "This is message 2" message to "Teacher 1" user
-    And I log out
-    When I log in as "teacher1"
-    And I am on site homepage
-    Then I should see "Student 1" in the "Messages" "block"
-
-  @javascript
-  Scenario: Use the block to send a message to a user.
-    Given I log in as "teacher1"
-    And I am on site homepage
-    And I click on "//a[normalize-space(.) = 'Messages']" "xpath_element" in the "Messages" "block"
-    And I send "This is message 1" message to "Student 1" user
-    And I log out
-    When I log in as "student1"
-    And I am on site homepage
-    Then I should see "Teacher 1" in the "Messages" "block"
diff --git a/blocks/messages/version.php b/blocks/messages/version.php
deleted file mode 100644 (file)
index 9faef4d..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Version details
- *
- * @package    block_messages
- * @copyright  1999 onwards Martin Dougiamas (http://dougiamas.com)
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-$plugin->version   = 2017051500;        // The current plugin version (Date: YYYYMMDDXX)
-$plugin->requires  = 2017050500;        // Requires this Moodle version
-$plugin->component = 'block_messages';  // Full name of the plugin (used for diagnostics)
index 6b8fc56..7ef8f0a 100644 (file)
@@ -9,6 +9,7 @@ information provided here is intended especially for developers.
 * Blocks can now be included in Moodle global search, with some limitations (at present, the search
   works only for blocks located directly on course pages or site home page). See the HTML block for
   an example.
+* Block block_messages is no longer a part of core.
 
 === 3.3 ===
 
index 1999702..a1c8e36 100644 (file)
Binary files a/calendar/amd/build/calendar.min.js and b/calendar/amd/build/calendar.min.js differ
index 979e92e..57db248 100644 (file)
Binary files a/calendar/amd/build/calendar_mini.min.js and b/calendar/amd/build/calendar_mini.min.js differ
diff --git a/calendar/amd/build/calendar_threemonth.min.js b/calendar/amd/build/calendar_threemonth.min.js
new file mode 100644 (file)
index 0000000..ac1bb8c
Binary files /dev/null and b/calendar/amd/build/calendar_threemonth.min.js differ
index 77b2153..99bf77b 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/modal_delete.min.js b/calendar/amd/build/modal_delete.min.js
new file mode 100644 (file)
index 0000000..78456c6
Binary files /dev/null and b/calendar/amd/build/modal_delete.min.js differ
index a33feed..c970b3f 100644 (file)
Binary files a/calendar/amd/build/repository.min.js and b/calendar/amd/build/repository.min.js differ
index 8dbeb18..a81f332 100644 (file)
Binary files a/calendar/amd/build/selectors.min.js and b/calendar/amd/build/selectors.min.js differ
index 0a9f53a..fdca2a5 100644 (file)
Binary files a/calendar/amd/build/summary_modal.min.js and b/calendar/amd/build/summary_modal.min.js differ
index 301ef42..18d2a61 100644 (file)
Binary files a/calendar/amd/build/view_manager.min.js and b/calendar/amd/build/view_manager.min.js differ
index 2958c83..0c436cc 100644 (file)
@@ -66,7 +66,8 @@ define([
         LOADING_ICON: '.loading-icon',
         VIEW_DAY_LINK: "[data-action='view-day-link']",
         CALENDAR_MONTH_WRAPPER: ".calendarwrapper",
-        COURSE_SELECTOR: 'select[name="course"]'
+        COURSE_SELECTOR: 'select[name="course"]',
+        TODAY: '.today',
     };
 
     /**
@@ -82,21 +83,6 @@ define([
         });
     };
 
-    /**
-     * Get the event source.
-     *
-     * @param {Object} subscription The event subscription object.
-     * @return {promise} The lang string promise.
-     */
-    var getEventSource = function(subscription) {
-        return Str.get_string('subsource', 'core_calendar', subscription).then(function(langStr) {
-            if (subscription.url) {
-                return '<a href="' + subscription.url + '">' + langStr + '</a>';
-            }
-            return langStr;
-        });
-    };
-
     /**
      * Render the event summary modal.
      *
@@ -109,31 +95,11 @@ define([
                 throw new Error('Error encountered while trying to fetch calendar event with ID: ' + eventId);
             }
             var eventData = getEventResponse.event;
-            var eventTypePromise = getEventType(eventData.eventtype);
-
-            // If the calendar event has event source, get the source's language string/link.
-            if (eventData.displayeventsource) {
-                eventData.subscription = JSON.parse(eventData.subscription);
-                var eventSourceParams = {
-                    url: eventData.subscription.url,
-                    name: eventData.subscription.name
-                };
-                var eventSourcePromise = getEventSource(eventSourceParams);
-
-                // Return event data with event type and event source info.
-                return $.when(eventTypePromise, eventSourcePromise).then(function(eventType, eventSource) {
-                    eventData.eventtype = eventType;
-                    eventData.source = eventSource;
-                    return eventData;
-                });
-            }
 
-            // Return event data with event type info.
-            return eventTypePromise.then(function(eventType) {
+            return getEventType(eventData.eventtype).then(function(eventType) {
                 eventData.eventtype = eventType;
                 return eventData;
             });
-
         }).then(function(eventData) {
             // Build the modal parameters from the event data.
             var modalParams = {
@@ -245,8 +211,7 @@ define([
                 templateContext: {
                     contextid: contextId
                 }
-            },
-            [root, SELECTORS.NEW_EVENT_BUTTON]
+            }
         );
     };
 
@@ -290,11 +255,14 @@ define([
             });
             modal.setCourseId(courseId);
             return;
-        });
+        })
+        .fail(Notification.exception);
     };
 
     /**
      * Register event listeners for the module.
+     *
+     * @param {object} root The calendar root element
      */
     var registerEventListeners = function(root) {
         // Bind click events to event links.
@@ -330,6 +298,24 @@ define([
         var eventFormPromise = registerEventFormModal(root);
         registerCalendarEventListeners(root, eventFormPromise);
 
+        // Bind click event on the new event button.
+        root.on('click', SELECTORS.NEW_EVENT_BUTTON, function(e) {
+            eventFormPromise.then(function(modal) {
+                // Attempt to find the cell for today.
+                // If it can't be found, then use the start time of the first day on the calendar.
+                var today = root.find(SELECTORS.TODAY);
+                if (!today.length) {
+                    modal.setStartTime(root.find(SELECTORS.DAY).attr('data-new-event-timestamp'));
+                }
+
+                modal.show();
+                return;
+            })
+            .fail(Notification.exception);
+
+            e.preventDefault();
+        });
+
         // Bind click events to calendar days.
         root.on('click', SELECTORS.DAY, function(e) {
             var target = $(e.target);
@@ -340,7 +326,8 @@ define([
                     modal.setStartTime(startTime);
                     modal.show();
                     return;
-                });
+                })
+                .fail(Notification.exception);
 
                 e.preventDefault();
             }
index 48a4f44..6853bf7 100644 (file)
@@ -38,6 +38,44 @@ function(
     CalendarViewManager
 ) {
 
+    /**
+     * Listen to and handle any calendar events fired by the calendar UI.
+     *
+     * @method registerCalendarEventListeners
+     * @param {object} root The calendar root element
+     */
+    var registerCalendarEventListeners = function(root) {
+        var body = $('body');
+        var namespace = '.' + root.attr('id');
+
+        body.on(CalendarEvents.created + namespace, root, reloadMonth);
+        body.on(CalendarEvents.deleted + namespace, root, reloadMonth);
+        body.on(CalendarEvents.updated + namespace, root, reloadMonth);
+        body.on(CalendarEvents.eventMoved + namespace, root, reloadMonth);
+    };
+
+    /**
+     * Reload the month view in this month.
+     *
+     * @param {EventFacade} e
+     */
+    var reloadMonth = function(e) {
+        var root = e.data;
+        var body = $('body');
+        var namespace = '.' + root.attr('id');
+
+        if (root.is(':visible')) {
+            CalendarViewManager.reloadCurrentMonth(root);
+        } else {
+            // The root has been removed.
+            // Remove all events in the namespace.
+            body.on(CalendarEvents.created + namespace);
+            body.on(CalendarEvents.deleted + namespace);
+            body.on(CalendarEvents.updated + namespace);
+            body.on(CalendarEvents.eventMoved + namespace);
+        }
+    };
+
     var registerEventListeners = function(root) {
         $('body').on(CalendarEvents.filterChanged, function(e, data) {
             var daysWithEvent = root.find(CalendarSelectors.eventType[data.type]);
@@ -52,6 +90,7 @@ function(
 
             CalendarViewManager.init(root);
             registerEventListeners(root);
+            registerCalendarEventListeners(root);
         }
     };
 });
diff --git a/calendar/amd/src/calendar_threemonth.js b/calendar/amd/src/calendar_threemonth.js
new file mode 100644 (file)
index 0000000..a043887
--- /dev/null
@@ -0,0 +1,123 @@
+// 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 handles display of multiple mini calendars in a view, and
+ * movement through them.
+ *
+ * @module     core_calendar/calendar_threemonth
+ * @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/templates',
+    'core_calendar/view_manager',
+],
+function(
+    $,
+    CalendarSelectors,
+    CalendarEvents,
+    Templates,
+    CalendarViewManager
+) {
+
+    /**
+     * Listen to and handle any calendar events fired by the calendar UI.
+     *
+     * @method registerCalendarEventListeners
+     * @param {object} root The calendar root element
+     */
+    var registerCalendarEventListeners = function(root) {
+        var body = $('body');
+        body.on(CalendarEvents.monthChanged, function(e, year, month, courseId) {
+            // We have to use a queue here because the calling code is decoupled from these listeners.
+            // It's possible for the event to be called multiple times before one call is fully resolved.
+            root.queue(function(next) {
+                return processRequest(e, year, month, courseId)
+                .then(function() {
+                    return next();
+                });
+            });
+        });
+
+        var processRequest = function(e, year, month, courseId) {
+            var newCurrentMonth = root.find('[data-year="' + year + '"][data-month="' + month + '"]');
+            var newParent = newCurrentMonth.closest(CalendarSelectors.calendarPeriods.month);
+            var allMonths = root.find(CalendarSelectors.calendarPeriods.month);
+
+            var previousMonth = $(allMonths[0]);
+            var nextMonth = $(allMonths[2]);
+
+            var placeHolder = $('<span>');
+            placeHolder.attr('data-template', 'core_calendar/threemonth_month');
+            placeHolder.attr('data-includenavigation', false);
+            var placeHolderContainer = $('<div>');
+            placeHolderContainer.hide();
+            placeHolderContainer.append(placeHolder);
+
+            var requestYear;
+            var requestMonth;
+            var oldMonth;
+
+            if (newParent.is(previousMonth)) {
+                // Fetch the new previous month.
+                placeHolderContainer.insertBefore(previousMonth);
+
+                requestYear = previousMonth.data('previousYear');
+                requestMonth = previousMonth.data('previousMonth');
+                oldMonth = nextMonth;
+            } else if (newParent.is(nextMonth)) {
+                // Fetch the new next month.
+                placeHolderContainer.insertAfter(nextMonth);
+                requestYear = nextMonth.data('nextYear');
+                requestMonth = nextMonth.data('nextMonth');
+                oldMonth = previousMonth;
+            }
+
+            return CalendarViewManager.refreshMonthContent(
+                placeHolder,
+                requestYear,
+                requestMonth,
+                courseId,
+                placeHolder
+            )
+            .then(function() {
+                var slideUpPromise = $.Deferred();
+                var slideDownPromise = $.Deferred();
+                oldMonth.slideUp('fast', function() {
+                    $(this).remove();
+                    slideUpPromise.resolve();
+                });
+                placeHolderContainer.slideDown('fast', function() {
+                    slideDownPromise.resolve();
+                });
+
+                return $.when(slideUpPromise, slideDownPromise);
+            });
+        };
+    };
+
+    return {
+        init: function(root) {
+            root = $(root);
+
+            registerCalendarEventListeners(root);
+        }
+    };
+});
index 29b5fd2..9134944 100644 (file)
@@ -26,6 +26,7 @@ define([], function() {
     return {
         created: 'calendar-events:created',
         deleted: 'calendar-events:deleted',
+        deleteAll: 'calendar-events:delete_all',
         updated: 'calendar-events:updated',
         editEvent: 'calendar-events:edit_event',
         editActionEvent: 'calendar-events:edit_action_event',
diff --git a/calendar/amd/src/modal_delete.js b/calendar/amd/src/modal_delete.js
new file mode 100644 (file)
index 0000000..9541dec
--- /dev/null
@@ -0,0 +1,112 @@
+// 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/>.
+
+/**
+ * Contain the logic for the save/cancel modal.
+ *
+ * @module     core_calendar/modal_delete
+ * @class      modal_delete
+ * @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/notification',
+    'core/custom_interaction_events',
+    'core/modal',
+    'core/modal_events',
+    'core/modal_registry',
+    'core_calendar/events',
+],
+function(
+    $,
+    Notification,
+    CustomEvents,
+    Modal,
+    ModalEvents,
+    ModalRegistry,
+    CalendarEvents
+) {
+
+    var registered = false;
+    var SELECTORS = {
+        DELETE_ONE_BUTTON: '[data-action="deleteone"]',
+        DELETE_ALL_BUTTON: '[data-action="deleteall"]',
+        CANCEL_BUTTON: '[data-action="cancel"]',
+    };
+
+    /**
+     * Constructor for the Modal.
+     *
+     * @param {object} root The root jQuery element for the modal
+     */
+    var ModalDelete = function(root) {
+        Modal.call(this, root);
+    };
+
+    ModalDelete.TYPE = 'core_calendar-modal_delete';
+    ModalDelete.prototype = Object.create(Modal.prototype);
+    ModalDelete.prototype.constructor = ModalDelete;
+
+    /**
+     * Set up all of the event handling for the modal.
+     *
+     * @method registerEventListeners
+     */
+    ModalDelete.prototype.registerEventListeners = function() {
+        // Apply parent event listeners.
+        Modal.prototype.registerEventListeners.call(this);
+
+        this.getModal().on(CustomEvents.events.activate, SELECTORS.DELETE_ONE_BUTTON, function(e, data) {
+            var saveEvent = $.Event(ModalEvents.save);
+            this.getRoot().trigger(saveEvent, this);
+
+            if (!saveEvent.isDefaultPrevented()) {
+                this.hide();
+                data.originalEvent.preventDefault();
+            }
+        }.bind(this));
+
+        this.getModal().on(CustomEvents.events.activate, SELECTORS.DELETE_ALL_BUTTON, function(e, data) {
+            var saveEvent = $.Event(CalendarEvents.deleteAll);
+            this.getRoot().trigger(saveEvent, this);
+
+            if (!saveEvent.isDefaultPrevented()) {
+                this.hide();
+                data.originalEvent.preventDefault();
+            }
+        }.bind(this));
+
+        this.getModal().on(CustomEvents.events.activate, SELECTORS.CANCEL_BUTTON, function(e, data) {
+            var cancelEvent = $.Event(ModalEvents.cancel);
+            this.getRoot().trigger(cancelEvent, this);
+
+            if (!cancelEvent.isDefaultPrevented()) {
+                this.hide();
+                data.originalEvent.preventDefault();
+            }
+        }.bind(this));
+    };
+
+    // Automatically register with the modal registry the first time this module is imported so that you can create modals
+    // of this type using the modal factory.
+    if (!registered) {
+        ModalRegistry.register(ModalDelete.TYPE, ModalDelete, 'calendar/event_delete_modal');
+        registered = true;
+    }
+
+    return ModalDelete;
+});
index 55c9db9..5e1321c 100644 (file)
@@ -29,16 +29,19 @@ define(['jquery', 'core/ajax'], function($, Ajax) {
      *
      * @method deleteEvent
      * @param {int} eventId The event id.
+     * @arapm {bool} deleteSeries Whether to delete all events in the series
      * @return {promise} Resolved with requested calendar event
      */
-    var deleteEvent = function(eventId) {
-
+    var deleteEvent = function(eventId, deleteSeries) {
+        if (typeof deleteSeries === 'undefined') {
+            deleteSeries = false;
+        }
         var request = {
             methodname: 'core_calendar_delete_calendar_events',
             args: {
                 events: [{
                     eventid: eventId,
-                    repeat: 1
+                    repeat: deleteSeries,
                 }]
             }
         };
@@ -87,16 +90,20 @@ define(['jquery', 'core/ajax'], function($, Ajax) {
      * Get calendar data for the month view.
      *
      * @method getCalendarMonthData
-     * @param {Number} time Timestamp.
+     * @param {Number} year Year
+     * @param {Number} month Month
      * @param {Number} courseid The course id.
+     * @param {Bool} includenavigation Whether to include navigation.
      * @return {promise} Resolved with the month view data.
      */
-    var getCalendarMonthData = function(time, courseid) {
+    var getCalendarMonthData = function(year, month, courseid, includenavigation) {
         var request = {
             methodname: 'core_calendar_get_calendar_monthly_view',
             args: {
-                time: time,
-                courseid: courseid
+                year: year,
+                month: month,
+                courseid: courseid,
+                includenavigation: includenavigation,
             }
         };
 
index 89bb65a..a22a86d 100644 (file)
@@ -36,5 +36,8 @@ define([], function() {
             group: "[data-popover-eventtype-group]",
             user: "[data-popover-eventtype-user]",
         },
+        calendarPeriods: {
+            month: "[data-period='month']",
+        },
     };
 });
index da138c3..ff9317f 100644 (file)
  * @copyright  2017 Simey Lameze <simey@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/str', 'core/notification', 'core/custom_interaction_events', 'core/modal',
-    'core/modal_registry', 'core/modal_factory', 'core/modal_events', 'core_calendar/repository',
-    'core_calendar/events'],
-    function($, Str, Notification, CustomEvents, Modal, ModalRegistry, ModalFactory, ModalEvents, CalendarRepository,
-             CalendarEvents) {
+define([
+    'jquery',
+    'core/str',
+    'core/notification',
+    'core/custom_interaction_events',
+    'core/modal',
+    'core/modal_registry',
+    'core/modal_factory',
+    'core/modal_events',
+    'core_calendar/repository',
+    'core_calendar/events',
+    'core_calendar/modal_delete',
+],
+function(
+    $,
+    Str,
+    Notification,
+    CustomEvents,
+    Modal,
+    ModalRegistry,
+    ModalFactory,
+    ModalEvents,
+    CalendarRepository,
+    CalendarEvents,
+    ModalDelete
+) {
 
     var registered = false;
     var SELECTORS = {
@@ -89,6 +110,18 @@ define(['jquery', 'core/str', 'core/notification', 'core/custom_interaction_even
         return this.getBody().find(SELECTORS.ROOT).attr('data-event-id');
     };
 
+    /**
+     * Get the number of events in the series for the event being shown in
+     * this modal. This value is not cached because it will change
+     * depending on which event is being displayed.
+     *
+     * @method getEventCount
+     * @return {int}
+     */
+    ModalEventSummary.prototype.getEventCount = function() {
+        return this.getBody().find(SELECTORS.ROOT).attr('data-event-event-count');
+    };
+
     /**
      * Get the url for the event being shown in this modal.
      *
@@ -163,34 +196,76 @@ define(['jquery', 'core/str', 'core/notification', 'core/custom_interaction_even
                 key: 'deleteevent',
                 component: 'calendar'
             },
-            {
+        ];
+
+        var eventCount = parseInt(summaryModal.getEventCount(), 10);
+        var deletePromise;
+        var isRepeatedEvent = eventCount > 1;
+        if (isRepeatedEvent) {
+            deleteStrings.push({
+                key: 'confirmeventseriesdelete',
+                component: 'calendar',
+                param: {
+                    name: eventTitle,
+                    count: eventCount,
+                },
+            });
+
+            deletePromise = ModalFactory.create(
+                {
+                    type: ModalDelete.TYPE
+                },
+                summaryModal.getDeleteButton()
+            );
+        } else {
+            deleteStrings.push({
                 key: 'confirmeventdelete',
                 component: 'calendar',
                 param: eventTitle
-            }
-        ];
+            });
+
+            deletePromise = ModalFactory.create(
+                {
+                    type: ModalFactory.types.SAVE_CANCEL
+                },
+                summaryModal.getDeleteButton()
+            );
+        }
+
         var eventId = summaryModal.getEventId();
         var stringsPromise = Str.get_strings(deleteStrings);
-        var deletePromise = ModalFactory.create(
-            {
-                type: ModalFactory.types.SAVE_CANCEL
-            },
-            summaryModal.getDeleteButton()
-        );
 
-        $.when(stringsPromise, deletePromise).then(function(strings, deleteModal) {
+        $.when(stringsPromise, deletePromise)
+        .then(function(strings, deleteModal) {
             deleteModal.setTitle(strings[0]);
             deleteModal.setBody(strings[1]);
-            deleteModal.setSaveButtonText(strings[0]);
+            if (!isRepeatedEvent) {
+                deleteModal.setSaveButtonText(strings[0]);
+            }
+
             deleteModal.getRoot().on(ModalEvents.save, function() {
-                CalendarRepository.deleteEvent(eventId).then(function() {
-                    $('body').trigger(CalendarEvents.deleted, [eventId]);
-                    summaryModal.hide();
-                    return;
-                }).catch(Notification.exception);
+                CalendarRepository.deleteEvent(eventId, false)
+                    .then(function() {
+                        $('body').trigger(CalendarEvents.deleted, [eventId, false]);
+                        summaryModal.hide();
+                        return;
+                    })
+                    .catch(Notification.exception);
             });
+
+            deleteModal.getRoot().on(CalendarEvents.deleteAll, function() {
+                CalendarRepository.deleteEvent(eventId, true)
+                    .then(function() {
+                        $('body').trigger(CalendarEvents.deleted, [eventId, true]);
+                        summaryModal.hide();
+                        return;
+                    })
+                    .catch(Notification.exception);
+            });
+
             return deleteModal;
-        }).fail(Notification.exception);
+        })
+        .fail(Notification.exception);
     }
 
     // Automatically register with the modal registry the first time this module is imported so that you can create modals
index f8636ef..9470a10 100644 (file)
@@ -42,7 +42,7 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
             root.on('click', SELECTORS.CALENDAR_NAV_LINK, function(e) {
                 var courseId = $(root).find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('courseid');
                 var link = $(e.currentTarget);
-                changeMonth(root, link.attr('href'), link.data('time'), courseId);
+                changeMonth(root, link.attr('href'), link.data('year'), link.data('month'), courseId);
 
                 e.preventDefault();
             });
@@ -51,19 +51,25 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
         /**
          * Refresh the month content.
          *
-         * @param {Number} time The calendar time to be shown
+         * @param {object} root The root element.
+         * @param {Number} year Year
+         * @param {Number} month Month
          * @param {Number} courseid The id of the course whose events are shown
+         * @param {object} target The element being replaced. If not specified, the calendarwrapper is used.
          * @return {promise}
          */
-        var refreshMonthContent = function(root, time, courseid) {
+        var refreshMonthContent = function(root, year, month, courseid, target) {
             startLoading(root);
 
-            return CalendarRepository.getCalendarMonthData(time, courseid)
+            target = target || root.find(SELECTORS.CALENDAR_MONTH_WRAPPER);
+
+            var includenavigation = root.data('includenavigation');
+            return CalendarRepository.getCalendarMonthData(year, month, courseid, includenavigation)
                 .then(function(context) {
                     return Templates.render(root.attr('data-template'), context);
                 })
                 .then(function(html, js) {
-                    return Templates.replaceNode(root.find(SELECTORS.CALENDAR_MONTH_WRAPPER), html, js);
+                    return Templates.replaceNode(target, html, js);
                 })
                 .then(function() {
                     $('body').trigger(CalendarEvents.viewUpdated);
@@ -78,13 +84,15 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
         /**
          * Handle changes to the current calendar view.
          *
+         * @param {object} root The root element.
          * @param {String} url The calendar url to be shown
-         * @param {Number} time The calendar time to be shown
+         * @param {Number} year Year
+         * @param {Number} month Month
          * @param {Number} courseid The id of the course whose events are shown
          * @return {promise}
          */
-        var changeMonth = function(root, url, time, courseid) {
-            return refreshMonthContent(root, time, courseid)
+        var changeMonth = function(root, url, year, month, courseid) {
+            return refreshMonthContent(root, year, month, courseid)
                 .then(function() {
                     if (url.length && url !== '#') {
                         window.history.pushState({}, '', url);
@@ -92,7 +100,7 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
                     return arguments;
                 })
                 .then(function() {
-                    $('body').trigger(CalendarEvents.monthChanged, [time, courseid]);
+                    $('body').trigger(CalendarEvents.monthChanged, [year, month, courseid]);
                     return arguments;
                 });
         };
@@ -105,12 +113,13 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
          * @return {promise}
          */
         var reloadCurrentMonth = function(root, courseId) {
-            var time = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('current-time');
+            var year = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('year');
+            var month = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('month');
 
             if (!courseId) {
                 courseId = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('courseid');
             }
-            return refreshMonthContent(root, time, courseId);
+            return refreshMonthContent(root, year, month, courseId);
         };
 
         /**
index f83f273..f20c7f7 100644 (file)
@@ -28,7 +28,7 @@ defined('MOODLE_INTERNAL') || die();
 
 use \core_course\external\course_summary_exporter;
 use \renderer_base;
-
+require_once($CFG->dirroot . '/course/lib.php');
 /**
  * Class for displaying a calendar event.
  *
@@ -71,12 +71,26 @@ class calendar_event_exporter extends event_exporter_base {
         global $CFG;
 
         $values = parent::get_other_values($output);
+        $event = $this->event;
+
+        if ($moduleproxy = $event->get_course_module()) {
+            $modulename = $moduleproxy->get('modname');
+            $moduleid = $moduleproxy->get('id');
+            $url = new \moodle_url(sprintf('/mod/%s/view.php', $modulename), ['id' => $moduleid]);
+
+            // Build edit event url for action events.
+            $params = array('update' => $moduleid, 'return' => true, 'sesskey' => sesskey());
+            $editurl = new \moodle_url('/course/mod.php', $params);
+            $values['editurl'] = $editurl->out(false);
+        } else if ($event->get_type() == 'course') {
+            $url = course_get_url($event->get_course()->get('id') ?: SITEID);
+        } else {
+            // TODO MDL-58866 We do not have any way to find urls for events outside of course modules.
+            $course = $event->get_course()->get('id') ?: SITEID;
 
-        $eventid = $this->event->get_id();
-
-        $url = new \moodle_url($this->related['daylink'], [], "event_{$eventid}");
+            $url = course_get_url($course);
+        }
         $values['url'] = $url->out(false);
-
         $values['islastday'] = false;
         $today = $this->related['type']->timestamp_to_date_array($this->related['today']);
 
diff --git a/calendar/classes/external/date_exporter.php b/calendar/classes/external/date_exporter.php
new file mode 100644 (file)
index 0000000..4615b6c
--- /dev/null
@@ -0,0 +1,92 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class for normalising the date data.
+ *
+ * @package   core_calendar
+ * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_calendar\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\external\exporter;
+use renderer_base;
+use moodle_url;
+
+/**
+ * Class for normalising the date data.
+ *
+ * @package   core_calendar
+ * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class date_exporter extends exporter {
+
+    /**
+     * Constructor for date_exporter.
+     *
+     * @param array $data
+     * @param array $related The related information
+     */
+    public function __construct($data, $related = []) {
+        $data['timestamp'] = $data[0];
+        unset($data[0]);
+
+        parent::__construct($data, $related);
+    }
+
+    protected static function define_properties() {
+        return [
+            'seconds' => [
+                'type' => PARAM_INT,
+            ],
+            'minutes' => [
+                'type' => PARAM_INT,
+            ],
+            'hours' => [
+                'type' => PARAM_INT,
+            ],
+            'mday' => [
+                'type' => PARAM_INT,
+            ],
+            'wday' => [
+                'type' => PARAM_INT,
+            ],
+            'mon' => [
+                'type' => PARAM_INT,
+            ],
+            'year' => [
+                'type' => PARAM_INT,
+            ],
+            'yday' => [
+                'type' => PARAM_INT,
+            ],
+            'weekday' => [
+                'type' => PARAM_RAW,
+            ],
+            'month' => [
+                'type' => PARAM_RAW,
+            ],
+            'timestamp' => [
+                'type' => PARAM_INT,
+            ],
+        ];
+    }
+}
index 0f5b92b..2965451 100644 (file)
@@ -26,6 +26,8 @@ namespace core_calendar\external;
 
 defined('MOODLE_INTERNAL') || die();
 
+require_once($CFG->dirroot . '/calendar/lib.php');
+
 use core\external\exporter;
 use renderer_base;
 use moodle_url;
@@ -44,6 +46,10 @@ class day_exporter extends exporter {
      */
     protected $calendar;
 
+    /**
+     * @var moodle_url
+     */
+    protected $url;
     /**
      * Constructor.
      *
@@ -53,7 +59,11 @@ class day_exporter extends exporter {
      */
     public function __construct(\calendar_information $calendar, $data, $related) {
         $this->calendar = $calendar;
-
+        $this->url = new moodle_url('/calendar/view.php', [
+            'view' => 'day',
+            'time' => $calendar->time,
+            'course' => $this->calendar->course->id,
+        ]);
         parent::__construct($data, $related);
     }
 
@@ -87,15 +97,6 @@ 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,
-            ],
         ];
     }
 
@@ -124,6 +125,15 @@ class day_exporter extends exporter {
                 'type' => PARAM_RAW,
                 'multiple' => true,
             ],
+            'previousperiod' => [
+                'type' => PARAM_INT,
+            ],
+            'nextperiod' => [
+                'type' => PARAM_INT,
+            ],
+            'navigation' => [
+                'type' => PARAM_RAW,
+            ],
             'popovertitle' => [
                 'type' => PARAM_RAW,
                 'default' => '',
@@ -132,6 +142,12 @@ class day_exporter extends exporter {
                 'type' => PARAM_BOOL,
                 'default' => false,
             ],
+            'filter_selector' => [
+                'type' => PARAM_RAW,
+            ],
+            'new_event_button' => [
+                'type' => PARAM_RAW,
+            ],
         ];
     }
 
@@ -142,6 +158,7 @@ class day_exporter extends exporter {
      * @return array Keys are the property names, values are their values.
      */
     protected function get_other_values(renderer_base $output) {
+        $daytimestamp = $this->calendar->time;
         $timestamp = $this->data[0];
         // Need to account for user's timezone.
         $usernow = usergetdate(time());
@@ -156,24 +173,24 @@ class day_exporter extends exporter {
 
         $return = [
             'timestamp' => $timestamp,
-            'neweventtimestamp' => $neweventstarttime->getTimestamp()
+            'neweventtimestamp' => $neweventstarttime->getTimestamp(),
+            'previousperiod' => $this->get_previous_day_timestamp($daytimestamp),
+            'nextperiod' => $this->get_next_day_timestamp($daytimestamp),
+            'navigation' => $this->get_navigation(),
+            'filter_selector' => $this->get_course_filter_selector($output),
+            'new_event_button' => $this->get_new_event_button(),
         ];
 
-        $url = new moodle_url('/calendar/view.php', [
-                'view' => 'day',
-                'time' => $timestamp,
-                'course' => $this->calendar->course->id,
-            ]);
-        $return['viewdaylink'] = $url->out(false);
+        $return['viewdaylink'] = $this->url->out(false);
 
         $cache = $this->related['cache'];
-        $eventexporters = array_map(function($event) use ($cache, $output, $url) {
+        $eventexporters = array_map(function($event) use ($cache, $output) {
             $context = $cache->get_context($event);
             $course = $cache->get_course($event);
             $exporter = new calendar_event_exporter($event, [
                 'context' => $context,
                 'course' => $course,
-                'daylink' => $url,
+                'daylink' => $this->url,
                 'type' => $this->related['type'],
                 'today' => $this->data[0],
             ]);
@@ -185,10 +202,6 @@ class day_exporter extends exporter {
             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);
@@ -219,24 +232,112 @@ class day_exporter extends exporter {
     }
 
     /**
-     * Get the title for this popover.
+     * Get the previous day timestamp.
+     *
+     * @param int $daytimestamp The current day timestamp.
+     * @return int The previous day timestamp.
+     */
+    protected function get_previous_day_timestamp($daytimestamp) {
+        return $this->related['type']->get_prev_day($daytimestamp);
+    }
+
+    /**
+     * Get the next day timestamp.
+     *
+     * @param int $daytimestamp The current day timestamp.
+     * @return int The next day timestamp.
+     */
+    protected function get_next_day_timestamp($daytimestamp) {
+        return $this->related['type']->get_next_day($daytimestamp);
+    }
+
+    /**
+     * Get the calendar navigation controls.
+     *
+     * @return string The html code to the calendar top navigation.
+     */
+    protected function get_navigation() {
+        return calendar_top_controls('day', [
+            'id' => $this->calendar->courseid,
+            'time' => $this->calendar->time,
+        ]);
+    }
+
+    /**
+     * Get the course filter selector.
+     *
+     * This is a temporary solution, this code will be removed by MDL-60096.
      *
-     * @return string
+     * @param renderer_base $output
+     * @return string The html code for the course filter selector.
      */
-    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;
+    protected function get_course_filter_selector(renderer_base $output) {
+        global $CFG;
+        // TODO remove this code on MDL-60096.
+        if (!isloggedin() or isguestuser()) {
+            return '';
         }
 
-        if ($this->data['istoday']) {
-            $title = get_string('todayplustitle', 'calendar', $userdate);
+        if (has_capability('moodle/calendar:manageentries', \context_system::instance()) && !empty($CFG->calendar_adminseesall)) {
+            $courses = get_courses('all', 'c.shortname', 'c.id, c.shortname');
+        } else {
+            $courses = enrol_get_my_courses();
         }
 
-        return $title;
+        unset($courses[SITEID]);
+
+        $courseoptions = array();
+        $courseoptions[SITEID] = get_string('fulllistofcourses');
+        foreach ($courses as $course) {
+            $coursecontext = \context_course::instance($course->id);
+            $courseoptions[$course->id] = format_string($course->shortname, true, array('context' => $coursecontext));
+        }
+
+        if ($this->calendar->courseid !== SITEID) {
+            $selected = $this->calendar->courseid;
+        } else {
+            $selected = '';
+        }
+
+        $courseurl = new moodle_url($this->url);
+        $courseurl->remove_params('course');
+        $select = new \single_select($courseurl, 'courseselect', $courseoptions, $selected, null);
+        $select->class = 'm-r-1';
+        $label = get_string('dayviewfor', 'calendar');
+        if ($label !== null) {
+            $select->set_label($label);
+        } else {
+            $select->set_label(get_string('listofcourses'), array('class' => 'accesshide'));
+        }
+
+        return $output->render($select);
+    }
+
+    /**
+     * Get the course filter selector.
+     *
+     * This is a temporary solution, this code will be removed by MDL-60096.
+     *
+     * @return string The html code for the course filter selector.
+     */
+    protected function get_new_event_button() {
+        // TODO remove this code on MDL-60096.
+        $output = \html_writer::start_tag('div', array('class' => 'buttons'));
+        $output .= \html_writer::start_tag('form',
+                array('action' => CALENDAR_URL . 'event.php', 'method' => 'get'));
+        $output .= \html_writer::start_tag('div');
+        $output .= \html_writer::empty_tag('input',
+                array('type' => 'hidden', 'name' => 'action', 'value' => 'new'));
+        $output .= \html_writer::empty_tag('input',
+                array('type' => 'hidden', 'name' => 'course', 'value' => $this->calendar->courseid));
+        $output .= \html_writer::empty_tag('input',
+                array('type' => 'hidden', 'name' => 'time', 'value' => $this->calendar->time));
+        $attributes = array('type' => 'submit', 'value' => get_string('newevent', 'calendar'),
+            'class' => 'btn btn-secondary');
+        $output .= \html_writer::empty_tag('input', $attributes);
+        $output .= \html_writer::end_tag('div');
+        $output .= \html_writer::end_tag('form');
+        $output .= \html_writer::end_tag('div');
+        return $output;
     }
 }
index 917f07e..2642423 100644 (file)
@@ -50,17 +50,6 @@ class event_exporter extends event_exporter_base {
     protected static function define_other_properties() {
 
         $values = parent::define_other_properties();
-
-        $values['displayeventsource'] = ['type' => PARAM_BOOL];
-        $values['subscription'] = [
-            'type' => PARAM_RAW,
-            'optional' => true,
-            'default' => null,
-            'null' => NULL_ALLOWED
-        ];
-        $values['isactionevent'] = ['type' => PARAM_BOOL];
-        $values['iscourseevent'] = ['type' => PARAM_BOOL];
-        $values['candelete'] = ['type' => PARAM_BOOL];
         $values['url'] = ['type' => PARAM_URL];
         $values['action'] = [
             'type' => event_action_exporter::read_properties_definition(),
@@ -70,12 +59,6 @@ class event_exporter extends event_exporter_base {
             'type' => PARAM_URL,
             'optional' => true,
         ];
-        $values['groupname'] = [
-            'type' => PARAM_RAW,
-            'optional' => true,
-            'default' => null,
-            'null' => NULL_ALLOWED
-        ];
 
         return $values;
     }
@@ -94,21 +77,16 @@ class event_exporter extends event_exporter_base {
 
         $event = $this->event;
         $context = $this->related['context'];
-        $values['isactionevent'] = false;
-        $values['iscourseevent'] = false;
         if ($moduleproxy = $event->get_course_module()) {
             $modulename = $moduleproxy->get('modname');
             $moduleid = $moduleproxy->get('id');
             $url = new \moodle_url(sprintf('/mod/%s/view.php', $modulename), ['id' => $moduleid]);
 
-            $values['isactionevent'] = true;
-
             // Build edit event url for action events.
             $params = array('update' => $moduleid, 'return' => true, 'sesskey' => sesskey());
             $editurl = new \moodle_url('/course/mod.php', $params);
             $values['editurl'] = $editurl->out(false);
         } else if ($event->get_type() == 'course') {
-            $values['iscourseevent'] = true;
             $url = \course_get_url($this->related['course'] ?: SITEID);
         } else {
             // TODO MDL-58866 We do not have any way to find urls for events outside of course modules.
@@ -125,31 +103,7 @@ class event_exporter extends event_exporter_base {
             $values['action'] = $actionexporter->export($output);
         }
 
-        if ($course = $this->related['course']) {
-            $coursesummaryexporter = new course_summary_exporter($course, ['context' => $context]);
-            $values['course'] = $coursesummaryexporter->export($output);
-        }
 
-        // Handle event subscription.
-        $values['subscription'] = null;
-        $values['displayeventsource'] = false;
-        if ($event->get_subscription()) {
-            $subscription = calendar_get_subscription($event->get_subscription()->get('id'));
-            if (!empty($subscription) && $CFG->calendar_showicalsource) {
-                $values['displayeventsource'] = true;
-                $subscriptiondata = new \stdClass();
-                if (!empty($subscription->url)) {
-                    $subscriptiondata->url = $subscription->url;
-                }
-                $subscriptiondata->name = $subscription->name;
-                $values['subscription'] = json_encode($subscriptiondata);
-            }
-        }
-
-        if ($group = $event->get_group()) {
-            $values['groupname'] = format_string($group->get('name'), true,
-                ['context' => \context_course::instance($event->get_course()->get('id'))]);
-        }
 
         return $values;
     }
index 813cb58..cb3ba80 100644 (file)
@@ -35,6 +35,7 @@ use \core_calendar\local\event\entities\event_interface;
 use \core_calendar\local\event\entities\action_event_interface;
 use \core_course\external\course_summary_exporter;
 use \renderer_base;
+use moodle_url;
 
 /**
  * Class for displaying a calendar event.
@@ -87,6 +88,7 @@ class event_exporter_base extends exporter {
 
         if ($repeats = $event->get_repeats()) {
             $data->repeatid = $repeats->get_id();
+            $data->eventcount = $repeats->get_num() + 1;
         }
 
         if ($cm = $event->get_course_module()) {
@@ -136,6 +138,12 @@ class event_exporter_base extends exporter {
                 'default' => null,
                 'null' => NULL_ALLOWED
             ],
+            'eventcount' => [
+                'type' => PARAM_INT,
+                'optional' => true,
+                'default' => null,
+                'null' => NULL_ALLOWED
+            ],
             'modulename' => [
                 'type' => PARAM_TEXT,
                 'optional' => true,
@@ -171,12 +179,37 @@ class event_exporter_base extends exporter {
                 'type' => course_summary_exporter::read_properties_definition(),
                 'optional' => true,
             ],
+            'subscription' => [
+                'type' => event_subscription_exporter::read_properties_definition(),
+                'optional' => true,
+            ],
             'canedit' => [
                 'type' => PARAM_BOOL
             ],
             'candelete' => [
                 'type' => PARAM_BOOL
             ],
+            'deleteurl' => [
+                'type' => PARAM_URL
+            ],
+            'editurl' => [
+                'type' => PARAM_URL
+            ],
+            'formattedtime' => [
+                'type' => PARAM_RAW,
+            ],
+            'isactionevent' => [
+                'type' => PARAM_BOOL
+            ],
+            'iscourseevent' => [
+                'type' => PARAM_BOOL
+            ],
+            'groupname' => [
+                'type' => PARAM_RAW,
+                'optional' => true,
+                'default' => null,
+                'null' => NULL_ALLOWED
+            ],
         ];
     }
 
@@ -191,19 +224,48 @@ class event_exporter_base extends exporter {
         $event = $this->event;
         $legacyevent = container::get_event_mapper()->from_event_to_legacy_event($event);
         $context = $this->related['context'];
+        $values['isactionevent'] = false;
+        $values['iscourseevent'] = false;
+        if ($moduleproxy = $event->get_course_module()) {
+            $values['isactionevent'] = true;
+        } else if ($event->get_type() == 'course') {
+            $values['iscourseevent'] = true;
+        }
         $timesort = $event->get_times()->get_sort_time()->getTimestamp();
         $iconexporter = new event_icon_exporter($event, ['context' => $context]);
 
         $values['icon'] = $iconexporter->export($output);
 
+        $subscriptionexporter = new event_subscription_exporter($event);
+        $values['subscription'] = $subscriptionexporter->export($output);
+
         if ($course = $this->related['course']) {
             $coursesummaryexporter = new course_summary_exporter($course, ['context' => $context]);
             $values['course'] = $coursesummaryexporter->export($output);
         }
+        $courseid = (!$course) ? SITEID : $course->id;
 
         $values['canedit'] = calendar_edit_event_allowed($legacyevent, true);
         $values['candelete'] = calendar_delete_event_allowed($legacyevent);
 
+        $deleteurl = new moodle_url('/calendar/delete.php', ['id' => $event->get_id(), 'course' => $courseid]);
+        $values['deleteurl'] = $deleteurl->out(false);
+
+        $editurl = new moodle_url('/calendar/event.php', ['action' => 'edit', 'id' => $event->get_id(),
+                'course' => $courseid]);
+        $values['editurl'] = $editurl->out(false);
+        $values['formattedtime'] = calendar_format_event_time($legacyevent, time(), null, false,
+                $timesort);
+
+        if ($course = $this->related['course']) {
+            $coursesummaryexporter = new course_summary_exporter($course, ['context' => $context]);
+            $values['course'] = $coursesummaryexporter->export($output);
+        }
+
+        if ($group = $event->get_group()) {
+            $values['groupname'] = format_string($group->get('name'), true,
+                ['context' => \context_course::instance($event->get_course()->get('id'))]);
+        }
         return $values;
     }
 
diff --git a/calendar/classes/external/event_subscription_exporter.php b/calendar/classes/external/event_subscription_exporter.php
new file mode 100644 (file)
index 0000000..5a8075b
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains event class for displaying a calendar event's subscription.
+ *
+ * @package   core_calendar
+ * @copyright 2017 Simey Lameze <simey@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_calendar\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+use \core\external\exporter;
+use \core_calendar\local\event\entities\event_interface;
+
+/**
+ * Class for displaying a calendar event's subscription.
+ *
+ * @package   core_calendar
+ * @copyright 2017 Simey Lameze <simey@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class event_subscription_exporter extends exporter {
+
+    /**
+     * Constructor.
+     *
+     * @param event_interface $event
+     */
+    public function __construct(event_interface $event) {
+        global $CFG;
+
+        $data = new \stdClass();
+        $data->displayeventsource = false;
+        if ($event->get_subscription()) {
+            $subscription = calendar_get_subscription($event->get_subscription()->get('id'));
+            if (!empty($subscription) && $CFG->calendar_showicalsource) {
+                $data->displayeventsource = true;
+                if (!empty($subscription->url)) {
+                    $data->url = $subscription->url;
+                }
+                $data->name = $subscription->name;
+            }
+        }
+
+        parent::__construct($data);
+    }
+
+    /**
+     * Return the list of properties.
+     *
+     * @return array
+     */
+    protected static function define_properties() {
+        return [
+            'displayeventsource' => [
+                'type' => PARAM_BOOL
+            ],
+            'name' => [
+                'type' => PARAM_RAW,
+                'optional' => true
+            ],
+            'url' => [
+                'type' => PARAM_URL,
+                'optional' => true
+            ],
+        ];
+    }
+}
index 23bb42f..e4478bb 100644 (file)
@@ -82,9 +82,10 @@ class footer_options_exporter extends exporter {
      * @return string The iCal url.
      */
     protected function get_ical_url() {
-        return new moodle_url('/calendar/export_execute.php', ['preset_what' => 'all',
-                'preset_time' => 'recentupcoming', 'userid' => $this->userid, 'authtoken' => $this->token]);
-
+        if ($this->token) {
+            return new moodle_url('/calendar/export_execute.php', ['preset_what' => 'all',
+                    'preset_time' => 'recentupcoming', 'userid' => $this->userid, 'authtoken' => $this->token]);
+        }
     }
 
     /**
@@ -113,10 +114,12 @@ class footer_options_exporter extends exporter {
         $values = new stdClass();
 
         if (!empty($CFG->enablecalendarexport)) {
-            $exportbutton = $this->get_export_calendar_button();
-            $managesubscriptionbutton = $this->get_manage_subscriptions_button();
-            $values->exportcalendarbutton = $exportbutton->export_for_template($output);
-            $values->managesubscriptionbutton = $managesubscriptionbutton->export_for_template($output);
+            if ($exportbutton = $this->get_export_calendar_button()) {
+                $values->exportcalendarbutton = $exportbutton->export_for_template($output);
+            }
+            if ($managesubscriptionbutton = $this->get_manage_subscriptions_button()) {
+                $values->managesubscriptionbutton = $managesubscriptionbutton->export_for_template($output);
+            }
             $values->icalurl = $this->get_ical_url()->out(false);
         }
 
@@ -132,12 +135,15 @@ class footer_options_exporter extends exporter {
         return array(
             'exportcalendarbutton' => [
                 'type' => PARAM_RAW,
+                'default' => null,
             ],
             'managesubscriptionbutton' => [
                 'type' => PARAM_RAW,
+                'default' => null,
             ],
             'icalurl' => [
                 'type' => PARAM_URL,
+                'default' => null,
             ],
         );
     }
index 6d180aa..bb59946 100644 (file)
@@ -54,6 +54,11 @@ class month_exporter extends exporter {
      */
     protected $url;
 
+    /**
+     * @var bool $includenavigation Whether navigation should be included on the output.
+     */
+    protected $includenavigation = true;
+
     /**
      * Constructor for month_exporter.
      *
@@ -104,9 +109,6 @@ class month_exporter extends exporter {
             'filter_selector' => [
                 'type' => PARAM_RAW,
             ],
-            'navigation' => [
-                'type' => PARAM_RAW,
-            ],
             'weeks' => [
                 'type' => week_exporter::read_properties_definition(),
                 'multiple' => true,
@@ -118,16 +120,23 @@ class month_exporter extends exporter {
             'view' => [
                 'type' => PARAM_ALPHA,
             ],
-            'time' => [
-                'type' => PARAM_INT,
+            'date' => [
+                'type' => date_exporter::read_properties_definition(),
             ],
             'periodname' => [
                 // Note: We must use RAW here because the calendar type returns the formatted month name based on a
                 // calendar format.
                 'type' => PARAM_RAW,
             ],
+            'includenavigation' => [
+                'type' => PARAM_BOOL,
+                'default' => true,
+            ],
             'previousperiod' => [
-                'type' => PARAM_INT,
+                'type' => date_exporter::read_properties_definition(),
+            ],
+            'previousperiodlink' => [
+                'type' => PARAM_URL,
             ],
             'previousperiodname' => [
                 // Note: We must use RAW here because the calendar type returns the formatted month name based on a
@@ -135,13 +144,16 @@ class month_exporter extends exporter {
                 'type' => PARAM_RAW,
             ],
             'nextperiod' => [
-                'type' => PARAM_INT,
+                'type' => date_exporter::read_properties_definition(),
             ],
             'nextperiodname' => [
                 // Note: We must use RAW here because the calendar type returns the formatted month name based on a
                 // calendar format.
                 'type' => PARAM_RAW,
             ],
+            'nextperiodlink' => [
+                'type' => PARAM_URL,
+            ],
             'larrow' => [
                 // The left arrow defined by the theme.
                 'type' => PARAM_RAW,
@@ -160,24 +172,33 @@ 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();
+        $previousperiod = $this->get_previous_month_data();
+        $nextperiod = $this->get_next_month_data();
+        $date = $this->related['type']->timestamp_to_date_array($this->calendar->time);
+
+        $nextperiodlink = new moodle_url($this->url);
+        $nextperiodlink->param('time', $nextperiod[0]);
+
+        $previousperiodlink = new moodle_url($this->url);
+        $previousperiodlink->param('time', $previousperiod[0]);
 
         return [
             'courseid' => $this->calendar->courseid,
             'filter_selector' => $this->get_course_filter_selector($output),
-            'navigation' => $this->get_navigation($output),
             'weeks' => $this->get_weeks($output),
             'daynames' => $this->get_day_names($output),
             'view' => 'month',
-            'time' => $this->calendar->time,
+            'date' => (new date_exporter($date))->export($output),
             'periodname' => userdate($this->calendar->time, get_string('strftimemonthyear')),
-            'previousperiod' => $previousperiod,
-            'previousperiodname' => userdate($previousperiod, get_string('strftimemonthyear')),
-            'nextperiod' => $nextperiod,
-            'nextperiodname' => userdate($nextperiod, get_string('strftimemonthyear')),
+            'previousperiod' => (new date_exporter($previousperiod))->export($output),
+            'previousperiodname' => userdate($previousperiod[0], get_string('strftimemonthyear')),
+            'previousperiodlink' => $previousperiodlink->out(false),
+            'nextperiod' => (new date_exporter($nextperiod))->export($output),
+            'nextperiodname' => userdate($nextperiod[0], get_string('strftimemonthyear')),
+            'nextperiodlink' => $nextperiodlink->out(false),
             'larrow' => $output->larrow(),
             'rarrow' => $output->rarrow(),
+            'includenavigation' => $this->includenavigation,
         ];
     }
 
@@ -197,19 +218,6 @@ class month_exporter extends exporter {
         return $content;
     }
 
-    /**
-     * Get the calendar navigation controls.
-     *
-     * @param renderer_base $output
-     * @return string The html code to the calendar top navigation.
-     */
-    protected function get_navigation(renderer_base $output) {
-        return calendar_top_controls('month', [
-            'id' => $this->calendar->courseid,
-            'time' => $this->calendar->time,
-        ]);
-    }
-
     /**
      * Get the list of day names for display, re-ordered from the first day
      * of the week.
@@ -302,17 +310,30 @@ class month_exporter extends exporter {
         ];
     }
 
+    /**
+     * Get the current month timestamp.
+     *
+     * @return int The month timestamp.
+     */
+    protected function get_month_data() {
+        $date = $this->related['type']->timestamp_to_date_array($this->calendar->time);
+        $monthtime = $this->related['type']->convert_to_gregorian($date['year'], $date['month'], 1);
+
+        return make_timestamp($monthtime['year'], $monthtime['month']);
+    }
+
     /**
      * Get the previous month timestamp.
      *
      * @return int The previous month timestamp.
      */
-    protected function get_previous_month_timestamp() {
-        $date = $this->related['type']->timestamp_to_date_array($this->calendar->time);
-        $month = calendar_sub_month($date['mon'], $date['year']);
-        $monthtime = $this->related['type']->convert_to_gregorian($month[1], $month[0], 1);
+    protected function get_previous_month_data() {
+        $type = $this->related['type'];
+        $date = $type->timestamp_to_date_array($this->calendar->time);
+        list($date['mon'], $date['year']) = $type->get_prev_month($date['year'], $date['mon']);
+        $time = $type->convert_to_timestamp($date['year'], $date['mon'], 1);
 
-        return make_timestamp($monthtime['year'], $monthtime['month'], $monthtime['day'], $monthtime['hour'], $monthtime['minute']);
+        return $type->timestamp_to_date_array($time);
     }
 
     /**
@@ -320,11 +341,24 @@ class month_exporter extends exporter {
      *
      * @return int The next month timestamp.
      */
-    protected function get_next_month_timestamp() {
-        $date = $this->related['type']->timestamp_to_date_array($this->calendar->time);
-        $month = calendar_add_month($date['mon'], $date['year']);
-        $monthtime = $this->related['type']->convert_to_gregorian($month[1], $month[0], 1);
+    protected function get_next_month_data() {
+        $type = $this->related['type'];
+        $date = $type->timestamp_to_date_array($this->calendar->time);
+        list($date['mon'], $date['year']) = $type->get_next_month($date['year'], $date['mon']);
+        $time = $type->convert_to_timestamp($date['year'], $date['mon'], 1);
+
+        return $type->timestamp_to_date_array($time);
+    }
+
+    /**
+     * Set whether the navigation should be shown.
+     *
+     * @param   bool    $include
+     * @return  $this
+     */
+    public function set_includenavigation($include) {
+        $this->includenavigation = $include;
 
-        return make_timestamp($monthtime['year'], $monthtime['month'], $monthtime['day'], $monthtime['hour'], $monthtime['minute']);
+        return $this;
     }
 }
index 67442f5..e974154 100644 (file)
@@ -38,6 +38,27 @@ use moodle_url;
  */
 class week_day_exporter extends day_exporter {
 
+    /**
+     * Return the list of properties.
+     *
+     * @return array
+     */
+    protected static function define_properties() {
+        $return = parent::define_properties();
+        $return = array_merge($return, [
+            // These are additional params.
+            'istoday' => [
+                'type' => PARAM_BOOL,
+                'default' => false,
+            ],
+            'isweekend' => [
+                'type' => PARAM_BOOL,
+                'default' => false,
+            ],
+        ]);
+
+        return $return;
+    }
     /**
      * Return the list of additional properties.
      *
@@ -92,18 +113,18 @@ class week_day_exporter extends day_exporter {
             $usernow['seconds']
         );
 
-        $return = [
-            'timestamp' => $timestamp,
-            'neweventtimestamp' => $neweventstarttime->getTimestamp()
-        ];
+        $return = parent::get_other_values($output);
 
         $url = new moodle_url('/calendar/view.php', [
                 'view' => 'day',
                 'time' => $timestamp,
                 'course' => $this->calendar->course->id,
-            ]);
-        $return['viewdaylink'] = $url->out(false);
+        ]);
 
+        $return['viewdaylink'] = $url->out(false);
+        if ($popovertitle = $this->get_popover_title()) {
+            $return['popovertitle'] = $popovertitle;
+        }
         $cache = $this->related['cache'];
         $eventexporters = array_map(function($event) use ($cache, $output, $url) {
             $context = $cache->get_context($event);
index 5c5d7d6..0ba6d54 100644 (file)
@@ -231,15 +231,47 @@ class api {
     ) {
         $mapper = container::get_event_mapper();
         $legacyevent = $mapper->from_event_to_legacy_event($event);
+        $hascoursemodule = !empty($event->get_course_module());
         $starttime = $event->get_times()->get_start_time()->setDate(
             $startdate->format('Y'),
             $startdate->format('n'),
             $startdate->format('j')
         );
 
+        if ($hascoursemodule) {
+            $legacyevent->timestart = $starttime->getTimestamp();
+            // If this event is from an activity then we need to call
+            // the activity callback to let it validate that the changes
+            // to the event are correct.
+            component_callback(
+                'mod_' . $event->get_course_module()->get('modname'),
+                'core_calendar_validate_event_timestart',
+                [$legacyevent]
+            );
+        }
+
         // This function does our capability checks.
         $legacyevent->update((object) ['timestart' => $starttime->getTimestamp()]);
 
+        // Check that the user is allowed to manually edit calendar events before
+        // calling the event updated callback. The manual flag causes the code to
+        // check the user has the capabilities to modify the modules.
+        //
+        // We don't want to call the event update callback if the user isn't allowed
+        // to modify course modules because depending on the callback it can make
+        // some changes that would be considered security issues, such as updating the
+        // due date for and assignment.
+        if ($hascoursemodule && calendar_edit_event_allowed($legacyevent, true)) {
+            // If this event is from an activity then we need to call
+            // the activity callback to let it know that the event it
+            // created has been modified so it needs to update accordingly.
+            component_callback(
+                'mod_' . $event->get_course_module()->get('modname'),
+                'core_calendar_event_timestart_updated',
+                [$legacyevent]
+            );
+        }
+
         return $mapper->from_legacy_event_to_event($legacyevent);
     }
 }
index a211be1..75e4bad 100644 (file)
@@ -239,4 +239,32 @@ abstract class type_base {
             $gregorianinfo['minute'],
             0);
     }
+
+    /**
+     * Get the previous day.
+     *
+     * @param int $daytimestamp The day timestamp.
+     * @return int previous day timestamp
+     */
+    public function get_prev_day($daytimestamp) {
+        $date = new \DateTime();
+        $date->setTimestamp($daytimestamp);
+        $date->modify('-1 day');
+
+        return $date->getTimestamp();
+    }
+
+    /**
+     * Get the next day.
+     *
+     * @param int $daytimestamp The day timestamp.
+     * @return int the following day
+     */
+    public function get_next_day($daytimestamp) {
+        $date = new \DateTime();
+        $date->setTimestamp($daytimestamp);
+        $date->modify('+1 day');
+
+        return $date->getTimestamp();
+    }
 }
index afad7dd..336c0f0 100644 (file)
@@ -879,18 +879,22 @@ class core_calendar_external extends external_api {
     /**
      * Get data for the monthly calendar view.
      *
-     * @param   int     $time The time to be shown
+     * @param   int     $year The year to be shown
+     * @param   int     $month The month to be shown
      * @param   int     $courseid The course to be included
+     * @param   bool    $includenavigation Whether to include navigation
      * @return  array
      */
-    public static function get_calendar_monthly_view($time, $courseid) {
+    public static function get_calendar_monthly_view($year, $month, $courseid, $includenavigation) {
         global $CFG, $DB, $USER, $PAGE;
         require_once($CFG->dirroot."/calendar/lib.php");
 
         // Parameter validation.
         $params = self::validate_parameters(self::get_calendar_monthly_view_parameters(), [
-            'time' => $time,
+            'year' => $year,
+            'month' => $month,
             'courseid' => $courseid,
+            'includenavigation' => $includenavigation,
         ]);
 
         if ($courseid != SITEID && !empty($courseid)) {
@@ -906,10 +910,13 @@ class core_calendar_external extends external_api {
         $context = \context_user::instance($USER->id);
         self::validate_context($context);
 
+        $type = \core_calendar\type_factory::get_calendar_instance();
+
+        $time = $type->convert_to_timestamp($year, $month, 1);
         $calendar = new calendar_information(0, 0, 0, $time);
         $calendar->prepare_for_view($course, $courses);
 
-        list($data, $template) = calendar_get_view($calendar, 'month');
+        list($data, $template) = calendar_get_view($calendar, 'month', $params['includenavigation']);
 
         return $data;
     }
@@ -922,8 +929,16 @@ class core_calendar_external extends external_api {
     public static function get_calendar_monthly_view_parameters() {
         return new external_function_parameters(
             [
-                'time' => new external_value(PARAM_INT, 'Time to be viewed', VALUE_REQUIRED, '', NULL_NOT_ALLOWED),
+                'year' => new external_value(PARAM_INT, 'Month to be viewed', VALUE_REQUIRED),
+                'month' => new external_value(PARAM_INT, 'Year to be viewed', VALUE_REQUIRED),
                 'courseid' => new external_value(PARAM_INT, 'Course being viewed', VALUE_DEFAULT, SITEID, NULL_ALLOWED),
+                'includenavigation' => new external_value(
+                    PARAM_BOOL,
+                    'Whether to show course navigation',
+                    VALUE_DEFAULT,
+                    true,
+                    NULL_ALLOWED
+                ),
             ]
         );
     }
index 0c168ba..d511497 100644 (file)
@@ -973,15 +973,29 @@ class calendar_information {
                 $year =  $date['year'];
             }
             if (checkdate($month, $day, $year)) {
-                $this->time = make_timestamp($year, $month, $day);
+                $time = make_timestamp($year, $month, $day);
             } else {
-                $this->time = time();
+                $time = time();
             }
-        } else if (!empty($time)) {
-            $this->time = $time;
-        } else {
+        }
+
+        $this->set_time($time);
+    }
+
+    /**
+     * Set the time period of this instance.
+     *
+     * @param   int $time the unixtimestamp representing the date we want to view.
+     * @return  $this
+     */
+    public function set_time($time = null) {
+        if ($time === null) {
             $this->time = time();
+        } else {
+            $this->time = $time;
         }
+
+        return $this;
     }
 
     /**
@@ -1207,393 +1221,6 @@ function calendar_get_starting_weekday() {
     return $calendartype->get_starting_weekday();
 }
 
-/**
- * Generates the HTML for a miniature calendar.
- *
- * @param array $courses list of course to list events from
- * @param array $groups list of group
- * @param array $users user's info
- * @param int|bool $calmonth calendar month in numeric, default is set to false
- * @param int|bool $calyear calendar month in numeric, default is set to false
- * @param string|bool $placement the place/page the calendar is set to appear - passed on the the controls function
- * @param int|bool $courseid id of the course the calendar is displayed on - passed on the the controls function
- * @param int $time the unixtimestamp representing the date we want to view, this is used instead of $calmonth
- *     and $calyear to support multiple calendars
- * @return string $content return html table for mini calendar
- */
-function calendar_get_mini($courses, $groups, $users, $calmonth = false, $calyear = false, $placement = false,
-                           $courseid = false, $time = 0) {
-    global $CFG, $OUTPUT;
-
-    // Get the calendar type we are using.
-    $calendartype = \core_calendar\type_factory::get_calendar_instance();
-
-    $display = new \stdClass;
-
-    // Assume we are not displaying this month for now.
-    $display->thismonth = false;
-
-    $content = '';
-
-    // Do this check for backwards compatibility.
-    // The core should be passing a timestamp rather than month and year.
-    // If a month and year are passed they will be in Gregorian.
-    if (!empty($calmonth) && !empty($calyear)) {
-        // Ensure it is a valid date, else we will just set it to the current timestamp.
-        if (checkdate($calmonth, 1, $calyear)) {
-            $time = make_timestamp($calyear, $calmonth, 1);
-        } else {
-            $time = time();
-        }
-        $date = usergetdate($time);
-        if ($calmonth == $date['mon'] && $calyear == $date['year']) {
-            $display->thismonth = true;
-        }
-        // We can overwrite date now with the date used by the calendar type,
-        // if it is not Gregorian, otherwise there is no need as it is already in Gregorian.
-        if ($calendartype->get_name() != 'gregorian') {
-            $date = $calendartype->timestamp_to_date_array($time);
-        }
-    } else if (!empty($time)) {
-        // Get the specified date in the calendar type being used.
-        $date = $calendartype->timestamp_to_date_array($time);
-        $thisdate = $calendartype->timestamp_to_date_array(time());
-        if ($date['month'] == $thisdate['month'] && $date['year'] == $thisdate['year']) {
-            $display->thismonth = true;
-            // If we are the current month we want to set the date to the current date, not the start of the month.
-            $date = $thisdate;
-        }
-    } else {
-        // Get the current date in the calendar type being used.
-        $time = time();
-        $date = $calendartype->timestamp_to_date_array($time);
-        $display->thismonth = true;
-    }
-
-    list($d, $m, $y) = array($date['mday'], $date['mon'], $date['year']); // This is what we want to display.
-
-    // Get Gregorian date for the start of the month.
-    $gregoriandate = $calendartype->convert_to_gregorian($date['year'], $date['mon'], 1);
-
-    // Store the gregorian date values to be used later.
-    list($gy, $gm, $gd, $gh, $gmin) = array($gregoriandate['year'], $gregoriandate['month'], $gregoriandate['day'],
-        $gregoriandate['hour'], $gregoriandate['minute']);
-
-    // Get the max number of days in this month for this calendar type.
-    $display->maxdays = calendar_days_in_month($m, $y);
-    // Get the starting week day for this month.
-    $startwday = dayofweek(1, $m, $y);
-    // Get the days in a week.
-    $daynames = calendar_get_days();
-    // Store the number of days in a week.
-    $numberofdaysinweek = $calendartype->get_num_weekdays();
-
-    // Set the min and max weekday.
-    $display->minwday = calendar_get_starting_weekday();
-    $display->maxwday = $display->minwday + ($numberofdaysinweek - 1);
-
-    // These are used for DB queries, so we want unixtime, so we need to use Gregorian dates.
-    $display->tstart = make_timestamp($gy, $gm, $gd, $gh, $gmin, 0);
-    $display->tend = $display->tstart + ($display->maxdays * DAYSECS) - 1;
-
-    // Align the starting weekday to fall in our display range.
-    // This is simple, not foolproof.
-    if ($startwday < $display->minwday) {
-        $startwday += $numberofdaysinweek;
-    }
-
-    // Get the events matching our criteria. Don't forget to offset the timestamps for the user's TZ.
-    $events = calendar_get_legacy_events($display->tstart, $display->tend, $users, $groups, $courses);
-
-    // Set event course class for course events.
-    if (!empty($events)) {
-        foreach ($events as $eventid => $event) {
-            if (!empty($event->modulename)) {
-                $instances = get_fast_modinfo($event->courseid)->get_instances_of($event->modulename);
-                if (empty($instances[$event->instance]->uservisible)) {
-                    unset($events[$eventid]);
-                }
-            }
-        }
-    }
-
-    // This is either a genius idea or an idiot idea: in order to not complicate things, we use this rule: if, after
-    // possibly removing SITEID from $courses, there is only one course left, then clicking on a day in the month
-    // will also set the $SESSION->cal_courses_shown variable to that one course. Otherwise, we 'd need to add extra
-    // arguments to this function.
-    $hrefparams = array();
-    if (!empty($courses)) {
-        $courses = array_diff($courses, array(SITEID));
-        if (count($courses) == 1) {
-            $hrefparams['course'] = reset($courses);
-        }
-    }
-
-    // We want to have easy access by day, since the display is on a per-day basis.
-    calendar_events_by_day($events, $m, $y, $eventsbyday, $durationbyday, $typesbyday, $courses);
-
-    // Accessibility: added summary and <abbr> elements.
-    $summary = get_string('calendarheading', 'calendar', userdate($display->tstart, get_string('strftimemonthyear')));
-    // Begin table.
-    $content .= '<table class="minicalendar calendartable" summary="' . $summary . '">';
-    if (($placement !== false) && ($courseid !== false)) {
-        $content .= '<caption>' . calendar_top_controls($placement,
-                array('id' => $courseid, 'time' => $time)) . '</caption>';
-    }
-    $content .= '<tr class="weekdays">'; // Header row: day names.
-
-    // Print out the names of the weekdays.
-    for ($i = $display->minwday; $i <= $display->maxwday; $i++) {
-        $pos = $i % $numberofdaysinweek;
-        $content .= '<th scope="col"><abbr title="' . $daynames[$pos]['fullname'] . '">' .
-            $daynames[$pos]['shortname'] . "</abbr></th>\n";
-    }
-
-    $content .= '</tr><tr>'; // End of day names; prepare for day numbers.
-
-    // For the table display. $week is the row; $dayweek is the column.
-    $dayweek = $startwday;
-
-    // Padding (the first week may have blank days in the beginning).
-    for ($i = $display->minwday; $i < $startwday; ++$i) {
-        $content .= '<td class="dayblank">&nbsp;</td>' ."\n";
-    }
-
-    $weekend = CALENDAR_DEFAULT_WEEKEND;
-    if (isset($CFG->calendar_weekend)) {
-        $weekend = intval($CFG->calendar_weekend);
-    }
-
-    // Now display all the calendar.
-    $daytime = strtotime('-1 day', $display->tstart);
-    for ($day = 1; $day <= $display->maxdays; ++$day, ++$dayweek) {
-        $cellattributes = array();
-        $daytime = strtotime('+1 day', $daytime);
-        if ($dayweek > $display->maxwday) {
-            // We need to change week (table row).
-            $content .= '</tr><tr>';
-            $dayweek = $display->minwday;
-        }
-
-        // Reset vars.
-        if ($weekend & (1 << ($dayweek % $numberofdaysinweek))) {
-            // Weekend. This is true no matter what the exact range is.
-            $class = 'weekend day';
-        } else {
-            // Normal working day.
-            $class = 'day';
-        }
-
-        $eventids = array();
-        if (!empty($eventsbyday[$day])) {
-            $eventids = $eventsbyday[$day];
-        }
-
-        if (!empty($durationbyday[$day])) {
-            $eventids = array_unique(array_merge($eventids, $durationbyday[$day]));
-        }
-
-        $finishclass = false;
-
-        if (!empty($eventids)) {
-            // There is at least one event on this day.
-            $class .= ' hasevent';
-            $hrefparams['view'] = 'day';
-            $dayhref = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', $hrefparams), 0, 0, 0, $daytime);
-
-            $popupcontent = '';
-            foreach ($eventids as $eventid) {
-                if (!isset($events[$eventid])) {
-                    continue;
-                }
-                $event = new \calendar_event($events[$eventid]);
-                $popupalt  = '';
-                $component = 'moodle';
-                if (!empty($event->modulename)) {
-                    $popupicon = 'icon';
-                    $popupalt  = $event->modulename;
-                    $component = $event->modulename;
-                } else if ($event->courseid == SITEID) { // Site event.
-                    $popupicon = 'i/siteevent';
-                } else if ($event->courseid != 0 && $event->courseid != SITEID
-                    && $event->groupid == 0) { // Course event.
-                    $popupicon = 'i/courseevent';
-                } else if ($event->groupid) { // Group event.
-                    $popupicon = 'i/groupevent';
-                } else { // Must be a user event.
-                    $popupicon = 'i/userevent';
-                }
-
-                if ($event->timeduration) {
-                    $startdate = $calendartype->timestamp_to_date_array($event->timestart);
-                    $enddate = $calendartype->timestamp_to_date_array($event->timestart + $event->timeduration - 1);
-                    if ($enddate['mon'] == $m && $enddate['year'] == $y && $enddate['mday'] == $day) {
-                        $finishclass = true;
-                    }
-                }
-
-                $dayhref->set_anchor('event_' . $event->id);
-
-                $popupcontent .= \html_writer::start_tag('div');
-                $popupcontent .= $OUTPUT->pix_icon($popupicon, $popupalt, $component);
-                // Show ical source if needed.
-                if (!empty($event->subscription) && $CFG->calendar_showicalsource) {
-                    $a = new \stdClass();
-                    $a->name = format_string($event->name, true);
-                    $a->source = $event->subscription->name;
-                    $name = get_string('namewithsource', 'calendar', $a);
-                } else {
-                    if ($finishclass) {
-                        $samedate = $startdate['mon'] == $enddate['mon'] &&
-                            $startdate['year'] == $enddate['year'] &&
-                            $startdate['mday'] == $enddate['mday'];
-
-                        if ($samedate) {
-                            $name = format_string($event->name, true);
-                        } else {
-                            $name = format_string($event->name, true) . ' (' . get_string('eventendtime', 'calendar') . ')';
-                        }
-                    } else {
-                        $name = format_string($event->name, true);
-                    }
-                }
-                // Include course's shortname into the event name, if applicable.
-                if (!empty($event->courseid) && $event->courseid !== SITEID) {
-                    $course = get_course($event->courseid);
-                    $eventnameparams = (object)[
-                        'name' => $name,
-                        'course' => format_string($course->shortname, true, array('context' => $event->context))
-                    ];
-                    $name = get_string('eventnameandcourse', 'calendar', $eventnameparams);
-                }
-                $popupcontent .= \html_writer::link($dayhref, $name);
-                $popupcontent .= \html_writer::end_tag('div');
-            }
-
-            if ($display->thismonth && $day == $d) {
-                $popupdata = calendar_get_popup(true, $daytime, $popupcontent);
-            } else {
-                $popupdata = calendar_get_popup(false, $daytime, $popupcontent);
-            }
-
-            // Class and cell content.
-            if (isset($typesbyday[$day]['startglobal'])) {
-                $class .= ' calendar_event_site';
-            } else if (isset($typesbyday[$day]['startcourse'])) {
-                $class .= ' calendar_event_course';
-            } else if (isset($typesbyday[$day]['startgroup'])) {
-                $class .= ' calendar_event_group';
-            } else if (isset($typesbyday[$day]['startuser'])) {
-                $class .= ' calendar_event_user';
-            }
-            if ($finishclass) {
-                $class .= ' duration_finish';
-            }
-            $data = array(
-                'url' => $dayhref->out(false),
-                'day' => $day,
-                'content' => $popupdata['data-core_calendar-popupcontent'],
-                'title' => $popupdata['data-core_calendar-title']
-            );
-            $cell = $OUTPUT->render_from_template('core_calendar/minicalendar_day_link', $data);
-        } else {
-            $cell = $day;
-        }
-
-        $durationclass = false;
-        if (isset($typesbyday[$day]['durationglobal'])) {
-            $durationclass = ' duration_global';
-        } else if (isset($typesbyday[$day]['durationcourse'])) {
-            $durationclass = ' duration_course';
-        } else if (isset($typesbyday[$day]['durationgroup'])) {
-            $durationclass = ' duration_group';
-        } else if (isset($typesbyday[$day]['durationuser'])) {
-            $durationclass = ' duration_user';
-        }
-        if ($durationclass) {
-            $class .= ' duration ' . $durationclass;
-        }
-
-        // If event has a class set then add it to the table day <td> tag.
-        // Note: only one colour for minicalendar.
-        if (isset($eventsbyday[$day])) {
-            foreach ($eventsbyday[$day] as $eventid) {
-                if (!isset($events[$eventid])) {
-                    continue;
-                }
-                $event = $events[$eventid];
-                if (!empty($event->class)) {
-                    $class .= ' ' . $event->class;
-                }
-                break;
-            }
-        }
-
-        if ($display->thismonth && $day == $d) {
-            // The current cell is for today - add appropriate classes and additional information for styling.
-            $class .= ' today';
-            $today = get_string('today', 'calendar') . ' ' . userdate(time(), get_string('strftimedayshort'));
-
-            if (!isset($eventsbyday[$day]) && !isset($durationbyday[$day])) {
-                $class .= ' eventnone';
-                $popupdata = calendar_get_popup(true, false);
-                $data = array(
-                    'url' => '#',
-                    'day' => $day,
-                    'content' => $popupdata['data-core_calendar-popupcontent'],
-                    'title' => $popupdata['data-core_calendar-title']
-                );
-                $cell = $OUTPUT->render_from_template('core_calendar/minicalendar_day_link', $data);
-            }
-            $cell = get_accesshide($today . ' ') . $cell;
-        }
-
-        // Just display it.
-        $cellattributes['class'] = $class;
-        $content .= \html_writer::tag('td', $cell, $cellattributes);
-    }
-
-    // Padding (the last week may have blank days at the end).
-    for ($i = $dayweek; $i <= $display->maxwday; ++$i) {
-        $content .= '<td class="dayblank">&nbsp;</td>';
-    }
-    $content .= '</tr>'; // Last row ends.
-
-    $content .= '</table>'; // Tabular display of days ends.
-    return $content;
-}
-
-/**
- * Gets the calendar popup.
- *
- * It called at multiple points in from calendar_get_mini.
- * Copied and modified from calendar_get_mini.
- *
- * @param bool $today false except when called on the current day.
- * @param mixed $timestart $events[$eventid]->timestart, OR false if there are no events.
- * @param string $popupcontent content for the popup window/layout.
- * @return string eventid for the calendar_tooltip popup window/layout.
- */
-function calendar_get_popup($today = false, $timestart, $popupcontent = '') {
-    $popupcaption = '';
-    if ($today) {
-        $popupcaption = get_string('today', 'calendar') . ' ';
-    }
-
-    if (false === $timestart) {
-        $popupcaption .= userdate(time(), get_string('strftimedayshort'));
-        $popupcontent = get_string('eventnone', 'calendar');
-
-    } else {
-        $popupcaption .= get_string('eventsfor', 'calendar', userdate($timestart, get_string('strftimedayshort')));
-    }
-
-    return array(
-        'data-core_calendar-title' => $popupcaption,
-        'data-core_calendar-popupcontent' => $popupcontent,
-    );
-}
-
 /**
  * Gets the calendar upcoming event.
  *
@@ -2372,9 +1999,35 @@ function calendar_edit_event_allowed($event, $manualedit = false) {
     }
 
     if ($manualedit && !empty($event->modulename)) {
-        // A user isn't allowed to directly edit an event generated
-        // by a module.
-        return false;
+        $hascallback = component_callback_exists(
+            'mod_' . $event->modulename,
+            'core_calendar_event_timestart_updated'
+        );
+
+        if (!$hascallback) {
+            // If the activity hasn't implemented the correct callback
+            // to handle changes to it's events then don't allow any
+            // manual changes to them.
+            return false;
+        }
+
+        $coursemodules = get_fast_modinfo($event->courseid)->instances;
+        $hasmodule = isset($coursemodules[$event->modulename]);
+        $hasinstance = isset($coursemodules[$event->modulename][$event->instance]);
+
+        // If modinfo doesn't know about the module, return false to be safe.
+        if (!$hasmodule || !$hasinstance) {
+            return false;
+        }
+
+        $coursemodule = $coursemodules[$event->modulename][$event->instance];
+        $context = context_module::instance($coursemodule->id);
+        // This is the capability that allows a user to modify the activity
+        // settings. Since the activity generated this event we need to check
+        // that the current user has the same capability before allowing them
+        // to update the event because the changes to the event will be
+        // reflected within the activity.
+        return has_capability('moodle/course:manageactivities', $context);
     }
 
     // You cannot edit URL based calendar subscription events presently.
@@ -3354,10 +3007,11 @@ function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses, $
  * Get the calendar view output.
  *
  * @param   \calendar_information $calendar The calendar being represented
- * @param   string      $view The type of calendar to have displayed
+ * @param   string  $view The type of calendar to have displayed
+ * @param   bool    $includenavigation Whether to include navigation
  * @return  array[array, string]
  */
-function calendar_get_view(\calendar_information $calendar, $view) {
+function calendar_get_view(\calendar_information $calendar, $view, $includenavigation = true) {
     global $PAGE, $CFG;
 
     $renderer = $PAGE->get_renderer('core_calendar');
@@ -3365,24 +3019,24 @@ function calendar_get_view(\calendar_information $calendar, $view) {
 
     // Calculate the bounds of the month.
     $date = $type->timestamp_to_date_array($calendar->time);
-    $tstart = $type->convert_to_timestamp($date['year'], $date['mon'], 1);
 
     if ($view === 'day') {
+        $tstart = $type->convert_to_timestamp($date['year'], $date['mon'], $date['mday']);
         $tend = $tstart + DAYSECS - 1;
-        $selectortitle = get_string('dayviewfor', 'calendar');
     } else if ($view === 'upcoming') {
         if (isset($CFG->calendar_lookahead)) {
             $defaultlookahead = intval($CFG->calendar_lookahead);
         } else {
             $defaultlookahead = CALENDAR_DEFAULT_UPCOMING_LOOKAHEAD;
         }
+        $tstart = $type->convert_to_timestamp($date['year'], $date['mon'], 1);
         $tend = $tstart + get_user_preferences('calendar_lookahead', $defaultlookahead);
-        $selectortitle = get_string('upcomingeventsfor', 'calendar');
     } else {
+        $tstart = $type->convert_to_timestamp($date['year'], $date['mon'], 1);
         $monthdays = $type->get_num_days_in_month($date['year'], $date['mon']);
         $tend = $tstart + ($monthdays * DAYSECS) - 1;
         $selectortitle = get_string('detailedmonthviewfor', 'calendar');
-        if ($view === 'mini') {
+        if ($view === 'mini' || $view === 'minithree') {
             $template = 'core_calendar/calendar_mini';
         } else {
             $template = 'core_calendar/calendar_month';
@@ -3437,10 +3091,20 @@ function calendar_get_view(\calendar_information $calendar, $view) {
     $related = [
         'events' => $events,
         'cache' => new \core_calendar\external\events_related_objects_cache($events),
+        'type' => $type,
     ];
 
-    $month = new \core_calendar\external\month_exporter($calendar, $type, $related);
-    $data = $month->export($renderer);
+    $data = [];
+    if ($view == "month" || $view == "mini" || $view == "minithree") {
+        $month = new \core_calendar\external\month_exporter($calendar, $type, $related);
+        $month->set_includenavigation($includenavigation);
+        $data = $month->export($renderer);
+    } else if ($view == "day") {
+        $daydata = $type->timestamp_to_date_array($tstart);
+        $day = new \core_calendar\external\day_exporter($calendar, $daydata, $related);
+        $data = $day->export($renderer);
+        $template = 'core_calendar/day_detailed';
+    }
 
     return [$data, $template];
 }
index dc597ab..74b98c6 100644 (file)
@@ -61,31 +61,45 @@ class core_calendar_renderer extends plugin_renderer_base {
     public function fake_block_threemonths(calendar_information $calendar) {
         // Get the calendar type we are using.
         $calendartype = \core_calendar\type_factory::get_calendar_instance();
+        $time = $calendartype->timestamp_to_date_array($calendar->time);
 
-        $date = $calendartype->timestamp_to_date_array($calendar->time);
-
-        $prevmonth = calendar_sub_month($date['mon'], $date['year']);
-        $prevmonthtime = $calendartype->convert_to_gregorian($prevmonth[1], $prevmonth[0], 1);
-        $prevmonthtime = make_timestamp($prevmonthtime['year'], $prevmonthtime['month'], $prevmonthtime['day'],
-            $prevmonthtime['hour'], $prevmonthtime['minute']);
-
-        $nextmonth = calendar_add_month($date['mon'], $date['year']);
-        $nextmonthtime = $calendartype->convert_to_gregorian($nextmonth[1], $nextmonth[0], 1);
-        $nextmonthtime = make_timestamp($nextmonthtime['year'], $nextmonthtime['month'], $nextmonthtime['day'],
-            $nextmonthtime['hour'], $nextmonthtime['minute']);
-
-        $content  = html_writer::start_tag('div', array('class' => 'minicalendarblock'));
-        $content .= calendar_get_mini($calendar->courses, $calendar->groups, $calendar->users,
-            false, false, 'display', $calendar->courseid, $prevmonthtime);
-        $content .= html_writer::end_tag('div');
-        $content .= html_writer::start_tag('div', array('class' => 'minicalendarblock'));
-        $content .= calendar_get_mini($calendar->courses, $calendar->groups, $calendar->users,
-            false, false, 'display', $calendar->courseid, $calendar->time);
-        $content .= html_writer::end_tag('div');
-        $content .= html_writer::start_tag('div', array('class' => 'minicalendarblock'));
-        $content .= calendar_get_mini($calendar->courses, $calendar->groups, $calendar->users,
-            false, false, 'display', $calendar->courseid, $nextmonthtime);
-        $content .= html_writer::end_tag('div');
+        $current = $calendar->time;
+        $prev = $calendartype->convert_to_timestamp(
+                $time['year'],
+                $time['mon'] - 1,
+                $time['mday']
+            );
+        $next = $calendartype->convert_to_timestamp(
+                $time['year'],
+                $time['mon'] + 1,
+                $time['mday']
+            );
+
+        $content = '';
+
+        // Previous.
+        $calendar->set_time($prev);
+        list($previousmonth, ) = calendar_get_view($calendar, 'minithree', false);
+
+        // Current month.
+        $calendar->set_time($current);
+        list($currentmonth, ) = calendar_get_view($calendar, 'minithree', false);
+
+        // Next month.
+        $calendar->set_time($next);
+        list($nextmonth, ) = calendar_get_view($calendar, 'minithree', false);
+
+        // Reset the time back.
+        $calendar->set_time($current);
+
+        $data = (object) [
+            'previousmonth' => $previousmonth,
+            'currentmonth' => $currentmonth,
+            'nextmonth' => $nextmonth,
+        ];
+
+        $template = 'core_calendar/calendar_threemonth';
+        $content .= $this->render_from_template($template, $data);
         return $content;
     }
 
@@ -133,67 +147,6 @@ class core_calendar_renderer extends plugin_renderer_base {
         return html_writer::tag('button', get_string('newevent', 'calendar'), $attributes);
     }
 
-    /**
-     * Displays the calendar for a single day
-     *
-     * @param calendar_information $calendar
-     * @return string
-     */
-    public function show_day(calendar_information $calendar, moodle_url $returnurl = null) {
-
-        if ($returnurl === null) {
-            $returnurl = $this->page->url;
-        }
-
-        $events = calendar_get_upcoming($calendar->courses, $calendar->groups, $calendar->users,
-            1, 100, $calendar->timestamp_today());
-
-        $output  = html_writer::start_tag('div', array('class'=>'header'));
-        $output .= $this->course_filter_selector($returnurl, get_string('dayviewfor', 'calendar'));
-        if (calendar_user_can_add_event($calendar->course)) {
-            $output .= $this->add_event_button($calendar->course->id, 0, 0, 0, $calendar->time);
-        }
-        $output .= html_writer::end_tag('div');
-        // Controls
-        $output .= html_writer::tag('div', calendar_top_controls('day', array('id' => $calendar->courseid,
-            'time' => $calendar->time)), array('class' => 'controls'));
-
-        if (empty($events)) {
-            // There is nothing to display today.
-            $output .= html_writer::span(get_string('daywithnoevents', 'calendar'), 'calendar-information calendar-no-results');
-        } else {
-            $output .= html_writer::start_tag('div', array('class' => 'eventlist'));
-            $underway = array();
-            // First, print details about events that start today
-            foreach ($events as $event) {
-                $event = new calendar_event($event);
-                $event->calendarcourseid = $calendar->courseid;
-                if ($event->timestart >= $calendar->timestamp_today() && $event->timestart <= $calendar->timestamp_tomorrow()-1) {  // Print it now
-                    $event->time = calendar_format_event_time($event, time(), null, false,
-                        $calendar->timestamp_today());
-                    $output .= $this->event($event);
-                } else {                                                                 // Save this for later
-                    $underway[] = $event;
-                }
-            }
-
-            // Then, show a list of all events that just span this day
-            if (!empty($underway)) {
-                $output .= html_writer::span(get_string('spanningevents', 'calendar'),
-                    'calendar-information calendar-span-multiple-days');
-                foreach ($underway as $event) {
-                    $event->time = calendar_format_event_time($event, time(), null, false,
-                        $calendar->timestamp_today());
-                    $output .= $this->event($event);
-                }
-            }
-
-            $output .= html_writer::end_tag('div');
-        }
-
-        return $output;
-    }
-
     /**
      * Displays an event
      *
@@ -255,10 +208,11 @@ class core_calendar_renderer extends plugin_renderer_base {
         // Show subscription source if needed.
         if (!empty($event->subscription) && $CFG->calendar_showicalsource) {
             if (!empty($event->subscription->url)) {
-                $source = html_writer::link($event->subscription->url, get_string('subsource', 'calendar', $event->subscription));
+                $source = html_writer::link($event->subscription->url,
+                        get_string('subscriptionsource', 'calendar', $event->subscription->name));
             } else {
                 // File based ical.
-                $source = get_string('subsource', 'calendar', $event->subscription);
+                $source = get_string('subscriptionsource', 'calendar', $event->subscription->name);
             }
             $output .= html_writer::tag('div', $source, array('class' => 'subscription'));
         }
index 6e28cbf..5b91ac6 100644 (file)
@@ -32,7 +32,7 @@
     }
 }}
 <div{{!
-  }} id="calendar-month-{{uniqid}}-{{time}}" {{!
+    }} id="calendar-month-{{date.year}}-{{date.month}}-{{uniqid}}" {{!
     }} data-template="core_calendar/month_mini" {{!
     }} data-includenavigation="{{#includenavigation}}true{{/includenavigation}}{{^includenavigation}}false{{/includenavigation}}"{{!
     }}>
@@ -40,6 +40,6 @@
 </div>
 {{#js}}
 require(['jquery', 'core_calendar/calendar_mini'], function($, CalendarMini) {
-    CalendarMini.init($("#calendar-month-{{uniqid}}-{{time}}"));
+    CalendarMini.init($("#calendar-month-{{date.year}}-{{date.month}}-{{uniqid}}"));
 });
 {{/js}}
diff --git a/calendar/templates/calendar_threemonth.mustache b/calendar/templates/calendar_threemonth.mustache
new file mode 100644 (file)
index 0000000..b9e41b3
--- /dev/null
@@ -0,0 +1,49 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template calendar/calendar_threemonth
+
+    Calendar view to show three months as a block.
+
+    The purpose of this template is to render a set of three months of calendar_mini in a block.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<div id="calendar-multi-{{uniqid}}">
+    {{#previousmonth}}
+        {{> core_calendar/threemonth_month}}
+    {{/previousmonth}}
+    {{#currentmonth}}
+        {{> core_calendar/threemonth_month}}
+    {{/currentmonth}}
+    {{#nextmonth}}
+        {{> core_calendar/threemonth_month}}
+    {{/nextmonth}}
+</div>
+{{#js}}
+require(['jquery', 'core_calendar/calendar_threemonth'], function($, CalendarThreeMonth) {
+    CalendarThreeMonth.init($("#calendar-multi-{{uniqid}}"));
+});
+{{/js}}
diff --git a/calendar/templates/day_detailed.mustache b/calendar/templates/day_detailed.mustache
new file mode 100644 (file)
index 0000000..cddfae3
--- /dev/null
@@ -0,0 +1,39 @@
+{{!
+    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/day_detailed
+
+    Calendar day view.
+
+    The purpose of this template is to render the calendar day view.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<div class="header">
+    {{{filter_selector}}}
+    {{{new_event_button}}}
+</div>
+{{> core_calendar/day_navigation }}
+{{> core_calendar/event_list }}
\ No newline at end of file
diff --git a/calendar/templates/day_navigation.mustache b/calendar/templates/day_navigation.mustache
new file mode 100644 (file)
index 0000000..fce9dc2
--- /dev/null
@@ -0,0 +1,39 @@
+{{!
+    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/day_navigation
+
+    Calendar day navigation.
+
+    The purpose of this template is to render the navigation to switch to previous and next months.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+{{#navigation}}
+<div class="controls" data-view="{{view}}">
+    {{{navigation}}}
+</div>
+{{/navigation}}
+
diff --git a/calendar/templates/event_delete_modal.mustache b/calendar/templates/event_delete_modal.mustache
new file mode 100644 (file)
index 0000000..5f33933
--- /dev/null
@@ -0,0 +1,47 @@
+{{!
+    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 core_calendar/event_delete_modal
+
+    Moodle modal template with save and cancel buttons.
+
+    The purpose of this template is to render a modal.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * title A cleaned string (use clean_text()) to display.
+    * body HTML content for the boday
+
+    Example context (json):
+    {
+        "title": "Example delete event seriesmodal",
+        "body": "Do you want to delete this event, or all events in the series?"
+    }
+}}
+
+{{< core/modal }}
+    {{$footer}}
+        <button type="button" class="btn btn-primary" data-action="deleteone">{{#str}} deleteoneevent, core_calendar {{/str}}</button>
+        <button type="button" class="btn btn-secondary" data-action="deleteall">{{#str}} deleteallevents, core_calendar {{/str}}</button>
+        <button type="button" class="btn btn-secondary" data-action="cancel">{{#str}} cancel {{/str}}</button>
+    {{/footer}}
+{{/ core/modal }}
diff --git a/calendar/templates/event_item.mustache b/calendar/templates/event_item.mustache
new file mode 100644 (file)
index 0000000..3f4f31a
--- /dev/null
@@ -0,0 +1,68 @@
+{{!
+    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_item
+
+    Calendar event item.
+
+    The purpose of this template is to render the event item.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<div class="event">
+    <div class="card">
+        <div class="box card-header clearfix p-y-1">
+            <div class="commands pull-xs-right">
+                {{#canedit}}
+                    {{#candelete}}
+                        <a href="{{deleteurl}}">
+                            {{#pix}}t/delete, core, {{#str}}delete{{/str}}{{/pix}}
+                        </a>
+                    {{/candelete}}
+                    <a href="{{editurl}}">
+                        {{#pix}}t/edit, core, {{#str}}edit{{/str}}{{/pix}}
+                    </a>
+                {{/canedit}}
+            </div>
+            {{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}}
+            <h3 class="name d-inline-block">{{name}}</h3>
+            <span class="date pull-xs-right m-r-1">{{{formattedtime}}}</span>
+        </div>
+        <div class="description card-block calendar_event_{{eventtype}}">
+            <p>{{{description}}}</p>
+            {{#iscourseevent}}
+                <div><a href="{{url}}">{{course.fullname}}</a></div>
+            {{/iscourseevent}}
+            {{> core_calendar/event_subscription}}
+            {{#isactionevent}}
+                <a href="{{url}}">{{#str}} gotoactivity, core_calendar {{/str}}</a>
+            {{/isactionevent}}
+            {{#groupname}}
+                <div><a href="{{url}}">{{{course.fullname}}}</a></div>
+                <div>{{{groupname}}}</div>
+            {{/groupname}}
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/calendar/templates/event_list.mustache b/calendar/templates/event_list.mustache
new file mode 100644 (file)
index 0000000..dec4fa2
--- /dev/null
@@ -0,0 +1,43 @@
+{{!
+    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_list
+
+    Calendar event list.
+
+    The purpose of this template is to render a list of events.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<div class="eventlist">
+    {{#events}}
+        {{> core_calendar/event_item }}
+    {{/events}}
+    {{^events}}
+        <span class="calendar-information calendar-no-results">
+            {{#str}}daywithnoevents, calendar{{/str}}
+        </span>
+    {{/events}}
+</div>
\ No newline at end of file
diff --git a/calendar/templates/event_subscription.mustache b/calendar/templates/event_subscription.mustache
new file mode 100644 (file)
index 0000000..0a57928
--- /dev/null
@@ -0,0 +1,48 @@
+{{!
+    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_subscription
+
+    Calendar event subscription.
+
+    The purpose of this template is to render the event subscription data.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+
+{{#subscription}}
+    {{#displayeventsource}}
+        <div>
+            {{#url}}
+                <p><a href="{{url}}">{{#str}}subscriptionsource, core_calendar, {{name}}{{/str}}</a></p>
+            {{/url}}
+            {{^url}}
+                <p>{{#str}}subscriptionsource, core_calendar, {{name}}{{/str}}</p>
+            {{/url}}
+        </div>
+    {{/displayeventsource}}
+{{/subscription}}
+
+
index 2ad48aa..b139b69 100644 (file)
         "groupname": "Group 1"
     }
 }}
-<div data-region="summary-modal-container" data-event-id="{{id}}" data-event-title="{{name}}"
-     data-action-event="{{isactionevent}}" data-edit-url="{{editurl}}">
+<div{{!
+    }} data-region="summary-modal-container"{{!
+    }} data-event-id="{{id}}"{{!
+    }} data-event-title="{{name}}"{{!
+    }} data-event-event-count="{{eventcount}}"{{!
+    }} data-event-="{{repeatid}}"{{!
+    }} data-action-event="{{isactionevent}}"{{!
+    }} data-edit-url="{{editurl}}"{{!
+    }}>
     <h4>{{#str}} when, core_calendar {{/str}}</h4>
     {{#userdate}} {{timestart}}, {{#str}} strftimerecentfull {{/str}} {{/userdate}}
     <br>
@@ -42,9 +49,7 @@
     {{#iscourseevent}}
         <div><a href="{{url}}">{{course.fullname}}</a></div>
     {{/iscourseevent}}
-    {{#source}}
-        <div>{{{source}}}</div>
-    {{/source}}
+    {{> core_calendar/event_subscription}}
     {{#groupname}}
         <div><a href="{{url}}">{{{course.fullname}}}</a></div>
         <div>{{{groupname}}}</div>
index 583f500..a6e7c2d 100644 (file)
     }
 }}
 {{< core/modal }}
-{{$footer}}
-{{#canedit}}
-    {{#candelete}}
-        <button type="button" class="btn btn-secondary" data-action="delete">{{#str}} delete {{/str}}</button>
-    {{/candelete}}
-    <button type="button" class="btn btn-primary" data-action="edit">{{#str}} edit {{/str}}</button>
-{{/canedit}}
-{{#isactionevent}}
-    <a href="{{url}}">{{#str}} gotoactivity, core_calendar {{/str}}</a>
-{{/isactionevent}}
-{{/footer}}
+    {{$footer}}
+        {{#candelete}}
+            <button type="button" class="btn btn-secondary" data-action="delete">{{#str}} delete {{/str}}</button>
+        {{/candelete}}
+        {{#isactionevent}}
+            <a href="{{url}}">{{#str}} gotoactivity, core_calendar {{/str}}</a>
+        {{/isactionevent}}
+        {{^isactionevent}}
+            {{#canedit}}
+                <button type="button" class="btn btn-primary" data-action="edit">{{#str}} edit {{/str}}</button>
+            {{/canedit}}
+        {{/isactionevent}}
+    {{/footer}}
 {{/ core/modal }}
index 05c2941..d9769b2 100644 (file)
@@ -33,5 +33,7 @@
     {{#managesubscriptionbutton}}
         {{> core/single_button }}
     {{/managesubscriptionbutton}}
-    <a href="{{icalurl}}" title="{{#str}} quickdownloadcalendar, calendar {{/str}}" class="ical-link m-l-1">iCal</a>
+    {{#icalurl}}
+        <a href="{{icalurl}}" title="{{#str}} quickdownloadcalendar, calendar {{/str}}" class="ical-link m-l-1">iCal</a>
+    {{/icalurl}}
 </div>
similarity index 94%
rename from calendar/templates/month_header.mustache
rename to calendar/templates/header.mustache
index 56d6e49..626470d 100644 (file)
@@ -15,9 +15,9 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template calendar/month_header
+    @template calendar/header
 
-    Calendar month header.
+    Calendar header.
 
     The purpose of this template is to render the month header.
 
index 463b687..e6ca777 100644 (file)
@@ -31,8 +31,8 @@
     {
     }
 }}
-<div class="calendarwrapper" data-courseid="{{courseid}}" data-current-time="{{time}}">
-    {{> core_calendar/month_header }}
+<div class="calendarwrapper" data-courseid="{{courseid}}" data-month="{{date.mon}}" data-year="{{date.year}}">
+    {{> core_calendar/header }}
     {{> core_calendar/month_navigation }}
     {{> core/overlay_loading}}
     <table id="month-detailed-{{uniqid}}" class="calendarmonth calendartable card-deck m-b-0">
index b14afc9..bc180c7 100644 (file)
     {
     }
 }}
-<div id="month-mini-{{uniqid}}-{{time}}" class="calendarwrapper" data-courseid="{{courseid}}" data-current-time="{{time}}">
+<div{{!
+    }} id="month-mini-{{date.year}}-{{date.month}}-{{uniqid}}"{{!
+    }} class="calendarwrapper"{{!
+    }} data-courseid="{{courseid}}"{{!
+    }} data-month="{{date.mon}}"{{!
+    }} data-year="{{date.year}}"{{!
+    }}>
     {{> 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>
+            {{#includenavigation}}
+                <a{{!
+                    }} href="#"{{!
+                    }} class="arrow_link previous"{{!
+                    }} title="{{#str}}monthprev, calendar{{/str}}"{{!
+                    }} data-year="{{previousperiod.year}}"{{!
+                    }} data-month="{{previousperiod.mon}}"{{!
+                }}>
+                    <span class="arrow">{{{larrow}}}</span>
+                </a>
+                <span class="hide"> | </span>
+                <span class="current">
+                    <a href="{{{url}}}" title="{{#str}}monththis, calendar{{/str}}">{{periodname}}</a>
+                </span>
+                <span class="hide"> | </span>
+                <a{{!
+                    }} href="#"{{!
+                    }} class="arrow_link next"{{!
+                    }} title="{{#str}}monthprev, calendar{{/str}}"{{!
+                    }} data-year="{{nextperiod.year}}"{{!
+                    }} data-month="{{nextperiod.mon}}"{{!
+                }}>
+                    <span class="arrow">{{{rarrow}}}</span>
+                </a>
+            {{/includenavigation}}
+            {{^includenavigation}}
+                <h3>
+                    <a href="{{{url}}}" title="{{#str}}monththis, calendar{{/str}}">{{periodname}}</a>
+                </h3>
+            {{/includenavigation}}
         </caption>
         <thead>
           <tr>
@@ -145,7 +174,7 @@ require([
         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}}")
+        $("#month-mini-{{date.year}}-{{date.month}}-{{uniqid}}")
             .find(CalendarSelectors.popoverType[data.type])
             .toggleClass('hidden', !!data.hidden);
         M.util.js_complete("month-mini-{{uniqid}}-filterChanged");
index f0f67dd..68180c2 100644 (file)
     {
     }
 }}
-{{#navigation}}
 <div id="month-navigation-{{uniqid}}" class="controls" data-view="{{view}}">
-    {{{navigation}}}
+    <div class="calendar-controls">
+        <a{{!
+            }} href="{{previousperiodlink}}"{{!
+            }} class="arrow_link previous"{{!
+            }} title="{{#str}}monthprev, calendar{{/str}}"{{!
+            }} data-year="{{previousperiod.year}}"{{!
+            }} data-month="{{previousperiod.mon}}"{{!
+            }} data-drop-zone="nav-link" {{!
+        }}>
+            <span class="arrow">{{{larrow}}}</span>
+            &nbsp;
+            <span class="arrow_text">{{previousperiodname}}</span>
+        </a>
+        <span class="hide"> | </span>
+        <h2 class="current">{{periodname}}</h2>
+        <span class="hide"> | </span>
+        <a{{!
+            }} href="{{nextperiodlink}}"{{!
+            }} class="arrow_link next"{{!
+            }} title="{{#str}}monthnext, calendar{{/str}}"{{!
+            }} data-year="{{nextperiod.year}}"{{!
+            }} data-month="{{nextperiod.mon}}"{{!
+            }} data-drop-zone="nav-link" {{!
+        }}>
+            <span class="arrow_text">{{nextperiodname}}</span>
+            &nbsp;
+            <span class="arrow">{{{rarrow}}}</span>
+        </a>
+    </div>
 </div>
-{{/navigation}}
 {{#js}}
 require(['jquery', 'core_calendar/month_navigation_drag_drop'], function($, DragDrop) {
     var root = $('#month-navigation-{{uniqid}}');
diff --git a/calendar/templates/threemonth_month.mustache b/calendar/templates/threemonth_month.mustache
new file mode 100644 (file)
index 0000000..8d17536
--- /dev/null
@@ -0,0 +1,39 @@
+{{!
+    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_threemonth
+
+    Calendar view to show three months as a block.
+
+    The purpose of this template is to render a set of three months of calendar_mini in a block.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<div data-period="month" class="calendarwrapper"{{!
+    }} data-previous-year="{{previousperiod.year}}" data-previous-month="{{previousperiod.mon}}" {{!
+    }} data-next-year="{{nextperiod.year}}" data-next-month="{{nextperiod.mon}}" {{!
+    }}>
+    {{> core_calendar/calendar_mini}}
+</div>
index e99279e..0007c3c 100644 (file)
@@ -1,6 +1,12 @@
 This files describes API changes in /calendar/* ,
 information provided here is intended especially for developers.
 
+=== 3.4 ===
+* calendar_get_mini has been deprecated. Please update to use the new
+  exporters and renderers.
+* added core_calendar_validate_event_timestart and core_calendar_event_timestart_updated callbacks for module events
+  when the update_event_start_day function is used in the local api.
+
 === 3.3 ===
 * calendar_event_hook() has been removed. Developers should be using the Moodle events system to achieve this behaviour,
   rather than using a hacky calendar specific implementation.
index 6447c8d..fe6a82e 100644 (file)
@@ -51,22 +51,10 @@ require_once($CFG->dirroot.'/calendar/lib.php');
 
 $courseid = optional_param('course', SITEID, PARAM_INT);
 $view = optional_param('view', 'upcoming', PARAM_ALPHA);
-$day = optional_param('cal_d', 0, PARAM_INT);
-$mon = optional_param('cal_m', 0, PARAM_INT);
-$year = optional_param('cal_y', 0, PARAM_INT);
 $time = optional_param('time', 0, PARAM_INT);
 
 $url = new moodle_url('/calendar/view.php');
 
-// If a day, month and year were passed then convert it to a timestamp. If these were passed
-// then we can assume the day, month and year are passed as Gregorian, as no where in core
-// should we be passing these values rather than the time. This is done for BC.
-if (!empty($day) && !empty($mon) && !empty($year)) {
-    if (checkdate($mon, $day, $year)) {
-        $time = make_timestamp($year, $mon, $day);
-    }
-}
-
 if (empty($time)) {
     $time = time();
 }
@@ -135,7 +123,8 @@ echo $OUTPUT->heading(get_string('calendar', 'calendar'));
 if ($view == 'day' || $view == 'upcoming') {
     switch($view) {
         case 'day':
-            echo $renderer->show_day($calendar);
+            list($data, $template) = calendar_get_view($calendar, $view);
+            echo $renderer->render_from_template($template, $data);
         break;
         case 'upcoming':
             $defaultlookahead = CALENDAR_DEFAULT_UPCOMING_LOOKAHEAD;
index 0604f3e..fc16c0c 100644 (file)
@@ -1349,7 +1349,8 @@ class api {
                   JOIN {user} u
                     ON u.id = uc.userid
                  WHERE (uc.status = :waitingforreview
-                    OR (uc.status = :inreview AND uc.reviewerid = :reviewerid))";
+                    OR (uc.status = :inreview AND uc.reviewerid = :reviewerid))
+                   AND u.deleted = 0";
         $ordersql = " ORDER BY c.shortname ASC";
         $params = array(
             'inreview' => user_competency::STATUS_IN_REVIEW,
index e29f9d6..56e3ff0 100644 (file)
@@ -4564,6 +4564,9 @@ class core_competency_api_testcase extends advanced_testcase {
     }
 
     public function test_list_user_competencies_to_review() {
+        global $CFG;
+        require_once($CFG->dirroot . '/user/lib.php');
+
         $dg = $this->getDataGenerator();
         $this->resetAfterTest();
         $ccg = $dg->get_plugin_generator('core_competency');
@@ -4580,6 +4583,7 @@ class core_competency_api_testcase extends advanced_testcase {
 
         $u1 = $dg->create_user();
         $u2 = $dg->create_user();
+        $u3 = $dg->create_user();
         $f1 = $ccg->create_framework();
         $c1 = $ccg->create_competency(['competencyframeworkid' => $f1->get('id')]);
         $c2 = $ccg->create_competency(['competencyframeworkid' => $f1->get('id')]);
@@ -4596,14 +4600,23 @@ class core_competency_api_testcase extends advanced_testcase {
             'status' => user_competency::STATUS_IDLE]);
         $uc2c = $ccg->create_user_competency(['userid' => $u2->id, 'competencyid' => $c3->get('id'),
             'status' => user_competency::STATUS_IN_REVIEW]);
+        $uc3a = $ccg->create_user_competency(['userid' => $u3->id, 'competencyid' => $c3->get('id'),
+            'status' => user_competency::STATUS_WAITING_FOR_REVIEW]);
 
         // The reviewer can review all plans waiting for review, or in review where they are the reviewer.
         $this->setUser($reviewer);
         $result = api::list_user_competencies_to_review();
-        $this->assertEquals(3, $result['count']);
+        $this->assertEquals(4, $result['count']);
         $this->assertEquals($uc2a->get('id'), $result['competencies'][0]->usercompetency->get('id'));
         $this->assertEquals($uc1b->get('id'), $result['competencies'][1]->usercompetency->get('id'));
         $this->assertEquals($uc1c->get('id'), $result['competencies'][2]->usercompetency->get('id'));
+        $this->assertEquals($uc3a->get('id'), $result['competencies'][3]->usercompetency->get('id'));
+
+        // Now, let's delete user 3.
+        // It should not be listed on user competencies to review any more.
+        user_delete_user($u3);
+        $result = api::list_user_competencies_to_review();
+        $this->assertEquals(3, $result['count']);
 
         // The reviewer cannot view the plans when they do not have the permission in the user's context.
         role_assign($roleprohibit, $reviewer->id, context_user::instance($u2->id)->id);
index 0366c4d..7dd4a92 100644 (file)
@@ -407,21 +407,43 @@ function enrol_meta_sync($courseid = NULL, $verbose = false) {
     $onecourse = $courseid ? "AND e.courseid = :courseid" : "";
     list($enabled, $params) = $DB->get_in_or_equal(explode(',', $CFG->enrol_plugins_enabled), SQL_PARAMS_NAMED, 'e');
     $params['courseid'] = $courseid;
-    $sql = "SELECT ue.userid, ue.enrolid, pue.pstatus, pue.ptimestart, pue.ptimeend
+    // The query builds a a list of all the non-meta enrolments that are on courses (the children) that are linked to by a meta
+    // enrolment, it then groups them by the course that linked to them (the parents).
+    //
+    // It will only return results where the there is a difference between the status of the parent and the lowest status
+    // of the children (remember that 0 is active, any other status is some form of inactive), or the time the earliest non-zero
+    // start time of a child is different to the parent, or the longest effective end date has changed.
+    //
+    // The last two case statements in the HAVING clause are designed to ignore any inactive child records when calculating
+    // the start and end time.
+    $sql = "SELECT ue.userid, ue.enrolid,
+                   MIN(xpue.status + xpe.status) AS pstatus,
+                   MIN(CASE WHEN (xpue.status + xpe.status = 0) THEN xpue.timestart ELSE 9999999999 END) AS ptimestart,
+                   MAX(CASE WHEN (xpue.status + xpe.status = 0) THEN
+                                 (CASE WHEN xpue.timeend = 0 THEN 9999999999 ELSE xpue.timeend END)
+                            ELSE 0 END) AS ptimeend
               FROM {user_enrolments} ue
               JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'meta' $onecourse)
-              JOIN (SELECT xpue.userid, xpe.courseid, MIN(xpue.status + xpe.status) AS pstatus,
-                      MIN(CASE WHEN (xpue.status + xpe.status = 0) THEN xpue.timestart ELSE 9999999999 END) AS ptimestart,
-                      MAX(CASE WHEN (xpue.status + xpe.status = 0) THEN
-                                (CASE WHEN xpue.timeend = 0 THEN 9999999999 ELSE xpue.timeend END)
-                                ELSE 0 END) AS ptimeend
-                      FROM {user_enrolments} xpue
-                      JOIN {enrol} xpe ON (xpe.id = xpue.enrolid AND xpe.enrol <> 'meta' AND xpe.enrol $enabled)
-                  GROUP BY xpue.userid, xpe.courseid
-                   ) pue ON (pue.courseid = e.customint1 AND pue.userid = ue.userid)
-             WHERE (pue.pstatus = 0 AND ue.status > 0) OR (pue.pstatus > 0 and ue.status = 0)
-             OR ((CASE WHEN pue.ptimestart = 9999999999 THEN 0 ELSE pue.ptimestart END) <> ue.timestart)
-             OR ((CASE WHEN pue.ptimeend = 9999999999 THEN 0 ELSE pue.ptimeend END) <> ue.timeend)";
+              JOIN {user_enrolments} xpue ON (xpue.userid = ue.userid)
+              JOIN {enrol} xpe ON (xpe.id = xpue.enrolid AND xpe.enrol <> 'meta'
+                   AND xpe.enrol $enabled AND xpe.courseid = e.customint1)
+          GROUP BY ue.userid, ue.enrolid
+            HAVING (MIN(xpue.status + xpe.status) = 0 AND MIN(ue.status) > 0)
+                   OR (MIN(xpue.status + xpe.status) > 0 AND MIN(ue.status) = 0)
+                   OR ((CASE WHEN
+                                  MIN(CASE WHEN (xpue.status + xpe.status = 0) THEN xpue.timestart ELSE 9999999999 END) = 9999999999
+                             THEN 0
+                             ELSE
+                                  MIN(CASE WHEN (xpue.status + xpe.status = 0) THEN xpue.timestart ELSE 9999999999 END)
+                              END) <> MIN(ue.timestart))
+                   OR ((CASE
+                         WHEN MAX(CASE WHEN (xpue.status + xpe.status = 0)
+                                       THEN (CASE WHEN xpue.timeend = 0 THEN 9999999999 ELSE xpue.timeend END)
+                                       ELSE 0 END) = 9999999999
+                         THEN 0 ELSE MAX(CASE WHEN (xpue.status + xpe.status = 0)
+                                              THEN (CASE WHEN xpue.timeend = 0 THEN 9999999999 ELSE xpue.timeend END)
+                                              ELSE 0 END)
+                          END) <> MAX(ue.timeend))";
     $rs = $DB->get_recordset_sql($sql, $params);
     foreach($rs as $ue) {
         if (!isset($instances[$ue->enrolid])) {
index 4508ae0..d52dc6f 100644 (file)
@@ -578,6 +578,112 @@ class core_enrollib_testcase extends advanced_testcase {
         $this->assertEquals($course2->id, $courses[$course2->id]->id);
     }
 
+    /**
+     * Tests the enrol_get_my_courses function when using the $allaccessible parameter, which
+     * includes a wider range of courses (enrolled courses + other accessible ones).
+     */
+    public function test_enrol_get_my_courses_all_accessible() {
+        global $DB, $CFG;
+
+        $this->resetAfterTest(true);
+
+        // Create test user and 4 courses, two of which have guest access enabled.
+        $user = $this->getDataGenerator()->create_user();
+        $course1 = $this->getDataGenerator()->create_course(
+                (object)array('shortname' => 'Z',
+                'enrol_guest_status_0' => ENROL_INSTANCE_DISABLED,
+                'enrol_guest_password_0' => ''));
+        $course2 = $this->getDataGenerator()->create_course(
+                (object)array('shortname' => 'Y',
+                'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
+                'enrol_guest_password_0' => ''));
+        $course3 = $this->getDataGenerator()->create_course(
+                (object)array('shortname' => 'X',
+                'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
+                'enrol_guest_password_0' => 'frog'));
+        $course4 = $this->getDataGenerator()->create_course(
+                (object)array('shortname' => 'W',
+                'enrol_guest_status_0' => ENROL_INSTANCE_DISABLED,
+                'enrol_guest_password_0' => ''));
+
+        // User is enrolled in first course.
+        $this->getDataGenerator()->enrol_user($user->id, $course1->id);
+
+        // Check enrol_get_my_courses basic use (without all accessible).
+        $this->setUser($user);
+        $courses = enrol_get_my_courses();
+        $this->assertEquals([$course1->id], array_keys($courses));
+
+        // Turn on all accessible, now they can access the second course too.
+        $courses = enrol_get_my_courses(null, 'id', 0, [], true);
+        $this->assertEquals([$course1->id, $course2->id], array_keys($courses));
+
+        // Log in as guest to third course.
+        load_temp_course_role(context_course::instance($course3->id), $CFG->guestroleid);
+        $courses = enrol_get_my_courses(null, 'id', 0, [], true);
+        $this->assertEquals([$course1->id, $course2->id, $course3->id], array_keys($courses));
+
+        // Check fields parameter still works. Fields default (certain base fields).
+        $this->assertObjectHasAttribute('id', $courses[$course3->id]);
+        $this->assertObjectHasAttribute('shortname', $courses[$course3->id]);
+        $this->assertObjectNotHasAttribute('summary', $courses[$course3->id]);
+
+        // Specified fields (one, string).
+        $courses = enrol_get_my_courses('summary', 'id', 0, [], true);
+        $this->assertObjectHasAttribute('id', $courses[$course3->id]);
+        $this->assertObjectHasAttribute('shortname', $courses[$course3->id]);
+        $this->assertObjectHasAttribute('summary', $courses[$course3->id]);
+        $this->assertObjectNotHasAttribute('summaryformat', $courses[$course3->id]);
+
+        // Specified fields (two, string).
+        $courses = enrol_get_my_courses('summary, summaryformat', 'id', 0, [], true);
+        $this->assertObjectHasAttribute('summary', $courses[$course3->id]);
+        $this->assertObjectHasAttribute('summaryformat', $courses[$course3->id]);
+
+        // Specified fields (two, array).
+        $courses = enrol_get_my_courses(['summary', 'summaryformat'], 'id', 0, [], true);
+        $this->assertObjectHasAttribute('summary', $courses[$course3->id]);
+        $this->assertObjectHasAttribute('summaryformat', $courses[$course3->id]);
+
+        // Check sort parameter still works.
+        $courses = enrol_get_my_courses(null, 'shortname', 0, [], true);
+        $this->assertEquals([$course3->id, $course2->id, $course1->id], array_keys($courses));
+
+        // Check filter parameter still works.
+        $courses = enrol_get_my_courses(null, 'id', 0, [$course2->id, $course3->id, $course4->id], true);
+        $this->assertEquals([$course2->id, $course3->id], array_keys($courses));
+
+        // Check limit parameter.
+        $courses = enrol_get_my_courses(null, 'id', 2, [], true);
+        $this->assertEquals([$course1->id, $course2->id], array_keys($courses));
+
+        // Now try access for a different user who has manager role at system level.
+        $manager = $this->getDataGenerator()->create_user();
+        $managerroleid = $DB->get_field('role', 'id', ['shortname' => 'manager']);
+        role_assign($managerroleid, $manager->id, \context_system::instance()->id);
+        $this->setUser($manager);
+
+        // With default get enrolled, they don't have any courses.
+        $courses = enrol_get_my_courses();
+        $this->assertCount(0, $courses);
+
+        // But with all accessible, they have 4 because they have moodle/course:view everywhere.
+        $courses = enrol_get_my_courses(null, 'id', 0, [], true);
+        $this->assertEquals([$course1->id, $course2->id, $course3->id, $course4->id],
+                array_keys($courses));
+
+        // If we prohibit manager from course:view on course 1 though...
+        assign_capability('moodle/course:view', CAP_PROHIBIT, $managerroleid,
+                \context_course::instance($course1->id));
+        $courses = enrol_get_my_courses(null, 'id', 0, [], true);
+        $this->assertEquals([$course2->id, $course3->id, $course4->id], array_keys($courses));
+
+        // Check for admin user, which has a slightly different query.
+        $this->setAdminUser();
+        $courses = enrol_get_my_courses(null, 'id', 0, [], true);
+        $this->assertEquals([$course1->id, $course2->id, $course3->id, $course4->id], array_keys($courses));
+    }
+
     /**
      * test_course_users
      *
similarity index 60%
rename from blocks/messages/lang/en/block_messages.php
rename to install/lang/mi/langconfig.php
index c41e51c..0d7b2d1 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Strings for component 'block_messages', language 'en', branch 'MOODLE_20_STABLE'
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
  *
- * @package   block_messages
- * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
+ * @package   installer
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['messages:addinstance'] = 'Add a new messages block';
-$string['messages:myaddinstance'] = 'Add a new messages block to Dashboard';
-$string['pluginname'] = 'Messages';
+defined('MOODLE_INTERNAL') || die();
+
+$string['thislanguage'] = 'Māori (Te Reo)';
index f66a64d..e3461a2 100644 (file)
@@ -35,11 +35,11 @@ $string['cliansweryes'] = 'y';
 $string['cliincorrectvalueerror'] = 'Error, valor incorrècta « {$a->value} » pel paramètre « {$a->option} »';
 $string['cliincorrectvalueretry'] = 'Valor incorrècta, ensajar tornamai';
 $string['clitypevalue'] = 'tipe valor';
-$string['clitypevaluedefault'] = 'tipe valor, tapez Entrée per utilizar la valor per defaut ({$a})';
+$string['clitypevaluedefault'] = 'tipe valor, picatz Entrada per utilizar la valor per defaut ({$a})';
 $string['cliunknowoption'] = 'Options non reconegudas :
  {$a}.
-Utilisez l\'option --help.';
+Utilizatz l\'opcion --help.';
 $string['cliyesnoprompt'] = 'Picatz y (per òc) o n (per non)';
-$string['environmentrequireinstall'] = 'doit èsser installat e activat';
+$string['environmentrequireinstall'] = 'deu èsser installat e activat';
 $string['environmentrequireversion'] = 'la version {$a->needed} es requesida ; utilizatz actualament la version {$a->current}';
 $string['upgradekeyset'] = 'Metre a jorn la clau (daissar void per la definir pas)';
index acdf2ec..d6d4850 100644 (file)
@@ -31,3 +31,4 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['databasename'] = 'Databasens namn';
+$string['welcomep50'] = 'Användningen av alla applikationer i detta paket regleras av sina respektive licenser. Det fullständiga paketet <strong>{$a->installername}</strong> är <a href="http://www.opensource.org/docs/definition_plain.html">Öppen källkod</a> och distribueras under <a href="http://www.gnu.org/copyleft/gpl.html">GPL</a> licensen.';
index dc90581..f2ad60b 100644 (file)
@@ -835,6 +835,8 @@ $string['pathtopgdumpinvalid'] = 'Invalid path to pg_dump - either wrong path or
 $string['pathtopsql'] = 'Path to psql';
 $string['pathtopsqldesc'] = 'This is only necessary to enter if you have more than one psql on your system (for example if you have more than one version of postgresql installed)';
 $string['pathtopsqlinvalid'] = 'Invalid path to psql - either wrong path or not executable';
+$string['pathtopython'] = 'Path to Python';
+$string['pathtopythondesc'] = 'Path to your executable Python binary.';
 $string['pcreunicodewarning'] = 'It is strongly recommended to use PCRE PHP extension that is compatible with Unicode characters.';
 $string['perfdebug'] = 'Performance info';
 $string['performance'] = 'Performance';
index 17d12c0..13e1118 100644 (file)
@@ -25,6 +25,7 @@
 $string['analysablenotused'] = 'Analysable {$a->analysableid} not used: {$a->errors}';
 $string['analysablenotvalidfortarget'] = 'Analysable {$a->analysableid} is not valid for this target: {$a->result}';
 $string['analysisinprogress'] = 'Still being analysed by a previous execution';
+$string['analytics'] = 'Analytics';
 $string['analyticslogstore'] = 'Log store used for analytics';
 $string['analyticslogstore_help'] = 'The log store that will be used by the analytics API to read users\' activity';
 $string['analyticssettings'] = 'Analytics settings';
index 30a936c..a0bf7d0 100644 (file)
@@ -40,6 +40,7 @@ $string['colpoll'] = 'Update';
 $string['colactions'] = 'Actions';
 $string['commontasks'] = 'Options';
 $string['confirmeventdelete'] = 'Are you sure you want to delete the "{$a}" event?';
+$string['confirmeventseriesdelete'] = 'The "{$a->name}" event is part of a series. Do you want to delete just this event, or all {$a->count} events in the series?';
 $string['course'] = 'Course';
 $string['courseevent'] = 'Course event';
 $string['courseevents'] = 'Course events';
@@ -52,6 +53,8 @@ $string['daywithnoevents'] = 'There are no events this day.';
 $string['default'] = 'Default';
 $string['deleteevent'] = 'Delete event';
 $string['deleteevents'] = 'Delete events';
+$string['deleteoneevent'] = 'Delete this event';
+$string['deleteallevents'] = 'Delete all events';
 $string['detailedmonthviewfor'] = 'Detailed month view for:';
 $string['detailedmonthviewtitle'] = 'Detailed month view: {$a}';
 $string['durationminutes'] = 'Duration in minutes';
@@ -206,8 +209,8 @@ $string['spanningevents'] = 'Events underway';
 $string['subscriptions'] = 'Subscriptions';
 $string['subscriptionname'] = 'Calendar name';
 $string['subscriptionremoved'] = 'Calendar subscription {$a} removed';
+$string['subscriptionsource'] = 'Event source: {$a}';
 $string['subscriptionupdated'] = 'Calendar subscription {$a} updated';
-$string['subsource'] = 'Event source: {$a->name}';
 $string['sun'] = 'Sun';
 $string['sunday'] = 'Sunday';
 $string['thu'] = 'Thu';
@@ -265,3 +268,4 @@ $string['showcourseevents'] = 'Show course events';
 $string['showglobalevents'] = 'Show global events';
 $string['showgroupsevents'] = 'Show group events';
 $string['showuserevents'] = 'Show user events';
+$string['subsource'] = 'Event source: {$a->name}';
index c60077d..587a61f 100644 (file)
@@ -2475,20 +2475,28 @@ function get_component_string($component, $contextlevel) {
 
 /**
  * Gets the list of roles assigned to this context and up (parents)
- * from the list of roles that are visible on user profile page
- * and participants page.
+ * from the aggregation of:
+ * a) the list of roles that are visible on user profile page and participants page (profileroles setting) and;
+ * b) if applicable, those roles that are assigned in the context.
  *
  * @param context $context
  * @return array
  */
 function get_profile_roles(context $context) {
     global $CFG, $DB;
+    // If the current user can assign roles, then they can see all roles on the profile and participants page,
+    // provided the roles are assigned to at least 1 user in the context. If not, only the policy-defined roles.
+    if (has_capability('moodle/role:assign', $context)) {
+        $rolesinscope = array_keys(get_all_roles($context));
+    } else {
+        $rolesinscope = empty($CFG->profileroles) ? [] : array_map('trim', explode(',', $CFG->profileroles));
+    }
 
-    if (empty($CFG->profileroles)) {
-        return array();
+    if (empty($rolesinscope)) {
+        return [];
     }
 
-    list($rallowed, $params) = $DB->get_in_or_equal(explode(',', $CFG->profileroles), SQL_PARAMS_NAMED, 'a');
+    list($rallowed, $params) = $DB->get_in_or_equal($rolesinscope, SQL_PARAMS_NAMED, 'a');
     list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p');
     $params = array_merge($params, $cparams);
 
@@ -2547,18 +2555,23 @@ function get_roles_used_in_context(context $context) {
  */
 function get_user_roles_in_course($userid, $courseid) {
     global $CFG, $DB;
-
-    if (empty($CFG->profileroles)) {
-        return '';
-    }
-
     if ($courseid == SITEID) {
         $context = context_system::instance();
     } else {
         $context = context_course::instance($courseid);
     }
+    // If the current user can assign roles, then they can see all roles on the profile and participants page,
+    // provided the roles are assigned to at least 1 user in the context. If not, only the policy-defined roles.
+    if (has_capability('moodle/role:assign', $context)) {
+        $rolesinscope = array_keys(get_all_roles($context));
+    } else {
+        $rolesinscope = empty($CFG->profileroles) ? [] : array_map('trim', explode(',', $CFG->profileroles));
+    }
+    if (empty($rolesinscope)) {
+        return '';
+    }
 
-    list($rallowed, $params) = $DB->get_in_or_equal(explode(',', $CFG->profileroles), SQL_PARAMS_NAMED, 'a');
+    list($rallowed, $params) = $DB->get_in_or_equal($rolesinscope, SQL_PARAMS_NAMED, 'a');
     list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p');
     $params = array_merge($params, $cparams);
 
index 197b241..75b98b4 100644 (file)
@@ -1647,7 +1647,7 @@ class core_plugin_manager {
         $plugins = array(
             'qformat' => array('blackboard', 'learnwise'),
             'auth' => array('radius', 'fc', 'nntp', 'pam', 'pop3', 'imap'),
-            'block' => array('course_overview'),
+            'block' => array('course_overview', 'messages'),
             'enrol' => array('authorize'),
             'report' => array('search'),
             'repository' => array('alfresco'),
@@ -1715,7 +1715,7 @@ class core_plugin_manager {
                 'calendar_upcoming', 'comments', 'community',
                 'completionstatus', 'course_list', 'course_summary',
                 'feedback', 'globalsearch', 'glossary_random', 'html',
-                'login', 'lp', 'mentees', 'messages', 'mnet_hosts', 'myoverview', 'myprofile',
+                'login', 'lp', 'mentees', 'mnet_hosts', 'myoverview', 'myprofile',
                 'navigation', 'news_items', 'online_users', 'participants',
                 'private_files', 'quiz_results', 'recent_activity',
                 'rss_client', 'search_forums', 'section_links',
index df03600..ffa1522 100644 (file)
@@ -2245,7 +2245,12 @@ function xmldb_main_upgrade($oldversion) {
             ['hub', '%' . $DB->sql_like_escape('_' . $cleanoldhuburl)]);
         foreach ($entries as $entry) {
             $newname = substr($entry->name, 0, -strlen($cleanoldhuburl)) . $cleannewhuburl;
-            $DB->update_record('config_plugins', ['id' => $entry->id, 'name' => $newname]);
+            try {
+                $DB->update_record('config_plugins', ['id' => $entry->id, 'name' => $newname]);
+            } catch (dml_exception $e) {
+                // Entry with new name already exists, remove the one with an old name.
+                $DB->delete_records('config_plugins', ['id' => $entry->id]);
+            }
         }
 
         // Update published courses.
@@ -2494,5 +2499,55 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2017091201.00);
     }
 
+    if ($oldversion < 2017092201.00) {
+
+        // Remove duplicate registrations.
+        $newhuburl = "https://moodle.net";
+        $registrations = $DB->get_records('registration_hubs', ['huburl' => $newhuburl], 'confirmed DESC, id ASC');
+        if (count($registrations) > 1) {
+            $reg = array_shift($registrations);
+            $DB->delete_records_select('registration_hubs', 'huburl = ? AND id <> ?', [$newhuburl, $reg->id]);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2017092201.00);
+    }
+
+    if ($oldversion < 2017092202.00) {
+
+        if (!file_exists($CFG->dirroot . '/blocks/messages/block_messages.php')) {
+
+            // Delete instances.
+            $instances = $DB->get_records_list('block_instances', 'blockname', ['messages']);
+            $instanceids = array_keys($instances);
+
+            if (!empty($instanceids)) {
+                $DB->delete_records_list('block_positions', 'blockinstanceid', $instanceids);
+                $DB->delete_records_list('block_instances', 'id', $instanceids);
+                list($sql, $params) = $DB->get_in_or_equal($instanceids, SQL_PARAMS_NAMED);
+                $params['contextlevel'] = CONTEXT_BLOCK;
+                $DB->delete_records_select('context', "contextlevel=:contextlevel AND instanceid " . $sql, $params);
+
+                $preferences = array();
+                foreach ($instances as $instanceid => $instance) {
+                    $preferences[] = 'block' . $instanceid . 'hidden';
+                    $preferences[] = 'docked_block_instance_' . $instanceid;
+                }
+                $DB->delete_records_list('user_preferences', 'name', $preferences);
+            }
+
+            // Delete the block from the block table.
+            $DB->delete_records('block', array('name' => 'messages'));
+
+            // Remove capabilities.
+            capabilities_cleanup('block_messages');
+
+            // Clean config.
+            unset_all_config_for_plugin('block_messages');
+        }
+
+        upgrade_main_savepoint(true, 2017092202.00);
+    }
+
     return true;
 }
index a9e900f..6d35d85 100644 (file)
@@ -6383,3 +6383,53 @@ function get_user_access_sitewide($userid) {
 
     return $accessdata;
 }
+
+/**
+ * Generates the HTML for a miniature calendar.
+ *
+ * @param array $courses list of course to list events from
+ * @param array $groups list of group
+ * @param array $users user's info
+ * @param int|bool $calmonth calendar month in numeric, default is set to false
+ * @param int|bool $calyear calendar month in numeric, default is set to false
+ * @param string|bool $placement the place/page the calendar is set to appear - passed on the the controls function
+ * @param int|bool $courseid id of the course the calendar is displayed on - passed on the the controls function
+ * @param int $time the unixtimestamp representing the date we want to view, this is used instead of $calmonth
+ *     and $calyear to support multiple calendars
+ * @return string $content return html table for mini calendar
+ * @deprecated since Moodle 3.4. MDL-59333
+ */
+function calendar_get_mini($courses, $groups, $users, $calmonth = false, $calyear = false, $placement = false,
+                           $courseid = false, $time = 0) {
+    global $PAGE;
+
+    debugging('calendar_get_mini() has been deprecated. Please update your code to use calendar_get_view.',
+        DEBUG_DEVELOPER);
+
+    if (!empty($calmonth) && !empty($calyear)) {
+        // Do this check for backwards compatibility.
+        // The core should be passing a timestamp rather than month and year.
+        // If a month and year are passed they will be in Gregorian.
+        // Ensure it is a valid date, else we will just set it to the current timestamp.
+        if (checkdate($calmonth, 1, $calyear)) {
+            $time = make_timestamp($calyear, $calmonth, 1);
+        } else {
+            $time = time();
+        }
+    } else if (empty($time)) {
+        // Get the current date in the calendar type being used.
+        $time = time();
+    }
+
+    if ($courseid == SITEID) {
+        $course = get_site();
+    } else {
+        $course = get_course($courseid);
+    }
+    $calendar = new calendar_information(0, 0, 0, $time);
+    $calendar->prepare_for_view($course, $courses);
+
+    $renderer = $PAGE->get_renderer('core_calendar');
+    list($data, $template) = calendar_get_view($calendar, 'mini');
+    return $renderer->render_from_template($template, $data);
+}
index a498039..6077d59 100644 (file)
@@ -548,19 +548,25 @@ function enrol_add_course_navigation(navigation_node $coursenode, $course) {
  *   so name the fields you really need, which will
  *   be added and uniq'd
  *
+ * If $allaccessible is true, this will additionally return courses that the current user is not
+ * enrolled in, but can access because they are open to the user for other reasons (course view
+ * permission, currently viewing course as a guest, or course allows guest access without
+ * password).
+ *
  * @param string|array $fields
  * @param string $sort
  * @param int $limit max number of courses
  * @param array $courseids the list of course ids to filter by
+ * @param bool $allaccessible Include courses user is not enrolled in, but can access
  * @return array
  */
 function enrol_get_my_courses($fields = null, $sort = 'visible DESC,sortorder ASC',
-                              $limit = 0, $courseids = []) {
-    global $DB, $USER;
+          $limit = 0, $courseids = [], $allaccessible = false) {
+    global $DB, $USER, $CFG;
 
-    // Guest account does not have any courses
-    if (isguestuser() or !isloggedin()) {
-        return(array());
+    // Guest account does not have any enrolled courses.
+    if (!$allaccessible && (isguestuser() or !isloggedin())) {
+        return array();
     }
 
     $basefields = array('id', 'category', 'sortorder',
@@ -621,22 +627,86 @@ function enrol_get_my_courses($fields = null, $sort = 'visible DESC,sortorder AS
         $params = array_merge($params, $courseidsparams);
     }
 
-    //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
+    $courseidsql = "";
+    // Logged-in, non-guest users get their enrolled courses.
+    if (!isguestuser() && isloggedin()) {
+        $courseidsql .= "
+                SELECT DISTINCT e.courseid
+                  FROM {enrol} e
+                  JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
+                 WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1
+                       AND (ue.timeend = 0 OR ue.timeend > :now2)";
+        $params['userid'] = $USER->id;
+        $params['active'] = ENROL_USER_ACTIVE;
+        $params['enabled'] = ENROL_INSTANCE_ENABLED;
+        $params['now1'] = round(time(), -2); // Improves db caching.
+        $params['now2'] = $params['now1'];
+    }
+
+    // When including non-enrolled but accessible courses...
+    if ($allaccessible) {
+        if (is_siteadmin()) {
+            // Site admins can access all courses.
+            $courseidsql = "SELECT DISTINCT c2.id AS courseid FROM {course} c2";
+        } else {
+            // If we used the enrolment as well, then this will be UNIONed.
+            if ($courseidsql) {
+                $courseidsql .= " UNION ";
+            }
+
+            // Include courses with guest access and no password.
+            $courseidsql .= "
+                    SELECT DISTINCT e.courseid
+                      FROM {enrol} e
+                     WHERE e.enrol = 'guest' AND e.password = :emptypass AND e.status = :enabled2";
+            $params['emptypass'] = '';
+            $params['enabled2'] = ENROL_INSTANCE_ENABLED;
+
+            // Include courses where the current user is currently using guest access (may include
+            // those which require a password).
+            $courseids = [];
+            $accessdata = get_user_accessdata($USER->id);
+            foreach ($accessdata['ra'] as $contextpath => $roles) {
+                if (array_key_exists($CFG->guestroleid, $roles)) {
+                    // Work out the course id from context path.
+                    $context = context::instance_by_id(preg_replace('~^.*/~', '', $contextpath));
+                    if ($context instanceof context_course) {
+                        $courseids[$context->instanceid] = true;
+                    }
+                }
+            }
+
+            // Include courses where the current user has moodle/course:view capability.
+            $courses = get_user_capability_course('moodle/course:view', null, false);
+            if (!$courses) {
+                $courses = [];
+            }
+            foreach ($courses as $course) {
+                $courseids[$course->id] = true;
+            }
+
+            // If there are any in either category, list them individually.
+            if ($courseids) {
+                list ($allowedsql, $allowedparams) = $DB->get_in_or_equal(
+                        array_keys($courseids), SQL_PARAMS_NAMED);
+                $courseidsql .= "
+                        UNION
+                       SELECT DISTINCT c3.id AS courseid
+                         FROM {course} c3
+                        WHERE c3.id $allowedsql";
+                $params = array_merge($params, $allowedparams);
+            }
+        }
+    }
+
+    // Note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why
+    // we have the subselect there.
     $sql = "SELECT $coursefields $ccselect
               FROM {course} c
-              JOIN (SELECT DISTINCT e.courseid
-                      FROM {enrol} e
-                      JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
-                     WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)
-                   ) en ON (en.courseid = c.id)
+              JOIN ($courseidsql) en ON (en.courseid = c.id)
            $ccjoin
              WHERE $wheres
           $orderby";
-    $params['userid']  = $USER->id;
-    $params['active']  = ENROL_USER_ACTIVE;
-    $params['enabled'] = ENROL_INSTANCE_ENABLED;
-    $params['now1']    = round(time(), -2); // improves db caching
-    $params['now2']    = $params['now1'];
 
     $courses = $DB->get_records_sql($sql, $params, 0, $limit);
 
index 1ec6554..3d6ff5b 100644 (file)
@@ -568,14 +568,13 @@ abstract class file_system {
      * @return string The MIME type.
      */
     public function mimetype_from_hash($contenthash, $filename) {
-        $pathname = $this->get_remote_path_from_hash($contenthash);
+        $pathname = $this->get_local_path_from_hash($contenthash);
         $mimetype = file_storage::mimetype($pathname, $filename);
 
-        if (!$this->is_file_readable_locally_by_hash($contenthash, false) && $mimetype === 'document/unknown') {
+        if ($mimetype === 'document/unknown' && !$this->is_file_readable_locally_by_hash($contenthash)) {
             // The type is unknown, but the full checks weren't completed because the file isn't locally available.
             // Ensure we have a local copy and try again.
             $pathname = $this->get_local_path_from_hash($contenthash, true);
-
             $mimetype = file_storage::mimetype_from_file($pathname);
         }
 
@@ -593,18 +592,7 @@ abstract class file_system {
             // Files with an empty filesize are treated as directories and have no mimetype.
             return null;
         }
-        $pathname = $this->get_remote_path_from_storedfile($file);
-        $mimetype = file_storage::mimetype($pathname, $file->get_filename());
-
-        if (!$this->is_file_readable_locally_by_storedfile($file) && $mimetype === 'document/unknown') {
-            // The type is unknown, but the full checks weren't completed because the file isn't locally available.
-            // Ensure we have a local copy and try again.
-            $pathname = $this->get_local_path_from_storedfile($file, true);
-
-            $mimetype = file_storage::mimetype_from_file($pathname);
-        }
-
-        return $mimetype;
+        return $this->mimetype_from_hash($file->get_contenthash(), $file->get_filename());
     }
 
     /**
index 1331246..36f186f 100644 (file)
@@ -138,6 +138,7 @@ class stored_file {
     protected function update($dataobject) {
         global $DB;
         $updatereferencesneeded = false;
+        $updatemimetype = false;
         $keys = array_keys((array)$this->file_record);
         $filepreupdate = clone($this->file_record);
         foreach ($dataobject as $field => $value) {
@@ -202,6 +203,10 @@ class stored_file {
                     $updatereferencesneeded = true;
                 }
 
+                if ($updatereferencesneeded || ($field === 'filename' && $this->file_record->filename != $value)) {
+                    $updatemimetype = true;
+                }
+
                 // adding the field
                 $this->file_record->$field = $value;
             } else {
@@ -209,8 +214,10 @@ class stored_file {
             }
         }
         // Validate mimetype field
-        $mimetype = $this->filesystem->mimetype_from_storedfile($this);
-        $this->file_record->mimetype = $mimetype;
+        if ($updatemimetype) {
+            $mimetype = $this->filesystem->mimetype_from_storedfile($this);
+            $this->file_record->mimetype = $mimetype;
+        }
 
         $DB->update_record('files', $this->file_record);
         if ($updatereferencesneeded) {
index adf539f..038cad1 100644 (file)
@@ -951,14 +951,13 @@ class core_files_file_system_testcase extends advanced_testcase {
      * a locally available file whose filename does not suggest mimetype.
      */
     public function test_mimetype_from_hash_using_file_content() {
-        $filepath = '/path/to/file/not/currently/on/disk';
         $filecontent = 'example content';
         $contenthash = file_storage::hash_from_string($filecontent);
         $filename = 'example';
 
         $filepath = __DIR__ . "/fixtures/testimage.jpg";
-        $fs = $this->get_testable_mock(['get_remote_path_from_hash']);
-        $fs->method('get_remote_path_from_hash')->willReturn($filepath);
+        $fs = $this->get_testable_mock(['get_local_path_from_hash']);
+        $fs->method('get_local_path_from_hash')->willReturn($filepath);
 
         $result = $fs->mimetype_from_hash($contenthash, $filename);
         $this->assertEquals('image/jpeg', $result);
@@ -1023,8 +1022,8 @@ class core_files_file_system_testcase extends advanced_testcase {
      */
     public function test_mimetype_from_storedfile_using_file_content() {
         $filepath = __DIR__ . "/fixtures/testimage.jpg";
-        $fs = $this->get_testable_mock(['get_remote_path_from_storedfile']);
-        $fs->method('get_remote_path_from_storedfile')->willReturn($filepath);
+        $fs = $this->get_testable_mock(['get_local_path_from_hash']);
+        $fs->method('get_local_path_from_hash')->willReturn($filepath);
 
         $file = $this->get_stored_file('example content');
 
@@ -1040,14 +1039,12 @@ class core_files_file_system_testcase extends advanced_testcase {
         $filepath = __DIR__ . "/fixtures/testimage.jpg";
 
         $fs = $this->get_testable_mock([
-            'get_remote_path_from_storedfile',
-            'is_file_readable_locally_by_storedfile',
-            'get_local_path_from_storedfile',
+            'is_file_readable_locally_by_hash',
+            'get_local_path_from_hash',
         ]);
 
-        $fs->method('get_remote_path_from_storedfile')->willReturn('/path/to/remote/file');
-        $fs->method('is_file_readable_locally_by_storedfile')->willReturn(false);
-        $fs->method('get_local_path_from_storedfile')->willReturn($filepath);
+        $fs->method('is_file_readable_locally_by_hash')->willReturn(false);
+        $fs->method('get_local_path_from_hash')->will($this->onConsecutiveCalls('/path/to/remote/file', $filepath));
 
         $file = $this->get_stored_file('example content');
 
index 4bebfd5..2dcbe2b 100644 (file)
@@ -323,6 +323,10 @@ class filetypes_util {
         $others = [];
 
         foreach (core_filetypes::get_types() as $extension => $info) {
+            // Reserved for unknown file types. Not available here.
+            if ($extension === 'xxx') {
+                continue;
+            }
             $extension = '.'.$extension;
             if ($onlytypes && !$this->is_whitelisted($extension, $onlytypes)) {
                 continue;
@@ -496,6 +500,8 @@ class filetypes_util {
             if ($type === '*') {
                 // Any file is considered as a known type.
                 continue;
+            } else if ($type === '.xxx') {
+                $unknown[$type] = true;
             } else if ($this->is_filetype_group($type)) {
                 // The type is a group that exists.
                 continue;
index 68eba3a..7fb1b5f 100644 (file)
@@ -288,6 +288,17 @@ class filetypes_util_testcase extends advanced_testcase {
             }
         }
 
+        // Confirm that the reserved type '.xxx' isn't present in the 'Other files' section.
+        $types = array_reduce($data, function($carry, $group) {
+            if ($group->name === 'Other files') {
+                return $group->types;
+            }
+        });
+        $typekeys = array_map(function($type) {
+            return $type->key;
+        }, $types);
+        $this->assertNotContains('.xxx', $typekeys);
+
         // All these three files are in both "image" and also "web_image"
         // groups. We display both groups.
         $data = $util->data_for_browser('jpg png gif', true, '.gif');
@@ -456,6 +467,10 @@ class filetypes_util_testcase extends advanced_testcase {
                 'filetypes' => '.txt application/xml web_file ©ç√√ß∂å√©åß©√ .png ricefield/rat document',
                 'expected' => ['.©ç√√ß∂å√©åß©√', 'ricefield/rat']
             ],
+            'Reserved file type xxx included' => [
+                'filetypes' => '.xxx .html .jpg',
+                'expected' => ['.xxx']
+            ]
         ];
     }
 
index 84889e2..faa9f6c 100644 (file)
@@ -40,15 +40,38 @@ class processor implements  \core_analytics\classifier, \core_analytics\regresso
      */
     const REQUIRED_PIP_PACKAGE_VERSION = '0.0.2';
 
+    /**
+     * The path to the Python bin.
+     *
+     * @var string
+     */
+    protected $pathtopython;
+
+    /**
+     * The constructor.
+     */
+    public function __construct() {
+        global $CFG;
+
+        // Set the python location if there is a value.
+        if (!empty($CFG->pathtopython)) {
+            $this->pathtopython = $CFG->pathtopython;
+        }
+    }
+
     /**
      * Is the plugin ready to be used?.
      *
-     * @return bool
+     * @return bool|string Returns true on success, a string detailing the error otherwise
      */
     public function is_ready() {
+        if (empty($this->pathtopython)) {
+            $settingurl = new \moodle_url('/admin/settings.php', array('section' => 'systempaths'));
+            return get_string('pythonpathnotdefined', 'mlbackend_python', $settingurl->out());
+        }
 
         // Check the installed pip package version.
-        $cmd = 'python -m moodlemlbackend.version';
+        $cmd = "{$this->pathtopython} -m moodlemlbackend.version";
 
         $output = null;
         $exitcode = null;
@@ -84,7 +107,7 @@ class processor implements  \core_analytics\classifier, \core_analytics\regresso
         // Obtain the physical route to the file.
         $datasetpath = $this->get_file_path($dataset);
 
-        $cmd = 'python -m moodlemlbackend.training ' .
+        $cmd = "{$this->pathtopython} -m moodlemlbackend.training " .
             escapeshellarg($uniqueid) . ' ' .
             escapeshellarg($outputdir) . ' ' .
             escapeshellarg($datasetpath);
@@ -125,7 +148,7 @@ class processor implements  \core_analytics\classifier, \core_analytics\regresso
         // Obtain the physical route to the file.
         $datasetpath = $this->get_file_path($dataset);
 
-        $cmd = 'python -m moodlemlbackend.prediction ' .
+        $cmd = "{$this->pathtopython} -m moodlemlbackend.prediction " .
             escapeshellarg($uniqueid) . ' ' .
             escapeshellarg($outputdir) . ' ' .
             escapeshellarg($datasetpath);
@@ -168,7 +191,7 @@ class processor implements  \core_analytics\classifier, \core_analytics\regresso
         // Obtain the physical route to the file.
         $datasetpath = $this->get_file_path($dataset);
 
-        $cmd = 'python -m moodlemlbackend.evaluation ' .
+        $cmd = "{$this->pathtopython} -m moodlemlbackend.evaluation " .
             escapeshellarg($uniqueid) . ' ' .
             escapeshellarg($outputdir) . ' ' .
             escapeshellarg($datasetpath) . ' ' .
index fad23ba..a3baa85 100644 (file)
@@ -25,3 +25,4 @@
 $string['packageinstalledshouldbe'] = '"moodlemlbackend" python package should be updated. The required version is "{$a->required}" and the installed version is "{$a->installed}"';
 $string['pluginname'] = 'Python machine learning backend';
 $string['pythonpackagenotinstalled'] = '"moodlemlbackend" python package is not installed or there is a problem with it. Please execute "{$a}" from command line interface for more info';
+$string['pythonpathnotdefined'] = 'The path to your executable Python binary has not been defined. Please visit "{$a}" to set it.';
index da9cb84..280b119 100644 (file)
@@ -134,7 +134,9 @@ class navigation_node implements renderable {
     /** @var bool Set to true if we KNOW that this node can be expanded.  */
     public $isexpandable = false;
     /** @var array */
-    protected $namedtypes = array(0=>'system',10=>'category',20=>'course',30=>'structure',40=>'activity',50=>'resource',60=>'custom',70=>'setting',71=>'siteadmin', 80=>'user');
+    protected $namedtypes = array(0 => 'system', 10 => 'category', 20 => 'course', 30 => 'structure', 40 => 'activity',
+                                  50 => 'resource', 60 => 'custom', 70 => 'setting', 71 => 'siteadmin', 80 => 'user',
+                                  90 => 'container');
     /** @var moodle_url */
     protected static $fullmeurl = null;
     /** @var bool toogles auto matching of active node */
index 04f76fe..f192822 100644 (file)
@@ -1533,6 +1533,7 @@ class core_accesslib_testcase extends advanced_testcase {
 
         $teacherrole = $DB->get_record('role', array('shortname'=>'editingteacher'), '*', MUST_EXIST);
         $studentrole = $DB->get_record('role', array('shortname'=>'student'), '*', MUST_EXIST);
+        $managerrole = $DB->get_record('role', array('shortname' => 'manager'), '*', MUST_EXIST);
         $course = $this->getDataGenerator()->create_course();
         $coursecontext = context_course::instance($course->id);
         $teacherrename = (object)array('roleid'=>$teacherrole->id, 'name'=>'Učitel', 'contextid'=>$coursecontext->id);
@@ -1541,6 +1542,7 @@ class core_accesslib_testcase extends advanced_testcase {
         $roleids = explode(',', $CFG->profileroles); // Should include teacher and student in new installs.
         $this->assertTrue(in_array($teacherrole->id, $roleids));
         $this->assertTrue(in_array($studentrole->id, $roleids));
+        $this->assertFalse(in_array($managerrole->id, $roleids));
 
         $user1 = $this->getDataGenerator()->create_user();
         role_assign($teacherrole->id, $user1->id, $coursecontext->id);
@@ -1548,6 +1550,8 @@ class core_accesslib_testcase extends advanced_testcase {
         $user2 = $this->getDataGenerator()->create_user();
         role_assign($studentrole->id, $user2->id, $coursecontext->id);
         $user3 = $this->getDataGenerator()->create_user();
+        $user4 = $this->getDataGenerator()->create_user();
+        role_assign($managerrole->id, $user4->id, $coursecontext->id);
 
         $roles = get_user_roles_in_course($user1->id, $course->id);
         $this->assertEquals(1, preg_match_all('/,/', $roles, $matches));
@@ -1559,6 +1563,25 @@ class core_accesslib_testcase extends advanced_testcase {
 
         $roles = get_user_roles_in_course($user3->id, $course->id);
         $this->assertSame('', $roles);
+
+        // Managers should be able to see a link to their own role type, given they can assign it in the context.
+        $this->setUser($user4);
+        $roles = get_user_roles_in_course($user4->id, $course->id);
+        $this->assertNotEmpty($roles);
+        $this->assertEquals(1, count(explode(',', $roles)));
+        $this->assertTrue(strpos($roles, role_get_name($managerrole, $coursecontext)) !== false);
+
+        // Managers should see 2 roles if viewing a user who has been enrolled as a student and a teacher in the course.
+        $roles = get_user_roles_in_course($user1->id, $course->id);
+        $this->assertEquals(2, count(explode(',', $roles)));
+        $this->assertTrue(strpos($roles, role_get_name($studentrole, $coursecontext)) !== false);
+        $this->assertTrue(strpos($roles, role_get_name($teacherrole, $coursecontext)) !== false);
+
+        // Students should not see the manager role if viewing a manager's profile.
+        $this->setUser($user2);
+        $roles = get_user_roles_in_course($user4->id, $course->id);
+        $this->assertEmpty($roles); // Should see 0 roles on the manager's profile.
+        $this->assertFalse(strpos($roles, role_get_name($managerrole, $coursecontext)) !== false);
     }
 
     /**
@@ -3166,6 +3189,143 @@ class core_accesslib_testcase extends advanced_testcase {
         $this->assertTrue(array_key_exists($student->id, $users));
         $this->assertFalse(array_key_exists($guest->id, $users));
     }
+
+    /**
+     * Test the get_profile_roles() function.
+     */
+    public function test_get_profile_roles() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        // Assign a student role.
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+        $user1 = $this->getDataGenerator()->create_user();
+        role_assign($studentrole->id, $user1->id, $coursecontext);
+
+        // Assign an editing teacher role.
+        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'), '*', MUST_EXIST);
+        $user2 = $this->getDataGenerator()->create_user();
+        role_assign($teacherrole->id, $user2->id, $coursecontext);
+
+        // Create a custom role that can be assigned at course level, but don't assign it yet.
+        create_role('Custom role', 'customrole', 'Custom course role');
+        $customrole = $DB->get_record('role', array('shortname' => 'customrole'), '*', MUST_EXIST);
+        set_role_contextlevels($customrole->id, [CONTEXT_COURSE]);
+        allow_assign($teacherrole->id, $customrole->id); // Allow teacher to assign the role in the course.
+
+        // Set the site policy 'profileroles' to show student, teacher and non-editing teacher roles (i.e. not the custom role).
+        $neteacherrole = $DB->get_record('role', array('shortname' => 'teacher'), '*', MUST_EXIST);
+        set_config('profileroles', "{$studentrole->id}, {$teacherrole->id}, {$neteacherrole->id}");
+
+        // A student in the course (given they can't assign roles) should see those roles which are:
+        // - listed in the 'profileroles' site policy AND
+        // - are assigned in the course context (or parent contexts).
+        // In this case, the non-editing teacher role is not assigned and should not be returned.
+        $expected = [
+            $teacherrole->id => (object) [
+                'id' => $teacherrole->id,
+                'name' => '',
+                'shortname' => $teacherrole->shortname,
+                'sortorder' => $teacherrole->sortorder,
+                'coursealias' => null
+            ],
+            $studentrole->id => (object) [
+                'id' => $studentrole->id,
+                'name' => '',
+                'shortname' => $studentrole->shortname,
+                'sortorder' => $studentrole->sortorder,
+                'coursealias' => null
+            ]
+        ];
+        $this->setUser($user1);
+        $this->assertEquals($expected, get_profile_roles($coursecontext));
+
+        // An editing teacher should also see only 2 roles at this stage as only 2 roles are assigned: 'teacher' and 'student'.
+        $this->setUser($user2);
+        $this->assertEquals($expected, get_profile_roles($coursecontext));
+
+        // Assign a custom role in the course.
+        $user3 = $this->getDataGenerator()->create_user();
+        role_assign($customrole->id, $user3->id, $coursecontext);
+
+        // Confirm that the teacher can see the custom role now that it's assigned.
+        $expectedteacher = [
+            $teacherrole->id => (object) [
+                'id' => $teacherrole->id,
+                'name' => '',
+                'shortname' => $teacherrole->shortname,
+                'sortorder' => $teacherrole->sortorder,
+                'coursealias' => null
+            ],
+            $studentrole->id => (object) [
+                'id' => $studentrole->id,
+                'name' => '',
+                'shortname' => $studentrole->shortname,
+                'sortorder' => $studentrole->sortorder,
+                'coursealias' => null
+            ],
+            $customrole->id => (object) [
+                'id' => $customrole->id,
+                'name' => 'Custom role',
+                'shortname' => $customrole->shortname,
+                'sortorder' => $customrole->sortorder,
+                'coursealias' => null
+            ]
+        ];
+        $this->setUser($user2);
+        $this->assertEquals($expectedteacher, get_profile_roles($coursecontext));
+
+        // And that the student can't, because the role isn't included in the 'profileroles' site policy.
+        $expectedstudent = [
+            $teacherrole->id => (object) [
+                'id' => $teacherrole->id,
+                'name' => '',
+                'shortname' => $teacherrole->shortname,
+                'sortorder' => $teacherrole->sortorder,
+                'coursealias' => null
+            ],
+            $studentrole->id => (object) [
+                'id' => $studentrole->id,
+                'name' => '',
+                'shortname' => $studentrole->shortname,
+                'sortorder' => $studentrole->sortorder,
+                'coursealias' => null
+            ]
+        ];
+        $this->setUser($user1);
+        $this->assertEquals($expectedstudent, get_profile_roles($coursecontext));
+
+        // If we have no roles listed in the site policy, the teacher should be able to see the assigned roles.
+        $expectedteacher = [
+            $studentrole->id => (object) [
+                'id' => $studentrole->id,
+                'name' => '',
+                'shortname' => $studentrole->shortname,
+                'sortorder' => $studentrole->sortorder,
+                'coursealias' => null
+            ],
+            $customrole->id => (object) [
+                'id' => $customrole->id,
+                'name' => 'Custom role',
+                'shortname' => $customrole->shortname,
+                'sortorder' => $customrole->sortorder,
+                'coursealias' => null
+            ],
+            $teacherrole->id => (object) [
+                'id' => $teacherrole->id,
+                'name' => '',
+                'shortname' => $teacherrole->shortname,
+                'sortorder' => $teacherrole->sortorder,
+                'coursealias' => null
+            ],
+        ];
+        set_config('profileroles', "");
+        $this->setUser($user2);
+        $this->assertEquals($expectedteacher, get_profile_roles($coursecontext));
+    }
 }
 
 /**
index 085e7d9..0848ee8 100644 (file)
@@ -56,6 +56,7 @@ class core_navigationlib_testcase extends advanced_testcase {
         $demo4 = $demo3->add('demo4', $inactiveurl, navigation_node::TYPE_COURSE,  null, 'demo4', new pix_icon('i/course', ''));
         $demo5 = $demo3->add('demo5', $activeurl, navigation_node::TYPE_COURSE, null, 'demo5', new pix_icon('i/course', ''));
         $demo5->add('activity1', null, navigation_node::TYPE_ACTIVITY, null, 'activity1')->make_active();
+        $demo6 = $demo3->add('demo6', null, navigation_node::TYPE_CONTAINER, 'container node test', 'demo6');
         $hiddendemo1 = $this->node->add('hiddendemo1', $inactiveurl, navigation_node::TYPE_CATEGORY, null, 'hiddendemo1', new pix_icon('i/course', ''));
         $hiddendemo1->hidden = true;
         $hiddendemo1->add('hiddendemo2', $inactiveurl, navigation_node::TYPE_COURSE, null, 'hiddendemo2', new pix_icon('i/course', ''))->helpbutton = 'Here is a help button';
@@ -239,9 +240,11 @@ class core_navigationlib_testcase extends advanced_testcase {
         $csstype2 = $this->node->get('demo3')->get('demo5')->get_css_type();
         $this->node->get('demo3')->get('demo5')->type = 1000;
         $csstype3 = $this->node->get('demo3')->get('demo5')->get_css_type();
+        $csstype4 = $this->node->get('demo3')->get('demo6')->get_css_type();
         $this->assertSame('type_category', $csstype1);
         $this->assertSame('type_course', $csstype2);
         $this->assertSame('type_unknown', $csstype3);
+        $this->assertSame('type_container', $csstype4);
     }
 
     public function test_node_make_active() {
index f7b9e21..246ecf7 100644 (file)
@@ -130,10 +130,10 @@ class login_signup_form extends moodleform implements renderable, templatable {
                 $challenge_field = $this->_form->_submitValues['recaptcha_challenge_field'];
                 $response_field = $this->_form->_submitValues['recaptcha_response_field'];
                 if (true !== ($result = $recaptcha_element->verify($challenge_field, $response_field))) {
-                    $errors['recaptcha'] = $result;
+                    $errors['recaptcha_element'] = get_string('incorrectpleasetryagain', 'auth');
                 }
             } else {
-                $errors['recaptcha'] = get_string('missingrecaptchachallengefield');
+                $errors['recaptcha_element'] = get_string('missingrecaptchachallengefield');
             }
         }
 
index 17e342b..d75de5a 100644 (file)
@@ -360,7 +360,7 @@ class assign_submission_onlinetext extends assign_submission_plugin {
                 require_once($CFG->libdir . '/plagiarismlib.php');
 
                 $plagiarismlinks .= plagiarism_get_links(array('userid' => $submission->userid,
-                    'content' => trim($text),
+                    'content' => trim($onlinetextsubmission->onlinetext),
                     'cmid' => $this->assignment->get_course_module()->id,
                     'course' => $this->assignment->get_course()->id,
                     'assignment' => $submission->assignment));
@@ -444,7 +444,7 @@ class assign_submission_onlinetext extends assign_submission_plugin {
                 require_once($CFG->libdir . '/plagiarismlib.php');
 
                 $plagiarismlinks .= plagiarism_get_links(array('userid' => $submission->userid,
-                    'content' => trim($result),
+                    'content' => trim($onlinetextsubmission->onlinetext),
                     'cmid' => $this->assignment->get_course_module()->id,
                     'course' => $this->assignment->get_course()->id,
                     'assignment' => $submission->assignment));
index 0e1dcd1..da2b4b2 100644 (file)
@@ -594,7 +594,8 @@ class mod_chat_external extends external_api {
                             'intro' => new external_value(PARAM_RAW, 'The Chat intro'),
                             'introformat' => new external_format_value('intro'),
                             'introfiles' => new external_files('Files in the introduction text', VALUE_OPTIONAL),
-                            'chatmethod' => new external_value(PARAM_ALPHA, 'chat method (sockets, daemon)', VALUE_OPTIONAL),
+                            'chatmethod' => new external_value(PARAM_PLUGIN, 'chat method (sockets, ajax, header_js)',
+                                VALUE_OPTIONAL),
                             'keepdays' => new external_value(PARAM_INT, 'keep days', VALUE_OPTIONAL),
                             'studentlogs' => new external_value(PARAM_INT, 'student logs visible to everyone', VALUE_OPTIONAL),
                             'chattime' => new external_value(PARAM_INT, 'chat time', VALUE_OPTIONAL),
index e9698d2..f17fb2e 100644 (file)
@@ -223,9 +223,13 @@ class mod_chat_external_testcase extends externallib_advanced_testcase {
      * Test get_chats_by_courses
      */
     public function test_get_chats_by_courses() {
-        global $DB, $USER;
+        global $DB, $USER, $CFG;
         $this->resetAfterTest(true);
         $this->setAdminUser();
+
+        // Set global chat method.
+        $CFG->chat_method = 'header_js';
+
         $course1 = self::getDataGenerator()->create_course();
         $chatoptions1 = array(
                               'course' => $course1->id,
@@ -272,6 +276,7 @@ class mod_chat_external_testcase extends externallib_advanced_testcase {
 
         $this->assertCount(1, $chats['chats']);
         $this->assertEquals('Second Chat', $chats['chats'][0]['name']);
+        $this->assertEquals('header_js', $chats['chats'][0]['chatmethod']);
         // We see 17 fields.
         $this->assertCount(17, $chats['chats'][0]);
         // As an Admin you can see some chat properties like 'section'.
index 9b1cbfe..91db243 100644 (file)
@@ -100,6 +100,7 @@ $string['notenrolledchoose'] = 'Sorry, only enrolled users are allowed to make c
 $string['notopenyet'] = 'Sorry, this activity is not available until {$a}';
 $string['numberofuser'] = 'Number of responses';
 $string['numberofuserinpercentage'] = 'Percentage of responses';
+$string['openafterclose'] = 'You have specified an open date after the close date';
 $string['option'] = 'Option';
 $string['optionno'] = 'Option {no}';
 $string['options'] = 'Options';
index 2689910..85441c3 100644 (file)
@@ -1240,6 +1240,76 @@ function mod_choice_core_calendar_provide_event_action(calendar_event $event,
     );
 }
 
+/**
+ * This function will check that the given event is valid for it's
+ * corresponding choice module.
+ *
+ * An exception is thrown if the event fails validation.
+ *
+ * @throws \moodle_exception
+ * @param \calendar_event $event
+ * @return bool
+ */
+function mod_choice_core_calendar_validate_event_timestart(\calendar_event $event) {
+    global $DB;
+
+    $record = $DB->get_record('choice', ['id' => $event->instance], '*', MUST_EXIST);
+
+    if ($event->eventtype == CHOICE_EVENT_TYPE_OPEN) {
+        // The start time of the open event can't be equal to or after the
+        // close time of the choice activity.
+        if (!empty($record->timeclose) && $event->timestart > $record->timeclose) {
+            throw new \moodle_exception('openafterclose', 'choice');
+        }
+    } else if ($event->eventtype == CHOICE_EVENT_TYPE_CLOSE) {
+        // The start time of the close event can't be equal to or earlier than the
+        // open time of the choice activity.
+        if (!empty($record->timeopen) && $event->timestart < $record->timeopen) {
+            throw new \moodle_exception('closebeforeopen', 'choice');
+        }
+    }
+
+    return true;
+}
+
+/**
+ * This function will update the choice module according to the
+ * event that has been modified.
+ *
+ * It will set the timeopen or timeclose value of the choice instance
+ * according to the type of event provided.
+ *
+ * @throws \moodle_exception
+ * @param \calendar_event $event
+ */
+function mod_choice_core_calendar_event_timestart_updated(\calendar_event $event) {
+    global $DB;
+
+    if ($event->eventtype == CHOICE_EVENT_TYPE_OPEN) {
+        // If the event is for the choice activity opening then we should
+        // set the start time of the choice activity to be the new start
+        // time of the event.
+        $record = $DB->get_record('choice', ['id' => $event->instance], '*', MUST_EXIST);
+
+        if ($record->timeopen != $event->timestart) {
+            $record->timeopen = $event->timestart;
+            $record->timemodified = time();
+            $DB->update_record('choice', $record);
+        }
+    } else if ($event->eventtype == CHOICE_EVENT_TYPE_CLOSE) {
+        // If the event is for the choice activity closing then we should
+        // set the end time of the choice activity to be the new start
+        // time of the event.
+        $record = $DB->get_record('choice', ['id' => $event->instance], '*', MUST_EXIST);
+
+        if ($record->timeclose != $event->timestart) {
+            $record->timeclose = $event->timestart;
+            $record->timemodified = time();
+            $DB->update_record('choice', $record);
+        }
+    }
+}
+
 /**
  * Get icon mapping for font-awesome.
  */
index 118a7d7..5d1c19c 100644 (file)
@@ -346,12 +346,13 @@ class mod_choice_lib_testcase extends externallib_advanced_testcase {
         // Create a course.
         $course = $this->getDataGenerator()->create_course();
 
+        $timeclose = time() - DAYSECS;
         // Create a choice.
         $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id,
-            'timeclose' => time() - DAYSECS));
+            'timeclose' => $timeclose));
 
         // Create a calendar event.
-        $event = $this->create_action_event($course->id, $choice->id, CHOICE_EVENT_TYPE_OPEN);
+        $event = $this->create_action_event($course->id, $choice->id, CHOICE_EVENT_TYPE_OPEN, $timeclose - 1);
 
         // Create an action factory.
         $factory = new \core_calendar\action_factory();
@@ -371,12 +372,15 @@ class mod_choice_lib_testcase extends externallib_advanced_testcase {
         // Create a course.
         $course = $this->getDataGenerator()->create_course();
 
+        $timeopen = time() + DAYSECS;
+        $timeclose = $timeopen + DAYSECS;
+
         // Create a choice.
         $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id,
-            'timeopen' => time() + DAYSECS));
+            'timeopen' => $timeopen, 'timeclose' => $timeclose));
 
         // Create a calendar event.
-        $event = $this->create_action_event($course->id, $choice->id, CHOICE_EVENT_TYPE_OPEN);
+        $event = $this->create_action_event($course->id, $choice->id, CHOICE_EVENT_TYPE_OPEN, $timeopen);
 
         // Create an action factory.
         $factory = new \core_calendar\action_factory();
@@ -426,9 +430,10 @@ class mod_choice_lib_testcase extends externallib_advanced_testcase {
      * @param int $courseid
      * @param int $instanceid The choice id.
      * @param string $eventtype The event type. eg. CHOICE_EVENT_TYPE_OPEN.
+     * @param int|null $timestart The start timestamp for the event
      * @return bool|calendar_event
      */
-    private function create_action_event($courseid, $instanceid, $eventtype) {
+    private function create_action_event($courseid, $instanceid, $eventtype, $timestart = null) {
         $event = new stdClass();
         $event->name = 'Calendar event';
         $event->modulename = 'choice';
@@ -436,7 +441,12 @@ class mod_choice_lib_testcase extends externallib_advanced_testcase {
         $event->instance = $instanceid;
         $event->type = CALENDAR_EVENT_TYPE_ACTION;
         $event->eventtype = $eventtype;
-        $event->timestart = time();
+
+        if ($timestart) {
+            $event->timestart = $timestart;
+        } else {
+            $event->timestart = time();
+        }
 
         return calendar_event::create($event);
     }
@@ -478,4 +488,330 @@ class mod_choice_lib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(mod_choice_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions);
         $this->assertEquals(mod_choice_get_completion_active_rule_descriptions(new stdClass()), []);
     }
+
+    /**
+     * You can't create a choice module event when the module doesn't exist.
+     */
+    public function test_mod_choice_core_calendar_validate_event_timestart_no_activity() {
+        global $CFG;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'choice',
+            'instance' => 1234,
+            'eventtype' => CHOICE_EVENT_TYPE_OPEN,
+            'timestart' => time(),
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        $this->expectException('moodle_exception');
+        mod_choice_core_calendar_validate_event_timestart($event);
+    }
+
+    /**
+     * A CHOICE_EVENT_TYPE_OPEN must be before the close time of the choice activity.
+     */
+    public function test_mod_choice_core_calendar_validate_event_timestart_valid_open_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $choicegenerator = $generator->get_plugin_generator('mod_choice');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $choice = $choicegenerator->create_instance(['course' => $course->id]);
+        $choice->timeopen = $timeopen;
+        $choice->timeclose = $timeclose;
+        $DB->update_record('choice', $choice);
+
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'choice',
+            'instance' => $choice->id,
+            'eventtype' => CHOICE_EVENT_TYPE_OPEN,
+            'timestart' => $timeopen,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        $this->assertTrue(mod_choice_core_calendar_validate_event_timestart($event));
+    }
+
+    /**
+     * A CHOICE_EVENT_TYPE_OPEN can not have a start time set after the close time
+     * of the choice activity.
+     */
+    public function test_mod_choice_core_calendar_validate_event_timestart_invalid_open_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $choicegenerator = $generator->get_plugin_generator('mod_choice');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $choice = $choicegenerator->create_instance(['course' => $course->id]);
+        $choice->timeopen = $timeopen;
+        $choice->timeclose = $timeclose;
+        $DB->update_record('choice', $choice);
+
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'choice',
+            'instance' => $choice->id,
+            'eventtype' => CHOICE_EVENT_TYPE_OPEN,
+            'timestart' => $timeclose + 1,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        $this->expectException('moodle_exception');
+        mod_choice_core_calendar_validate_event_timestart($event);
+    }
+
+    /**
+     * A CHOICE_EVENT_TYPE_CLOSE must be after the open time of the choice activity.
+     */
+    public function test_mod_choice_core_calendar_validate_event_timestart_valid_close_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $choicegenerator = $generator->get_plugin_generator('mod_choice');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $choice = $choicegenerator->create_instance(['course' => $course->id]);
+        $choice->timeopen = $timeopen;
+        $choice->timeclose = $timeclose;
+        $DB->update_record('choice', $choice);
+
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'choice',
+            'instance' => $choice->id,
+            'eventtype' => CHOICE_EVENT_TYPE_CLOSE,
+            'timestart' => $timeclose,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        $this->assertTrue(mod_choice_core_calendar_validate_event_timestart($event));
+    }
+
+    /**
+     * A CHOICE_EVENT_TYPE_CLOSE can not have a start time set before the open time
+     * of the choice activity.
+     */
+    public function test_mod_choice_core_calendar_validate_event_timestart_invalid_close_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $choicegenerator = $generator->get_plugin_generator('mod_choice');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $choice = $choicegenerator->create_instance(['course' => $course->id]);
+        $choice->timeopen = $timeopen;
+        $choice->timeclose = $timeclose;
+        $DB->update_record('choice', $choice);
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'choice',
+            'instance' => $choice->id,
+            'eventtype' => CHOICE_EVENT_TYPE_CLOSE,
+            'timestart' => $timeopen - 1,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        $this->expectException('moodle_exception');
+        mod_choice_core_calendar_validate_event_timestart($event);
+    }
+
+    /**
+     * An unkown event type should not change the choice instance.
+     */
+    public function test_mod_choice_core_calendar_event_timestart_updated_unknown_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $choicegenerator = $generator->get_plugin_generator('mod_choice');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $choice = $choicegenerator->create_instance(['course' => $course->id]);
+        $choice->timeopen = $timeopen;
+        $choice->timeclose = $timeclose;
+        $DB->update_record('choice', $choice);
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'choice',
+            'instance' => $choice->id,
+            'eventtype' => CHOICE_EVENT_TYPE_OPEN . "SOMETHING ELSE",
+            'timestart' => 1,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        mod_choice_core_calendar_event_timestart_updated($event);
+
+        $choice = $DB->get_record('choice', ['id' => $choice->id]);
+        $this->assertEquals($timeopen, $choice->timeopen);
+        $this->assertEquals($timeclose, $choice->timeclose);
+    }
+
+    /**
+     * A CHOICE_EVENT_TYPE_OPEN event should update the timeopen property of
+     * the choice activity.
+     */
+    public function test_mod_choice_core_calendar_event_timestart_updated_open_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $choicegenerator = $generator->get_plugin_generator('mod_choice');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $timemodified = 1;
+        $newtimeopen = $timeopen - DAYSECS;
+        $choice = $choicegenerator->create_instance(['course' => $course->id]);
+        $choice->timeopen = $timeopen;
+        $choice->timeclose = $timeclose;
+        $choice->timemodified = $timemodified;
+        $DB->update_record('choice', $choice);
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'choice',
+            'instance' => $choice->id,
+            'eventtype' => CHOICE_EVENT_TYPE_OPEN,
+            'timestart' => $newtimeopen,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        mod_choice_core_calendar_event_timestart_updated($event);
+
+        $choice = $DB->get_record('choice', ['id' => $choice->id]);
+        // Ensure the timeopen property matches the event timestart.
+        $this->assertEquals($newtimeopen, $choice->timeopen);
+        // Ensure the timeclose isn't changed.
+        $this->assertEquals($timeclose, $choice->timeclose);
+        // Ensure the timemodified property has been changed.
+        $this->assertNotEquals($timemodified, $choice->timemodified);
+    }
+
+    /**
+     * A CHOICE_EVENT_TYPE_CLOSE event should update the timeclose property of
+     * the choice activity.
+     */
+    public function test_mod_choice_core_calendar_event_timestart_updated_close_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $choicegenerator = $generator->get_plugin_generator('mod_choice');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $timemodified = 1;
+        $newtimeclose = $timeclose + DAYSECS;
+        $choice = $choicegenerator->create_instance(['course' => $course->id]);
+        $choice->timeopen = $timeopen;
+        $choice->timeclose = $timeclose;
+        $choice->timemodified = $timemodified;
+        $DB->update_record('choice', $choice);
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'choice',
+            'instance' => $choice->id,
+            'eventtype' => CHOICE_EVENT_TYPE_CLOSE,
+            'timestart' => $newtimeclose,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        mod_choice_core_calendar_event_timestart_updated($event);
+
+        $choice = $DB->get_record('choice', ['id' => $choice->id]);
+        // Ensure the timeclose property matches the event timestart.
+        $this->assertEquals($newtimeclose, $choice->timeclose);
+        // Ensure the timeopen isn't changed.
+        $this->assertEquals($timeopen, $choice->timeopen);
+        // Ensure the timemodified property has been changed.
+        $this->assertNotEquals($timemodified, $choice->timemodified);
+    }
 }
index 8c49f47..2241ebc 100644 (file)
@@ -40,7 +40,7 @@ class mod_choice_restore_date_testcase extends restore_date_testcase {
         global $DB, $USER;
 
         $time = 100000;
-        $record = ['timeopen' => $time, 'timeclose' => $time];
+        $record = ['timeopen' => $time, 'timeclose' => $time + 1];
         list($course, $choice) = $this->create_course_and_module('choice', $record);
 
         $options = $DB->get_records('choice_options', ['choiceid' => $choice->id]);
index 1d40e4b..e4bb89b 100644 (file)
@@ -61,11 +61,11 @@ if ($mform->is_cancelled()) {
 
 } else if ($formdata = $mform->get_data()) {
     $formdata = file_postupdate_standard_filemanager($formdata, 'files', $options, $context, 'mod_folder', 'content', 0);
-    $DB->set_field('folder', 'revision', $folder->revision+1, array('id'=>$folder->id));
-
-    // Update the variable of the folder revision so we can pass it as an accurate snapshot later.
+    $folder->timemodified = time();
     $folder->revision = $folder->revision + 1;
 
+    $DB->update_record('folder', $folder);
+
     $params = array(
         'context' => $context,
         'objectid' => $folder->id
index 5d2c7df..841dc2e 100644 (file)
@@ -535,4 +535,177 @@ class mod_workshop_external extends external_api {
             'warnings' => new external_warnings()
         ));
     }
+
+    /**
+     * Returns the description of the external function parameters.
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.4
+     */
+    public static function update_submission_parameters() {
+        return new external_function_parameters(array(
+            'submissionid' => new external_value(PARAM_INT, 'Submission id'),
+            'title' => new external_value(PARAM_TEXT, 'Submission title'),
+            'content' => new external_value(PARAM_RAW, 'Submission text content', VALUE_DEFAULT, ''),
+            'contentformat' => new external_value(PARAM_INT, 'The format used for the content', VALUE_DEFAULT, FORMAT_MOODLE),
+            'inlineattachmentsid' => new external_value(PARAM_INT, 'The draft file area id for inline attachments in the content',
+                VALUE_DEFAULT, 0),
+            'attachmentsid' => new external_value(PARAM_INT, 'The draft file area id for attachments', VALUE_DEFAULT, 0),
+        ));
+    }
+
+
+    /**
+     * Updates the given submission.
+     *
+     * @param int $submissionid         the submission id
+     * @param string $title             the submission title
+     * @param string  $content          the submission text content
+     * @param int  $contentformat       the format used for the content
+     * @param int $inlineattachmentsid  the draft file area id for inline attachments in the content
+     * @param int $attachmentsid        the draft file area id for attachments
+     * @return array whether the submission was updated and warnings.
+     * @since Moodle 3.4
+     * @throws moodle_exception
+     */
+    public static function update_submission($submissionid, $title, $content = '', $contentformat = FORMAT_MOODLE,
+            $inlineattachmentsid = 0, $attachmentsid = 0) {
+        global $USER, $DB;
+
+        $params = self::validate_parameters(self::update_submission_parameters(), array(
+            'submissionid' => $submissionid,
+            'title' => $title,
+            'content' => $content,
+            'contentformat' => $contentformat,
+            'inlineattachmentsid' => $inlineattachmentsid,
+            'attachmentsid' => $attachmentsid,
+        ));
+        $warnings = array();
+
+        // Get and validate the submission and workshop.
+        $submission = $DB->get_record('workshop_submissions', array('id' => $params['submissionid']), '*', MUST_EXIST);
+        list($workshop, $course, $cm, $context) = self::validate_workshop($submission->workshopid);
+        require_capability('mod/workshop:submit', $context);
+
+        // Check if we can update the submission.
+        $canupdatesubmission = $submission->authorid == $USER->id;
+        $canupdatesubmission = $canupdatesubmission && $workshop->modifying_submission_allowed($USER->id);
+        $canupdatesubmission = $canupdatesubmission && $workshop->check_examples_assessed($USER->id);
+        if (!$canupdatesubmission) {
+            throw new moodle_exception('nopermissions', 'error', '', 'update submission');
+        }
+
+        // Prepare the submission object.
+        $submission->title = trim($params['title']);
+        if (empty($submission->title)) {
+            throw new moodle_exception('errorinvalidparam', 'webservice', '', 'title');
+        }
+        $submission->content_editor = array(
+            'text' => $params['content'],
+            'format' => $params['contentformat'],
+            'itemid' => $params['inlineattachmentsid'],
+        );
+        $submission->attachment_filemanager = $params['attachmentsid'];
+
+        $errors = $workshop->validate_submission_data((array) $submission);
+        // We can get several errors, return them in warnings.
+        if (!empty($errors)) {
+            $status = false;
+            foreach ($errors as $itemname => $message) {
+                $warnings[] = array(
+                    'item' => $itemname,
+                    'itemid' => 0,
+                    'warningcode' => 'fielderror',
+                    'message' => s($message)
+                );
+            }
+        } else {
+            $status = true;
+            $submission->id = $workshop->edit_submission($submission);
+        }
+
+        return array(
+            'status' => $status,
+            'warnings' => $warnings
+        );
+    }
+
+    /**
+     * Returns the description of the external function return value.
+     *
+     * @return external_description
+     * @since Moodle 3.4
+     */
+    public static function update_submission_returns() {
+        return new external_single_structure(array(
+            'status' => new external_value(PARAM_BOOL, 'True if the submission was updated false otherwise.'),
+            'warnings' => new external_warnings()
+        ));
+    }
+
+    /**
+     * Returns the description of the external function parameters.
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.4
+     */
+    public static function delete_submission_parameters() {
+        return new external_function_parameters(
+            array(
+                'submissionid' => new external_value(PARAM_INT, 'Submission id'),
+            )
+        );
+    }
+
+
+    /**
+     * Deletes the given submission.
+     *
+     * @param int $submissionid the submission id.
+     * @return array containing the result status and warnings.
+     * @since Moodle 3.4
+     * @throws moodle_exception
+     */
+    public static function delete_submission($submissionid) {
+        global $USER, $DB;
+
+        $params = self::validate_parameters(self::delete_submission_parameters(), array('submissionid' => $submissionid));
+        $warnings = array();
+
+        // Get and validate the submission and workshop.
+        $submission = $DB->get_record('workshop_submissions', array('id' => $params['submissionid']), '*', MUST_EXIST);
+        list($workshop, $course, $cm, $context) = self::validate_workshop($submission->workshopid);
+
+        // Check if we can delete the submission.
+        if (!has_capability('mod/workshop:deletesubmissions', $context)) {
+            require_capability('mod/workshop:submit', $context);
+            // We can delete our own submission, on time and not yet assessed.
+            $candeletesubmission = $submission->authorid == $USER->id;
+            $candeletesubmission = $candeletesubmission && $workshop->modifying_submission_allowed($USER->id);
+            $candeletesubmission = $candeletesubmission && count($workshop->get_assessments_of_submission($submission->id)) == 0;
+            if (!$candeletesubmission) {
+                throw new moodle_exception('nopermissions', 'error', '', 'delete submission');
+            }
+        }
+
+        $workshop->delete_submission($submission);
+
+        return array(
+            'status' => true,
+            'warnings' => $warnings
+        );
+    }
+
+    /**
+     * Returns the description of the external function return value.
+     *
+     * @return external_description
+     * @since Moodle 3.4
+     */
+    public static function delete_submission_returns() {
+        return new external_single_structure(array(
+            'status' => new external_value(PARAM_BOOL, 'True if the submission was deleted.'),
+            'warnings' => new external_warnings()
+        ));
+    }
 }
index 5a19f66..416821a 100644 (file)
@@ -69,4 +69,20 @@ $functions = array(
         'capabilities'  => 'mod/workshop:submit',
         'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
+    'mod_workshop_update_submission' => array(
+        'classname'     => 'mod_workshop_external',
+        'methodname'    => 'update_submission',
+        'description'   => 'Update the given submission.',
+        'type'          => 'write',
+        'capabilities'  => 'mod/workshop:submit',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    ),
+    'mod_workshop_delete_submission' => array(
+        'classname'     => 'mod_workshop_external',
+        'methodname'    => 'delete_submission',
+        'description'   => 'Deletes the given submission.',
+        'type'          => 'write',
+        'capabilities'  => 'mod/workshop:submit',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    ),
 );
index a7454db..a79b623 100644 (file)
@@ -1204,6 +1204,20 @@ class workshop {
         $fs->delete_area_files($this->context->id, 'mod_workshop', 'submission_attachment', $submission->id);
 
         $DB->delete_records('workshop_submissions', array('id' => $submission->id));
+
+        // Event information.
+        $params = array(
+            'context' => $this->context,
+            'courseid' => $this->course->id,
+            'relateduserid' => $submission->authorid,
+            'other' => array(
+                'submissiontitle' => $submission->title
+            )
+        );
+        $params['objectid'] = $submission->id;
+        $event = \mod_workshop\event\submission_deleted::create($params);
+        $event->add_record_snapshot('workshop', $this->dbrecord);
+        $event->trigger();
     }
 
     /**
index 9de6ccb..0772b96 100644 (file)
@@ -119,20 +119,6 @@ if ($submission->id and $delete and $confirm and $deletable) {
     require_sesskey();
     $workshop->delete_submission($submission);
 
-    // Event information.
-    $params = array(
-        'context' => $workshop->context,
-        'courseid' => $workshop->course->id,
-        'relateduserid' => $submission->authorid,
-        'other' => array(
-            'submissiontitle' => $submission->title
-        )
-    );
-    $params['objectid'] = $submission->id;
-    $event = \mod_workshop\event\submission_deleted::create($params);
-    $event->add_record_snapshot('workshop', $workshoprecord);
-    $event->trigger();
-
     redirect($workshop->view_url());
 }
 
index 3247e78..3647f96 100644 (file)
@@ -536,4 +536,258 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('fielderror', $result['warnings'][1]['warningcode']);
         $this->assertEquals('attachment_filemanager', $result['warnings'][1]['item']);
     }
+
+    /**
+     * Helper method to create a submission for testing for the given user.
+     *
+     * @param int $user the submission will be created by this student.
+     * @return int the submission id
+     */
+    protected function create_test_submission($user) {
+        // Test user with full capabilities.
+        $this->setUser($user);
+
+        $title = 'Submission title';
+        $content = 'Submission contents';
+
+        // Create a file in a draft area for inline attachments.
+        $fs = get_file_storage();
+        $draftidinlineattach = file_get_unused_draft_itemid();
+        $usercontext = context_user::instance($this->student->id);
+        $filenameimg = 'shouldbeanimage.txt';
+        $filerecordinline = array(
+            'contextid' => $usercontext->id,
+            'component' => 'user',
+            'filearea'  => 'draft',
+            'itemid'    => $draftidinlineattach,
+            'filepath'  => '/',
+            'filename'  => $filenameimg,
+        );
+        $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
+
+        // Create a file in a draft area for regular attachments.
+        $draftidattach = file_get_unused_draft_itemid();
+        $filerecordattach = $filerecordinline;
+        $attachfilename = 'attachment.txt';
+        $filerecordattach['filename'] = $attachfilename;
+        $filerecordattach['itemid'] = $draftidattach;
+        $fs->create_file_from_string($filerecordattach, 'simple text attachment');
+
+        // Switch to submission phase.
+        $workshop = new workshop($this->workshop, $this->cm, $this->course);
+        $workshop->switch_phase(workshop::PHASE_SUBMISSION);
+
+        $result = mod_workshop_external::add_submission($this->workshop->id, $title, $content, FORMAT_MOODLE, $draftidinlineattach,
+            $draftidattach);
+        return $result['submissionid'];
+    }
+
+    /**
+     * Test test_update_submission.
+     */
+    public function test_update_submission() {
+
+        // Create the submission that will be updated.
+        $submissionid = $this->create_test_submission($this->student);
+
+        // Test user with full capabilities.
+        $this->setUser($this->student);
+
+        $title = 'Submission new title';
+        $content = 'Submission new contents';
+
+        // Create a different file in a draft area for inline attachments.
+        $fs = get_file_storage();
+        $draftidinlineattach = file_get_unused_draft_itemid();
+        $usercontext = context_user::instance($this->student->id);
+        $filenameimg = 'shouldbeanimage_new.txt';
+        $filerecordinline = array(
+            'contextid' => $usercontext->id,
+            'component' => 'user',
+            'filearea'  => 'draft',
+            'itemid'    => $draftidinlineattach,
+            'filepath'  => '/',
+            'filename'  => $filenameimg,
+        );
+        $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
+
+        // Create a different file in a draft area for regular attachments.
+        $draftidattach = file_get_unused_draft_itemid();
+        $filerecordattach = $filerecordinline;
+        $attachfilename = 'attachment_new.txt';
+        $filerecordattach['filename'] = $attachfilename;
+        $filerecordattach['itemid'] = $draftidattach;
+        $fs->create_file_from_string($filerecordattach, 'simple text attachment');
+
+        $result = mod_workshop_external::update_submission($submissionid, $title, $content, FORMAT_MOODLE, $draftidinlineattach,
+            $draftidattach);
+        $result = external_api::clean_returnvalue(mod_workshop_external::update_submission_returns(), $result);
+        $this->assertEmpty($result['warnings']);
+
+        // Check submission updated.
+        $workshop = new workshop($this->workshop, $this->cm, $this->course);
+        $submission = $workshop->get_submission_by_id($submissionid);
+        $this->assertTrue($result['status']);
+        $this->assertEquals($title, $submission->title);
+        $this->assertEquals($content, $submission->content);
+
+        // Check files.
+        $contentfiles = $fs->get_area_files($this->context->id, 'mod_workshop', 'submission_content', $submission->id);
+        $this->assertCount(2, $contentfiles);
+        foreach ($contentfiles as $file) {
+            if ($file->is_directory()) {
+                continue;
+            } else {
+                $this->assertEquals($filenameimg, $file->get_filename());
+            }
+        }
+        $contentfiles = $fs->get_area_files($this->context->id, 'mod_workshop', 'submission_attachment', $submission->id);
+        $this->assertCount(2, $contentfiles);
+        foreach ($contentfiles as $file) {
+            if ($file->is_directory()) {
+                continue;
+            } else {
+                $this->assertEquals($attachfilename, $file->get_filename());
+            }
+        }
+    }
+
+    /**
+     * Test test_update_submission belonging to other user.
+     */
+    public function test_update_submission_of_other_user() {
+        // Create the submission that will be updated.
+        $submissionid = $this->create_test_submission($this->student);
+
+        $this->setUser($this->teacher);
+
+        $this->expectException('moodle_exception');
+        mod_workshop_external::update_submission($submissionid, 'Test');
+    }
+
+    /**
+     * Test test_update_submission invalid phase.
+     */
+    public function test_update_submission_invalid_phase() {
+        // Create the submission that will be updated.
+        $submissionid = $this->create_test_submission($this->student);
+
+        $this->setUser($this->student);
+
+        // Switch to assessment phase.
+        $workshop = new workshop($this->workshop, $this->cm, $this->course);
+        $workshop->switch_phase(workshop::PHASE_ASSESSMENT);
+
+        $this->expectException('moodle_exception');
+        mod_workshop_external::update_submission($submissionid, 'Test');
+    }
+
+    /**
+     * Test test_update_submission empty title.
+     */
+    public function test_update_submission_empty_title() {
+        // Create the submission that will be updated.
+        $submissionid = $this->create_test_submission($this->student);
+
+        $this->setUser($this->student);
+
+        $this->expectException('moodle_exception');
+        mod_workshop_external::update_submission($submissionid, '');
+    }
+
+    /**
+     * Test test_delete_submission.
+     */
+    public function test_delete_submission() {
+
+        // Create the submission that will be deleted.
+        $submissionid = $this->create_test_submission($this->student);
+
+        $this->setUser($this->student);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+
+        $result = mod_workshop_external::delete_submission($submissionid);
+        $result = external_api::clean_returnvalue(mod_workshop_external::delete_submission_returns(), $result);
+        $this->assertEmpty($result['warnings']);
+        $this->assertTrue($result['status']);
+        $workshop = new workshop($this->workshop, $this->cm, $this->course);
+        $submission = $workshop->get_submission_by_author($this->student->id);
+        $this->assertFalse($submission);
+
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = array_shift($events);
+
+        // Checking event.
+        $this->assertInstanceOf('\mod_workshop\event\submission_deleted', $event);
+        $this->assertEquals($this->context, $event->get_context());
+    }
+
+    /**
+     * Test test_delete_submission_with_assessments.
+     */
+    public function test_delete_submission_with_assessments() {
+
+        // Create the submission that will be deleted.
+        $submissionid = $this->create_test_submission($this->student);
+
+        $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
+        $workshopgenerator->create_assessment($submissionid, $this->teacher->id, array(
+            'weight' => 3,
+            'grade' => 95.00000,
+        ));
+
+        $this->setUser($this->student);
+        $this->expectException('moodle_exception');
+        mod_workshop_external::delete_submission($submissionid);
+    }
+
+    /**
+     * Test test_delete_submission_invalid_phase.
+     */
+    public function test_delete_submission_invalid_phase() {
+
+        // Create the submission that will be deleted.
+        $submissionid = $this->create_test_submission($this->student);
+
+        // Switch to assessment phase.
+        $workshop = new workshop($this->workshop, $this->cm, $this->course);
+        $workshop->switch_phase(workshop::PHASE_ASSESSMENT);
+
+        $this->setUser($this->student);
+        $this->expectException('moodle_exception');
+        mod_workshop_external::delete_submission($submissionid);
+    }
+
+    /**
+     * Test test_delete_submission_as_teacher.
+     */
+    public function test_delete_submission_as_teacher() {
+
+        // Create the submission that will be deleted.
+        $submissionid = $this->create_test_submission($this->student);
+
+        $this->setUser($this->teacher);
+        $result = mod_workshop_external::delete_submission($submissionid);
+        $result = external_api::clean_returnvalue(mod_workshop_external::delete_submission_returns(), $result);
+        $this->assertEmpty($result['warnings']);
+        $this->assertTrue($result['status']);
+    }
+
+    /**
+     * Test test_delete_submission_other_user.
+     */
+    public function test_delete_submission_other_user() {
+
+        $anotheruser = self::getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($anotheruser->id, $this->course->id, $this->studentrole->id, 'manual');
+        // Create the submission that will be deleted.
+        $submissionid = $this->create_test_submission($this->student);
+
+        $this->setUser($anotheruser);
+        $this->expectException('moodle_exception');
+        mod_workshop_external::delete_submission($submissionid);
+    }
 }
index 5788945..937d9b4 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2017051505;        // The current module version (YYYYMMDDXX)
+$plugin->version   = 2017051507;        // The current module version (YYYYMMDDXX)
 $plugin->requires  = 2017050500;        // Requires this Moodle version.
 $plugin->component = 'mod_workshop';
 $plugin->cron      = 60;                // Give as a chance every minute.
index bc7d8a1..67d625c 100644 (file)
--- a/