Merge branch 'MDL-37810-master' of https://github.com/snake/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 18 Sep 2017 23:45:29 +0000 (01:45 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 18 Sep 2017 23:45:29 +0000 (01:45 +0200)
39 files changed:
admin/settings/analytics.php
admin/settings/server.php
admin/settings/top.php
admin/tool/analytics/settings.php
calendar/amd/build/calendar_mini.min.js
calendar/amd/build/calendar_threemonth.min.js [new file with mode: 0644]
calendar/amd/build/repository.min.js
calendar/amd/build/selectors.min.js
calendar/amd/build/view_manager.min.js
calendar/amd/src/calendar_mini.js
calendar/amd/src/calendar_threemonth.js [new file with mode: 0644]
calendar/amd/src/repository.js
calendar/amd/src/selectors.js
calendar/amd/src/view_manager.js
calendar/classes/external/date_exporter.php [new file with mode: 0644]
calendar/classes/external/month_exporter.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/month_detailed.mustache
calendar/templates/month_mini.mustache
calendar/templates/month_navigation.mustache
calendar/templates/threemonth_month.mustache [new file with mode: 0644]
calendar/view.php
competency/classes/api.php
competency/tests/api_test.php
lang/en/admin.php
lang/en/analytics.php
lib/mlbackend/python/classes/processor.php
lib/mlbackend/python/lang/en/mlbackend_python.php
login/signup_form.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

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 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 a33feed..fff1bc9 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 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 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 55c9db9..56c8764 100644 (file)
@@ -87,16 +87,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 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);
         };
 
         /**
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 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 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..9b4257a 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;
     }
 
     /**
@@ -3354,10 +3368,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');
@@ -3382,7 +3397,7 @@ function calendar_get_view(\calendar_information $calendar, $view) {
         $monthdays = $type->get_num_days_in_month($date['year'], $date['mon']);
         $tend = $tstart + ($monthdays * DAYSECS) - 1;
         $selectortitle = get_string('detailedmonthviewfor', 'calendar');
-        if ($view === 'mini') {
+        if ($view === 'mini' || $view === 'minithree') {
             $template = 'core_calendar/calendar_mini';
         } else {
             $template = 'core_calendar/calendar_month';
@@ -3440,6 +3455,9 @@ function calendar_get_view(\calendar_information $calendar, $view) {
     ];
 
     $month = new \core_calendar\external\month_exporter($calendar, $type, $related);
+
+    $month->set_includenavigation($includenavigation);
+
     $data = $month->export($renderer);
 
     return [$data, $template];
index dc597ab..c0ce327 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;
     }
 
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}}
index 463b687..79925c4 100644 (file)
@@ -31,7 +31,7 @@
     {
     }
 }}
-<div class="calendarwrapper" data-courseid="{{courseid}}" data-current-time="{{time}}">
+<div class="calendarwrapper" data-courseid="{{courseid}}" data-month="{{date.mon}}" data-year="{{date.year}}">
     {{> core_calendar/month_header }}
     {{> core_calendar/month_navigation }}
     {{> core/overlay_loading}}
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 6447c8d..8124af0 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();
 }
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 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 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 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 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.