Merge branch 'MDL-59884-master' of git://github.com/lameze/moodle
authorJun Pataleta <jun@moodle.com>
Wed, 30 Aug 2017 02:10:30 +0000 (10:10 +0800)
committerJun Pataleta <jun@moodle.com>
Wed, 30 Aug 2017 02:10:30 +0000 (10:10 +0800)
153 files changed:
admin/tool/lp/templates/form-user-selector-suggestion.mustache
analytics/classes/classifier.php [new file with mode: 0644]
analytics/classes/course.php
analytics/classes/local/analyser/base.php
analytics/classes/local/indicator/base.php
analytics/classes/local/indicator/binary.php
analytics/classes/local/indicator/community_of_inquiry_activity.php
analytics/classes/local/indicator/discrete.php
analytics/classes/local/indicator/linear.php
analytics/classes/local/target/base.php
analytics/classes/local/target/binary.php
analytics/classes/local/target/discrete.php
analytics/classes/local/target/linear.php
analytics/classes/local/time_splitting/base.php
analytics/classes/model.php
analytics/classes/predictor.php
analytics/classes/regressor.php [new file with mode: 0644]
analytics/classes/site.php
analytics/tests/model_test.php
backup/moodle2/backup_course_task.class.php
backup/util/helper/tests/backup_encode_content_test.php
blocks/myoverview/templates/course-event-list.mustache
calendar/amd/build/event_form.min.js
calendar/amd/src/event_form.js
calendar/classes/local/event/forms/create.php
calendar/classes/local/event/mappers/create_update_form_mapper.php
calendar/renderer.php
calendar/tests/externallib_test.php
course/classes/analytics/indicator/completion_enabled.php [new file with mode: 0644]
course/classes/analytics/indicator/no_teacher.php
course/classes/analytics/indicator/potential_cognitive_depth.php [new file with mode: 0644]
course/classes/analytics/indicator/potential_social_breadth.php [new file with mode: 0644]
course/lib.php
course/tests/indicators_test.php [new file with mode: 0644]
enrol/locallib.php
enrol/manual/templates/form-user-selector-suggestion.mustache
group/classes/output/user_groups_editable.php
lang/en/calendar.php
lang/en/enrol.php
lang/en/error.php
lang/en/moodle.php
lib/amd/build/form-autocomplete.min.js
lib/amd/build/inplace_editable.min.js
lib/amd/src/form-autocomplete.js
lib/amd/src/inplace_editable.js
lib/classes/analytics/target/no_teaching.php
lib/classes/oauth2/api.php
lib/classes/oauth2/client.php
lib/classes/oauth2/refresh_system_tokens_task.php
lib/classes/oauth2/rest.php
lib/db/install.xml
lib/db/upgrade.php
lib/filelib.php
lib/form/datetimeselector.php
lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector-debug.js
lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector-min.js
lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector.js
lib/form/yui/src/dateselector/js/dateselector.js
lib/formslib.php
lib/mlbackend/php/classes/processor.php
lib/mlbackend/python/classes/processor.php
mod/assign/backup/moodle2/restore_assign_stepslib.php
mod/assign/classes/analytics/indicator/cognitive_depth.php
mod/assign/classes/analytics/indicator/social_breadth.php
mod/assign/db/upgrade.php
mod/assign/feedback/editpdf/classes/combined_document.php
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/submission/comments/lib.php
mod/assign/submission/comments/locallib.php
mod/assign/tests/behat/assign_comments_no_error.feature [new file with mode: 0644]
mod/assign/tests/behat/rescale_grades.feature
mod/assign/tests/lib_test.php
mod/assign/tests/locallib_test.php
mod/assign/upgradelib.php
mod/assign/version.php
mod/book/classes/analytics/indicator/activity_base.php
mod/book/classes/analytics/indicator/cognitive_depth.php
mod/book/classes/analytics/indicator/social_breadth.php
mod/chat/classes/analytics/indicator/cognitive_depth.php
mod/chat/classes/analytics/indicator/social_breadth.php
mod/choice/classes/analytics/indicator/cognitive_depth.php
mod/choice/classes/analytics/indicator/social_breadth.php
mod/data/classes/analytics/indicator/cognitive_depth.php
mod/data/classes/analytics/indicator/social_breadth.php
mod/feedback/classes/analytics/indicator/cognitive_depth.php
mod/feedback/classes/analytics/indicator/social_breadth.php
mod/folder/classes/analytics/indicator/activity_base.php
mod/folder/classes/analytics/indicator/cognitive_depth.php
mod/folder/classes/analytics/indicator/social_breadth.php
mod/forum/classes/analytics/indicator/cognitive_depth.php
mod/forum/classes/analytics/indicator/social_breadth.php
mod/glossary/classes/analytics/indicator/cognitive_depth.php
mod/glossary/classes/analytics/indicator/social_breadth.php
mod/imscp/classes/analytics/indicator/activity_base.php
mod/imscp/classes/analytics/indicator/cognitive_depth.php
mod/imscp/classes/analytics/indicator/social_breadth.php
mod/label/classes/analytics/indicator/activity_base.php
mod/label/classes/analytics/indicator/cognitive_depth.php
mod/label/classes/analytics/indicator/social_breadth.php
mod/lesson/classes/analytics/indicator/cognitive_depth.php
mod/lesson/classes/analytics/indicator/social_breadth.php
mod/lti/classes/analytics/indicator/cognitive_depth.php
mod/lti/classes/analytics/indicator/social_breadth.php
mod/page/classes/analytics/indicator/activity_base.php
mod/page/classes/analytics/indicator/cognitive_depth.php
mod/page/classes/analytics/indicator/social_breadth.php
mod/quiz/classes/analytics/indicator/cognitive_depth.php
mod/quiz/classes/analytics/indicator/social_breadth.php
mod/resource/classes/analytics/indicator/activity_base.php
mod/resource/classes/analytics/indicator/cognitive_depth.php
mod/resource/classes/analytics/indicator/social_breadth.php
mod/scorm/classes/analytics/indicator/cognitive_depth.php
mod/scorm/classes/analytics/indicator/social_breadth.php
mod/survey/classes/analytics/indicator/activity_base.php
mod/survey/classes/analytics/indicator/cognitive_depth.php
mod/survey/classes/analytics/indicator/social_breadth.php
mod/url/classes/analytics/indicator/activity_base.php
mod/url/classes/analytics/indicator/cognitive_depth.php
mod/url/classes/analytics/indicator/social_breadth.php
mod/wiki/classes/analytics/indicator/cognitive_depth.php
mod/wiki/classes/analytics/indicator/social_breadth.php
mod/workshop/classes/analytics/indicator/cognitive_depth.php
mod/workshop/classes/analytics/indicator/social_breadth.php
question/engine/questionusage.php
report/insights/classes/output/insight.php
report/insights/classes/output/insights_list.php
repository/filepicker.js
theme/boost/scss/moodle/forms.scss
theme/boost/scss/moodle/user.scss
theme/boost/templates/core/form_autocomplete_selection.mustache
theme/bootstrapbase/less/moodle.less
theme/bootstrapbase/less/moodle/calendar.less
theme/bootstrapbase/less/moodle/forms.less
theme/bootstrapbase/less/moodle/modal.less
theme/bootstrapbase/less/moodle/user.less
theme/bootstrapbase/less/moodle/variables.less [deleted file]
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/block_myoverview/course-event-list-item.mustache
theme/bootstrapbase/templates/block_myoverview/course-event-list.mustache
theme/bootstrapbase/templates/block_myoverview/course-summary.mustache
theme/bootstrapbase/templates/block_myoverview/courses-view-course-item.mustache
theme/bootstrapbase/templates/block_myoverview/event-list-item.mustache
user/action_redir.php
user/classes/output/user_roles_editable.php
user/classes/participants_table.php
user/index.php
user/renderer.php
user/templates/status_field.mustache
user/templates/unified_filter.mustache
user/tests/behat/view_participants.feature
version.php

index 0946f78..61557f9 100644 (file)
@@ -49,7 +49,7 @@
     }
 }}
 <span>
-    <img height="18" src="{{profileimageurlsmall}}" alt="" role="presentation">
+    <img height="12" src="{{profileimageurlsmall}}" alt="" role="presentation">
     <span>{{fullname}}</span>
     {{#hasidentity}}
     <span><small>{{identity}}</small></span>
diff --git a/analytics/classes/classifier.php b/analytics/classes/classifier.php
new file mode 100644 (file)
index 0000000..be0d3a0
--- /dev/null
@@ -0,0 +1,69 @@
+<?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/>.
+
+/**
+ * Classifier interface.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Classifier interface.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface classifier extends predictor {
+
+    /**
+     * Train this processor classification model using the provided supervised learning dataset.
+     *
+     * @param string $uniqueid
+     * @param \stored_file $dataset
+     * @param string $outputdir
+     * @return \stdClass
+     */
+    public function train_classification($uniqueid, \stored_file $dataset, $outputdir);
+
+    /**
+     * Classifies the provided dataset samples.
+     *
+     * @param string $uniqueid
+     * @param \stored_file $dataset
+     * @param string $outputdir
+     * @return \stdClass
+     */
+    public function classify($uniqueid, \stored_file $dataset, $outputdir);
+
+    /**
+     * Evaluates this processor classification model using the provided supervised learning dataset.
+     *
+     * @param string $uniqueid
+     * @param float $maxdeviation
+     * @param int $niterations
+     * @param \stored_file $dataset
+     * @param string $outputdir
+     * @return \stdClass
+     */
+    public function evaluate_classification($uniqueid, $maxdeviation, $niterations, \stored_file $dataset, $outputdir);
+}
index aae5748..791d7c2 100644 (file)
@@ -140,10 +140,18 @@ class course implements \core_analytics\analysable {
         $this->now = time();
 
         // Get the course users, including users assigned to student and teacher roles at an higher context.
-        $studentroles = array_keys(get_archetype_roles('student'));
+        $cache = \cache::make_from_params(\cache_store::MODE_REQUEST, 'core_analytics', 'rolearchetypes');
+
+        if (!$studentroles = $cache->get('student')) {
+            $studentroles = array_keys(get_archetype_roles('student'));
+            $cache->set('student', $studentroles);
+        }
         $this->studentids = $this->get_user_ids($studentroles);
 
-        $teacherroles = array_keys(get_archetype_roles('editingteacher') + get_archetype_roles('teacher'));
+        if (!$teacherroles = $cache->get('teacher')) {
+            $teacherroles = array_keys(get_archetype_roles('editingteacher') + get_archetype_roles('teacher'));
+            $cache->set('teacher', $teacherroles);
+        }
         $this->teacherids = $this->get_user_ids($teacherroles);
     }
 
index 9bf10c8..b179dcb 100644 (file)
@@ -469,6 +469,9 @@ abstract class base {
             return $result;
         }
 
+        // Add target metadata.
+        $this->add_target_metadata($data);
+
         // Write all calculated data to a file.
         $file = $dataset->store($data);
 
@@ -636,4 +639,28 @@ abstract class base {
             $DB->insert_record('analytics_predict_samples', $predictionrange);
         }
     }
+
+    /**
+     * Adds target metadata to the dataset.
+     *
+     * @param array $data
+     * @return void
+     */
+    protected function add_target_metadata(&$data) {
+        $data[0][] = 'targetcolumn';
+        $data[1][] = $this->analysabletarget->get_id();
+        if ($this->analysabletarget->is_linear()) {
+            $data[0][] = 'targettype';
+            $data[1][] = 'linear';
+            $data[0][] = 'targetmin';
+            $data[1][] = $this->analysabletarget::get_min_value();
+            $data[0][] = 'targetmax';
+            $data[1][] = $this->analysabletarget::get_max_value();
+        } else {
+            $data[0][] = 'targettype';
+            $data[1][] = 'discrete';
+            $data[0][] = 'targetclasses';
+            $data[1][] = json_encode($this->analysabletarget::get_classes());
+        }
+    }
 }
index 30fdf38..7c6aa3a 100644 (file)
@@ -121,6 +121,22 @@ abstract class base extends \core_analytics\calculable {
         return self::MIN_VALUE;
     }
 
+    /**
+     * Hook to allow indicators to pre-fill data that is shared accross time range calculations.
+     *
+     * Useful to fill analysable-dependant data that does not depend on the time ranges. Use
+     * instance vars to cache data that can be re-used across samples calculations but changes
+     * between time ranges (indicator instances are reset between time ranges to avoid unexpected
+     * problems).
+     *
+     * You are also responsible of emptying previous analysable caches.
+     *
+     * @param \core_analytics\analysable $analysable
+     * @return void
+     */
+    public function fill_per_analysable_caches(\core_analytics\analysable $analysable) {
+    }
+
     /**
      * Calculates the indicator.
      *
index 7f4c346..a730996 100644 (file)
@@ -46,6 +46,23 @@ abstract class binary extends discrete {
         return array(0);
     }
 
+    /**
+     * It should always be displayed.
+     *
+     * Binary values have no subtypes by default, please overwrite if
+     * your indicator is adding extra features.
+     *
+     * @param float $value
+     * @param string $subtype
+     * @return bool
+     */
+    public function should_be_displayed($value, $subtype) {
+        if ($subtype != false) {
+            return false;
+        }
+        return true;
+    }
+
     /**
      * get_display_value
      *
index 60d5e0f..8a886eb 100644 (file)
@@ -60,12 +60,36 @@ abstract class community_of_inquiry_activity extends linear {
      */
     const INDICATOR_SOCIAL = "social";
 
+    /**
+     * Max cognitive depth level accepted.
+     */
+    const MAX_COGNITIVE_LEVEL = 5;
+
+    /**
+     * Max social breadth level accepted.
+     */
+    const MAX_SOCIAL_LEVEL = 5;
+
+    /**
+     * Fetch the course grades of this activity type instances.
+     *
+     * @param \core_analytics\analysable $analysable
+     * @return void
+     */
+    public function fill_per_analysable_caches(\core_analytics\analysable $analysable) {
+
+        // Better to check it, we can not be 100% it will be a \core_analytics\course object.
+        if ($analysable instanceof \core_analytics\course) {
+            $this->fetch_student_grades($analysable);
+        }
+    }
+
     /**
      * Returns the activity type. No point in changing this class in children classes.
      *
      * @var string The activity name (e.g. assign or quiz)
      */
-    protected final function get_activity_type() {
+    public final function get_activity_type() {
         $class = get_class($this);
         $package = stristr($class, "\\", true);
         $type = str_replace("mod_", "", $package);
@@ -81,7 +105,7 @@ abstract class community_of_inquiry_activity extends linear {
      * @param \cm_info $cm
      * @return int
      */
-    protected function get_cognitive_depth_level(\cm_info $cm) {
+    public function get_cognitive_depth_level(\cm_info $cm) {
         throw new \coding_exception('Overwrite get_cognitive_depth_level method to set your activity potential cognitive ' .
             'depth level');
     }
@@ -92,7 +116,7 @@ abstract class community_of_inquiry_activity extends linear {
      * @param \cm_info $cm
      * @return int
      */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         throw new \coding_exception('Overwrite get_social_breadth_level method to set your activity potential social ' .
             'breadth level');
     }
@@ -396,8 +420,8 @@ abstract class community_of_inquiry_activity extends linear {
         }
 
         if ($this->grades === null) {
-            $courseactivities = $this->course->get_all_activities($this->get_activity_type());
-            $this->grades = $this->course->get_student_grades($courseactivities);
+            // Even if this is probably already filled during fill_per_analysable_caches.
+            $this->fetch_student_grades($this->course);
         }
 
         if ($cm = $this->retrieve('cm', $sampleid)) {
@@ -506,7 +530,7 @@ abstract class community_of_inquiry_activity extends linear {
         foreach ($useractivities as $contextid => $cm) {
 
             $potentiallevel = $this->get_cognitive_depth_level($cm);
-            if (!is_int($potentiallevel) || $potentiallevel > 5 || $potentiallevel < 1) {
+            if (!is_int($potentiallevel) || $potentiallevel > self::MAX_COGNITIVE_LEVEL || $potentiallevel < 1) {
                 throw new \coding_exception('Activities\' potential cognitive depth go from 1 to 5.');
             }
             $scoreperlevel = $scoreperactivity / $potentiallevel;
@@ -593,12 +617,19 @@ abstract class community_of_inquiry_activity extends linear {
         foreach ($useractivities as $contextid => $cm) {
 
             $potentiallevel = $this->get_social_breadth_level($cm);
-            if (!is_int($potentiallevel) || $potentiallevel > 2 || $potentiallevel < 1) {
-                throw new \coding_exception('Activities\' potential social breadth go from 1 to 2.');
+            if (!is_int($potentiallevel) || $potentiallevel > self::MAX_SOCIAL_LEVEL || $potentiallevel < 1) {
+                throw new \coding_exception('Activities\' potential social breadth go from 1 to ' .
+                    community_of_inquiry_activity::MAX_SOCIAL_LEVEL . '.');
             }
             $scoreperlevel = $scoreperactivity / $potentiallevel;
             switch ($potentiallevel) {
                 case 2:
+                case 3:
+                case 4:
+                case 5:
+                    // Core activities social breadth only reaches level 2, until core activities social
+                    // breadth do not reach level 5 we limit it to what we currently support, which is level 2.
+
                     // Social breadth level 2 is to view feedback. (Same as cognitive level 3).
 
                     if ($this->any_feedback('viewed', $cm, $contextid, $user)) {
@@ -645,10 +676,21 @@ abstract class community_of_inquiry_activity extends linear {
         throw new \coding_exception("Indicator type is invalid.");
     }
 
+    /**
+     * Gets the course student grades.
+     *
+     * @param \core_analytics\course $course
+     * @return void
+     */
+    protected function fetch_student_grades(\core_analytics\course $course) {
+        $courseactivities = $course->get_all_activities($this->get_activity_type());
+        $this->grades = $course->get_student_grades($courseactivities);
+    }
+
     /**
      * Defines indicator type.
      *
      * @return string
      */
-    abstract protected function get_indicator_type();
+    abstract public function get_indicator_type();
 }
index 0b51e0d..8a8aa66 100644 (file)
@@ -85,7 +85,7 @@ abstract class discrete extends base {
      */
     public function get_display_value($value, $subtype = false) {
 
-        $displayvalue = array_search($subtype, static::get_classes());
+        $displayvalue = array_search($subtype, static::get_classes(), false);
 
         debugging('Please overwrite \core_analytics\local\indicator\discrete::get_display_value to show something ' .
             'different than the default "' . $displayvalue . '"', DEBUG_DEVELOPER);
index 691aa8a..56902c0 100644 (file)
@@ -63,7 +63,7 @@ abstract class linear extends base {
     }
 
     /**
-     * should_be_displayed
+     * Show only the main feature.
      *
      * @param float $value
      * @param string $subtype
index 466255e..1a4f978 100644 (file)
@@ -130,6 +130,8 @@ abstract class base extends \core_analytics\calculable {
     /**
      * Callback to execute once a prediction has been returned from the predictions processor.
      *
+     * Note that the analytics_predictions db record is not yet inserted.
+     *
      * @param int $modelid
      * @param int $sampleid
      * @param int $rangeindex
@@ -231,7 +233,7 @@ abstract class base extends \core_analytics\calculable {
      */
     protected function min_prediction_score() {
         // The default minimum discards predictions with a low score.
-        return \core_analytics\model::MIN_SCORE;
+        return \core_analytics\model::PREDICTION_MIN_SCORE;
     }
 
     /**
index ea04410..f8c3b04 100644 (file)
@@ -78,7 +78,7 @@ abstract class binary extends discrete {
             throw new \moodle_exception('errorpredictionformat', 'analytics');
         }
 
-        if (in_array($value, $this->ignored_predicted_classes())) {
+        if (in_array($value, $this->ignored_predicted_classes(), false)) {
             // Just in case, if it is ignored the prediction should not even be recorded but if it would, it is ignored now,
             // which should mean that is it nothing serious.
             return self::OUTCOME_VERY_POSITIVE;
index 0799b4d..cbd8fe0 100644 (file)
@@ -42,17 +42,18 @@ abstract class discrete extends base {
      */
     public function is_linear() {
         // Not supported yet.
-        throw new \coding_exception('Sorry, this version\'s prediction processors only support targets with binary values.');
+        throw new \coding_exception('Sorry, this version\'s prediction processors only support targets with binary values.' .
+            ' You can write your own and overwrite this method though.');
     }
 
     /**
      * Is the provided class one of this target valid classes?
      *
-     * @param string $class
+     * @param mixed $class
      * @return bool
      */
     protected static function is_a_class($class) {
-        return (in_array($class, static::get_classes()));
+        return (in_array($class, static::get_classes(), false));
     }
 
     /**
@@ -99,7 +100,7 @@ abstract class discrete extends base {
             throw new \moodle_exception('errorpredictionformat', 'analytics');
         }
 
-        if (in_array($value, $this->ignored_predicted_classes())) {
+        if (in_array($value, $this->ignored_predicted_classes(), false)) {
             // Just in case, if it is ignored the prediction should not even be recorded.
             return self::OUTCOME_OK;
         }
@@ -138,15 +139,16 @@ abstract class discrete extends base {
      * Returns the predicted classes that will be ignored.
      *
      * Better be keen to add more than less classes here, the callback is always able to discard some classes. As an example
-     * a target with classes 'grade 0-3', 'grade 3-6', 'grade 6-8' and 'grade 8-10' is interested in flagging both 'grade 0-3'
-     * and 'grade 3-6'. On the other hand, a target like dropout risk with classes 'yes', 'no' may just be interested in 'yes'.
+     * a target with classes 'grade 0-3', 'grade 3-6', 'grade 6-8' and 'grade 8-10' is interested in flagging both 'grade 6-8'
+     * and 'grade 8-10' as ignored. On the other hand, a target like dropout risk with classes 'yes', 'no' may just be
+     * interested in 'yes'.
      *
      * @return array List of values that will be ignored (array keys are ignored).
      */
     protected function ignored_predicted_classes() {
         // Coding exception as this will only be called if this target have non-linear values.
-        throw new \coding_exception('Overwrite ignored_predicted_classes() and return an array with the classes that triggers ' .
-            'the callback');
+        throw new \coding_exception('Overwrite ignored_predicted_classes() and return an array with the classes that should not ' .
+            'trigger the callback');
     }
 
     /**
@@ -162,10 +164,8 @@ abstract class discrete extends base {
             return false;
         }
 
-        if (!$this->is_linear()) {
-            if (in_array($predictedvalue, $this->ignored_predicted_classes())) {
-                return false;
-            }
+        if (in_array($predictedvalue, $this->ignored_predicted_classes())) {
+            return false;
         }
 
         return true;
index 8d8e258..d16ad96 100644 (file)
@@ -42,7 +42,8 @@ abstract class linear extends base {
      */
     public function is_linear() {
         // Not supported yet.
-        throw new \coding_exception('Sorry, this version\'s prediction processors only support targets with binary values.');
+        throw new \coding_exception('Sorry, this version\'s prediction processors only support targets with binary values.' .
+            ' You can write your own and overwrite this method though.');
     }
 
     /**
@@ -52,7 +53,7 @@ abstract class linear extends base {
      * @param string $ignoredsubtype
      * @return int
      */
-    public function get_calculated_outcome($value, $ignoredsubtype = false) {
+    public function get_calculation_outcome($value, $ignoredsubtype = false) {
 
         // This is very generic, targets will probably be interested in overwriting this.
         $diff = static::get_max_value() - static::get_min_value();
@@ -67,7 +68,7 @@ abstract class linear extends base {
      *
      * @return float
      */
-    protected static function get_max_value() {
+    public static function get_max_value() {
         // Coding exception as this will only be called if this target have linear values.
         throw new \coding_exception('Overwrite get_max_value() and return the target max value');
     }
@@ -77,11 +78,33 @@ abstract class linear extends base {
      *
      * @return float
      */
-    protected static function get_min_value() {
+    public static function get_min_value() {
         // Coding exception as this will only be called if this target have linear values.
         throw new \coding_exception('Overwrite get_min_value() and return the target min value');
     }
 
+    /**
+     * Should the model callback be triggered?
+     *
+     * @param mixed $predictedvalue
+     * @param float $predictionscore
+     * @return bool
+     */
+    public function triggers_callback($predictedvalue, $predictionscore) {
+
+        if (!parent::triggers_callback($predictedvalue, $predictionscore)) {
+            return false;
+        }
+
+        // People may not want to set a boundary.
+        $boundary = $this->get_callback_boundary();
+        if (!empty($boundary) && floatval($predictedvalue) < $boundary) {
+            return false;
+        }
+
+        return true;
+    }
+
     /**
      * Returns the minimum value that triggers the callback.
      *
index 109084e..f45934a 100644 (file)
@@ -235,6 +235,9 @@ abstract class base {
         $newcalculations = array();
         foreach ($indicators as $indicator) {
 
+            // Hook to allow indicators to store analysable-dependant data.
+            $indicator->fill_per_analysable_caches($this->analysable);
+
             // Per-range calculations.
             foreach ($ranges as $rangeindex => $range) {
 
@@ -371,12 +374,9 @@ abstract class base {
         $metadata = array(
             'timesplitting' => $this->get_id(),
             // If no target the first column is the sampleid, if target the last column is the target.
+            // This will need to be updated when we support unsupervised learning models.
             'nfeatures' => count(current($dataset)) - 1
         );
-        if ($target) {
-            $metadata['targetclasses'] = json_encode($target::get_classes());
-            $metadata['targettype'] = ($target->is_linear()) ? 'linear' : 'discrete';
-        }
 
         // The first 2 samples will be used to store metadata about the dataset.
         $metadatacolumns = [];
index 04a95f1..568dcbf 100644 (file)
@@ -80,6 +80,11 @@ class model {
      */
     const MIN_SCORE = 0.7;
 
+    /**
+     * Minimum prediction confidence (from 0 to 1) to accept a prediction as reliable enough.
+     */
+    const PREDICTION_MIN_SCORE = 0.6;
+
     /**
      * Maximum standard deviation between different evaluation repetitions to consider that evaluation results are stable.
      */
@@ -524,8 +529,13 @@ class model {
             $outputdir = $this->get_output_dir(array('evaluation', $dashestimesplittingid));
 
             // Evaluate the dataset, the deviation we accept in the results depends on the amount of iterations.
-            $predictorresult = $predictor->evaluate($this->model->id, self::ACCEPTED_DEVIATION,
+            if ($this->get_target()->is_linear()) {
+                $predictorresult = $predictor->evaluate_regression($this->get_unique_id(), self::ACCEPTED_DEVIATION,
+                self::EVALUATION_ITERATIONS, $dataset, $outputdir);
+            } else {
+                $predictorresult = $predictor->evaluate_classification($this->get_unique_id(), self::ACCEPTED_DEVIATION,
                 self::EVALUATION_ITERATIONS, $dataset, $outputdir);
+            }
 
             $result->status = $predictorresult->status;
             $result->info = $predictorresult->info;
@@ -599,7 +609,11 @@ class model {
         $samplesfile = $datasets[$this->model->timesplitting];
 
         // Train using the dataset.
-        $predictorresult = $predictor->train($this->get_unique_id(), $samplesfile, $outputdir);
+        if ($this->get_target()->is_linear()) {
+            $predictorresult = $predictor->train_regression($this->get_unique_id(), $samplesfile, $outputdir);
+        } else {
+            $predictorresult = $predictor->train_classification($this->get_unique_id(), $samplesfile, $outputdir);
+        }
 
         $result = new \stdClass();
         $result->status = $predictorresult->status;
@@ -678,8 +692,12 @@ class model {
             $result->predictions = $this->get_static_predictions($indicatorcalculations);
 
         } else {
-            // Prediction process runs on the machine learning backend.
-            $predictorresult = $predictor->predict($this->get_unique_id(), $samplesfile, $outputdir);
+            // Estimation and classification processes run on the machine learning backend side.
+            if ($this->get_target()->is_linear()) {
+                $predictorresult = $predictor->estimate($this->get_unique_id(), $samplesfile, $outputdir);
+            } else {
+                $predictorresult = $predictor->classify($this->get_unique_id(), $samplesfile, $outputdir);
+            }
             $result->status = $predictorresult->status;
             $result->info = $predictorresult->info;
             $result->predictions = $this->format_predictor_predictions($predictorresult);
@@ -757,9 +775,12 @@ class model {
                 list($sampleid, $rangeindex) = $this->get_time_splitting()->infer_sample_info($uniquesampleid);
 
                 // Store the predicted values.
-                $samplecontext = $this->save_prediction($sampleid, $rangeindex, $prediction->prediction,
+                list($record, $samplecontext) = $this->prepare_prediction_record($sampleid, $rangeindex, $prediction->prediction,
                     $prediction->predictionscore, json_encode($indicatorcalculations[$uniquesampleid]));
 
+                // We will later bulk-insert them all.
+                $records[$uniquesampleid] = $record;
+
                 // Also store all samples context to later generate insights or whatever action the target wants to perform.
                 $samplecontexts[$samplecontext->id] = $samplecontext;
 
@@ -768,6 +789,8 @@ class model {
             }
         }
 
+        $this->save_predictions($records);
+
         return $samplecontexts;
     }
 
@@ -894,7 +917,7 @@ class model {
      * @param string $calculations
      * @return \context
      */
-    protected function save_prediction($sampleid, $rangeindex, $prediction, $predictionscore, $calculations) {
+    protected function prepare_prediction_record($sampleid, $rangeindex, $prediction, $predictionscore, $calculations) {
         global $DB;
 
         $context = $this->get_analyser()->sample_access_context($sampleid);
@@ -908,9 +931,18 @@ class model {
         $record->predictionscore = $predictionscore;
         $record->calculations = $calculations;
         $record->timecreated = time();
-        $DB->insert_record('analytics_predictions', $record);
 
-        return $context;
+        return array($record, $context);
+    }
+
+    /**
+     * Save the prediction objects.
+     *
+     * @param \stdClass[] $records
+     */
+    protected function save_predictions($records) {
+        global $DB;
+        $DB->insert_records('analytics_predictions', $records);
     }
 
     /**
@@ -1194,7 +1226,7 @@ class model {
 
         // Generate a unique id for this site, this model and this time splitting method, considering the last time
         // that the model target and indicators were updated.
-        $ids = array($CFG->wwwroot, $CFG->dirroot, $CFG->prefix, $this->model->id, $this->model->version);
+        $ids = array($CFG->wwwroot, $CFG->prefix, $this->model->id, $this->model->version);
         $this->uniqueid = sha1(implode('$$', $ids));
 
         return $this->uniqueid;
index 944c76e..4b4548b 100644 (file)
@@ -41,36 +41,4 @@ interface predictor {
      * @return bool
      */
     public function is_ready();
-
-    /**
-     * Train the provided dataset.
-     *
-     * @param int $modelid
-     * @param \stored_file $dataset
-     * @param string $outputdir
-     * @return \stdClass
-     */
-    public function train($modelid, \stored_file $dataset, $outputdir);
-
-    /**
-     * Predict the provided dataset samples.
-     *
-     * @param int $modelid
-     * @param \stored_file $dataset
-     * @param string $outputdir
-     * @return \stdClass
-     */
-    public function predict($modelid, \stored_file $dataset, $outputdir);
-
-    /**
-     * evaluate
-     *
-     * @param int $modelid
-     * @param float $maxdeviation
-     * @param int $niterations
-     * @param \stored_file $dataset
-     * @param string $outputdir
-     * @return \stdClass
-     */
-    public function evaluate($modelid, $maxdeviation, $niterations, \stored_file $dataset, $outputdir);
 }
diff --git a/analytics/classes/regressor.php b/analytics/classes/regressor.php
new file mode 100644 (file)
index 0000000..c2d2a89
--- /dev/null
@@ -0,0 +1,69 @@
+<?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/>.
+
+/**
+ * Regressors interface.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Regressors interface.
+ *
+ * @package   core_analytics
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface regressor extends predictor {
+
+    /**
+     * Train this processor regression model using the provided supervised learning dataset.
+     *
+     * @param string $uniqueid
+     * @param \stored_file $dataset
+     * @param string $outputdir
+     * @return \stdClass
+     */
+    public function train_regression($uniqueid, \stored_file $dataset, $outputdir);
+
+    /**
+     * Estimates linear values for the provided dataset samples.
+     *
+     * @param string $uniqueid
+     * @param \stored_file $dataset
+     * @param mixed $outputdir
+     * @return void
+     */
+    public function estimate($uniqueid, \stored_file $dataset, $outputdir);
+
+    /**
+     * Evaluates this processor regression model using the provided supervised learning dataset.
+     *
+     * @param string $uniqueid
+     * @param float $maxdeviation
+     * @param int $niterations
+     * @param \stored_file $dataset
+     * @param string $outputdir
+     * @return \stdClass
+     */
+    public function evaluate_regression($uniqueid, $maxdeviation, $niterations, \stored_file $dataset, $outputdir);
+}
index d616d4b..69fe722 100644 (file)
@@ -73,21 +73,14 @@ class site implements \core_analytics\analysable {
             return $this->start;
         }
 
-        if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
-            $this->start = 0;
-            return $this->start;
-        }
-
-        // Basically a SELECT MIN(timecreated) FROM ...
-        $events = $logstore->get_events_select("", array(), "timecreated ASC", 0, 1);
-        if ($events) {
-            // There should be just 1 event.
-            $event = reset($events);
-            $this->start = intval($event->timecreated);
-        } else {
-            $this->start = 0;
+        // Much faster than reading the first log in the site.
+        $admins = get_admins();
+        $this->start = 9999999999;
+        foreach ($admins as $admin) {
+            if ($admin->firstaccess < $this->start) {
+                $this->start = $admin->firstaccess;
+            }
         }
-
         return $this->start;
     }
 
@@ -101,21 +94,7 @@ class site implements \core_analytics\analysable {
             return $this->end;
         }
 
-        if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
-            $this->end = time();
-            return $this->end;
-        }
-
-        // Basically a SELECT MAX(timecreated) FROM ...
-        $events = $logstore->get_events_select("", array(), "timecreated DESC", 0, 1);
-        if ($events) {
-            // There should be just 1 event.
-            $event = reset($events);
-            $this->end = intval($event->timecreated);
-        } else {
-            $this->end = time();
-        }
-
+        $this->end = time();
         return $this->end;
     }
 }
index d4cf05a..b8e1768 100644 (file)
@@ -150,12 +150,11 @@ class analytics_model_testcase extends advanced_testcase {
 
         global $DB;
 
-        // 2 built-in models + the testing one.
-        $this->assertCount(3, $DB->get_records('analytics_models'));
+        $count = $DB->count_records('analytics_models');
 
         // No new models added if the builtin ones already exist.
         \core_analytics\manager::add_builtin_models();
-        $this->assertCount(3, $DB->get_records('analytics_models'));
+        $this->assertCount($count, $DB->get_records('analytics_models'));
 
         $target = \core_analytics\manager::get_target('\core\analytics\target\no_teaching');
         $this->assertTrue(\core_analytics\model::exists($target));
index f9ae5dc..d9060e3 100644 (file)
@@ -167,15 +167,25 @@ class backup_course_task extends backup_task {
     /**
      * Helper method, used by encode_content_links.
      * @param string $content content in which to encode links.
-     * @param unknown_type $name the name of this type of encoded link.
-     * @param unknown_type $path the path that identifies this type of link, up
+     * @param string $name the name of this type of encoded link.
+     * @param string $path the path that identifies this type of link, up
      *      to the ?paramname= bit.
      * @return string content with one type of link encoded.
      */
     static private function encode_links_helper($content, $name, $path) {
         global $CFG;
-        $base = preg_quote($CFG->wwwroot . $path, '/');
-        return preg_replace('/(' . $base . ')([0-9]+)/', '$@' . $name . '*$2@$', $content);
+        // We want to convert both http and https links.
+        $root = $CFG->wwwroot;
+        $httpsroot = str_replace('http://', 'https://', $root);
+        $httproot = str_replace('https://', 'http://', $root);
+
+        $httpsbase = preg_quote($httpsroot . $path, '/');
+        $httpbase = preg_quote($httproot . $path, '/');
+
+        $return = preg_replace('/(' . $httpsbase . ')([0-9]+)/', '$@' . $name . '*$2@$', $content);
+        $return = preg_replace('/(' . $httpbase . ')([0-9]+)/', '$@' . $name . '*$2@$', $return);
+
+        return $return;
     }
 
 // Protected API starts here
index 26299d9..63554d3 100644 (file)
@@ -44,13 +44,33 @@ class backup_course_task_testcase extends basic_testcase {
      */
     public function test_course_encode_content_links() {
         global $CFG;
+        $httpsroot = "https://moodle.org";
+        $httproot = "http://moodle.org";
+        $oldroot = $CFG->wwwroot;
+
+        // HTTPS root and links of both types in content.
+        $CFG->wwwroot = $httpsroot;
         $encoded = backup_course_task::encode_content_links(
-                $CFG->wwwroot . '/course/view.php?id=123, ' .
-                $CFG->wwwroot . '/grade/index.php?id=123, ' .
-                $CFG->wwwroot . '/grade/report/index.php?id=123, ' .
-                $CFG->wwwroot . '/badges/view.php?type=2&id=123 and ' .
-                $CFG->wwwroot . '/user/index.php?id=123.');
-        $this->assertEquals('$@COURSEVIEWBYID*123@$, $@GRADEINDEXBYID*123@$, ' .
+                $httproot . '/course/view.php?id=123, ' .
+                $httpsroot . '/course/view.php?id=123, ' .
+                $httpsroot . '/grade/index.php?id=123, ' .
+                $httpsroot . '/grade/report/index.php?id=123, ' .
+                $httpsroot . '/badges/view.php?type=2&id=123 and ' .
+                $httpsroot . '/user/index.php?id=123.');
+        $this->assertEquals('$@COURSEVIEWBYID*123@$, $@COURSEVIEWBYID*123@$, $@GRADEINDEXBYID*123@$, ' .
                 '$@GRADEREPORTINDEXBYID*123@$, $@BADGESVIEWBYID*123@$ and $@USERINDEXVIEWBYID*123@$.', $encoded);
+
+        // HTTP root and links of both types in content.
+        $CFG->wwwroot = $httproot;
+        $encoded = backup_course_task::encode_content_links(
+            $httproot . '/course/view.php?id=123, ' .
+            $httpsroot . '/course/view.php?id=123, ' .
+            $httproot . '/grade/index.php?id=123, ' .
+            $httproot . '/grade/report/index.php?id=123, ' .
+            $httproot . '/badges/view.php?type=2&id=123 and ' .
+            $httproot . '/user/index.php?id=123.');
+        $this->assertEquals('$@COURSEVIEWBYID*123@$, $@COURSEVIEWBYID*123@$, $@GRADEINDEXBYID*123@$, ' .
+            '$@GRADEREPORTINDEXBYID*123@$, $@BADGESVIEWBYID*123@$ and $@USERINDEXVIEWBYID*123@$.', $encoded);
+        $CFG->wwwroot = $oldroot;
     }
 }
index d9e729a..d7f9fb2 100644 (file)
@@ -97,7 +97,7 @@
              role="presentation">
         <p class="text-muted m-t-1">{{#str}} noevents, block_myoverview {{/str}}</p>
         <a href="{{viewurl}}" class="btn btn-secondary {{#visible}}text-primary{{/visible}}"
-           aria-label="{{#str}} viewcoursename, block_myoverview, {{fullnamedisplay}} {{/str}}">
+           aria-label="{{#str}} viewcoursename, block_myoverview, {{{fullnamedisplay}}} {{/str}}">
             {{#str}} viewcourse, block_myoverview {{/str}}
         </a>
     </div>
index 49c3a88..d90e7c8 100644 (file)
Binary files a/calendar/amd/build/event_form.min.js and b/calendar/amd/build/event_form.min.js differ
index 0a4e33e..e6abfd8 100644 (file)
@@ -232,7 +232,7 @@ define(['jquery', 'core/templates'], function($, Templates) {
                     element.removeClass('hidden');
                     element.prop('disabled', false);
 
-                    if (selectedIndex === null) {
+                    if (selectedIndex === null || element.attr('selected')) {
                         selectedIndex = index;
                     }
                 } else {
index 4a4172d..e6c025f 100644 (file)
@@ -45,6 +45,10 @@ class create extends \moodleform {
         $starttime = isset($this->_customdata['starttime']) ? $this->_customdata['starttime'] : 0;
         $eventtypes = calendar_get_all_allowed_types();
 
+        if (empty($eventtypes)) {
+            print_error('nopermissiontoupdatecalendar');
+        }
+
         $mform->setDisableShortforms();
         $mform->disable_form_change_checker();
 
@@ -94,6 +98,12 @@ class create extends \moodleform {
 
         $errors = parent::validation($data, $files);
         $coursekey = isset($data['groupcourseid']) ? 'groupcourseid' : 'courseid';
+        $eventtypes = calendar_get_all_allowed_types();
+        $eventtype = isset($data['eventtype']) ? $data['eventtype'] : null;
+
+        if (empty($eventtype) || !isset($eventtypes[$eventtype])) {
+            $errors['eventtype'] = get_string('invalideventtype', 'calendar');
+        }
 
         if (isset($data[$coursekey]) && $data[$coursekey] > 0) {
             if ($course = $DB->get_record('course', ['id' => $data[$coursekey]])) {
index bedcccc..b0bb691 100644 (file)
@@ -90,7 +90,11 @@ class create_update_form_mapper implements create_update_form_mapper_interface {
 
         // Default course id if none is set.
         if (!isset($properties->courseid)) {
-            $properties->courseid = 0;
+            if ($properties->eventtype === 'site') {
+                $properties->courseid = SITEID;
+            } else {
+                $properties->courseid = 0;
+            }
         }
 
         // Decode the form fields back into valid event property.
index 6e7cab8..c5b8531 100644 (file)
@@ -389,7 +389,7 @@ class core_calendar_renderer extends plugin_renderer_base {
         $courseurl = new moodle_url($returnurl);
         $courseurl->remove_params('course');
         $select = new single_select($courseurl, 'course', $courseoptions, $selected, null);
-        $select->class = 'cal_courses_flt m-r-1';
+        $select->class = 'm-r-1';
         if ($label !== null) {
             $select->set_label($label);
         } else {
index 8098f72..4537acf 100644 (file)
@@ -1414,4 +1414,785 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
             $result
         );
     }
+
+    /**
+     * Submit a request where the time duration until is earlier than the time
+     * start in order to get a validation error from the server.
+     */
+    public function test_submit_create_update_form_validation_error() {
+        $user = $this->getDataGenerator()->create_user();
+        $timestart = new DateTime();
+        $interval = new DateInterval("P1D"); // One day.
+        $timedurationuntil = new DateTime();
+        $timedurationuntil->sub($interval);
+        $formdata = [
+            'id' => 0,
+            'userid' => $user->id,
+            'modulename' => '',
+            'instance' => 0,
+            'visible' => 1,
+            'name' => 'Test',
+            'timestart' => [
+                'day' => $timestart->format('j'),
+                'month' => $timestart->format('n'),
+                'year' => $timestart->format('Y'),
+                'hour' => $timestart->format('G'),
+                'minute' => 0,
+            ],
+            'eventtype' => 'user',
+            'description' => [
+                'text' => '',
+                'format' => 1,
+            ],
+            'duration' => 1,
+            'timedurationuntil' => [
+                'day' => $timedurationuntil->format('j'),
+                'month' => $timedurationuntil->format('n'),
+                'year' => $timedurationuntil->format('Y'),
+                'hour' => $timedurationuntil->format('G'),
+                'minute' => 0,
+            ]
+        ];
+
+        $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata);
+
+        $querystring = http_build_query($formdata, '', '&amp;');
+
+        $this->resetAfterTest(true);
+        $this->setUser($user);
+
+        $result = external_api::clean_returnvalue(
+            core_calendar_external::submit_create_update_form_returns(),
+            core_calendar_external::submit_create_update_form($querystring)
+        );
+
+        $this->assertTrue($result['validationerror']);
+    }
+
+    /**
+     * A user with the moodle/calendar:manageownentries capability at the
+     * system context should be able to create a user event.
+     */
+    public function test_submit_create_update_form_create_user_event() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $roleid = $generator->create_role();
+        $context = \context_system::instance();
+        $timestart = new DateTime();
+        $interval = new DateInterval("P1D"); // One day.
+        $timedurationuntil = new DateTime();
+        $timedurationuntil->add($interval);
+        $formdata = [
+            'id' => 0,
+            'userid' => $user->id,
+            'modulename' => '',
+            'instance' => 0,
+            'visible' => 1,
+            'name' => 'Test',
+            'timestart' => [
+                'day' => $timestart->format('j'),
+                'month' => $timestart->format('n'),
+                'year' => $timestart->format('Y'),
+                'hour' => $timestart->format('G'),
+                'minute' => 0,
+            ],
+            'eventtype' => 'user',
+            'description' => [
+                'text' => '',
+                'format' => 1,
+            ],
+            'duration' => 1,
+            'timedurationuntil' => [
+                'day' => $timedurationuntil->format('j'),
+                'month' => $timedurationuntil->format('n'),
+                'year' => $timedurationuntil->format('Y'),
+                'hour' => $timedurationuntil->format('G'),
+                'minute' => 0,
+            ]
+        ];
+
+        $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata);
+        $querystring = http_build_query($formdata, '', '&');
+
+        $generator->role_assign($roleid, $user->id, $context->id);
+        assign_capability('moodle/calendar:manageownentries', CAP_ALLOW, $roleid, $context, true);
+
+        $user->ignoresesskey = true;
+        $this->resetAfterTest(true);
+        $this->setUser($user);
+
+        $result = external_api::clean_returnvalue(
+            core_calendar_external::submit_create_update_form_returns(),
+            core_calendar_external::submit_create_update_form($querystring)
+        );
+
+        $event = $result['event'];
+        $this->assertEquals($user->id, $event['userid']);
+        $this->assertEquals($formdata['eventtype'], $event['eventtype']);
+        $this->assertEquals($formdata['name'], $event['name']);
+    }
+
+    /**
+     * A user without the moodle/calendar:manageownentries capability at the
+     * system context should not be able to create a user event.
+     */
+    public function test_submit_create_update_form_create_user_event_no_permission() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $roleid = $generator->create_role();
+        $context = \context_system::instance();
+        $timestart = new DateTime();
+        $interval = new DateInterval("P1D"); // One day.
+        $timedurationuntil = new DateTime();
+        $timedurationuntil->add($interval);
+        $formdata = [
+            'id' => 0,
+            'userid' => $user->id,
+            'modulename' => '',
+            'instance' => 0,
+            'visible' => 1,
+            'name' => 'Test',
+            'timestart' => [
+                'day' => $timestart->format('j'),
+                'month' => $timestart->format('n'),
+                'year' => $timestart->format('Y'),
+                'hour' => $timestart->format('G'),
+                'minute' => 0,
+            ],
+            'eventtype' => 'user',
+            'description' => [
+                'text' => '',
+                'format' => 1,
+            ],
+            'duration' => 1,
+            'timedurationuntil' => [
+                'day' => $timedurationuntil->format('j'),
+                'month' => $timedurationuntil->format('n'),
+                'year' => $timedurationuntil->format('Y'),
+                'hour' => $timedurationuntil->format('G'),
+                'minute' => 0,
+            ]
+        ];
+
+        $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata);
+        $querystring = http_build_query($formdata, '', '&');
+
+        $generator->role_assign($roleid, $user->id, $context->id);
+        assign_capability('moodle/calendar:manageownentries', CAP_PROHIBIT, $roleid, $context, true);
+
+        $user->ignoresesskey = true;
+        $this->resetAfterTest(true);
+        $this->setUser($user);
+
+        $this->expectException('moodle_exception');
+
+        external_api::clean_returnvalue(
+            core_calendar_external::submit_create_update_form_returns(),
+            core_calendar_external::submit_create_update_form($querystring)
+        );
+    }
+
+    /**
+     * A user with the moodle/calendar:manageentries capability at the
+     * site course context should be able to create a site event.
+     */
+    public function test_submit_create_update_form_create_site_event() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $context = context_course::instance(SITEID);
+        $roleid = $generator->create_role();
+        $timestart = new DateTime();
+        $interval = new DateInterval("P1D"); // One day.
+        $timedurationuntil = new DateTime();
+        $timedurationuntil->add($interval);
+        $formdata = [
+            'id' => 0,
+            'userid' => $user->id,
+            'modulename' => '',
+            'instance' => 0,
+            'visible' => 1,
+            'name' => 'Test',
+            'timestart' => [
+                'day' => $timestart->format('j'),
+                'month' => $timestart->format('n'),
+                'year' => $timestart->format('Y'),
+                'hour' => $timestart->format('G'),
+                'minute' => 0,
+            ],
+            'eventtype' => 'site',
+            'description' => [
+                'text' => '',
+                'format' => 1,
+            ],
+            'duration' => 1,
+            'timedurationuntil' => [
+                'day' => $timedurationuntil->format('j'),
+                'month' => $timedurationuntil->format('n'),
+                'year' => $timedurationuntil->format('Y'),
+                'hour' => $timedurationuntil->format('G'),
+                'minute' => 0,
+            ]
+        ];
+
+        $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata);
+        $querystring = http_build_query($formdata, '', '&');
+
+        $generator->role_assign($roleid, $user->id, $context->id);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+
+        $user->ignoresesskey = true;
+        $this->resetAfterTest(true);
+        $this->setUser($user);
+
+        $result = external_api::clean_returnvalue(
+            core_calendar_external::submit_create_update_form_returns(),
+            core_calendar_external::submit_create_update_form($querystring)
+        );
+
+        $event = $result['event'];
+        $this->assertEquals($user->id, $event['userid']);
+        $this->assertEquals($formdata['eventtype'], $event['eventtype']);
+        $this->assertEquals($formdata['name'], $event['name']);
+    }
+
+    /**
+     * A user without the moodle/calendar:manageentries capability at the
+     * site course context should not be able to create a site event.
+     */
+    public function test_submit_create_update_form_create_site_event_no_permission() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $context = context_course::instance(SITEID);
+        $roleid = $generator->create_role();
+        $timestart = new DateTime();
+        $interval = new DateInterval("P1D"); // One day.
+        $timedurationuntil = new DateTime();
+        $timedurationuntil->add($interval);
+        $formdata = [
+            'id' => 0,
+            'userid' => $user->id,
+            'modulename' => '',
+            'instance' => 0,
+            'visible' => 1,
+            'name' => 'Test',
+            'timestart' => [
+                'day' => $timestart->format('j'),
+                'month' => $timestart->format('n'),
+                'year' => $timestart->format('Y'),
+                'hour' => $timestart->format('G'),
+                'minute' => 0,
+            ],
+            'eventtype' => 'site',
+            'description' => [
+                'text' => '',
+                'format' => 1,
+            ],
+            'duration' => 1,
+            'timedurationuntil' => [
+                'day' => $timedurationuntil->format('j'),
+                'month' => $timedurationuntil->format('n'),
+                'year' => $timedurationuntil->format('Y'),
+                'hour' => $timedurationuntil->format('G'),
+                'minute' => 0,
+            ]
+        ];
+
+        $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata);
+        $querystring = http_build_query($formdata, '', '&');
+
+        $generator->role_assign($roleid, $user->id, $context->id);
+
+        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
+
+        $user->ignoresesskey = true;
+        $this->resetAfterTest(true);
+        $this->setUser($user);
+
+        $result = external_api::clean_returnvalue(
+            core_calendar_external::submit_create_update_form_returns(),
+            core_calendar_external::submit_create_update_form($querystring)
+        );
+
+        $this->assertTrue($result['validationerror']);
+    }
+
+    /**
+     * A user that has the moodle/calendar:manageentries in a course that they
+     * are enrolled in should be able to create a course event in that course.
+     */
+    public function test_submit_create_update_form_create_course_event() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course = $generator->create_course();
+        $context = context_course::instance($course->id);
+        $roleid = $generator->create_role();
+        $timestart = new DateTime();
+        $interval = new DateInterval("P1D"); // One day.
+        $timedurationuntil = new DateTime();
+        $timedurationuntil->add($interval);
+        $formdata = [
+            'id' => 0,
+            'userid' => $user->id,
+            'modulename' => '',
+            'instance' => 0,
+            'visible' => 1,
+            'name' => 'Test',
+            'timestart' => [
+                'day' => $timestart->format('j'),
+                'month' => $timestart->format('n'),
+                'year' => $timestart->format('Y'),
+                'hour' => $timestart->format('G'),
+                'minute' => 0,
+            ],
+            'eventtype' => 'course',
+            'courseid' => $course->id,
+            'description' => [
+                'text' => '',
+                'format' => 1,
+            ],
+            'duration' => 1,
+            'timedurationuntil' => [
+                'day' => $timedurationuntil->format('j'),
+                'month' => $timedurationuntil->format('n'),
+                'year' => $timedurationuntil->format('Y'),
+                'hour' => $timedurationuntil->format('G'),
+                'minute' => 0,
+            ]
+        ];
+
+        $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata);
+        $querystring = http_build_query($formdata, '', '&');
+
+        $generator->enrol_user($user->id, $course->id, 'student');
+        $generator->role_assign($roleid, $user->id, $context->id);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+
+        $user->ignoresesskey = true;
+        $this->resetAfterTest(true);
+        $this->setUser($user);
+
+        $result = external_api::clean_returnvalue(
+            core_calendar_external::submit_create_update_form_returns(),
+            core_calendar_external::submit_create_update_form($querystring)
+        );
+
+        $event = $result['event'];
+        $this->assertEquals($user->id, $event['userid']);
+        $this->assertEquals($formdata['eventtype'], $event['eventtype']);
+        $this->assertEquals($formdata['name'], $event['name']);
+        $this->assertEquals($formdata['courseid'], $event['course']['id']);
+    }
+
+    /**
+     * A user without the moodle/calendar:manageentries capability in a course
+     * that they are enrolled in should not be able to create a course event in that course.
+     */
+    public function test_submit_create_update_form_create_course_event_no_permission() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course = $generator->create_course();
+        $context = context_course::instance($course->id);
+        $roleid = $generator->create_role();
+        $timestart = new DateTime();
+        $interval = new DateInterval("P1D"); // One day.
+        $timedurationuntil = new DateTime();
+        $timedurationuntil->add($interval);
+        $formdata = [
+            'id' => 0,
+            'userid' => $user->id,
+            'modulename' => '',
+            'instance' => 0,
+            'visible' => 1,
+            'name' => 'Test',
+            'timestart' => [
+                'day' => $timestart->format('j'),
+                'month' => $timestart->format('n'),
+                'year' => $timestart->format('Y'),
+                'hour' => $timestart->format('G'),
+                'minute' => 0,
+            ],
+            'eventtype' => 'course',
+            'courseid' => $course->id,
+            'description' => [
+                'text' => '',
+                'format' => 1,
+            ],
+            'duration' => 1,
+            'timedurationuntil' => [
+                'day' => $timedurationuntil->format('j'),
+                'month' => $timedurationuntil->format('n'),
+                'year' => $timedurationuntil->format('Y'),
+                'hour' => $timedurationuntil->format('G'),
+                'minute' => 0,
+            ]
+        ];
+
+        $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata);
+        $querystring = http_build_query($formdata, '', '&');
+
+        $generator->enrol_user($user->id, $course->id, 'student');
+        $generator->role_assign($roleid, $user->id, $context->id);
+
+        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
+
+        $user->ignoresesskey = true;
+        $this->resetAfterTest(true);
+        $this->setUser($user);
+
+        $result = external_api::clean_returnvalue(
+            core_calendar_external::submit_create_update_form_returns(),
+            core_calendar_external::submit_create_update_form($querystring)
+        );
+
+        $this->assertTrue($result['validationerror']);
+    }
+
+    /**
+     * A user should not be able to create an event for a course that they are
+     * not enrolled in.
+     */
+    public function test_submit_create_update_form_create_course_event_not_enrolled() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course = $generator->create_course();
+        $course2 = $generator->create_course();
+        $context = context_course::instance($course->id);
+        $roleid = $generator->create_role();
+        $timestart = new DateTime();
+        $interval = new DateInterval("P1D"); // One day.
+        $timedurationuntil = new DateTime();
+        $timedurationuntil->add($interval);
+        $formdata = [
+            'id' => 0,
+            'userid' => $user->id,
+            'modulename' => '',
+            'instance' => 0,
+            'visible' => 1,
+            'name' => 'Test',
+            'timestart' => [
+                'day' => $timestart->format('j'),
+                'month' => $timestart->format('n'),
+                'year' => $timestart->format('Y'),
+                'hour' => $timestart->format('G'),
+                'minute' => 0,
+            ],
+            'eventtype' => 'course',
+            'courseid' => $course2->id, // Not enrolled.
+            'description' => [
+                'text' => '',
+                'format' => 1,
+            ],
+            'duration' => 1,
+            'timedurationuntil' => [
+                'day' => $timedurationuntil->format('j'),
+                'month' => $timedurationuntil->format('n'),
+                'year' => $timedurationuntil->format('Y'),
+                'hour' => $timedurationuntil->format('G'),
+                'minute' => 0,
+            ]
+        ];
+
+        $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata);
+        $querystring = http_build_query($formdata, '', '&');
+
+        $generator->enrol_user($user->id, $course->id, 'student');
+        $generator->role_assign($roleid, $user->id, $context->id);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+
+        $user->ignoresesskey = true;
+        $this->resetAfterTest(true);
+        $this->setUser($user);
+
+        $this->expectException('moodle_exception');
+
+        external_api::clean_returnvalue(
+            core_calendar_external::submit_create_update_form_returns(),
+            core_calendar_external::submit_create_update_form($querystring)
+        );
+    }
+
+    /**
+     * A user should be able to create an event for a group that they are a member of in
+     * a course in which they are enrolled and have the moodle/calendar:manageentries capability.
+     */
+    public function test_submit_create_update_form_create_group_event_group_member_manage_course() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course = $generator->create_course();
+        $group = $generator->create_group(array('courseid' => $course->id));
+        $context = context_course::instance($course->id);
+        $roleid = $generator->create_role();
+        $timestart = new DateTime();
+        $interval = new DateInterval("P1D"); // One day.
+        $timedurationuntil = new DateTime();
+        $timedurationuntil->add($interval);
+        $formdata = [
+            'id' => 0,
+            'userid' => $user->id,
+            'modulename' => '',
+            'instance' => 0,
+            'visible' => 1,
+            'name' => 'Test',
+            'timestart' => [
+                'day' => $timestart->format('j'),
+                'month' => $timestart->format('n'),
+                'year' => $timestart->format('Y'),
+                'hour' => $timestart->format('G'),
+                'minute' => 0,
+            ],
+            'eventtype' => 'group',
+            'groupid' => "{$course->id}-{$group->id}", // The form format.
+            'groupcourseid' => $course->id,
+            'description' => [
+                'text' => '',
+                'format' => 1,
+            ],
+            'duration' => 1,
+            'timedurationuntil' => [
+                'day' => $timedurationuntil->format('j'),
+                'month' => $timedurationuntil->format('n'),
+                'year' => $timedurationuntil->format('Y'),
+                'hour' => $timedurationuntil->format('G'),
+                'minute' => 0,
+            ]
+        ];
+
+        $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata);
+        $querystring = http_build_query($formdata, '', '&');
+
+        $generator->enrol_user($user->id, $course->id, 'student');
+        $generator->role_assign($roleid, $user->id, $context->id);
+        $generator->create_group_member(['groupid' => $group->id, 'userid' => $user->id]);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+
+        $user->ignoresesskey = true;
+        $this->resetAfterTest(true);
+        $this->setUser($user);
+
+        $result = external_api::clean_returnvalue(
+            core_calendar_external::submit_create_update_form_returns(),
+            core_calendar_external::submit_create_update_form($querystring)
+        );
+
+        $event = $result['event'];
+        $this->assertEquals($user->id, $event['userid']);
+        $this->assertEquals($formdata['eventtype'], $event['eventtype']);
+        $this->assertEquals($formdata['name'], $event['name']);
+        $this->assertEquals($group->id, $event['groupid']);
+    }
+
+    /**
+     * A user should be able to create an event for a group that they are a member of in
+     * a course in which they are enrolled and have the moodle/calendar:managegroupentries capability.
+     */
+    public function test_submit_create_update_form_create_group_event_group_member_manage_group_entries() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course = $generator->create_course();
+        $group = $generator->create_group(array('courseid' => $course->id));
+        $context = context_course::instance($course->id);
+        $roleid = $generator->create_role();
+        $timestart = new DateTime();
+        $interval = new DateInterval("P1D"); // One day.
+        $timedurationuntil = new DateTime();
+        $timedurationuntil->add($interval);
+        $formdata = [
+            'id' => 0,
+            'userid' => $user->id,
+            'modulename' => '',
+            'instance' => 0,
+            'visible' => 1,
+            'name' => 'Test',
+            'timestart' => [
+                'day' => $timestart->format('j'),
+                'month' => $timestart->format('n'),
+                'year' => $timestart->format('Y'),
+                'hour' => $timestart->format('G'),
+                'minute' => 0,
+            ],
+            'eventtype' => 'group',
+            'groupid' => "{$course->id}-{$group->id}", // The form format.
+            'groupcourseid' => $course->id,
+            'description' => [
+                'text' => '',
+                'format' => 1,
+            ],
+            'duration' => 1,
+            'timedurationuntil' => [
+                'day' => $timedurationuntil->format('j'),
+                'month' => $timedurationuntil->format('n'),
+                'year' => $timedurationuntil->format('Y'),
+                'hour' => $timedurationuntil->format('G'),
+                'minute' => 0,
+            ]
+        ];
+
+        $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata);
+        $querystring = http_build_query($formdata, '', '&');
+
+        $generator->enrol_user($user->id, $course->id, 'student');
+        $generator->role_assign($roleid, $user->id, $context->id);
+        $generator->create_group_member(['groupid' => $group->id, 'userid' => $user->id]);
+
+        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
+        assign_capability('moodle/calendar:managegroupentries', CAP_ALLOW, $roleid, $context, true);
+
+        $user->ignoresesskey = true;
+        $this->resetAfterTest(true);
+        $this->setUser($user);
+
+        $result = external_api::clean_returnvalue(
+            core_calendar_external::submit_create_update_form_returns(),
+            core_calendar_external::submit_create_update_form($querystring)
+        );
+
+        $event = $result['event'];
+        $this->assertEquals($user->id, $event['userid']);
+        $this->assertEquals($formdata['eventtype'], $event['eventtype']);
+        $this->assertEquals($formdata['name'], $event['name']);
+        $this->assertEquals($group->id, $event['groupid']);
+    }
+
+    /**
+     * A user should be able to create an event for any group in a course in which
+     * they are enrolled and have the moodle/site:accessallgroups capability.
+     */
+    public function test_submit_create_update_form_create_group_event_access_all_groups() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course = $generator->create_course();
+        $group = $generator->create_group(array('courseid' => $course->id));
+        $context = context_course::instance($course->id);
+        $roleid = $generator->create_role();
+        $timestart = new DateTime();
+        $interval = new DateInterval("P1D"); // One day.
+        $timedurationuntil = new DateTime();
+        $timedurationuntil->add($interval);
+        $formdata = [
+            'id' => 0,
+            'userid' => $user->id,
+            'modulename' => '',
+            'instance' => 0,
+            'visible' => 1,
+            'name' => 'Test',
+            'timestart' => [
+                'day' => $timestart->format('j'),
+                'month' => $timestart->format('n'),
+                'year' => $timestart->format('Y'),
+                'hour' => $timestart->format('G'),
+                'minute' => 0,
+            ],
+            'eventtype' => 'group',
+            'groupid' => "{$course->id}-{$group->id}", // The form format.
+            'groupcourseid' => $course->id,
+            'description' => [
+                'text' => '',
+                'format' => 1,
+            ],
+            'duration' => 1,
+            'timedurationuntil' => [
+                'day' => $timedurationuntil->format('j'),
+                'month' => $timedurationuntil->format('n'),
+                'year' => $timedurationuntil->format('Y'),
+                'hour' => $timedurationuntil->format('G'),
+                'minute' => 0,
+            ]
+        ];
+
+        $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata);
+        $querystring = http_build_query($formdata, '', '&');
+
+        $generator->enrol_user($user->id, $course->id, 'student');
+        $generator->role_assign($roleid, $user->id, $context->id);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+        assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, $context, true);
+
+        $user->ignoresesskey = true;
+        $this->resetAfterTest(true);
+        $this->setUser($user);
+
+        $result = external_api::clean_returnvalue(
+            core_calendar_external::submit_create_update_form_returns(),
+            core_calendar_external::submit_create_update_form($querystring)
+        );
+
+        $event = $result['event'];
+        $this->assertEquals($user->id, $event['userid']);
+        $this->assertEquals($formdata['eventtype'], $event['eventtype']);
+        $this->assertEquals($formdata['name'], $event['name']);
+        $this->assertEquals($group->id, $event['groupid']);
+    }
+
+    /**
+     * A user should not be able to create an event for any group that they are not a
+     * member of in a course in which they are enrolled but don't have the
+     * moodle/site:accessallgroups capability.
+     */
+    public function test_submit_create_update_form_create_group_event_non_member_no_permission() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course = $generator->create_course();
+        $group = $generator->create_group(array('courseid' => $course->id));
+        $context = context_course::instance($course->id);
+        $roleid = $generator->create_role();
+        $timestart = new DateTime();
+        $interval = new DateInterval("P1D"); // One day.
+        $timedurationuntil = new DateTime();
+        $timedurationuntil->add($interval);
+        $formdata = [
+            'id' => 0,
+            'userid' => $user->id,
+            'modulename' => '',
+            'instance' => 0,
+            'visible' => 1,
+            'name' => 'Test',
+            'timestart' => [
+                'day' => $timestart->format('j'),
+                'month' => $timestart->format('n'),
+                'year' => $timestart->format('Y'),
+                'hour' => $timestart->format('G'),
+                'minute' => 0,
+            ],
+            'eventtype' => 'group',
+            'groupid' => "{$course->id}-{$group->id}", // The form format.
+            'groupcourseid' => $course->id,
+            'description' => [
+                'text' => '',
+                'format' => 1,
+            ],
+            'duration' => 1,
+            'timedurationuntil' => [
+                'day' => $timedurationuntil->format('j'),
+                'month' => $timedurationuntil->format('n'),
+                'year' => $timedurationuntil->format('Y'),
+                'hour' => $timedurationuntil->format('G'),
+                'minute' => 0,
+            ]
+        ];
+
+        $formdata = \core_calendar\local\event\forms\create::mock_generate_submit_keys($formdata);
+        $querystring = http_build_query($formdata, '', '&');
+
+        $generator->enrol_user($user->id, $course->id, 'student');
+        $generator->role_assign($roleid, $user->id, $context->id);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+        assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $roleid, $context, true);
+
+        $user->ignoresesskey = true;
+        $this->resetAfterTest(true);
+        $this->setUser($user);
+
+        $result = external_api::clean_returnvalue(
+            core_calendar_external::submit_create_update_form_returns(),
+            core_calendar_external::submit_create_update_form($querystring)
+        );
+
+        $this->assertTrue($result['validationerror']);
+    }
 }
diff --git a/course/classes/analytics/indicator/completion_enabled.php b/course/classes/analytics/indicator/completion_enabled.php
new file mode 100644 (file)
index 0000000..f2598e0
--- /dev/null
@@ -0,0 +1,89 @@
+<?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/>.
+
+/**
+ * Completion enabled set indicator.
+ *
+ * @package   core_course
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_course\analytics\indicator;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/lib/completionlib.php');
+
+/**
+ * Completion enabled set indicator.
+ *
+ * @package   core_course
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class completion_enabled extends \core_analytics\local\indicator\binary {
+
+    /**
+     * get_name
+     *
+     * @return new \lang_string
+     */
+    public static function get_name() : \lang_string {
+        return new \lang_string('indicator:completionenabled', 'moodle');
+    }
+
+    /**
+     * required_sample_data
+     *
+     * @return string[]
+     */
+    public static function required_sample_data() {
+        // Minimum course although it also accepts course_modules.
+        return array('course');
+    }
+
+    /**
+     * Is completion enabled? Work both with courses and activities.
+     *
+     * @param int $sampleid
+     * @param string $sampleorigin
+     * @param int|false $notusedstarttime
+     * @param int|false $notusedendtime
+     * @return float
+     */
+    public function calculate_sample($sampleid, $sampleorigin, $notusedstarttime = false, $notusedendtime = false) {
+
+        $course = $this->retrieve('course', $sampleid);
+
+        $cm = false;
+        if ($sampleorigin === 'course_modules') {
+            $cm = $this->retrieve('course_modules', $sampleid);
+        }
+
+        $completion = new \completion_info($course);
+
+        if (!$completion->is_enabled($cm)) {
+            $value = self::get_min_value();
+        } else if (!$cm && !$completion->has_criteria()) {
+            // Course completion enabled with no criteria counts as nothing.
+            $value = self::get_min_value();
+        } else {
+            $value = self::get_max_value();
+        }
+        return $value;
+    }
+}
index 99051c7..ade6901 100644 (file)
@@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die();
 /**
  * No teacher indicator.
  *
- * @package   core_analytics
+ * @package   core_course
  * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -64,6 +64,23 @@ class no_teacher extends \core_analytics\local\indicator\binary {
         return array('context', 'course');
     }
 
+    /**
+     * Reversed because the indicator is in 'negative' and the max returned value means teacher present.
+     *
+     * @param float $value
+     * @param string $subtype
+     * @return string
+     */
+    public function get_display_value($value, $subtype = false) {
+
+        // No subtypes for binary values by default.
+        if ($value == -1) {
+            return get_string('yes');
+        } else if ($value == 1) {
+            return get_string('no');
+        }
+    }
+
     /**
      * calculate_sample
      *
diff --git a/course/classes/analytics/indicator/potential_cognitive_depth.php b/course/classes/analytics/indicator/potential_cognitive_depth.php
new file mode 100644 (file)
index 0000000..ea96e42
--- /dev/null
@@ -0,0 +1,136 @@
+<?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/>.
+
+/**
+ * Potential cognitive depth indicator.
+ *
+ * @package   core_course
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_course\analytics\indicator;
+
+defined('MOODLE_INTERNAL') || die();
+
+use \core_analytics\local\indicator\community_of_inquiry_activity;
+
+/**
+ * Potential cognitive depth indicator.
+ *
+ * It extends linear instead of discrete as there is a linear relation between
+ * the different cognitive levels activities can reach.
+ *
+ * @package   core_course
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class potential_cognitive_depth extends \core_analytics\local\indicator\linear {
+
+    /**
+     * get_name
+     *
+     * @return \lang_string
+     */
+    public static function get_name() : \lang_string {
+        return new \lang_string('indicator:potentialcognitive', 'moodle');
+    }
+
+    /**
+     * Specify the required data to process this indicator.
+     *
+     * @return string[]
+     */
+    public static function required_sample_data() {
+        // We require course because, although this indicator can also work with course_modules we can't
+        // calculate anything without the course.
+        return array('course');
+    }
+
+    /**
+     * calculate_sample
+     *
+     * @throws \coding_exception
+     * @param int $sampleid
+     * @param string $sampleorigin
+     * @param int|false $notusedstarttime
+     * @param int|false $notusedendtime
+     * @return float
+     */
+    public function calculate_sample($sampleid, $sampleorigin, $notusedstarttime = false, $notusedendtime = false) {
+
+        if ($sampleorigin === 'course_modules') {
+            $cm = $this->retrieve('course_modules', $sampleid);
+            $cminfo = \cm_info::create($cm);
+
+            $cognitivedepthindicator = $this->get_cognitive_indicator($cminfo->modname);
+            $potentiallevel = $cognitivedepthindicator->get_cognitive_depth_level($cminfo);
+            if ($potentiallevel > community_of_inquiry_activity::MAX_COGNITIVE_LEVEL) {
+                throw new \coding_exception('Maximum cognitive depth level is ' .
+                    community_of_inquiry_activity::MAX_COGNITIVE_LEVEL . ', ' . $potentiallevel . ' provided by ' .
+                        get_class($this));
+            }
+
+        } else {
+            $course = $this->retrieve('course', $sampleid);
+            $modinfo = get_fast_modinfo($course);
+
+            $cms = $modinfo->get_cms();
+            if (!$cms) {
+                return self::get_min_value();
+            }
+
+            $potentiallevel = 0;
+            foreach ($cms as $cm) {
+                if (!$cognitivedepthindicator = $this->get_cognitive_indicator($cm->modname)) {
+                    continue;
+                }
+                $level = $cognitivedepthindicator->get_cognitive_depth_level($cm);
+                if ($level > community_of_inquiry_activity::MAX_COGNITIVE_LEVEL) {
+                    throw new \coding_exception('Maximum cognitive depth level is ' .
+                        community_of_inquiry_activity::MAX_COGNITIVE_LEVEL . ', ' . $level . ' provided by ' . get_class($this));
+                }
+                if ($level > $potentiallevel) {
+                    $potentiallevel = $level;
+                }
+            }
+        }
+
+        // Values from -1 to 1 range split in 5 parts (the max cognitive depth level).
+        // Note that we divide by 4 because we start from -1.
+        $levelscore = round((self::get_max_value() - self::get_min_value()) / 4, 2);
+        // We substract $levelscore because we want to start from the lower score and there is no cognitive depth level 0.
+        return self::get_min_value() + ($levelscore * $potentiallevel) - $levelscore;
+    }
+
+    /**
+     * Returns the cognitive depth class of this indicator.
+     *
+     * @param string $modname
+     * @return \core_analytics\local\indicator\base|false
+     */
+    protected function get_cognitive_indicator($modname) {
+        $indicators = \core_analytics\manager::get_all_indicators();
+        foreach ($indicators as $indicator) {
+            if ($indicator instanceof community_of_inquiry_activity &&
+                    $indicator->get_indicator_type() === community_of_inquiry_activity::INDICATOR_COGNITIVE &&
+                    $indicator->get_activity_type() === $modname) {
+                return $indicator;
+            }
+        }
+        return false;
+    }
+}
diff --git a/course/classes/analytics/indicator/potential_social_breadth.php b/course/classes/analytics/indicator/potential_social_breadth.php
new file mode 100644 (file)
index 0000000..dc149b5
--- /dev/null
@@ -0,0 +1,148 @@
+<?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/>.
+
+/**
+ * Potential social breadth indicator.
+ *
+ * @package   core_course
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_course\analytics\indicator;
+
+defined('MOODLE_INTERNAL') || die();
+
+use \core_analytics\local\indicator\community_of_inquiry_activity;
+
+/**
+ * Potential social breadth indicator.
+ *
+ * It extends linear instead of discrete as there is a linear relation between
+ * the different social levels activities can reach.
+ *
+ * @package   core_course
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class potential_social_breadth extends \core_analytics\local\indicator\linear {
+
+    /**
+     * get_name
+     *
+     * @return \lang_string
+     */
+    public static function get_name() : \lang_string {
+        return new \lang_string('indicator:potentialsocial', 'moodle');
+    }
+
+    /**
+     * Specify the required data to process this indicator.
+     *
+     * @return string[]
+     */
+    public static function required_sample_data() {
+        // We require course because, although this indicator can also work with course_modules we can't
+        // calculate anything without the course.
+        return array('course');
+    }
+
+    /**
+     * calculate_sample
+     *
+     * @param int $sampleid
+     * @param string $sampleorigin
+     * @param int|false $notusedstarttime
+     * @param int|false $notusedendtime
+     * @return float
+     */
+    public function calculate_sample($sampleid, $sampleorigin, $notusedstarttime = false, $notusedendtime = false) {
+
+        if ($sampleorigin === 'course_modules') {
+            $cm = $this->retrieve('course_modules', $sampleid);
+            $cminfo = \cm_info::create($cm);
+
+            $socialbreadthindicator = $this->get_social_indicator($cminfo->modname);
+            $potentiallevel = $socialbreadthindicator->get_social_breadth_level($cminfo);
+            if ($potentiallevel > community_of_inquiry_activity::MAX_SOCIAL_LEVEL) {
+                $this->level_not_accepted($potentiallevel);
+            }
+
+        } else {
+            $course = $this->retrieve('course', $sampleid);
+            $modinfo = get_fast_modinfo($course);
+
+            $cms = $modinfo->get_cms();
+            if (!$cms) {
+                return self::get_min_value();
+            }
+
+            $potentiallevel = 0;
+            foreach ($cms as $cm) {
+                if (!$socialbreadthindicator = $this->get_social_indicator($cm->modname)) {
+                    continue;
+                }
+                $level = $socialbreadthindicator->get_social_breadth_level($cm);
+                if ($level > community_of_inquiry_activity::MAX_SOCIAL_LEVEL) {
+                    $this->level_not_accepted($level);
+                }
+                if ($level > $potentiallevel) {
+                    $potentiallevel = $level;
+                }
+            }
+        }
+
+        // Core activities social breadth only reaches level 2, until core activities social
+        // breadth do not reach level 5 we limit it to what we currently support, which is level 2.
+        if ($potentiallevel > 2) {
+            $potentiallevel = 2;
+        }
+
+        // Supporting only social breadth level 1 and 2 the possible values are -1 or 1.
+        $levelscore = round(self::get_max_value() - self::get_min_value(), 2);
+
+        // We substract $levelscore because we want to start from the lower socre and there is no cognitive depth level 0.
+        return self::get_min_value() + ($levelscore * $potentiallevel) - $levelscore;
+    }
+
+    /**
+     * Returns the social breadth class of this indicator.
+     *
+     * @param string $modname
+     * @return \core_analytics\local\indicator\base|false
+     */
+    protected function get_social_indicator($modname) {
+        $indicators = \core_analytics\manager::get_all_indicators();
+        foreach ($indicators as $indicator) {
+            if ($indicator instanceof community_of_inquiry_activity &&
+                    $indicator->get_indicator_type() === community_of_inquiry_activity::INDICATOR_SOCIAL &&
+                    $indicator->get_activity_type() === $modname) {
+                return $indicator;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Throw a \coding_exception.
+     *
+     * @param int $level
+     */
+    protected function level_not_accepted($level) {
+        throw new \coding_exception('Activities\' potential social breadth go from 1 to ' .
+            community_of_inquiry_activity::MAX_SOCIAL_LEVEL . '.');
+    }
+}
index 8b22a9a..2ccc6f1 100644 (file)
@@ -3893,18 +3893,17 @@ function course_get_user_navigation_options($context, $course = null) {
 
     // Frontpage settings?
     if ($isfrontpage) {
-        if ($course->id == SITEID) {
-            $options->participants = has_capability('moodle/site:viewparticipants', $sitecontext);
-        } else {
-            $options->participants = has_capability('moodle/course:viewparticipants', context_course::instance($course->id));
-        }
-
+        // We are on the front page, so make sure we use the proper capability (site:viewparticipants).
+        $options->participants = has_capability('moodle/site:viewparticipants', $sitecontext) ||
+            has_capability('moodle/course:enrolreview', $sitecontext);
         $options->badges = !empty($CFG->enablebadges) && has_capability('moodle/badges:viewbadges', $sitecontext);
         $options->tags = !empty($CFG->usetags) && $isloggedin;
         $options->search = !empty($CFG->enableglobalsearch) && has_capability('moodle/search:query', $sitecontext);
         $options->calendar = $isloggedin;
     } else {
-        $options->participants = has_capability('moodle/course:viewparticipants', $context);
+        // We are in a course, so make sure we use the proper capability (course:viewparticipants).
+        $options->participants = has_capability('moodle/course:viewparticipants', $context) ||
+            has_capability('moodle/course:enrolreview', $context);
         $options->badges = !empty($CFG->enablebadges) && !empty($CFG->badges_allowcoursebadges) &&
                             has_capability('moodle/badges:viewbadges', $context);
         // Add view grade report is permitted.
diff --git a/course/tests/indicators_test.php b/course/tests/indicators_test.php
new file mode 100644 (file)
index 0000000..b9222d7
--- /dev/null
@@ -0,0 +1,315 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for core_course indicators.
+ *
+ * @package   core_course
+ * @category  analytics
+ * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/../../completion/criteria/completion_criteria_self.php');
+
+
+/**
+ * Unit tests for core_course indicators.
+ *
+ * @package   core_course
+ * @category  analytics
+ * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_course_indicators_testcase extends advanced_testcase {
+
+    /**
+     * test_no_teacher
+     *
+     * @return void
+     */
+    public function test_no_teacher() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+        $coursecontext1 = \context_course::instance($course1->id);
+        $coursecontext2 = \context_course::instance($course2->id);
+
+        $user = $this->getDataGenerator()->create_user();
+
+        $this->getDataGenerator()->enrol_user($user->id, $course1->id, 'student');
+        $this->getDataGenerator()->enrol_user($user->id, $course2->id, 'teacher');
+
+        $indicator = new \core_course\analytics\indicator\no_teacher();
+
+        $sampleids = array($course1->id => $course1->id, $course2->id => $course2->id);
+        $data = array(
+            $course1->id => array(
+                'context' => $coursecontext1,
+                'course' => $course1,
+            ),
+            $course2->id => array(
+                'context' => $coursecontext2,
+                'course' => $course2,
+            ));
+        $indicator->add_sample_data($data);
+
+        list($values, $ignored) = $indicator->calculate($sampleids, 'course');
+        $this->assertEquals($indicator::get_min_value(), $values[$course1->id][0]);
+        $this->assertEquals($indicator::get_max_value(), $values[$course2->id][0]);
+    }
+
+    /**
+     * test_completion_enabled
+     *
+     * @return void
+     */
+    public function test_completion_enabled() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $course1 = $this->getDataGenerator()->create_course(array('enablecompletion' => 0));
+        $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
+        $course3 = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
+
+        // Criteria only for the last one.
+        $criteriadata = new stdClass();
+        $criteriadata->id = $course3->id;
+        $criteriadata->criteria_self = 1;
+        $criterion = new completion_criteria_self();
+        $criterion->update_config($criteriadata);
+
+        $indicator = new \core_course\analytics\indicator\completion_enabled();
+
+        $sampleids = array($course1->id => $course1->id, $course2->id => $course2->id, $course3->id => $course3->id);
+        $data = array(
+            $course1->id => array(
+                'course' => $course1,
+            ),
+            $course2->id => array(
+                'course' => $course2,
+            ),
+            $course3->id => array(
+                'course' => $course3,
+            ));
+        $indicator->add_sample_data($data);
+
+        // Calculate using course samples.
+        list($values, $ignored) = $indicator->calculate($sampleids, 'course');
+        $this->assertEquals($indicator::get_min_value(), $values[$course1->id][0]);
+        $this->assertEquals($indicator::get_min_value(), $values[$course2->id][0]);
+        $this->assertEquals($indicator::get_max_value(), $values[$course3->id][0]);
+
+        // Calculate using course_modules samples.
+        $indicator->clear_sample_data();
+        $data1 = $this->getDataGenerator()->create_module('data', array('course' => $course3->id),
+                                                             array('completion' => 0));
+        $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course3->id),
+                                                             array('completion' => 1));
+
+        $sampleids = array($data1->cmid => $data1->cmid, $data2->cmid => $data2->cmid);
+        $cm1 = $DB->get_record('course_modules', array('id' => $data1->cmid));
+        $cm2 = $DB->get_record('course_modules', array('id' => $data2->cmid));
+        $data = array(
+            $cm1->id => array(
+                'course' => $course3,
+                'course_modules' => $cm1,
+            ),
+            $cm2->id => array(
+                'course' => $course3,
+                'course_modules' => $cm2,
+            ));
+        $indicator->add_sample_data($data);
+
+        list($values, $ignored) = $indicator->calculate($sampleids, 'course_modules');
+        $this->assertEquals($indicator::get_min_value(), $values[$cm1->id][0]);
+        $this->assertEquals($indicator::get_max_value(), $values[$cm2->id][0]);
+    }
+
+    /**
+     * test_potential_cognitive
+     *
+     * @return void
+     */
+    public function test_potential_cognitive() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $course1 = $this->getDataGenerator()->create_course();
+
+        $course2 = $this->getDataGenerator()->create_course();
+        $page = $this->getDataGenerator()->create_module('page', array('course' => $course2->id));
+
+        $course3 = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course3->id));
+        $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course3->id));
+
+        $course4 = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course4->id));
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course4->id));
+
+        $indicator = new \core_course\analytics\indicator\potential_cognitive_depth();
+
+        $sampleids = array($course1->id => $course1->id, $course2->id => $course2->id, $course3->id => $course3->id,
+            $course4->id => $course4->id);
+        $data = array(
+            $course1->id => array(
+                'course' => $course1,
+            ),
+            $course2->id => array(
+                'course' => $course2,
+            ),
+            $course3->id => array(
+                'course' => $course3,
+            ),
+            $course4->id => array(
+                'course' => $course4,
+            ));
+        $indicator->add_sample_data($data);
+
+        list($values, $ignored) = $indicator->calculate($sampleids, 'course');
+        $this->assertEquals($indicator::get_min_value(), $values[$course1->id][0]);
+
+        // General explanation about the points, the max level is 5 so level 1 is -1, level 2 is -0.5, level 3 is 0,
+        // level 4 is 0.5 and level 5 is 1.
+
+        // Page cognitive is level 1 (the lower one).
+        $this->assertEquals($indicator::get_min_value(), $values[$course2->id][0]);
+
+        // The maximum cognitive depth level is 5, assign level is 5 therefore the potential cognitive depth is the max.
+        $this->assertEquals($indicator::get_max_value(), $values[$course3->id][0]);
+
+        // Forum level is 4.
+        $this->assertEquals(0.5, $values[$course4->id][0]);
+
+        // Calculate using course_modules samples.
+        $course5 = $this->getDataGenerator()->create_course();
+        $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course5->id));
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course5->id));
+
+        $sampleids = array($assign->cmid => $assign->cmid, $forum->cmid => $forum->cmid);
+        $cm1 = $DB->get_record('course_modules', array('id' => $assign->cmid));
+        $cm2 = $DB->get_record('course_modules', array('id' => $forum->cmid));
+        $data = array(
+            $cm1->id => array(
+                'course' => $course5,
+                'course_modules' => $cm1,
+            ),
+            $cm2->id => array(
+                'course' => $course5,
+                'course_modules' => $cm2,
+            ));
+        $indicator->clear_sample_data();
+        $indicator->add_sample_data($data);
+
+        list($values, $ignored) = $indicator->calculate($sampleids, 'course_modules');
+        // Assign level is 5, the maximum level.
+        $this->assertEquals($indicator::get_max_value(), $values[$cm1->id][0]);
+        // Forum level is 4.
+        $this->assertEquals(0.5, $values[$cm2->id][0]);
+
+    }
+
+    /**
+     * test_potential_social
+     *
+     * @return void
+     */
+    public function test_potential_social() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $course1 = $this->getDataGenerator()->create_course();
+
+        $course2 = $this->getDataGenerator()->create_course();
+        $page = $this->getDataGenerator()->create_module('page', array('course' => $course2->id));
+
+        $course3 = $this->getDataGenerator()->create_course();
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course3->id));
+        $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course3->id));
+
+        $course4 = $this->getDataGenerator()->create_course();
+        $page = $this->getDataGenerator()->create_module('page', array('course' => $course4->id));
+        $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course4->id));
+
+        $indicator = new \core_course\analytics\indicator\potential_social_breadth();
+
+        $sampleids = array($course1->id => $course1->id, $course2->id => $course2->id, $course3->id => $course3->id,
+            $course4->id => $course4->id);
+        $data = array(
+            $course1->id => array(
+                'course' => $course1,
+            ),
+            $course2->id => array(
+                'course' => $course2,
+            ),
+            $course3->id => array(
+                'course' => $course3,
+            ),
+            $course4->id => array(
+                'course' => $course4,
+            ));
+        $indicator->add_sample_data($data);
+
+        list($values, $ignored) = $indicator->calculate($sampleids, 'course');
+        $this->assertEquals($indicator::get_min_value(), $values[$course1->id][0]);
+
+        // General explanation about the points, the max level is 2 so level 1 is -1, level 2 is 1.
+
+        // Page social is level 1 (the lower level).
+        $this->assertEquals($indicator::get_min_value(), $values[$course2->id][0]);
+
+        // Forum is level 2 and assign is level 2.
+        $this->assertEquals($indicator::get_max_value(), $values[$course3->id][0]);
+
+        // Page is level 1 and assign is level 2, so potential level is the max one.
+        $this->assertEquals($indicator::get_max_value(), $values[$course4->id][0]);
+
+        // Calculate using course_modules samples.
+        $course5 = $this->getDataGenerator()->create_course();
+        $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course5->id));
+        $page = $this->getDataGenerator()->create_module('page', array('course' => $course5->id));
+
+        $sampleids = array($assign->cmid => $assign->cmid, $page->cmid => $page->cmid);
+        $cm1 = $DB->get_record('course_modules', array('id' => $assign->cmid));
+        $cm2 = $DB->get_record('course_modules', array('id' => $page->cmid));
+        $data = array(
+            $cm1->id => array(
+                'course' => $course5,
+                'course_modules' => $cm1,
+            ),
+            $cm2->id => array(
+                'course' => $course5,
+                'course_modules' => $cm2,
+            ));
+        $indicator->clear_sample_data();
+        $indicator->add_sample_data($data);
+
+        list($values, $ignored) = $indicator->calculate($sampleids, 'course_modules');
+        // Assign social is level 2 (the max level).
+        $this->assertEquals($indicator::get_max_value(), $values[$cm1->id][0]);
+        // Page social is level 1 (the lower level).
+        $this->assertEquals($indicator::get_min_value(), $values[$cm2->id][0]);
+    }
+}
index bf6ab9d..64356fa 100644 (file)
@@ -554,7 +554,7 @@ class course_enrolment_manager {
     }
 
     /**
-     * Gets all of the enrolment plugins that are active for this course.
+     * Gets all of the enrolment plugins that are available for this course.
      *
      * @param bool $onlyenabled return only enabled enrol plugins
      * @return array
index a55b867..ab9b862 100644 (file)
@@ -49,7 +49,7 @@
     }
 }}
 <span>
-    <img height="18" src="{{profileimageurlsmall}}" alt="" role="presentation">
+    <img height="12" src="{{profileimageurlsmall}}" alt="" role="presentation">
     <span>{{fullname}}</span>
     {{#hasidentity}}
     <span><small>{{identity}}</small></span>
index 831189a..9595a03 100644 (file)
@@ -58,7 +58,7 @@ class user_groups_editable extends \core\output\inplace_editable {
      */
     public function __construct($course, $context, $user, $coursegroups, $value) {
         // Check capabilities to get editable value.
-        $editable = has_capability('moodle/course:managegroups', $context);
+        $editable = has_capability('moodle/course:managegroups', $context) && !empty($coursegroups);
 
         // Invent an itemid.
         $itemid = $course->id . ':' . $user->id;
index 60b8075..eccd233 100644 (file)
@@ -148,6 +148,7 @@ $string['importfromurl'] = 'Calendar URL';
 $string['importfrominstructions'] = 'Please provide either a URL to a remote calendar, or upload a file.';
 $string['invalidtimedurationminutes'] = 'The duration in minutes you have entered is invalid. Please enter the duration in minutes greater than 0 or select no duration.';
 $string['invalidtimedurationuntil'] = 'The date and time you selected for duration until is before the start time of the event. Please correct this before proceeding.';
+$string['invalideventtype'] = 'The event type you have selected is invalid.';
 $string['iwanttoexport'] = 'Export';
 $string['less'] = 'Less';
 $string['managesubscriptions'] = 'Manage subscriptions';
@@ -228,6 +229,7 @@ $string['tuesday'] = 'Tuesday';
 $string['typeclose'] = 'Close event';
 $string['typecourse'] = 'Course event';
 $string['typedue'] = 'Due event';
+$string['typegradingdue'] = 'Grading due event';
 $string['typegroup'] = 'Group event';
 $string['typeopen'] = 'Open event';
 $string['typesite'] = 'Site event';
index ae18f7c..666a8a5 100644 (file)
@@ -139,6 +139,7 @@ $string['unenrolroleusers'] = 'Unenrol users';
 $string['uninstallmigrating'] = 'Migrating "{$a}" enrolments';
 $string['unknowajaxaction'] = 'Unknown action requested';
 $string['unlimitedduration'] = 'Unlimited';
+$string['userremovedfromselectiona'] = 'User "{$a}" was removed from the selection.';
 $string['usersearch'] = 'Search ';
 $string['withselectedusers'] = 'With selected users';
 $string['extremovedaction'] = 'External unenrol action';
index e65d8f8..cdfa6b7 100644 (file)
@@ -506,6 +506,7 @@ $string['statsnodata'] = 'There is no available data for that combination of cou
 $string['storedfilecannotcreatefile'] = 'Can not create local file pool file, please verify permissions in dataroot and available disk space.';
 $string['storedfilecannotcreatefiledirs'] = 'Can not create local file pool directories, please verify permissions in dataroot.';
 $string['storedfilecannotread'] = 'Cannot read file. Either the file does not exist or there is a permission problem.';
+$string['storedfilecannotreadfile'] = 'Cannot read file \'{$a}\'. Either the file does not exist or there is a permission problem.';
 $string['storedfilenotcreated'] = 'Can not create file "{$a->contextid}/{$a->component}/{$a->filearea}/{$a->itemid}{$a->filepath}{$a->filename}"';
 $string['storedfileproblem'] = 'Unknown exception related to local files ({$a})';
 $string['tagdisabled'] = 'Tags are disabled!';
index e0f3592..6c18422 100644 (file)
@@ -1031,8 +1031,14 @@ $string['indicator:anywrite'] = 'Any write action';
 $string['indicator:anywrite_help'] = '';
 $string['indicator:completeduserprofile'] = 'User profile is completed';
 $string['indicator:completeduserprofile_help'] = '';
+$string['indicator:completionenabled'] = 'Completion enabled';
+$string['indicator:completionenabled_help'] = '';
 $string['indicator:noteacher'] = 'There are no teachers';
 $string['indicator:noteacher_help'] = '';
+$string['indicator:potentialcognitive'] = 'Potential cognitive depth';
+$string['indicator:potentialcognitive_help'] = '';
+$string['indicator:potentialsocial'] = 'Potential social breadth';
+$string['indicator:potentialsocial_help'] = '';
 $string['indicator:readactions'] = 'Read actions amount';
 $string['indicator:readactions_help'] = '';
 $string['indicator:userforumstracking'] = 'User is tracking forums';
index e0e9f5d..ce69373 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
index 73071d8..814bc4a 100644 (file)
Binary files a/lib/amd/build/inplace_editable.min.js and b/lib/amd/build/inplace_editable.min.js differ
index 12f44eb..60c678f 100644 (file)
@@ -779,8 +779,9 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                 return false;
             }
 
+            originalSelect.css('visibility', 'hidden').attr('aria-hidden', true);
+
             // Hide the original select.
-            originalSelect.hide().attr('aria-hidden', true);
 
             // Find or generate some ids.
             var state = {
@@ -816,10 +817,11 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
             var renderSelection = templates.render('core/form_autocomplete_selection', context);
 
             return $.when(renderInput, renderDatalist, renderSelection).then(function(input, suggestions, selection) {
-                // Add our new UI elements to the page.
+                originalSelect.hide();
                 originalSelect.after(suggestions);
                 originalSelect.after(input);
                 originalSelect.after(selection);
+
                 // Update the form label to point to the text input.
                 originalLabel.attr('for', state.inputId);
                 // Add the event handlers.
index 20ca0fb..1b5916a 100644 (file)
@@ -171,7 +171,7 @@ define(['jquery',
 
         var turnEditingOnSelect = function(el, options) {
             var i,
-                inputelement = $('<select></select>').
+                inputelement = $('<select class="form-autocomplete-original-select"></select>').
                     attr('id', uniqueId('id_inplacevalue_', 20)).
                     addClass('custom-select'),
                 lbl = $('<label class="accesshide">' + mainelement.attr('data-editlabel') + '</label>')
@@ -274,11 +274,15 @@ define(['jquery',
             });
             saveelement.on('click', function(e) {
                 var val = JSON.stringify(inputelement.val());
+                // We need to empty the node to destroy all event handlers etc.
+                inputelement.empty();
                 turnEditingOff(el);
                 updateValue(el, val);
                 e.preventDefault();
             });
             cancelelement.on('click', function(e) {
+                // We need to empty the node to destroy all event handlers etc.
+                inputelement.empty();
                 turnEditingOff(el);
                 e.preventDefault();
             });
index 21937c4..ef54503 100644 (file)
@@ -75,14 +75,7 @@ class no_teaching extends \core_analytics\local\target\binary {
         $actions['viewcourse'] = new \core_analytics\prediction_action('viewcourse', $prediction,
             $url, $pix, get_string('view'));
 
-        if (has_capability('moodle/course:enrolreview', $sampledata['context'])) {
-            $url = new \moodle_url('/user/index.php', array('id' => $course->id));
-            $pix = new \pix_icon('i/enrolusers', get_string('enrolledusers', 'enrol'));
-            $actions['enrolusers'] = new \core_analytics\prediction_action('enrolusers', $prediction,
-                $url, $pix, get_string('enrolledusers', 'enrol'));
-        }
-
-        if (has_capability('moodle/course:viewparticipants', $sampledata['context'])) {
+        if (has_any_capability(['moodle/course:viewparticipants', 'moodle/course:enrolreview'], $sampledata['context'])) {
             $url = new \moodle_url('/user/index.php', array('id' => $course->id));
             $pix = new \pix_icon('i/cohort', get_string('participants'));
             $actions['viewparticipants'] = new \core_analytics\prediction_action('viewparticipants', $prediction,
index a27e605..bba05f6 100644 (file)
@@ -44,7 +44,7 @@ class api {
 
     /**
      * Create a google ready OAuth 2 service.
-     * @return core\oauth2\issuer
+     * @return \core\oauth2\issuer
      */
     private static function create_google() {
         $record = (object) [
@@ -70,7 +70,7 @@ class api {
 
     /**
      * Create a facebook ready OAuth 2 service.
-     * @return core\oauth2\issuer
+     * @return \core\oauth2\issuer
      */
     private static function create_facebook() {
         // Facebook is a custom setup.
@@ -125,7 +125,7 @@ class api {
 
     /**
      * Create a microsoft ready OAuth 2 service.
-     * @return core\oauth2\issuer
+     * @return \core\oauth2\issuer
      */
     private static function create_microsoft() {
         // Microsoft is a custom setup.
@@ -200,7 +200,7 @@ class api {
 
     /**
      * List all the issuers, ordered by the sortorder field
-     * @return core\oauth2\issuer[]
+     * @return \core\oauth2\issuer[]
      */
     public static function get_all_issuers() {
         return issuer::get_records([], 'sortorder');
@@ -210,7 +210,7 @@ class api {
      * Get a single issuer by id.
      *
      * @param int $id
-     * @return core\oauth2\issuer
+     * @return \core\oauth2\issuer
      */
     public static function get_issuer($id) {
         return new issuer($id);
@@ -220,7 +220,7 @@ class api {
      * Get a single endpoint by id.
      *
      * @param int $id
-     * @return core\oauth2\endpoint
+     * @return \core\oauth2\endpoint
      */
     public static function get_endpoint($id) {
         return new endpoint($id);
@@ -230,7 +230,7 @@ class api {
      * Get a single user field mapping by id.
      *
      * @param int $id
-     * @return core\oauth2\user_field_mapping
+     * @return \core\oauth2\user_field_mapping
      */
     public static function get_user_field_mapping($id) {
         return new user_field_mapping($id);
@@ -241,7 +241,7 @@ class api {
      * Never ever ever expose this to a webservice because it contains the refresh token which grants API access.
      *
      * @param \core\oauth2\issuer $issuer
-     * @return \core\oauth2\client
+     * @return system_account|false
      */
     public static function get_system_account(issuer $issuer) {
         return system_account::get_record(['issuerid' => $issuer->get('id')]);
@@ -286,8 +286,9 @@ class api {
      * Get an authenticated oauth2 client using the system account.
      * This call uses the refresh token to get an access token.
      *
-     * @param core\oauth2\issuer $issuer
-     * @return core\oauth2\client
+     * @param \core\oauth2\issuer $issuer
+     * @return \core\oauth2\client|false An authenticated client (or false if the token could not be upgraded)
+     * @throws moodle_exception Request for token upgrade failed for technical reasons
      */
     public static function get_system_oauth_client(issuer $issuer) {
         $systemaccount = self::get_system_account($issuer);
@@ -311,10 +312,10 @@ class api {
      * Get an authenticated oauth2 client using the current user account.
      * This call does the redirect dance back to the current page after authentication.
      *
-     * @param core\oauth2\issuer $issuer The desired OAuth issuer
+     * @param \core\oauth2\issuer $issuer The desired OAuth issuer
      * @param moodle_url $currenturl The url to the current page.
      * @param string $additionalscopes The additional scopes required for authorization.
-     * @return core\oauth2\client
+     * @return \core\oauth2\client
      */
     public static function get_user_oauth_client(issuer $issuer, moodle_url $currenturl, $additionalscopes = '') {
         $client = new \core\oauth2\client($issuer, $currenturl, $additionalscopes);
@@ -325,8 +326,8 @@ class api {
     /**
      * Get the list of defined endpoints for this OAuth issuer
      *
-     * @param core\oauth2\issuer $issuer The desired OAuth issuer
-     * @return core\oauth2\endpoint[]
+     * @param \core\oauth2\issuer $issuer The desired OAuth issuer
+     * @return \core\oauth2\endpoint[]
      */
     public static function get_endpoints(issuer $issuer) {
         return endpoint::get_records(['issuerid' => $issuer->get('id')]);
@@ -335,8 +336,8 @@ class api {
     /**
      * Get the list of defined mapping from OAuth user fields to moodle user fields.
      *
-     * @param core\oauth2\issuer $issuer The desired OAuth issuer
-     * @return core\oauth2\user_field_mapping[]
+     * @param \core\oauth2\issuer $issuer The desired OAuth issuer
+     * @return \core\oauth2\user_field_mapping[]
      */
     public static function get_user_field_mappings(issuer $issuer) {
         return user_field_mapping::get_records(['issuerid' => $issuer->get('id')]);
@@ -345,7 +346,7 @@ class api {
     /**
      * Guess an image from the discovery URL.
      *
-     * @param core\oauth2\issuer $issuer The desired OAuth issuer
+     * @param \core\oauth2\issuer $issuer The desired OAuth issuer
      */
     protected static function guess_image($issuer) {
         if (empty($issuer->get('image'))) {
@@ -447,7 +448,7 @@ class api {
      * Take the data from the mform and update the issuer.
      *
      * @param stdClass $data
-     * @return core\oauth2\issuer
+     * @return \core\oauth2\issuer
      */
     public static function update_issuer($data) {
         require_capability('moodle/site:config', context_system::instance());
@@ -466,7 +467,7 @@ class api {
      * Take the data from the mform and create the issuer.
      *
      * @param stdClass $data
-     * @return core\oauth2\issuer
+     * @return \core\oauth2\issuer
      */
     public static function create_issuer($data) {
         require_capability('moodle/site:config', context_system::instance());
@@ -485,7 +486,7 @@ class api {
      * Take the data from the mform and update the endpoint.
      *
      * @param stdClass $data
-     * @return core\oauth2\endpoint
+     * @return \core\oauth2\endpoint
      */
     public static function update_endpoint($data) {
         require_capability('moodle/site:config', context_system::instance());
@@ -501,7 +502,7 @@ class api {
      * Take the data from the mform and create the endpoint.
      *
      * @param stdClass $data
-     * @return core\oauth2\endpoint
+     * @return \core\oauth2\endpoint
      */
     public static function create_endpoint($data) {
         require_capability('moodle/site:config', context_system::instance());
@@ -516,7 +517,7 @@ class api {
      * Take the data from the mform and update the user field mapping.
      *
      * @param stdClass $data
-     * @return core\oauth2\user_field_mapping
+     * @return \core\oauth2\user_field_mapping
      */
     public static function update_user_field_mapping($data) {
         require_capability('moodle/site:config', context_system::instance());
@@ -532,7 +533,7 @@ class api {
      * Take the data from the mform and create the user field mapping.
      *
      * @param stdClass $data
-     * @return core\oauth2\user_field_mapping
+     * @return \core\oauth2\user_field_mapping
      */
     public static function create_user_field_mapping($data) {
         require_capability('moodle/site:config', context_system::instance());
@@ -710,7 +711,7 @@ class api {
      *
      * Requires moodle/site:config capability at the system context.
      *
-     * @param core\oauth2\issuer $issuer
+     * @param \core\oauth2\issuer $issuer
      * @param moodle_url $returnurl The url to the current page (we will be redirected back here after authentication).
      * @return boolean
      */
index 528e511..07a775d 100644 (file)
@@ -30,7 +30,6 @@ require_once($CFG->libdir . '/filelib.php');
 
 use moodle_url;
 use moodle_exception;
-use curl;
 use stdClass;
 
 /**
@@ -172,6 +171,7 @@ class client extends \oauth2_client {
      *
      * @param \core\oauth2\system_account $systemaccount
      * @return boolean true if token is upgraded succesfully
+     * @throws moodle_exception Request for token upgrade failed for technical reasons
      */
     public function upgrade_refresh_token(system_account $systemaccount) {
         $refreshtoken = $systemaccount->get('refreshtoken');
@@ -233,7 +233,7 @@ class client extends \oauth2_client {
      * Fetch the user info from the user info endpoint and map all
      * the fields back into moodle fields.
      *
-     * @return array (Moodle user fields for the logged in user).
+     * @return array|false Moodle user fields for the logged in user (or false if request failed)
      */
     public function get_userinfo() {
         $url = $this->get_issuer()->get_endpoint_url('userinfo');
@@ -244,7 +244,7 @@ class client extends \oauth2_client {
         $userinfo = new stdClass();
         try {
             $userinfo = json_decode($response);
-        } catch (Exception $e) {
+        } catch (\Exception $e) {
             return false;
         }
 
index 22cb45c..66bb0d7 100644 (file)
 namespace core\oauth2;
 
 use \core\task\scheduled_task;
+use core_user;
+use moodle_exception;
 
 defined('MOODLE_INTERNAL') || die();
 
 /**
- * Simple task to delete old messaging records.
+ * Task to refresh system tokens regularly. Admins are notified in case an authorisation expires.
  * @package    core
  * @copyright  2017 Damyon Wiese
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -50,6 +52,7 @@ class refresh_system_tokens_task extends scheduled_task {
      * @param \core\oauth2\issuer $issuer
      */
     protected function notify_admins(\core\oauth2\issuer $issuer) {
+        global $CFG;
         $admins = get_admins();
 
         if (empty($admins)) {
@@ -62,8 +65,8 @@ class refresh_system_tokens_task extends scheduled_task {
             $message = new \core\message\message();
             $message->courseid          = SITEID;
             $message->component         = 'moodle';
-            $message->name              = 'oauthrefreshtokenexpired';
-            $message->userfrom          = core\user::get_noreply_user();
+            $message->name              = 'errors';
+            $message->userfrom          = core_user::get_noreply_user();
             $message->userto            = $admin;
             $message->subject           = $short;
             $message->fullmessage       = $long;
@@ -84,7 +87,14 @@ class refresh_system_tokens_task extends scheduled_task {
         $issuers = \core\oauth2\api::get_all_issuers();
         foreach ($issuers as $issuer) {
             if ($issuer->is_system_account_connected()) {
-                if (!\core\oauth2\api::get_system_oauth_client($issuer)) {
+                try {
+                    // Try to get an authenticated client; renew token if necessary.
+                    // Returns false or throws a moodle_exception on error.
+                    $success = \core\oauth2\api::get_system_oauth_client($issuer);
+                } catch (moodle_exception $e) {
+                    $success = false;
+                }
+                if ($success === false) {
                     $this->notify_admins($issuer);
                 }
             }
index ca48a35..b6d1af3 100644 (file)
@@ -25,6 +25,7 @@ namespace core\oauth2;
 
 use curl;
 use coding_exception;
+use stdClass;
 
 defined('MOODLE_INTERNAL') || die();
 
@@ -64,7 +65,8 @@ abstract class rest {
      * @param string $functionname
      * @param array $functionargs
      * @param string $rawpost Optional param to include in the body of a post.
-     * @return string|object
+     * @param string $contenttype The MIME type for the request's Content-Type header.
+     * @return string|stdClass
      */
     public function call($functionname, $functionargs, $rawpost = false, $contenttype = false) {
         $functions = $this->get_api_functions();
index 399b613..d35b5a8 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20170801" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20170814" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="modelid" TYPE="foreign" FIELDS="modelid" REFTABLE="analytics_models" REFFIELDS="id"/>
       </KEYS>
-      <INDEXES>
-        <INDEX NAME="modelid" UNIQUE="false" FIELDS="modelid" COMMENT="Index on modelid"/>
-      </INDEXES>
     </TABLE>
     <TABLE NAME="analytics_predictions" COMMENT="Predictions">
       <FIELDS>
         <FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="sampleid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="rangeindex" TYPE="int" LENGTH="5" NOTNULL="true" SEQUENCE="false"/>
-        <FIELD NAME="prediction" TYPE="int" LENGTH="2" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="prediction" TYPE="number" LENGTH="10" NOTNULL="true" SEQUENCE="false" DECIMALS="2"/>
         <FIELD NAME="predictionscore" TYPE="number" LENGTH="10" NOTNULL="true" SEQUENCE="false" DECIMALS="5"/>
         <FIELD NAME="calculations" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="modelid" TYPE="foreign" FIELDS="modelid" REFTABLE="analytics_models" REFFIELDS="id"/>
+        <KEY NAME="contextid" TYPE="foreign" FIELDS="contextid" REFTABLE="context" REFFIELDS="id"/>
       </KEYS>
       <INDEXES>
         <INDEX NAME="modelidandcontextid" UNIQUE="false" FIELDS="modelid, contextid" COMMENT="Index on modelid and contextid"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="modelid" TYPE="foreign" FIELDS="modelid" REFTABLE="analytics_models" REFFIELDS="id"/>
+        <KEY NAME="fileid" TYPE="foreign" FIELDS="fileid" REFTABLE="files" REFFIELDS="id"/>
       </KEYS>
       <INDEXES>
         <INDEX NAME="modelidandanalysableidandtimesplitting" UNIQUE="false" FIELDS="modelid, analysableid, timesplitting" COMMENT="Index on modelid and analysableid and timesplitting"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="modelid" TYPE="foreign" FIELDS="modelid" REFTABLE="analytics_models" REFFIELDS="id"/>
       </KEYS>
       <INDEXES>
         <INDEX NAME="modelidandanalysableidandtimesplittingandrangeindex" UNIQUE="false" FIELDS="modelid, analysableid, timesplitting, rangeindex" COMMENT="Index on modelid and analysableid and timesplitting"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="modelid" TYPE="foreign" FIELDS="modelid" REFTABLE="analytics_models" REFFIELDS="id"/>
+        <KEY NAME="fileid" TYPE="foreign" FIELDS="fileid" REFTABLE="files" REFFIELDS="id"/>
       </KEYS>
       <INDEXES>
         <INDEX NAME="modelidandfileidandaction" UNIQUE="false" FIELDS="modelid, fileid, action" COMMENT="Index on modelid and fileid and action"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="contextid" TYPE="foreign" FIELDS="contextid" REFTABLE="context" REFFIELDS="id"/>
       </KEYS>
       <INDEXES>
         <INDEX NAME="starttime-endtime-contextid" UNIQUE="false" FIELDS="starttime, endtime, contextid"/>
index fcc7154..945b3bf 100644 (file)
@@ -2369,5 +2369,72 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2017082300.01);
     }
 
+    if ($oldversion < 2017082500.00) {
+        // Handle FKs for the table 'analytics_models_log'.
+        $table = new xmldb_table('analytics_models_log');
+
+        // Remove the existing index before adding FK (which creates an index).
+        $index = new xmldb_index('modelid', XMLDB_INDEX_NOTUNIQUE, array('modelid'));
+
+        // Conditionally launch drop index.
+        if ($dbman->index_exists($table, $index)) {
+            $dbman->drop_index($table, $index);
+        }
+
+        // Now, add the FK.
+        $key = new xmldb_key('modelid', XMLDB_KEY_FOREIGN, array('modelid'), 'analytics_models', array('id'));
+        $dbman->add_key($table, $key);
+
+        // Handle FKs for the table 'analytics_predictions'.
+        $table = new xmldb_table('analytics_predictions');
+        $key = new xmldb_key('modelid', XMLDB_KEY_FOREIGN, array('modelid'), 'analytics_models', array('id'));
+        $dbman->add_key($table, $key);
+
+        $key = new xmldb_key('contextid', XMLDB_KEY_FOREIGN, array('contextid'), 'context', array('id'));
+        $dbman->add_key($table, $key);
+
+        // Handle FKs for the table 'analytics_train_samples'.
+        $table = new xmldb_table('analytics_train_samples');
+        $key = new xmldb_key('modelid', XMLDB_KEY_FOREIGN, array('modelid'), 'analytics_models', array('id'));
+        $dbman->add_key($table, $key);
+
+        $key = new xmldb_key('fileid', XMLDB_KEY_FOREIGN, array('fileid'), 'files', array('id'));
+        $dbman->add_key($table, $key);
+
+        // Handle FKs for the table 'analytics_predict_samples'.
+        $table = new xmldb_table('analytics_predict_samples');
+        $key = new xmldb_key('modelid', XMLDB_KEY_FOREIGN, array('modelid'), 'analytics_models', array('id'));
+        $dbman->add_key($table, $key);
+
+        // Handle FKs for the table 'analytics_used_files'.
+        $table = new xmldb_table('analytics_used_files');
+        $key = new xmldb_key('modelid', XMLDB_KEY_FOREIGN, array('modelid'), 'analytics_models', array('id'));
+        $dbman->add_key($table, $key);
+
+        $key = new xmldb_key('fileid', XMLDB_KEY_FOREIGN, array('fileid'), 'files', array('id'));
+        $dbman->add_key($table, $key);
+
+        // Handle FKs for the table 'analytics_indicator_calc'.
+        $table = new xmldb_table('analytics_indicator_calc');
+        $key = new xmldb_key('contextid', XMLDB_KEY_FOREIGN, array('contextid'), 'context', array('id'));
+        $dbman->add_key($table, $key);
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2017082500.00);
+    }
+
+    if ($oldversion < 2017082800.00) {
+
+        // Changing type of field prediction on table analytics_predictions to number.
+        $table = new xmldb_table('analytics_predictions');
+        $field = new xmldb_field('prediction', XMLDB_TYPE_NUMBER, '10, 2', null, XMLDB_NOTNULL, null, null, 'rangeindex');
+
+        // Launch change of type for field prediction.
+        $dbman->change_field_type($table, $field);
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2017082800.00);
+    }
+
     return true;
 }
index 4b74d41..05cac2d 100644 (file)
@@ -731,11 +731,22 @@ function file_get_drafarea_files($draftitemid, $filepath = '/') {
                 $item->url = $itemurl->out();
                 $item->icon = $OUTPUT->image_url(file_file_icon($file, 24))->out(false);
                 $item->thumbnail = $OUTPUT->image_url(file_file_icon($file, 90))->out(false);
-                if ($imageinfo = $file->get_imageinfo()) {
-                    $item->realthumbnail = $itemurl->out(false, array('preview' => 'thumb', 'oid' => $file->get_timemodified()));
-                    $item->realicon = $itemurl->out(false, array('preview' => 'tinyicon', 'oid' => $file->get_timemodified()));
-                    $item->image_width = $imageinfo['width'];
-                    $item->image_height = $imageinfo['height'];
+
+                // The call to $file->get_imageinfo() fails with an exception if the file can't be read on the file system.
+                // We still want to add such files to the list, so the owner can view and delete them if needed. So, we only call
+                // get_imageinfo() on files that can be read, and we also spoof the file status based on whether it was found.
+                // We'll use the same status types used by stored_file->get_status(), where 0 = OK. 1 = problem, as these will be
+                // used by the widget to display a warning about the problem files.
+                // The value of stored_file->get_status(), and the file record are unaffected by this. It's only superficially set.
+                $item->status = $fs->get_file_system()->is_file_readable_remotely_by_storedfile($file) ? 0 : 1;
+                if ($item->status == 0) {
+                    if ($imageinfo = $file->get_imageinfo()) {
+                        $item->realthumbnail = $itemurl->out(false, array('preview' => 'thumb',
+                            'oid' => $file->get_timemodified()));
+                        $item->realicon = $itemurl->out(false, array('preview' => 'tinyicon', 'oid' => $file->get_timemodified()));
+                        $item->image_width = $imageinfo['width'];
+                        $item->image_height = $imageinfo['height'];
+                    }
                 }
             }
             $list[] = $item;
index fb2d0eb..7a6644e 100644 (file)
@@ -228,7 +228,7 @@ class MoodleQuickForm_date_time_selector extends MoodleQuickForm_group {
                 }
                 break;
             case 'createElement':
-                if ($arg[2]['optional']) {
+                if (isset($arg[2]['optional']) && $arg[2]['optional']) {
                     // When using the function addElement, rather than createElement, we still
                     // enter this case, making this check necessary.
                     if ($this->_usedcreateelement) {
index 217fc4a..3c92da3 100644 (file)
Binary files a/lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector-debug.js and b/lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector-debug.js differ
index ca09d67..238b10e 100644 (file)
Binary files a/lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector-min.js and b/lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector-min.js differ
index 217fc4a..3c92da3 100644 (file)
Binary files a/lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector.js and b/lib/form/yui/build/moodle-form-dateselector/moodle-form-dateselector.js differ
index bf84531..12b7eee 100644 (file)
@@ -1,5 +1,10 @@
 /* global CALENDAR, MOODLECALENDAR */
 
+var DIALOGUE_SELECTOR = ' [role=dialog]',
+    MENUBAR_SELECTOR = '[role=menubar]',
+    DOT = '.',
+    HAS_ZINDEX = 'moodle-has-zindex';
+
 /**
  * Add some custom methods to the node class to make our lives a little
  * easier within this module.
@@ -71,10 +76,19 @@ M.form.dateselector = {
             constrain: true // constrain panel to viewport.
         });
         this.panel.render(document.body);
-        // zIndex is added by panel.render() and is set to 0.
-        // Remove zIndex from panel, as this should be set by CSS. This can be done by removeAttr but
-        // ie8 fails and there is know issue for it.
-        Y.one('#dateselector-calendar-panel').setStyle('zIndex', null);
+
+        // Determine the correct zindex by looking at all existing dialogs and menubars in the page.
+        var highestzindex = 0;
+        Y.all(DIALOGUE_SELECTOR + ', ' + MENUBAR_SELECTOR + ', ' + DOT + HAS_ZINDEX).each(function(node) {
+            var zindex = this.findZIndex(node);
+            if (zindex > highestzindex) {
+                highestzindex = zindex;
+            }
+        }, this);
+        // Only set the zindex if we found a wrapper.
+        var zindexvalue = (highestzindex + 1).toString();
+        Y.one('#dateselector-calendar-panel').setStyle('zIndex', zindexvalue);
+
         this.panel.on('heightChange', this.fix_position, this);
 
         Y.one('#dateselector-calendar-panel').on('click', function(e) {
@@ -98,6 +112,14 @@ M.form.dateselector = {
                 config.sat]
         });
     },
+    findZIndex: function(node) {
+        // In most cases the zindex is set on the parent of the dialog.
+        var zindex = node.getStyle('zIndex') || node.ancestor().getStyle('zIndex');
+        if (zindex) {
+            return parseInt(zindex, 10);
+        }
+        return 0;
+    },
     cancel_any_timeout: function() {
         if (this.hidetimeout) {
             clearTimeout(this.hidetimeout);
index 66527b6..0a2293a 100644 (file)
@@ -1390,6 +1390,27 @@ abstract class moodleform {
             $_POST = $simulatedsubmitteddata;
         }
     }
+
+    /**
+     * Used by tests to generate valid submit keys for moodle forms that are
+     * submitted with ajax data.
+     *
+     * @throws \moodle_exception If called outside unit test environment
+     * @param array  $data Existing form data you wish to add the keys to.
+     * @return array
+     */
+    public static function mock_generate_submit_keys($data = []) {
+        if (!defined('PHPUNIT_TEST') || !PHPUNIT_TEST) {
+            throw new \moodle_exception("This function can only be used for unit testing.");
+        }
+
+        $formidentifier = get_called_class();
+        $formidentifier = str_replace('\\', '_', $formidentifier); // See MDL-56233 for more information.
+        $data['sesskey'] = sesskey();
+        $data['_qf__' . $formidentifier] = 1;
+
+        return $data;
+    }
 }
 
 /**
index 1981102..99458a1 100644 (file)
@@ -38,7 +38,7 @@ use Phpml\ModelManager;
  * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class processor implements \core_analytics\predictor {
+class processor implements \core_analytics\classifier, \core_analytics\regressor {
 
     /**
      * Size of training / prediction batches.
@@ -73,14 +73,14 @@ class processor implements \core_analytics\predictor {
     }
 
     /**
-     * Trains a machine learning algorithm with the provided training set.
+     * Train this processor classification model using the provided supervised learning dataset.
      *
      * @param string $uniqueid
      * @param \stored_file $dataset
      * @param string $outputdir
      * @return \stdClass
      */
-    public function train($uniqueid, \stored_file $dataset, $outputdir) {
+    public function train_classification($uniqueid, \stored_file $dataset, $outputdir) {
 
         // Output directory is already unique to the model.
         $modelfilepath = $outputdir . DIRECTORY_SEPARATOR . self::MODEL_FILENAME;
@@ -134,14 +134,14 @@ class processor implements \core_analytics\predictor {
     }
 
     /**
-     * Predicts the provided samples
+     * Classifies the provided dataset samples.
      *
      * @param string $uniqueid
      * @param \stored_file $dataset
      * @param string $outputdir
      * @return \stdClass
      */
-    public function predict($uniqueid, \stored_file $dataset, $outputdir) {
+    public function classify($uniqueid, \stored_file $dataset, $outputdir) {
 
         // Output directory is already unique to the model.
         $modelfilepath = $outputdir . DIRECTORY_SEPARATOR . self::MODEL_FILENAME;
@@ -199,7 +199,7 @@ class processor implements \core_analytics\predictor {
     }
 
     /**
-     * Evaluates the provided dataset.
+     * Evaluates this processor classification model using the provided supervised learning dataset.
      *
      * During evaluation we need to shuffle the evaluation dataset samples to detect deviated results,
      * if the dataset is massive we can not load everything into memory. We know that 2GB is the
@@ -216,7 +216,7 @@ class processor implements \core_analytics\predictor {
      * @param string $outputdir
      * @return \stdClass
      */
-    public function evaluate($uniqueid, $maxdeviation, $niterations, \stored_file $dataset, $outputdir) {
+    public function evaluate_classification($uniqueid, $maxdeviation, $niterations, \stored_file $dataset, $outputdir) {
         $fh = $dataset->get_content_file_handle();
 
         // The first lines are var names and the second one values.
@@ -351,6 +351,47 @@ class processor implements \core_analytics\predictor {
         return $resultobj;
     }
 
+    /**
+     * Train this processor regression model using the provided supervised learning dataset.
+     *
+     * @throws new \coding_exception
+     * @param string $uniqueid
+     * @param \stored_file $dataset
+     * @param string $outputdir
+     * @return \stdClass
+     */
+    public function train_regression($uniqueid, \stored_file $dataset, $outputdir) {
+        throw new \coding_exception('This predictor does not support regression yet.');
+    }
+
+    /**
+     * Estimates linear values for the provided dataset samples.
+     *
+     * @throws new \coding_exception
+     * @param string $uniqueid
+     * @param \stored_file $dataset
+     * @param mixed $outputdir
+     * @return void
+     */
+    public function estimate($uniqueid, \stored_file $dataset, $outputdir) {
+        throw new \coding_exception('This predictor does not support regression yet.');
+    }
+
+    /**
+     * Evaluates this processor regression model using the provided supervised learning dataset.
+     *
+     * @throws new \coding_exception
+     * @param string $uniqueid
+     * @param float $maxdeviation
+     * @param int $niterations
+     * @param \stored_file $dataset
+     * @param string $outputdir
+     * @return \stdClass
+     */
+    public function evaluate_regression($uniqueid, $maxdeviation, $niterations, \stored_file $dataset, $outputdir) {
+        throw new \coding_exception('This predictor does not support regression yet.');
+    }
+
     /**
      * Returns the Phi correlation coefficient.
      *
index 5b05fa6..84889e2 100644 (file)
@@ -33,7 +33,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class processor implements \core_analytics\predictor {
+class processor implements  \core_analytics\classifier, \core_analytics\regressor {
 
     /**
      * The required version of the python package that performs all calculations.
@@ -79,7 +79,7 @@ class processor implements \core_analytics\predictor {
      * @param string $outputdir
      * @return \stdClass
      */
-    public function train($uniqueid, \stored_file $dataset, $outputdir) {
+    public function train_classification($uniqueid, \stored_file $dataset, $outputdir) {
 
         // Obtain the physical route to the file.
         $datasetpath = $this->get_file_path($dataset);
@@ -113,14 +113,14 @@ class processor implements \core_analytics\predictor {
     }
 
     /**
-     * Returns predictions for the provided dataset samples.
+     * Classifies the provided dataset samples.
      *
      * @param string $uniqueid
      * @param \stored_file $dataset
      * @param string $outputdir
      * @return \stdClass
      */
-    public function predict($uniqueid, \stored_file $dataset, $outputdir) {
+    public function classify($uniqueid, \stored_file $dataset, $outputdir) {
 
         // Obtain the physical route to the file.
         $datasetpath = $this->get_file_path($dataset);
@@ -154,7 +154,7 @@ class processor implements \core_analytics\predictor {
     }
 
     /**
-     * Evaluates the provided dataset.
+     * Evaluates this processor classification model using the provided supervised learning dataset.
      *
      * @param string $uniqueid
      * @param float $maxdeviation
@@ -163,7 +163,7 @@ class processor implements \core_analytics\predictor {
      * @param string $outputdir
      * @return \stdClass
      */
-    public function evaluate($uniqueid, $maxdeviation, $niterations, \stored_file $dataset, $outputdir) {
+    public function evaluate_classification($uniqueid, $maxdeviation, $niterations, \stored_file $dataset, $outputdir) {
 
         // Obtain the physical route to the file.
         $datasetpath = $this->get_file_path($dataset);
@@ -195,6 +195,47 @@ class processor implements \core_analytics\predictor {
         return $resultobj;
     }
 
+    /**
+     * Train this processor regression model using the provided supervised learning dataset.
+     *
+     * @throws new \coding_exception
+     * @param string $uniqueid
+     * @param \stored_file $dataset
+     * @param string $outputdir
+     * @return \stdClass
+     */
+    public function train_regression($uniqueid, \stored_file $dataset, $outputdir) {
+        throw new \coding_exception('This predictor does not support regression yet.');
+    }
+
+    /**
+     * Estimates linear values for the provided dataset samples.
+     *
+     * @throws new \coding_exception
+     * @param string $uniqueid
+     * @param \stored_file $dataset
+     * @param mixed $outputdir
+     * @return void
+     */
+    public function estimate($uniqueid, \stored_file $dataset, $outputdir) {
+        throw new \coding_exception('This predictor does not support regression yet.');
+    }
+
+    /**
+     * Evaluates this processor regression model using the provided supervised learning dataset.
+     *
+     * @throws new \coding_exception
+     * @param string $uniqueid
+     * @param float $maxdeviation
+     * @param int $niterations
+     * @param \stored_file $dataset
+     * @param string $outputdir
+     * @return \stdClass
+     */
+    public function evaluate_regression($uniqueid, $maxdeviation, $niterations, \stored_file $dataset, $outputdir) {
+        throw new \coding_exception('This predictor does not support regression yet.');
+    }
+
     /**
      * Returns the path to the dataset file.
      *
index 981ad27..0146519 100644 (file)
@@ -240,7 +240,10 @@ class restore_assign_activity_structure_step extends restore_activity_structure_
             $flags->userid = $this->get_mappingid('user', $data->userid);
             $DB->insert_record('assign_user_flags', $flags);
         }
-
+        // Fix null grades that were rescaled.
+        if ($data->grade < 0 && $data->grade != ASSIGN_GRADE_NOT_SET) {
+            $data->grade = ASSIGN_GRADE_NOT_SET;
+        }
         $newitemid = $DB->insert_record('assign_grades', $data);
 
         // Note - the old contextid is required in order to be able to restore files stored in
index 98aa899..f75dd47 100644 (file)
@@ -46,22 +46,11 @@ class cognitive_depth extends activity_base {
         return new \lang_string('indicator:cognitivedepth', 'mod_assign');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_COGNITIVE;
     }
 
-    /**
-     * get_cognitive_depth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_cognitive_depth_level(\cm_info $cm) {
+    public function get_cognitive_depth_level(\cm_info $cm) {
         return 5;
     }
 
index 7874285..66f0d48 100644 (file)
@@ -46,22 +46,11 @@ class social_breadth extends activity_base {
         return new \lang_string('indicator:socialbreadth', 'mod_assign');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_SOCIAL;
     }
 
-    /**
-     * get_social_breadth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         return 2;
     }
 }
index f96c369..4a564ad 100644 (file)
@@ -155,5 +155,18 @@ function xmldb_assign_upgrade($oldversion) {
         upgrade_mod_savepoint(true, 2017061200, 'assign');
     }
 
+    if ($oldversion < 2017061205) {
+        require_once($CFG->dirroot.'/mod/assign/upgradelib.php');
+        $brokenassigns = get_assignments_with_rescaled_null_grades();
+
+        // Set config value.
+        foreach ($brokenassigns as $assign) {
+            set_config('has_rescaled_null_grades_' . $assign, 1, 'assign');
+        }
+
+        // Main savepoint reached.
+        upgrade_mod_savepoint(true, 2017061205, 'assign');
+    }
+
     return true;
 }
index bf19d14..91fc23b 100644 (file)
@@ -106,6 +106,7 @@ class combined_document {
                     case \core_files\conversion::STATUS_IN_PROGRESS:
                     case \core_files\conversion::STATUS_PENDING:
                         $pending = true;
+                        break;
 
                     case \core_files\conversion::STATUS_FAILED:
                         return self::STATUS_FAILED;
index 921c54c..87cf06f 100644 (file)
@@ -196,6 +196,9 @@ $string['expandreviewpanel'] = 'Expand review panel';
 $string['extensionduedate'] = 'Extension due date';
 $string['extensionnotafterduedate'] = 'Extension date must be after the due date';
 $string['extensionnotafterfromdate'] = 'Extension date must be after the allow submissions from date';
+$string['fixrescalednullgrades'] = 'This assignment contains some erroneous grades. You can <a href="{$a->link}">automatically repair</a> these grades. This may affect course totals.';
+$string['fixrescalednullgradesconfirm'] = 'Are you sure you want to fix erroneous grades? All broken grades will be removed. This may affect course totals.';
+$string['fixrescalednullgradesdone'] = 'Grades fixed.';
 $string['gradecanbechanged'] = 'Grade can be changed';
 $string['gradersubmissionupdatedtext'] = '{$a->username} has updated their assignment submission
 for \'{$a->assignment}\' at {$a->timeupdated}
index 7c6b896..4e7c7ec 100644 (file)
@@ -1585,7 +1585,8 @@ function assign_rescale_activity_grades($course, $cm, $oldmin, $oldmax, $newmin,
         'a' => $cm->instance
     );
 
-    $sql = 'UPDATE {assign_grades} set grade = (((grade - :p1) * :p2) + :p3) where assignment = :a';
+    // Only rescale grades that are greater than or equal to 0. Anything else is a special value.
+    $sql = 'UPDATE {assign_grades} set grade = (((grade - :p1) * :p2) + :p3) where assignment = :a and grade >= 0';
     $dbupdate = $DB->execute($sql, $params);
     if (!$dbupdate) {
         return false;
index fab4a6b..14103ad 100644 (file)
@@ -50,6 +50,9 @@ define('ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS', 'untilpass');
 // Special value means allow unlimited attempts.
 define('ASSIGN_UNLIMITED_ATTEMPTS', -1);
 
+// Special value means no grade has been set.
+define('ASSIGN_GRADE_NOT_SET', -1);
+
 // Grading states.
 define('ASSIGN_GRADING_STATUS_GRADED', 'graded');
 define('ASSIGN_GRADING_STATUS_NOT_GRADED', 'notgraded');
@@ -600,6 +603,8 @@ class assign {
             $o .= $this->view_batch_markingallocation($mform);
         } else if ($action == 'viewsubmitforgradingerror') {
             $o .= $this->view_error_page(get_string('submitforgrading', 'assign'), $notices);
+        } else if ($action == 'fixrescalednullgrades') {
+            $o .= $this->view_fix_rescaled_null_grades();
         } else {
             $o .= $this->view_submission_page();
         }
@@ -4234,6 +4239,8 @@ class assign {
         $this->require_view_grades();
         require_once($CFG->dirroot . '/mod/assign/gradeform.php');
 
+        $this->add_grade_notices();
+
         // Only load this if it is.
         $o .= $this->view_grading_table();
 
@@ -5207,6 +5214,8 @@ class assign {
 
         $instance = $this->get_instance();
 
+        $this->add_grade_notices();
+
         $o = '';
 
         $postfix = '';
@@ -8540,6 +8549,92 @@ class assign {
         $completion = new completion_info($this->get_course());
         $completion->set_module_viewed($this->get_course_module());
     }
+
+    /**
+     * Checks for any grade notices, and adds notifications. Will display on assignment main page and grading table.
+     *
+     * @return void The notifications API will render the notifications at the appropriate part of the page.
+     */
+    protected function add_grade_notices() {
+        if (has_capability('mod/assign:grade', $this->get_context()) && get_config('assign', 'has_rescaled_null_grades_' . $this->get_instance()->id)) {
+            $link = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id, 'action' => 'fixrescalednullgrades'));
+            \core\notification::warning(get_string('fixrescalednullgrades', 'mod_assign', ['link' => $link->out()]));
+        }
+    }
+
+    /**
+     * View fix rescaled null grades.
+     *
+     * @return bool True if null all grades are now fixed.
+     */
+    protected function fix_null_grades() {
+        global $DB;
+        $result = $DB->set_field_select(
+            'assign_grades',
+            'grade',
+            ASSIGN_GRADE_NOT_SET,
+            'grade <> ? AND grade < 0',
+            [ASSIGN_GRADE_NOT_SET]
+        );
+        $assign = clone $this->get_instance();
+        $assign->cmidnumber = $this->get_course_module()->idnumber;
+        assign_update_grades($assign);
+        return $result;
+    }
+
+    /**
+     * View fix rescaled null grades.
+     *
+     * @return void The notifications API will render the notifications at the appropriate part of the page.
+     */
+    protected function view_fix_rescaled_null_grades() {
+        global $OUTPUT;
+
+        $o = '';
+
+        require_capability('mod/assign:grade', $this->get_context());
+
+        $instance = $this->get_instance();
+
+        $o .= $this->get_renderer()->render(
+            new assign_header(
+                $instance,
+                $this->get_context(),
+                $this->show_intro(),
+                $this->get_course_module()->id
+            )
+        );
+
+        $confirm = optional_param('confirm', 0, PARAM_BOOL);
+
+        if ($confirm) {
+            confirm_sesskey();
+
+            // Fix the grades.
+            $this->fix_null_grades();
+            unset_config('has_rescaled_null_grades_' . $instance->id, 'assign');
+
+            // Display the notice.
+            $o .= $this->get_renderer()->notification(get_string('fixrescalednullgradesdone', 'assign'), 'notifysuccess');
+            $url = new moodle_url(
+                '/mod/assign/view.php',
+                array(
+                    'id' => $this->get_course_module()->id,
+                    'action' => 'grading'
+                )
+            );
+            $o .= $this->get_renderer()->continue_button($url);
+        } else {
+            // Ask for confirmation.
+            $continue = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id, 'action' => 'fixrescalednullgrades', 'confirm' => true, 'sesskey' => sesskey()));
+            $cancel = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
+            $o .= $OUTPUT->confirm(get_string('fixrescalednullgradesconfirm', 'mod_assign'), $continue, $cancel);
+        }
+
+        $o .= $this->view_footer();
+
+        return $o;
+    }
 }
 
 /**
index 09666b0..71caab6 100644 (file)
@@ -48,15 +48,6 @@ function assignsubmission_comments_comment_validate(stdClass $options) {
     if ($assignment->get_instance()->id != $submission->assignment) {
         throw new comment_exception('invalidcontext');
     }
-    $canview = false;
-    if ($submission->userid) {
-        $canview = $assignment->can_view_submission($submission->userid);
-    } else {
-        $canview = $assignment->can_view_group_submission($submission->groupid);
-    }
-    if (!$canview) {
-        throw new comment_exception('nopermissiontocomment');
-    }
 
     return true;
 }
index 16ac94d..d3b4a3b 100644 (file)
@@ -69,7 +69,6 @@ class assign_submission_comments extends assign_submission_plugin {
         $options->displaycancel = true;
 
         $comment = new comment($options);
-        $comment->set_view_permission(true);
 
         $o = $this->assignment->get_renderer()->container($comment->output(true), 'commentscontainer');
         return $o;
diff --git a/mod/assign/tests/behat/assign_comments_no_error.feature b/mod/assign/tests/behat/assign_comments_no_error.feature
new file mode 100644 (file)
index 0000000..96e6fb3
--- /dev/null
@@ -0,0 +1,26 @@
+@mod @mod_assign
+Feature: Switch role does not cause an error message in assignsubmission_comments
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+    And the following "users" exist:
+      | username |
+      | teacher1 |
+    And the following "course enrolments" exist:
+      | course | user     | role           |
+      | C1     | teacher1 | editingteacher |
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name           | Test assignment              |
+      | Description               | This is the description text |
+      | Students submit in groups | Yes                          |
+
+  Scenario: I switch role to student and an error doesn't occur
+    When I follow "Switch role to..." in the user menu
+    And I press "Student"
+    And I follow "Test assignment"
+    Then I should see "This is the description text"
index 75b0701..11a54a8 100644 (file)
@@ -12,10 +12,12 @@ Feature: Check that the assignment grade can be rescaled when the max grade is c
       | username | firstname | lastname | email |
       | teacher1 | Teacher | 1 | teacher1@example.com |
       | student1 | Student | 1 | student10@example.com |
+      | student2 | Student | 2 | student10@example.com |
     And the following "course enrolments" exist:
       | user | course | role |
       | teacher1 | C1 | editingteacher |
       | student1 | C1 | student |
+      | student2 | C1 | student |
     And the following "groups" exist:
       | name | course | idnumber |
       | Group 1 | C1 | G1 |
@@ -69,3 +71,18 @@ Feature: Check that the assignment grade can be rescaled when the max grade is c
     When I press "Save and display"
     And I navigate to "View all submissions" in current page administration
     Then "Student 1" row "Grade" column of "generaltable" table should contain "20.00"
+
+  Scenario: Rescaling should not produce negative grades
+    Given I navigate to "View all submissions" in current page administration
+    And I click on "Grade" "link" in the "Student 2" "table_row"
+    And I wait until the page is ready
+    And I follow "Assignment: Test assignment name"
+    And I navigate to "Edit settings" in current page administration
+    And I expand all fieldsets
+    And I set the field "Rescale existing grades" to "Yes"
+    And I set the field "Maximum grade" to "50"
+    When I press "Save and display"
+    And I navigate to "View all submissions" in current page administration
+    # Since the decimal places are always displayed (e.g. 0.00), this will catch any grade. The student should not have one.
+    # We can't just check for - (empty grade) because a negative grade would also have that character.
+    Then "Student 2" row "Grade" column of "generaltable" table should not contain "."
index 3603c76..322cb85 100644 (file)
@@ -655,4 +655,33 @@ class mod_assign_lib_testcase extends mod_assign_base_testcase {
         $this->assertEquals(mod_assign_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions);
         $this->assertEquals(mod_assign_get_completion_active_rule_descriptions(new stdClass()), []);
     }
+
+    /**
+     * Test that if some grades are not set, they are left alone and not rescaled
+     */
+    public function test_assign_rescale_activity_grades_some_unset() {
+        $this->resetAfterTest();
+
+        // As a teacher...
+        $this->setUser($this->editingteachers[0]);
+        $assign = $this->create_instance();
+
+        // Grade the student.
+        $data = ['grade' => 50];
+        $assign->testable_apply_grade_to_user((object)$data, $this->students[0]->id, 0);
+
+        // Try getting another students grade. This will give a grade of ASSIGN_GRADE_NOT_SET (-1).
+        $assign->get_user_grade($this->students[1]->id, true);
+
+        // Rescale.
+        assign_rescale_activity_grades($this->course, $assign->get_course_module(), 0, 100, 0, 10);
+
+        // Get the grades for both students.
+        $student0grade = $assign->get_user_grade($this->students[0]->id, true);
+        $student1grade = $assign->get_user_grade($this->students[1]->id, true);
+
+        // Make sure the real grade is scaled, but the ASSIGN_GRADE_NOT_SET stays the same.
+        $this->assertEquals($student0grade->grade, 5);
+        $this->assertEquals($student1grade->grade, ASSIGN_GRADE_NOT_SET);
+    }
 }
index 193f7fa..43391b6 100644 (file)
@@ -2953,4 +2953,147 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
         $completiondata = $completion->get_data($cm, false, $student2->id);
         $this->assertEquals(1, $completiondata->completionstate);
     }
+
+    public function get_assignments_with_rescaled_null_grades_provider() {
+        return [
+            'Negative less than one is errant' => [
+                'grade' => -0.64,
+                'count' => 1,
+            ],
+            'Negative more than one is errant' => [
+                'grade' => -30.18,
+                'count' => 1,
+            ],
+            'Negative one exactly is not errant' => [
+                'grade' => ASSIGN_GRADE_NOT_SET,
+                'count' => 0,
+            ],
+            'Positive grade is not errant' => [
+                'grade' => 1,
+                'count' => 0,
+            ],
+            'Large grade is not errant' => [
+                'grade' => 100,
+                'count' => 0,
+            ],
+            'Zero grade is not errant' => [
+                'grade' => 0,
+                'count' => 0,
+            ],
+        ];
+    }
+
+    /**
+     * Test determining if the assignment as any null grades that were rescaled.
+     * @dataProvider get_assignments_with_rescaled_null_grades_provider
+     */
+    public function test_get_assignments_with_rescaled_null_grades($grade, $count) {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $assign = $this->create_instance(array('grade' => 100));
+
+        // Try getting a student's grade. This will give a grade of -1.
+        // Then we can override it with a bad negative grade.
+        $assign->get_user_grade($this->students[0]->id, true);
+
+        // Set the grade to something errant.
+        $DB->set_field(
+            'assign_grades',
+            'grade',
+            $grade,
+            [
+                'userid' => $this->students[0]->id,
+                'assignment' => $assign->get_instance()->id,
+            ]
+        );
+
+        $this->assertCount($count, get_assignments_with_rescaled_null_grades());
+    }
+
+    /**
+     * Data provider for test_fix_null_grades
+     * @return array[] Test data for test_fix_null_grades. Each element should contain grade, expectedcount and gradebookvalue
+     */
+    public function fix_null_grades_provider() {
+        return [
+            'Negative less than one is errant' => [
+                'grade' => -0.64,
+                'gradebookvalue' => null,
+            ],
+            'Negative more than one is errant' => [
+                'grade' => -30.18,
+                'gradebookvalue' => null,
+            ],
+            'Negative one exactly is not errant, but shouldn\'t be pushed to gradebook' => [
+                'grade' => ASSIGN_GRADE_NOT_SET,
+                'gradebookvalue' => null,
+            ],
+            'Positive grade is not errant' => [
+                'grade' => 1,
+                'gradebookvalue' => 1,
+            ],
+            'Large grade is not errant' => [
+                'grade' => 100,
+                'gradebookvalue' => 100,
+            ],
+            'Zero grade is not errant' => [
+                'grade' => 0,
+                'gradebookvalue' => 0,
+            ],
+        ];
+    }
+
+    /**
+     * Test fix_null_grades
+     * @param number $grade The grade we should set in the assign grading table.
+     * @param number $expectedcount The finalgrade we expect in the gradebook after fixing the grades.
+     * @dataProvider fix_null_grades_provider
+     */
+    public function test_fix_null_grades($grade, $gradebookvalue) {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $studentid = $this->students[0]->id;
+
+        $assign = $this->create_instance();
+
+        // Try getting a student's grade. This will give a grade of -1.
+        // Then we can override it with a bad negative grade.
+        $assign->get_user_grade($studentid, true);
+
+        // Set the grade to something errant.
+        $DB->set_field(
+            'assign_grades',
+            'grade',
+            $grade,
+            [
+                'userid' => $studentid,
+                'assignment' => $assign->get_instance()->id,
+            ]
+        );
+        $assign->grade = $grade;
+        $assigntemp = clone $assign->get_instance();
+        $assigntemp->cmidnumber = $assign->get_course_module()->idnumber;
+        assign_update_grades($assigntemp);
+
+        // Check that the gradebook was updated with the assign grade. So we can guarentee test results later on.
+        $expectedgrade = $grade == -1 ? null : $grade; // Assign sends null to the gradebook for -1 grades.
+        $gradegrade = grade_grade::fetch(array('userid' => $studentid, 'itemid' => $assign->get_grade_item()->id));
+        $this->assertEquals($expectedgrade, $gradegrade->rawgrade);
+
+        // Call fix_null_grades().
+        $method = new ReflectionMethod(assign::class, 'fix_null_grades');
+        $method->setAccessible(true);
+        $result = $method->invoke($assign);
+
+        $this->assertSame(true, $result);
+
+        $gradegrade = grade_grade::fetch(array('userid' => $studentid, 'itemid' => $assign->get_grade_item()->id));
+
+        // Check that the grade was updated in the gradebook by fix_null_grades.
+        $this->assertEquals($gradebookvalue, $gradegrade->finalgrade);
+    }
 }
index c4fff77..10ab600 100644 (file)
@@ -414,3 +414,27 @@ class assign_upgrade_manager {
         return $newcm;
     }
 }
+
+/**
+ * Determines if the assignment as any null grades that were rescaled.
+ *
+ * Null grades are stored as -1 but should never be rescaled.
+ *
+ * @return int[] Array of the ids of all the assignments with rescaled null grades.
+ */
+function get_assignments_with_rescaled_null_grades() {
+    global $DB;
+
+    $query = 'SELECT id, assignment FROM {assign_grades}
+              WHERE grade < 0 AND grade <> -1';
+
+    $assignments = array_values($DB->get_records_sql($query));
+
+    $getassignmentid = function ($assignment) {
+        return $assignment->assignment;
+    };
+
+    $assignments = array_map($getassignmentid, $assignments);
+
+    return $assignments;
+}
index 2c7e0e0..9bcfe69 100644 (file)
@@ -25,6 +25,6 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component = 'mod_assign'; // Full name of the plugin (used for diagnostics).
-$plugin->version  = 2017061200;    // The current module version (Date: YYYYMMDDXX).
+$plugin->version  = 2017061205;    // The current module version (Date: YYYYMMDDXX).
 $plugin->requires = 2017050500;    // Requires this Moodle version.
 $plugin->cron     = 60;
index d5ebbd0..33164e7 100644 (file)
@@ -34,4 +34,13 @@ defined('MOODLE_INTERNAL') || die();
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity {
+
+    /**
+     * No need to fetch grades for resources.
+     *
+     * @param \core_analytics\course $course
+     * @return void
+     */
+    public function fetch_student_grades(\core_analytics\course $course) {
+    }
 }
index 705a537..0fdf7c9 100644 (file)
@@ -46,22 +46,11 @@ class cognitive_depth extends activity_base {
         return new \lang_string('indicator:cognitivedepth', 'mod_book');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_COGNITIVE;
     }
 
-    /**
-     * get_cognitive_depth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_cognitive_depth_level(\cm_info $cm) {
+    public function get_cognitive_depth_level(\cm_info $cm) {
         return 1;
     }
 }
index 45ec925..e58e6a0 100644 (file)
@@ -46,22 +46,11 @@ class social_breadth extends activity_base {
         return new \lang_string('indicator:socialbreadth', 'mod_book');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_SOCIAL;
     }
 
-    /**
-     * get_social_breadth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         return 1;
     }
 }
index 4cfaa82..4890052 100644 (file)
@@ -46,21 +46,10 @@ class cognitive_depth extends activity_base {
         return new \lang_string('indicator:cognitivedepth', 'mod_chat');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_COGNITIVE;
     }
 
-    /**
-     * get_cognitive_depth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
     public function get_cognitive_depth_level(\cm_info $cm) {
         return 4;
     }
index 4d92426..620e6a8 100644 (file)
@@ -46,22 +46,11 @@ class social_breadth extends activity_base {
         return new \lang_string('indicator:socialbreadth', 'mod_chat');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_SOCIAL;
     }
 
-    /**
-     * get_social_breadth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         return 2;
     }
 }
index f325b02..6517393 100644 (file)
@@ -46,22 +46,11 @@ class cognitive_depth extends activity_base {
         return new \lang_string('indicator:cognitivedepth', 'mod_choice');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_COGNITIVE;
     }
 
-    /**
-     * get_cognitive_depth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_cognitive_depth_level(\cm_info $cm) {
+    public function get_cognitive_depth_level(\cm_info $cm) {
         $this->fill_choice_data($cm);
 
         if ($this->choicedata[$cm->instance]->showresults == 0 || $this->choicedata[$cm->instance]->showresults == 4) {
index 0afd601..8d119ca 100644 (file)
@@ -46,22 +46,11 @@ class social_breadth extends activity_base {
         return new \lang_string('indicator:socialbreadth', 'mod_choice');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_SOCIAL;
     }
 
-    /**
-     * get_social_breadth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         $this->fill_choice_data($cm);
         return 2;
     }
index 29b6fbb..a527838 100644 (file)
@@ -46,21 +46,10 @@ class cognitive_depth extends activity_base {
         return new \lang_string('indicator:cognitivedepth', 'mod_data');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_COGNITIVE;
     }
 
-    /**
-     * get_cognitive_depth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
     public function get_cognitive_depth_level(\cm_info $cm) {
         return 2;
     }
index 0c2da04..2309c69 100644 (file)
@@ -46,22 +46,11 @@ class social_breadth extends activity_base {
         return new \lang_string('indicator:socialbreadth', 'mod_data');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_SOCIAL;
     }
 
-    /**
-     * get_social_breadth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         return 1;
     }
 }
index 93bb252..cf5403f 100644 (file)
@@ -46,22 +46,11 @@ class cognitive_depth extends activity_base {
         return new \lang_string('indicator:cognitivedepth', 'mod_feedback');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_COGNITIVE;
     }
 
-    /**
-     * get_cognitive_depth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_cognitive_depth_level(\cm_info $cm) {
+    public function get_cognitive_depth_level(\cm_info $cm) {
         $this->fill_publishstats($cm);
 
         if (!empty($this->publishstats[$cm->instance])) {
index 0ad198e..64ba678 100644 (file)
@@ -46,22 +46,11 @@ class social_breadth extends activity_base {
         return new \lang_string('indicator:socialbreadth', 'mod_feedback');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_SOCIAL;
     }
 
-    /**
-     * get_social_breadth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         $this->fill_publishstats($cm);
 
         return 2;
index fd86460..96bd395 100644 (file)
@@ -34,4 +34,13 @@ defined('MOODLE_INTERNAL') || die();
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity {
+
+    /**
+     * No need to fetch grades for resources.
+     *
+     * @param \core_analytics\course $course
+     * @return void
+     */
+    public function fetch_student_grades(\core_analytics\course $course) {
+    }
 }
index b3f49da..39d6207 100644 (file)
@@ -46,22 +46,11 @@ class cognitive_depth extends activity_base {
         return new \lang_string('indicator:cognitivedepth', 'mod_folder');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_COGNITIVE;
     }
 
-    /**
-     * get_cognitive_depth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_cognitive_depth_level(\cm_info $cm) {
+    public function get_cognitive_depth_level(\cm_info $cm) {
         return 1;
     }
 }
index 932e162..5572822 100644 (file)
@@ -46,22 +46,11 @@ class social_breadth extends activity_base {
         return new \lang_string('indicator:socialbreadth', 'mod_folder');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_SOCIAL;
     }
 
-    /**
-     * get_social_breadth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         return 1;
     }
 }
index 8696dfb..3641ccf 100644 (file)
@@ -46,21 +46,10 @@ class cognitive_depth extends activity_base {
         return new \lang_string('indicator:cognitivedepth', 'mod_forum');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_COGNITIVE;
     }
 
-    /**
-     * get_cognitive_depth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
     public function get_cognitive_depth_level(\cm_info $cm) {
         return 4;
     }
index a7508b2..e74c272 100644 (file)
@@ -46,22 +46,11 @@ class social_breadth extends activity_base {
         return new \lang_string('indicator:socialbreadth', 'mod_forum');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_SOCIAL;
     }
 
-    /**
-     * get_social_breadth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         return 2;
     }
 }
index f7f1b2c..312f153 100644 (file)
@@ -46,21 +46,10 @@ class cognitive_depth extends activity_base {
         return new \lang_string('indicator:cognitivedepth', 'mod_glossary');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_COGNITIVE;
     }
 
-    /**
-     * get_cognitive_depth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
     public function get_cognitive_depth_level(\cm_info $cm) {
         return 2;
     }
index b1e64b7..b47bf00 100644 (file)
@@ -46,22 +46,11 @@ class social_breadth extends activity_base {
         return new \lang_string('indicator:socialbreadth', 'mod_glossary');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_SOCIAL;
     }
 
-    /**
-     * get_social_breadth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         return 1;
     }
 }
index c5070ad..23068aa 100644 (file)
@@ -34,4 +34,13 @@ defined('MOODLE_INTERNAL') || die();
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity {
+
+    /**
+     * No need to fetch grades for resources.
+     *
+     * @param \core_analytics\course $course
+     * @return void
+     */
+    public function fetch_student_grades(\core_analytics\course $course) {
+    }
 }
index 39fa772..c7388d2 100644 (file)
@@ -46,22 +46,11 @@ class cognitive_depth extends activity_base {
         return new \lang_string('indicator:cognitivedepth', 'mod_imscp');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_COGNITIVE;
     }
 
-    /**
-     * get_cognitive_depth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_cognitive_depth_level(\cm_info $cm) {
+    public function get_cognitive_depth_level(\cm_info $cm) {
         return 1;
     }
 
index 953875f..50ffd0a 100644 (file)
@@ -46,22 +46,11 @@ class social_breadth extends activity_base {
         return new \lang_string('indicator:socialbreadth', 'mod_imscp');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_SOCIAL;
     }
 
-    /**
-     * get_social_breadth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         return 1;
     }
 }
index b924651..59b5717 100644 (file)
@@ -34,4 +34,13 @@ defined('MOODLE_INTERNAL') || die();
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity {
+
+    /**
+     * No need to fetch grades for resources.
+     *
+     * @param \core_analytics\course $course
+     * @return void
+     */
+    public function fetch_student_grades(\core_analytics\course $course) {
+    }
 }
index 3daa74b..adacede 100644 (file)
@@ -46,22 +46,11 @@ class cognitive_depth extends activity_base {
         return new \lang_string('indicator:cognitivedepth', 'mod_label');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_COGNITIVE;
     }
 
-    /**
-     * get_cognitive_depth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_cognitive_depth_level(\cm_info $cm) {
+    public function get_cognitive_depth_level(\cm_info $cm) {
         return 1;
     }
 
index 18afd3f..86452ec 100644 (file)
@@ -46,22 +46,11 @@ class social_breadth extends activity_base {
         return new \lang_string('indicator:socialbreadth', 'mod_label');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_SOCIAL;
     }
 
-    /**
-     * get_social_breadth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         return 1;
     }
 }
index 012d420..838f30d 100644 (file)
@@ -46,22 +46,11 @@ class cognitive_depth extends activity_base {
         return new \lang_string('indicator:cognitivedepth', 'mod_lesson');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_COGNITIVE;
     }
 
-    /**
-     * get_cognitive_depth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_cognitive_depth_level(\cm_info $cm) {
+    public function get_cognitive_depth_level(\cm_info $cm) {
         return 5;
     }
 
index 83fd27a..cd3c970 100644 (file)
@@ -46,22 +46,11 @@ class social_breadth extends activity_base {
         return new \lang_string('indicator:socialbreadth', 'mod_lesson');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_SOCIAL;
     }
 
-    /**
-     * get_social_breadth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         return 2;
     }
 }
index bcfe466..a600f88 100644 (file)
@@ -46,21 +46,10 @@ class cognitive_depth extends activity_base {
         return new \lang_string('indicator:cognitivedepth', 'mod_lti');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_COGNITIVE;
     }
 
-    /**
-     * get_cognitive_depth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
     public function get_cognitive_depth_level(\cm_info $cm) {
         return 3;
     }
index 24e7c5f..bd20137 100644 (file)
@@ -46,22 +46,11 @@ class social_breadth extends activity_base {
         return new \lang_string('indicator:socialbreadth', 'mod_lti');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_SOCIAL;
     }
 
-    /**
-     * get_social_breadth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         return 2;
     }
 }
index e5585e6..d91b040 100644 (file)
@@ -34,4 +34,13 @@ defined('MOODLE_INTERNAL') || die();
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity {
+
+    /**
+     * No need to fetch grades for resources.
+     *
+     * @param \core_analytics\course $course
+     * @return void
+     */
+    public function fetch_student_grades(\core_analytics\course $course) {
+    }
 }
index b988204..4e3eaca 100644 (file)
@@ -46,22 +46,11 @@ class cognitive_depth extends activity_base {
         return new \lang_string('indicator:cognitivedepth', 'mod_page');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_COGNITIVE;
     }
 
-    /**
-     * get_cognitive_depth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_cognitive_depth_level(\cm_info $cm) {
+    public function get_cognitive_depth_level(\cm_info $cm) {
         return 1;
     }
 }
index 852d348..1d27c5c 100644 (file)
@@ -46,22 +46,11 @@ class social_breadth extends activity_base {
         return new \lang_string('indicator:socialbreadth', 'mod_page');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_SOCIAL;
     }
 
-    /**
-     * get_social_breadth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         return 1;
     }
 }
index 86d5155..3c7d0a9 100644 (file)
@@ -46,21 +46,10 @@ class cognitive_depth extends activity_base {
         return new \lang_string('indicator:cognitivedepth', 'mod_quiz');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_COGNITIVE;
     }
 
-    /**
-     * get_cognitive_depth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
     public function get_cognitive_depth_level(\cm_info $cm) {
         return 5;
     }
index b110426..4dd2a5c 100644 (file)
@@ -46,22 +46,11 @@ class social_breadth extends activity_base {
         return new \lang_string('indicator:socialbreadth', 'mod_quiz');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_SOCIAL;
     }
 
-    /**
-     * get_social_breadth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         return 2;
     }
 }
index 8e8627a..6acb74b 100644 (file)
@@ -34,4 +34,13 @@ defined('MOODLE_INTERNAL') || die();
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity {
+
+    /**
+     * No need to fetch grades for resources.
+     *
+     * @param \core_analytics\course $course
+     * @return void
+     */
+    public function fetch_student_grades(\core_analytics\course $course) {
+    }
 }
index 28b4a66..995b63b 100644 (file)
@@ -46,22 +46,11 @@ class cognitive_depth extends activity_base {
         return new \lang_string('indicator:cognitivedepth', 'mod_resource');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_COGNITIVE;
     }
 
-    /**
-     * get_cognitive_depth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_cognitive_depth_level(\cm_info $cm) {
+    public function get_cognitive_depth_level(\cm_info $cm) {
         return 1;
     }
 }
index dd62403..4dad871 100644 (file)
@@ -46,22 +46,11 @@ class social_breadth extends activity_base {
         return new \lang_string('indicator:socialbreadth', 'mod_resource');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_SOCIAL;
     }
 
-    /**
-     * get_social_breadth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         return 1;
     }
 }
index 2ea656c..4dc11eb 100644 (file)
@@ -46,21 +46,10 @@ class cognitive_depth extends activity_base {
         return new \lang_string('indicator:cognitivedepth', 'mod_scorm');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_COGNITIVE;
     }
 
-    /**
-     * get_cognitive_depth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
     public function get_cognitive_depth_level(\cm_info $cm) {
         return 3;
     }
index c45096a..1ea6049 100644 (file)
@@ -46,22 +46,11 @@ class social_breadth extends activity_base {
         return new \lang_string('indicator:socialbreadth', 'mod_scorm');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_SOCIAL;
     }
 
-    /**
-     * get_social_breadth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         return 2;
     }
 }
index d6f39d4..80c8f38 100644 (file)
@@ -34,4 +34,13 @@ defined('MOODLE_INTERNAL') || die();
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity {
+
+    /**
+     * No need to fetch grades.
+     *
+     * @param \core_analytics\course $course
+     * @return void
+     */
+    public function fetch_student_grades(\core_analytics\course $course) {
+    }
 }
index b991e1b..f2958f6 100644 (file)
@@ -46,22 +46,11 @@ class cognitive_depth extends activity_base {
         return new \lang_string('indicator:cognitivedepth', 'mod_survey');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_COGNITIVE;
     }
 
-    /**
-     * get_cognitive_depth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_cognitive_depth_level(\cm_info $cm) {
+    public function get_cognitive_depth_level(\cm_info $cm) {
         return 2;
     }
 }
index b1bc1fd..958c088 100644 (file)
@@ -46,22 +46,11 @@ class social_breadth extends activity_base {
         return new \lang_string('indicator:socialbreadth', 'mod_survey');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_SOCIAL;
     }
 
-    /**
-     * get_social_breadth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         return 1;
     }
 }
index 82bc6b9..ed5e55e 100644 (file)
@@ -34,4 +34,13 @@ defined('MOODLE_INTERNAL') || die();
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity {
+
+    /**
+     * No need to fetch grades for resources.
+     *
+     * @param \core_analytics\course $course
+     * @return void
+     */
+    public function fetch_student_grades(\core_analytics\course $course) {
+    }
 }
index 46c256b..784510e 100644 (file)
@@ -46,22 +46,11 @@ class cognitive_depth extends activity_base {
         return new \lang_string('indicator:cognitivedepth', 'mod_url');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_COGNITIVE;
     }
 
-    /**
-     * get_cognitive_depth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_cognitive_depth_level(\cm_info $cm) {
+    public function get_cognitive_depth_level(\cm_info $cm) {
         return 1;
     }
 }
index d4038a8..28020b1 100644 (file)
@@ -46,22 +46,11 @@ class social_breadth extends activity_base {
         return new \lang_string('indicator:socialbreadth', 'mod_url');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_SOCIAL;
     }
 
-    /**
-     * get_social_breadth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         return 1;
     }
 }
index c84a33a..973a25d 100644 (file)
@@ -46,21 +46,10 @@ class cognitive_depth extends activity_base {
         return new \lang_string('indicator:cognitivedepth', 'mod_wiki');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_COGNITIVE;
     }
 
-    /**
-     * get_cognitive_depth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
     public function get_cognitive_depth_level(\cm_info $cm) {
         return 2;
     }
index 44269f3..ec5874c 100644 (file)
@@ -46,22 +46,11 @@ class social_breadth extends activity_base {
         return new \lang_string('indicator:socialbreadth', 'mod_wiki');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_SOCIAL;
     }
 
-    /**
-     * get_social_breadth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         return 1;
     }
 }
index 90f64c8..85d35d0 100644 (file)
@@ -46,21 +46,10 @@ class cognitive_depth extends activity_base {
         return new \lang_string('indicator:cognitivedepth', 'mod_workshop');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_COGNITIVE;
     }
 
-    /**
-     * get_cognitive_depth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
     public function get_cognitive_depth_level(\cm_info $cm) {
         return 5;
     }
index 0f5d748..6bc70d7 100644 (file)
@@ -46,22 +46,11 @@ class social_breadth extends activity_base {
         return new \lang_string('indicator:socialbreadth', 'mod_workshop');
     }
 
-    /**
-     * get_indicator_type
-     *
-     * @return string
-     */
-    protected function get_indicator_type() {
+    public function get_indicator_type() {
         return self::INDICATOR_SOCIAL;
     }
 
-    /**
-     * get_social_breadth_level
-     *
-     * @param \cm_info $cm
-     * @return int
-     */
-    protected function get_social_breadth_level(\cm_info $cm) {
+    public function get_social_breadth_level(\cm_info $cm) {
         return 2;
     }
 }
index 49c0ef5..453fcd2 100644 (file)
@@ -568,7 +568,8 @@ class question_usage_by_activity {
         }
 
         foreach ($this->questionattempts as $qa) {
-            $qa->start($this->preferredbehaviour, $qa->select_variant($variantstrategy));
+            $qa->start($this->preferredbehaviour, $qa->select_variant($variantstrategy), array(),
+                    $timestamp, $userid);
             $this->observer->notify_attempt_modified($qa);
         }
     }
index 51b7fe6..134283e 100644 (file)
@@ -134,7 +134,7 @@ class insight implements \renderable, \templatable {
      * Returns a CSS class from the calculated value outcome.
      *
      * @param \core_analytics\calculable $calculable
-     * @param mixed $value
+     * @param float $value
      * @param string|false $subtype
      * @return string
      */
@@ -159,8 +159,8 @@ class insight implements \renderable, \templatable {
             default:
                 throw new \coding_exception('The outcome returned by ' . get_class($calculable) . '::get_calculation_outcome is ' .
                     'not one of the accepted values. Please use \core_analytics\calculable::OUTCOME_VERY_POSITIVE, ' .
-                    '\core_analytics\calculable::OUTCOME_OK, \core_analytics\calculable::OUTCOME_NEGATIVE or ' .
-                    '\core_analytics\calculable::OUTCOME_VERY_NEGATIVE');
+                    '\core_analytics\calculable::OUTCOME_OK, \core_analytics\calculable::OUTCOME_NEGATIVE, ' .
+                    '\core_analytics\calculable::OUTCOME_VERY_NEGATIVE or \core_analytics\calculable::OUTCOME_NEUTRAL');
         }
         return $style;
     }
index 71d7fed..3f67c4a 100644 (file)
@@ -91,12 +91,16 @@ class insights_list implements \renderable, \templatable {
         $total = 0;
 
         if ($this->model->uses_insights()) {
-            list($total, $predictions) = $this->model->get_predictions($this->context, $this->page, $this->perpage);
+            $predictionsdata = $this->model->get_predictions($this->context, $this->page, $this->perpage);
 
             $data->insights = array();
-            foreach ($predictions as $prediction) {
-                $insightrenderable = new \report_insights\output\insight($prediction, $this->model, true);
-                $data->insights[] = $insightrenderable->export_for_template($output);
+            if ($predictionsdata) {
+                list($total, $predictions) = $predictionsdata;
+
+                foreach ($predictions as $prediction) {
+                    $insightrenderable = new \report_insights\output\insight($prediction, $this->model, true);
+                    $data->insights[] = $insightrenderable->export_for_template($output);
+                }
             }
 
             if (empty($data->insights) && $this->page == 0) {
index 65d5846..277fb95 100644 (file)
@@ -427,6 +427,27 @@ YUI.add('moodle-core_filepicker', function(Y) {
             }
         }
 
+        // Notify the user if any of the files has a problem status.
+        var problemFiles = [];
+        fileslist.forEach(function(file) {
+            if (!file_is_folder(file) && file.hasOwnProperty('status') && file.status != 0) {
+                problemFiles.push(file);
+            }
+        });
+        if (problemFiles.length > 0) {
+            require(["core/notification", "core/str"], function(Notification, Str) {
+                problemFiles.forEach(function(problemFile) {
+                    Str.get_string('storedfilecannotreadfile', 'error', problemFile.fullname).then(function(string) {
+                        Notification.addNotification({
+                            message: string,
+                            type: "error"
+                        });
+                        return;
+                    }).catch(Notification.exception);
+                });
+            });
+        }
+
         // If table view, need some additional properties
         // before passing fileslist to the YUI tableview
         if (options.viewmode == 3) {
index 967a54c..d7f3701 100644 (file)
@@ -241,9 +241,24 @@ fieldset.coursesearchbox label {
 }
 
 /* Custom styles for autocomplete form element */
+/* These styles reserve a standard amount of space in the DOM to avoid flicker when the original select element is replaced */
+[data-fieldtype=autocomplete] select,
+[data-fieldtype=tags] select,
+.form-autocomplete-original-select {
+    visibility: hidden;
+    overflow: hidden;
+    width: 15rem;
+    height: 44px;
+    margin: 0;
+    padding: 0;
+    border: 0;
+    margin-top: $font-size-base * $line-height-base + $tag-padding-y;
+    vertical-align: bottom;
+}
 .form-autocomplete-selection {
-    margin: 0.2em;
-    min-height: 21px;
+    margin: $tag-padding-y;
+    // Padding top and bottom, plus m-b-1 and the 100% lineheight.
+    min-height: 2 * $tag-padding-y + 2 * $font-size-base;
 }
 
 .form-autocomplete-multiple [role=listitem] {
index ff0f2c1..aa7f3a6 100644 (file)
     text-align: center;
 }
 
-.userlist table#participants td,
-.userlist table#participants th {
+.userlist table#participants td {
+    text-align: left;
+    padding: 4px;
     vertical-align: middle;
+}
+
+.userlist table#participants th {
     text-align: left;
     padding: 4px;
 }
index 9231ccf..9169fb2 100644 (file)
 <div class="form-autocomplete-selection {{#multiple}}form-autocomplete-multiple{{/multiple}}" id="{{selectionId}}" role="list" aria-atomic="true" {{#multiple}}tabindex="0" aria-multiselectable="true"{{/multiple}}>
 <span class="accesshide">{{#str}}selecteditems, form{{/str}}</span>
     {{#items}}
-        <span role="listitem" data-value="{{value}}" aria-selected="true" class="tag tag-info m-b-1 m-r-1" style="font-size: larger">
+        <span role="listitem" data-value="{{value}}" aria-selected="true" class="tag tag-info m-b-1 m-r-1" style="font-size: 100%">
             {{#multiple}}<span aria-hidden="true">× </span>{{/multiple}}{{{label}}}
         </span>
     {{/items}}
     {{^items}}
-        <span>{{noSelectionString}}</span>
+        <span class="m-b-1 m-r-1">{{noSelectionString}}</span>
     {{/items}}
 </div>
index 0bc21d0..0a980d0 100644 (file)
@@ -3,9 +3,6 @@
 @import "fontawesome/font-awesome";
 @import "fontawesome/moodle-path";
 
-// Import the Moodle variables.
-@import "moodle/variables.less";
-
 // Old Moodle stuff from base theme.
 // Massive, needs broken up.
 @import "moodle/core";
index d89dd37..1111eb3 100644 (file)
@@ -22,9 +22,6 @@
 
 // Calendar restyling.
 .path-calendar {
-    #dateselector-calendar-panel {
-        z-index: @zindexModalContainer+4;
-    }
     .calendartable {
         width: 100%;
         th,
             width: 100%;
         }
     }
-    .cal_courses_flt {
-        float: left;
-
-        label {
-            margin-right: .45em;
-        }
-    }
     .maincalendar {
         vertical-align: top;
         padding: 0;
index 008de8d..dcaad6d 100644 (file)
@@ -445,9 +445,27 @@ input[size] {
 }
 
 /* Custom styles for autocomplete form element */
+
+[data-fieldtype=autocomplete] select,
+[data-fieldtype=tags] select,
+select.form-autocomplete-original-select {
+    visibility: hidden;
+    overflow: hidden;
+    width: 15rem;
+    height: 3 * @baseLineHeight + @baseFontSize / 2;
+    margin: 0;
+    margin-bottom: 0.2em;
+    padding: 0;
+    border: 0;
+    vertical-align: top;
+}
+.form-autocomplete-container {
+    display: inline-block;
+    min-height: 4.2rem;
+}
 .form-autocomplete-selection {
     margin: 0.2em;
-    min-height: 21px;
+    min-height: @baseLineHeight + @baseFontSize / 2;
 }
 .form-autocomplete-multiple [role=listitem] {
     cursor: pointer;
index 32f89c1..f1e1153 100644 (file)
@@ -10,7 +10,7 @@ body {
     right: 0;
     bottom: 0;
     left: 0;
-    z-index: @zindexModalContainer;
+    z-index: 4050;
     outline: 0;
     overflow-x: hidden;
     overflow-y: auto;
@@ -105,7 +105,6 @@ body {
 }
 
 .modal-backdrop {
-    z-index: @zindexModalContainerBackdrop;
     background-color: #aaa;
     opacity: 0.4;
 }
index 701a398..265d53e 100644 (file)
 .userlist table#participants {
     text-align: center;
 }
-.userlist table#participants td,
-.userlist table#participants th {
+.userlist table#participants td {
+    text-align: left;
+    padding: 4px;
     vertical-align: middle;
+}
+.userlist table#participants th {
     text-align: left;
     padding: 4px;
 }
diff --git a/theme/bootstrapbase/less/moodle/variables.less b/theme/bootstrapbase/less/moodle/variables.less
deleted file mode 100644 (file)
index b86ad2f..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-// Global variables for use within Moodle's less style sheets.
-// These should be unique and not override the variables defined
-// in Bootstrap.
-
-@zindexModalContainer: 4050;
-@zindexModalContainerBackdrop: 4049;
-
index 11af68b..42d89c0 100644 (file)
@@ -5572,9 +5572,6 @@ img.iconsmall {
 .calendar_event_user {
   background-color: #dce7ec;
 }
-.path-calendar #dateselector-calendar-panel {
-  z-index: 4054;
-}
 .path-calendar .calendartable {
   width: 100%;
 }
@@ -5607,12 +5604,6 @@ img.iconsmall {
   border-spacing: 2px;
   width: 100%;
 }
-.path-calendar .cal_courses_flt {
-  float: left;
-}
-.path-calendar .cal_courses_flt label {
-  margin-right: .45em;
-}
 .path-calendar .maincalendar {
   vertical-align: top;
   padding: 0;
@@ -9764,9 +9755,12 @@ body.path-question-type .mform fieldset.hidden {
 .userlist table#participants {
   text-align: center;
 }
-.userlist table#participants td,
-.userlist table#participants th {
+.userlist table#participants td {
+  text-align: left;
+  padding: 4px;
   vertical-align: middle;
+}
+.userlist table#participants th {
   text-align: left;
   padding: 4px;
 }
@@