Merge branch 'MDL-59614-master' of git://github.com/junpataleta/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 7 Aug 2017 23:48:35 +0000 (01:48 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 7 Aug 2017 23:48:35 +0000 (01:48 +0200)
49 files changed:
admin/cli/mysql_collation.php
admin/settings/analytics.php
analytics/classes/local/analyser/base.php
analytics/classes/local/time_splitting/equal_parts.php
analytics/classes/model.php
analytics/tests/prediction_test.php
auth/ldap/auth.php
auth/ldap/tests/plugin_test.php
blocks/participants/tests/behat/block_participants_course.feature
course/modlib.php
course/tests/behat/rename_roles.feature
grade/report/grader/lib.php
grade/report/lib.php
grade/tests/behat/grade_hidden_items.feature
group/classes/output/user_groups_editable.php
group/tests/behat/create_groups.feature
lang/en/analytics.php
lang/en/moodle.php
lib/classes/event/config_log_created.php [new file with mode: 0644]
lib/classes/lock/installation_lock_factory.php [new file with mode: 0644]
lib/classes/lock/lock_config.php
lib/classes/output/icon_system_fontawesome.php
lib/datalib.php
lib/db/install.xml
lib/db/upgrade.php
lib/form/tests/behat/filetypes.feature [deleted file]
lib/form/tests/behat/multi_select_dependencies.feature [deleted file]
lib/form/tests/fixtures/filetypes.php [deleted file]
lib/form/tests/fixtures/multi_select_dependencies.php [deleted file]
lib/grade/grade_grade.php
lib/tests/admintree_test.php
lib/tests/statslib_test.php
lib/upgrade.txt
mod/assign/lang/en/assign.php
mod/assign/mod_form.php
mod/forum/user.php
mod/lesson/continue.php
mod/lesson/lib.php
mod/lesson/locallib.php
mod/lesson/tests/lib_test.php
mod/lti/launch.php
mod/lti/view.php
theme/boost/scss/moodle/question.scss
theme/bootstrapbase/less/moodle/question.less
theme/bootstrapbase/style/moodle.css
user/lib.php
user/tests/behat/bulk_editenrolment.feature
user/tests/userlib_test.php
version.php

index ae3c709..ba8a49e 100644 (file)
@@ -203,7 +203,7 @@ if (!empty($options['collation'])) {
                 $DB->change_database_structure($sql);
             } else {
                 echo "ERROR (unknown column type: $column->type)\n";
-                $error++;
+                $errors++;
                 continue;
             }
             echo "CONVERTED\n";
index a1ac0b1..17724a3 100644 (file)
@@ -84,7 +84,7 @@ if ($hassiteconfig) {
             $timesplittingoptions[$key] = $timesplitting->get_name();
         }
         $settings->add(new admin_setting_configmultiselect('analytics/timesplittings',
-            new lang_string('enabledtimesplittings', 'analytics'), new lang_string('enabledtimesplittings_help', 'analytics'),
+            new lang_string('enabledtimesplittings', 'analytics'), new lang_string('timesplittingmethod_help', 'analytics'),
             $timesplittingdefaults, $timesplittingoptions)
         );
 
index c80e66e..cd95efa 100644 (file)
@@ -376,8 +376,8 @@ abstract class base {
             // All ranges are used when we are calculating data for training.
             $ranges = $timesplitting->get_all_ranges();
         } else {
-            // Only some ranges can be used for prediction (it depends on the time range where we are right now).
-            $ranges = $this->get_prediction_ranges($timesplitting);
+            // The latest range that has not yet been used for prediction (it depends on the time range where we are right now).
+            $ranges = $this->get_most_recent_prediction_range($timesplitting);
         }
 
         // There is no need to keep track of the evaluated samples and ranges as we always evaluate the whole dataset.
@@ -385,12 +385,12 @@ abstract class base {
 
             if (empty($ranges)) {
                 $result->status = \core_analytics\model::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
-                $result->message = get_string('nonewdata', 'analytics');
+                $result->message = get_string('noranges', 'analytics');
                 return $result;
             }
 
-            // We skip all samples that are already part of a training dataset, even if they have noe been used for training yet.
-            $sampleids = $this->filter_out_train_samples($sampleids, $timesplitting);
+            // We skip all samples that are already part of a training dataset, even if they have not been used for prediction.
+            $this->filter_out_train_samples($sampleids, $timesplitting);
 
             if (count($sampleids) === 0) {
                 $result->status = \core_analytics\model::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
@@ -400,13 +400,19 @@ abstract class base {
 
             // Only when processing data for predictions.
             if ($target === false) {
-                // We also filter out ranges that have already been used for predictions.
-                $ranges = $this->filter_out_prediction_ranges($ranges, $timesplitting);
+                // We also filter out samples and ranges that have already been used for predictions.
+                $this->filter_out_prediction_samples_and_ranges($sampleids, $ranges, $timesplitting);
+            }
+
+            if (count($sampleids) === 0) {
+                $result->status = \core_analytics\model::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
+                $result->message = get_string('nonewdata', 'analytics');
+                return $result;
             }
 
             if (count($ranges) === 0) {
                 $result->status = \core_analytics\model::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
-                $result->message = get_string('nonewtimeranges', 'analytics');
+                $result->message = get_string('nonewranges', 'analytics');
                 return $result;
             }
         }
@@ -469,7 +475,7 @@ abstract class base {
             if ($target) {
                 $this->save_train_samples($sampleids, $timesplitting, $file);
             } else {
-                $this->save_prediction_ranges($ranges, $timesplitting);
+                $this->save_prediction_samples($sampleids, $ranges, $timesplitting);
             }
         }
 
@@ -480,25 +486,28 @@ abstract class base {
     }
 
     /**
-     * Returns the ranges of a time splitting that can be used to predict.
+     * Returns the most recent range that can be used to predict.
      *
      * @param \core_analytics\local\time_splitting\base $timesplitting
      * @return array
      */
-    protected function get_prediction_ranges($timesplitting) {
+    protected function get_most_recent_prediction_range($timesplitting) {
 
         $now = time();
+        $ranges = $timesplitting->get_all_ranges();
+
+        // Opposite order as we are interested in the last range that can be used for prediction.
+        arsort($ranges);
 
         // We already provided the analysable to the time splitting method, there is no need to feed it back.
-        $predictionranges = array();
-        foreach ($timesplitting->get_all_ranges() as $rangeindex => $range) {
+        foreach ($ranges as $rangeindex => $range) {
             if ($timesplitting->ready_to_predict($range)) {
                 // We need to maintain the same indexes.
-                $predictionranges[$rangeindex] = $range;
+                return array($rangeindex => $range);
             }
         }
 
-        return $predictionranges;
+        return array();
     }
 
     /**
@@ -506,9 +515,8 @@ abstract class base {
      *
      * @param int[] $sampleids
      * @param \core_analytics\local\time_splitting\base $timesplitting
-     * @return int[]
      */
-    protected function filter_out_train_samples($sampleids, $timesplitting) {
+    protected function filter_out_train_samples(&$sampleids, $timesplitting) {
         global $DB;
 
         $params = array('modelid' => $this->modelid, 'analysableid' => $timesplitting->get_analysable()->get_id(),
@@ -526,32 +534,43 @@ abstract class base {
                 $sampleids = array_diff_key($sampleids, $usedsamples);
             }
         }
-
-        return $sampleids;
     }
 
     /**
      * Filters out samples that have already been used for prediction.
      *
+     * @param int[] $sampleids
      * @param array $ranges
      * @param \core_analytics\local\time_splitting\base $timesplitting
-     * @return int[]
      */
-    protected function filter_out_prediction_ranges($ranges, $timesplitting) {
+    protected function filter_out_prediction_samples_and_ranges(&$sampleids, &$ranges, $timesplitting) {
         global $DB;
 
+        if (count($ranges) > 1) {
+            throw new \coding_exception('$ranges argument should only contain one range');
+        }
+
+        $rangeindex = key($ranges);
+
         $params = array('modelid' => $this->modelid, 'analysableid' => $timesplitting->get_analysable()->get_id(),
-            'timesplitting' => $timesplitting->get_id());
+            'timesplitting' => $timesplitting->get_id(), 'rangeindex' => $rangeindex);
+        $predictedrange = $DB->get_record('analytics_predict_samples', $params);
 
-        $predictedranges = $DB->get_records('analytics_predict_ranges', $params);
-        foreach ($predictedranges as $predictedrange) {
-            if (!empty($ranges[$predictedrange->rangeindex])) {
-                unset($ranges[$predictedrange->rangeindex]);
-            }
+        if (!$predictedrange) {
+            // Nothing to filter out.
+            return;
         }
 
-        return $ranges;
+        $predictedrange->sampleids = json_decode($predictedrange->sampleids, true);
+        $missingsamples = array_diff_key($sampleids, $predictedrange->sampleids);
+        if (count($missingsamples) === 0) {
+            // All samples already calculated.
+            unset($ranges[$rangeindex]);
+            return;
+        }
 
+        // Replace the list of samples by the one excluding samples that already got predictions at this range.
+        $sampleids = $missingsamples;
     }
 
     /**
@@ -560,7 +579,7 @@ abstract class base {
      * @param int[] $sampleids
      * @param \core_analytics\local\time_splitting\base $timesplitting
      * @param \stored_file $file
-     * @return bool
+     * @return void
      */
     protected function save_train_samples($sampleids, $timesplitting, $file) {
         global $DB;
@@ -574,28 +593,40 @@ abstract class base {
         $trainingsamples->sampleids = json_encode($sampleids);
         $trainingsamples->timecreated = time();
 
-        return $DB->insert_record('analytics_train_samples', $trainingsamples);
+        $DB->insert_record('analytics_train_samples', $trainingsamples);
     }
 
     /**
      * Saves samples that have just been used for prediction.
      *
+     * @param int[] $sampleids
      * @param array $ranges
      * @param \core_analytics\local\time_splitting\base $timesplitting
      * @return void
      */
-    protected function save_prediction_ranges($ranges, $timesplitting) {
+    protected function save_prediction_samples($sampleids, $ranges, $timesplitting) {
         global $DB;
 
-        $predictionrange = new \stdClass();
-        $predictionrange->modelid = $this->modelid;
-        $predictionrange->analysableid = $timesplitting->get_analysable()->get_id();
-        $predictionrange->timesplitting = $timesplitting->get_id();
-        $predictionrange->timecreated = time();
+        if (count($ranges) > 1) {
+            throw new \coding_exception('$ranges argument should only contain one range');
+        }
+
+        $rangeindex = key($ranges);
 
-        foreach ($ranges as $rangeindex => $unused) {
-            $predictionrange->rangeindex = $rangeindex;
-            $DB->insert_record('analytics_predict_ranges', $predictionrange);
+        $params = array('modelid' => $this->modelid, 'analysableid' => $timesplitting->get_analysable()->get_id(),
+            'timesplitting' => $timesplitting->get_id(), 'rangeindex' => $rangeindex);
+        if ($predictionrange = $DB->get_record('analytics_predict_samples', $params)) {
+            // Append the new samples used for prediction.
+            $prevsamples = json_decode($predictionrange->sampleids, true);
+            $predictionrange->sampleids = json_encode($prevsamples + $sampleids);
+            $predictionrange->timemodified = time();
+            $DB->update_record('analytics_predict_samples', $predictionrange);
+        } else {
+            $predictionrange = (object)$params;
+            $predictionrange->sampleids = json_encode($sampleids);
+            $predictionrange->timecreated = time();
+            $predictionrange->timemodified = $predictionrange->timecreated;
+            $DB->insert_record('analytics_predict_samples', $predictionrange);
         }
     }
 }
index 090e693..14e8834 100644 (file)
@@ -60,8 +60,8 @@ abstract class equal_parts extends base {
 
             // Check the end of the previous time range.
             if ($i > 0 && $start === $ranges[$i - 1]['end']) {
-                // We deduct 1 second from the previous end so each timestamp only belongs to 1 range.
-                $ranges[$i - 1]['end'] = $ranges[$i - 1]['end'] - 1;
+                // We add 1 second so each timestamp only belongs to 1 range.
+                $start = $start + 1;
             }
 
             if ($i === ($nparts - 1)) {
index 985d655..aae6682 100644 (file)
@@ -1013,7 +1013,7 @@ class model {
      */
     public function any_prediction_obtained() {
         global $DB;
-        return $DB->record_exists('analytics_predict_ranges',
+        return $DB->record_exists('analytics_predict_samples',
             array('modelid' => $this->model->id, 'timesplitting' => $this->model->timesplitting));
     }
 
@@ -1317,8 +1317,8 @@ class model {
     private function clear_model() {
         global $DB;
 
-        $DB->delete_records('analytics_predict_ranges', array('modelid' => $this->model->id));
         $DB->delete_records('analytics_predictions', array('modelid' => $this->model->id));
+        $DB->delete_records('analytics_predict_samples', array('modelid' => $this->model->id));
         $DB->delete_records('analytics_train_samples', array('modelid' => $this->model->id));
         $DB->delete_records('analytics_used_files', array('modelid' => $this->model->id));
 
index a0d278b..c778a5c 100644 (file)
@@ -31,6 +31,8 @@ require_once(__DIR__ . '/fixtures/test_indicator_random.php');
 require_once(__DIR__ . '/fixtures/test_target_shortname.php');
 require_once(__DIR__ . '/fixtures/test_static_target_shortname.php');
 
+require_once(__DIR__ . '/../../course/lib.php');
+
 /**
  * Unit tests for evaluation, training and prediction.
  *
@@ -81,7 +83,7 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         }
 
         // 1 range for each analysable.
-        $predictedranges = $DB->get_records('analytics_predict_ranges', array('modelid' => $model->get_id()));
+        $predictedranges = $DB->get_records('analytics_predict_samples', array('modelid' => $model->get_id()));
         $this->assertCount(2, $predictedranges);
         $this->assertEquals(1, $DB->count_records('analytics_used_files',
             array('modelid' => $model->get_id(), 'action' => 'predicted')));
@@ -91,7 +93,7 @@ class core_analytics_prediction_testcase extends advanced_testcase {
 
         // No new generated files nor records as there are no new courses available.
         $model->predict();
-        $predictedranges = $DB->get_records('analytics_predict_ranges', array('modelid' => $model->get_id()));
+        $predictedranges = $DB->get_records('analytics_predict_samples', array('modelid' => $model->get_id()));
         $this->assertCount(2, $predictedranges);
         $this->assertEquals(1, $DB->count_records('analytics_used_files',
             array('modelid' => $model->get_id(), 'action' => 'predicted')));
@@ -104,11 +106,11 @@ class core_analytics_prediction_testcase extends advanced_testcase {
      *
      * @dataProvider provider_ml_training_and_prediction
      * @param string $timesplittingid
-     * @param int $npredictedranges
+     * @param int $predictedrangeindex
      * @param string $predictionsprocessorclass
      * @return void
      */
-    public function test_ml_training_and_prediction($timesplittingid, $npredictedranges, $predictionsprocessorclass) {
+    public function test_ml_training_and_prediction($timesplittingid, $predictedrangeindex, $predictionsprocessorclass) {
         global $DB;
 
         $this->resetAfterTest(true);
@@ -176,22 +178,75 @@ class core_analytics_prediction_testcase extends advanced_testcase {
             $this->assertEquals($correct[$sampleid], $predictiondata->prediction);
         }
 
-        // 2 ranges will be predicted.
-        $predictedranges = $DB->get_records('analytics_predict_ranges', array('modelid' => $model->get_id()));
-        $this->assertCount($npredictedranges, $predictedranges);
+        // 1 range will be predicted.
+        $predictedranges = $DB->get_records('analytics_predict_samples', array('modelid' => $model->get_id()));
+        $this->assertCount(1, $predictedranges);
+        foreach ($predictedranges as $predictedrange) {
+            $this->assertEquals($predictedrangeindex, $predictedrange->rangeindex);
+            $sampleids = json_decode($predictedrange->sampleids, true);
+            $this->assertCount(2, $sampleids);
+            $this->assertContains($course1->id, $sampleids);
+            $this->assertContains($course2->id, $sampleids);
+        }
         $this->assertEquals(1, $DB->count_records('analytics_used_files',
             array('modelid' => $model->get_id(), 'action' => 'predicted')));
-        // 2 predictions for each range.
-        $this->assertEquals(2 * $npredictedranges, $DB->count_records('analytics_predictions',
+        // 2 predictions.
+        $this->assertEquals(2, $DB->count_records('analytics_predictions',
             array('modelid' => $model->get_id())));
 
         // No new generated files nor records as there are no new courses available.
         $model->predict();
-        $predictedranges = $DB->get_records('analytics_predict_ranges', array('modelid' => $model->get_id()));
-        $this->assertCount($npredictedranges, $predictedranges);
+        $predictedranges = $DB->get_records('analytics_predict_samples', array('modelid' => $model->get_id()));
+        $this->assertCount(1, $predictedranges);
+        foreach ($predictedranges as $predictedrange) {
+            $this->assertEquals($predictedrangeindex, $predictedrange->rangeindex);
+        }
         $this->assertEquals(1, $DB->count_records('analytics_used_files',
             array('modelid' => $model->get_id(), 'action' => 'predicted')));
-        $this->assertEquals(2 * $npredictedranges, $DB->count_records('analytics_predictions',
+        $this->assertEquals(2, $DB->count_records('analytics_predictions',
+            array('modelid' => $model->get_id())));
+
+        // New samples that can be used for prediction.
+        $courseparams = $params + array('shortname' => 'cccccc', 'fullname' => 'cccccc', 'visible' => 0);
+        $course3 = $this->getDataGenerator()->create_course($courseparams);
+        $courseparams = $params + array('shortname' => 'dddddd', 'fullname' => 'dddddd', 'visible' => 0);
+        $course4 = $this->getDataGenerator()->create_course($courseparams);
+
+        $result = $model->predict();
+
+        $predictedranges = $DB->get_records('analytics_predict_samples', array('modelid' => $model->get_id()));
+        $this->assertCount(1, $predictedranges);
+        foreach ($predictedranges as $predictedrange) {
+            $this->assertEquals($predictedrangeindex, $predictedrange->rangeindex);
+            $sampleids = json_decode($predictedrange->sampleids, true);
+            $this->assertCount(4, $sampleids);
+            $this->assertContains($course1->id, $sampleids);
+            $this->assertContains($course2->id, $sampleids);
+            $this->assertContains($course3->id, $sampleids);
+            $this->assertContains($course4->id, $sampleids);
+        }
+        $this->assertEquals(2, $DB->count_records('analytics_used_files',
+            array('modelid' => $model->get_id(), 'action' => 'predicted')));
+        $this->assertEquals(4, $DB->count_records('analytics_predictions',
+            array('modelid' => $model->get_id())));
+
+        // New visible course (for training).
+        $course5 = $this->getDataGenerator()->create_course(array('shortname' => 'aaa', 'fullname' => 'aa'));
+        $course6 = $this->getDataGenerator()->create_course();
+        $result = $model->train();
+        $this->assertEquals(2, $DB->count_records('analytics_used_files',
+            array('modelid' => $model->get_id(), 'action' => 'trained')));
+
+        // Update one of the courses to not visible, it should be used again for prediction.
+        $course5->visible = 0;
+        update_course($course5);
+
+        $model->predict();
+        $this->assertEquals(1, $DB->count_records('analytics_predict_samples',
+            array('modelid' => $model->get_id())));
+        $this->assertEquals(2, $DB->count_records('analytics_used_files',
+            array('modelid' => $model->get_id(), 'action' => 'predicted')));
+        $this->assertEquals(4, $DB->count_records('analytics_predictions',
             array('modelid' => $model->get_id())));
 
         set_config('enabled_stores', '', 'tool_log');
@@ -205,8 +260,8 @@ class core_analytics_prediction_testcase extends advanced_testcase {
      */
     public function provider_ml_training_and_prediction() {
         $cases = array(
-            'no_splitting' => array('\core\analytics\time_splitting\no_splitting', 1),
-            'quarters' => array('\core\analytics\time_splitting\quarters', 4)
+            'no_splitting' => array('\core\analytics\time_splitting\no_splitting', 0),
+            'quarters' => array('\core\analytics\time_splitting\quarters', 3)
         );
 
         // We need to test all system prediction processors.
index dcfc2c5..6cf1f28 100644 (file)
@@ -928,7 +928,7 @@ class auth_plugin_ldap extends auth_plugin_base {
 
                 $id = user_create_user($user, false);
                 echo "\t"; print_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)); echo "\n";
-                $euser = $DB->get_record('user', array('id' => $id));
+                $user = $DB->get_record('user', array('id' => $id));
 
                 if (!empty($this->config->forcechangepassword)) {
                     set_user_preference('auth_forcepasswordchange', 1, $id);
index d50f67d..f746583 100644 (file)
@@ -110,7 +110,7 @@ class auth_ldap_plugin_testcase extends advanced_testcase {
         set_config('user_attribute', 'cn', 'auth_ldap');
         set_config('memberattribute', 'memberuid', 'auth_ldap');
         set_config('memberattribute_isdn', 0, 'auth_ldap');
-        set_config('creators', 'cn=creators,'.$topdn, 'auth_ldap');
+        set_config('coursecreatorcontext', 'cn=creators,'.$topdn, 'auth_ldap');
         set_config('removeuser', AUTH_REMOVEUSER_KEEP, 'auth_ldap');
 
         set_config('field_map_email', 'mail', 'auth_ldap');
index a850602..5fa4a0b 100644 (file)
@@ -29,7 +29,7 @@ Feature: People Block used in a course
     When I log in as "student1"
     And I am on "Course 1" course homepage
     And I click on "Participants" "link" in the "People" "block"
-    Then I should see "All participants" in the "#page-content" "css_element"
+    Then I should see "Participants" in the "#page-content" "css_element"
 
   Scenario: Student without permission can not view participants link
     Given the following "permission overrides" exist:
index d4cc82b..4d2348e 100644 (file)
@@ -60,8 +60,10 @@ function add_moduleinfo($moduleinfo, $course, $mform = null) {
     $newcm->module           = $moduleinfo->module;
     $newcm->instance         = 0; // Not known yet, will be updated later (this is similar to restore code).
     $newcm->visible          = $moduleinfo->visible;
-    $newcm->visibleoncoursepage = $moduleinfo->visibleoncoursepage;
     $newcm->visibleold       = $moduleinfo->visible;
+    if (isset($moduleinfo->visibleoncoursepage)) {
+        $newcm->visibleoncoursepage = $moduleinfo->visibleoncoursepage;
+    }
     if (isset($moduleinfo->cmidnumber)) {
         $newcm->idnumber         = $moduleinfo->cmidnumber;
     }
index c16ead4..8c8a5e8 100644 (file)
@@ -30,9 +30,10 @@ Feature: Rename roles within a course
     Then "Tutor" "button" should exist
     And "Learner" "button" should exist
     And I navigate to course participants
-    And the "roleid" select box should contain "Tutor"
-    And the "roleid" select box should contain "Learner"
-    And the "roleid" select box should not contain "Student"
+    And I open the autocomplete suggestions list
+    And I should see "Role: Tutor" in the ".form-autocomplete-suggestions" "css_element"
+    And I should see "Role: Learner" in the ".form-autocomplete-suggestions" "css_element"
+    And I should not see "Role: Student" in the ".form-autocomplete-suggestions" "css_element"
     And I am on "Course 1" course homepage
     And I navigate to "Edit settings" in current page administration
     And I set the following fields to these values:
@@ -44,5 +45,6 @@ Feature: Rename roles within a course
     And "Student" "button" should exist
     And "Learner" "button" should not exist
     And I navigate to course participants
-    And the "roleid" select box should contain "Non-editing teacher"
-    And the "roleid" select box should contain "Student"
+    And I open the autocomplete suggestions list
+    And I should see "Role: Non-editing teacher" in the ".form-autocomplete-suggestions" "css_element"
+    And I should see "Role: Student" in the ".form-autocomplete-suggestions" "css_element"
index c4b6e63..b4de80a 100644 (file)
@@ -974,7 +974,7 @@ class grade_report_grader extends grade_report {
                 $usergrades = $this->allgrades[$userid];
                 $hidingaffected = grade_grade::get_hiding_affected($usergrades, $allgradeitems);
                 $altered = $hidingaffected['altered'];
-                $unknown = $hidingaffected['unknown'];
+                $unknown = $hidingaffected['unknowngrades'];
                 unset($hidingaffected);
             }
 
@@ -996,7 +996,7 @@ class grade_report_grader extends grade_report {
                 // Get the decimal points preference for this item
                 $decimalpoints = $item->get_decimals();
 
-                if (in_array($itemid, $unknown)) {
+                if (array_key_exists($itemid, $unknown)) {
                     $gradeval = null;
                 } else if (array_key_exists($itemid, $altered)) {
                     $gradeval = $altered[$itemid];
index d925ef7..45c652e 100644 (file)
@@ -525,14 +525,14 @@ abstract class grade_report {
                     $aggregationweight = null;
                 }
             }
-        } else if (!empty($hiding_affected['unknown'][$course_item->id])) {
+        } else if (array_key_exists($course_item->id, $hiding_affected['unknowngrades'])) {
             //not sure whether or not this item depends on a hidden item
             if (!$this->showtotalsifcontainhidden[$courseid]) {
                 //hide the grade
                 $finalgrade = null;
             } else {
                 //use reprocessed marks that exclude hidden items
-                $finalgrade = $hiding_affected['unknown'][$course_item->id];
+                $finalgrade = $hiding_affected['unknowngrades'][$course_item->id];
 
                 if (array_key_exists($course_item->id, $hiding_affected['alteredgrademin'])) {
                     $grademin = $hiding_affected['alteredgrademin'][$course_item->id];
index ca3b518..e761671 100644 (file)
@@ -27,6 +27,14 @@ Feature: Student and teacher's view of aggregated grade items is consistent when
       | assign | C1 | a3 | Test assignment three | Submit something! | Sub category 2 | 100 |
       | assign | C1 | a4 | Test assignment four | Submit something! | Sub category 2 | 100 |
     And I log in as "admin"
+    And I am on "Course 1" course homepage
+    And I navigate to "Setup > Gradebook setup" in the course gradebook
+    And I press "Add grade item"
+    And I set the following fields to these values:
+      | Item name | calculated |
+    And I press "Save changes"
+    And I set "=[[a4]]/2" calculation for grade item "calculated" with idnumbers:
+      | Sub category 1 | sub1 |
     And I navigate to "Overview report" node in "Site administration > Grades > Report settings"
     And I set the field "s__grade_report_overview_showtotalsifcontainhidden" to "Show totals excluding hidden items"
     And I navigate to "User report" node in "Site administration > Grades > Report settings"
index f4927b5..831189a 100644 (file)
@@ -75,7 +75,7 @@ class user_groups_editable extends \core\output\inplace_editable {
         $options = [];
 
         foreach ($coursegroups as $group) {
-            $options[$group->id] = $group->name;
+            $options[$group->id] = format_string($group->name, true, ['context' => $this->context]);
         }
         $this->edithint = get_string('editusersgroupsa', 'group', fullname($user));
         $this->editlabel = get_string('editusersgroupsa', 'group', fullname($user));
index 4642d4d..aa11ab9 100644 (file)
@@ -47,11 +47,16 @@ Feature: Organize students into groups
     And the "members" select box should contain "Student 3"
     And the "members" select box should not contain "Student 0"
     And I navigate to course participants
-    And I set the field "Separate groups" to "Group 1"
+    And I open the autocomplete suggestions list
+    And I click on "Group: Group 1" item in the autocomplete list
+    And I press "Filter"
     And I should see "Student 0"
     And I should see "Student 1"
     And I should not see "Student 2"
-    And I set the field "Separate groups" to "Group 2"
+    And I click on "Group: Group 1" "text" in the ".form-autocomplete-selection" "css_element"
+    And I open the autocomplete suggestions list
+    And I click on "Group: Group 2" item in the autocomplete list
+    And I press "Filter"
     And I should see "Student 2"
     And I should see "Student 3"
     And I should not see "Student 0"
index 2dbae2c..81fa1bc 100644 (file)
@@ -30,7 +30,6 @@ $string['analyticslogstore_help'] = 'The log store that will be used by the anal
 $string['analyticssettings'] = 'Analytics settings';
 $string['coursetoolong'] = 'The course is too long';
 $string['enabledtimesplittings'] = 'Time splitting methods';
-$string['enabledtimesplittings_help'] = 'The time splitting method divides the course duration in parts, the predictions engine will run at the end of these parts. It is recommended that you only enable the time splitting methods you could be interested on using; the evaluation process will iterate through all of them so the more time splitting methods to go through the slower the evaluation process will be.';
 $string['erroralreadypredict'] = '{$a} file has already been used to predict';
 $string['errorcannotreaddataset'] = 'Dataset file {$a} can not be read';
 $string['errorcannotwritedataset'] = 'Dataset file {$a} can not be written';
@@ -60,15 +59,17 @@ $string['insightinfomessagehtml'] = 'The system generated some insights for you:
 $string['invalidtimesplitting'] = 'Model with id {$a} needs a time splitting method before it can be used to train';
 $string['invalidanalysablefortimesplitting'] = 'It can not be analysed using {$a} time splitting method';
 $string['nocourses'] = 'No courses to analyse';
-$string['nodata'] = 'No data available';
 $string['modeloutputdir'] = 'Models output directory';
 $string['modeloutputdirinfo'] = 'Directory where prediction processors store all evaluation info. Useful for debugging and research.';
 $string['noevaluationbasedassumptions'] = 'Models based on assumptions can not be evaluated';
+$string['nodata'] = 'No data to analyse';
 $string['noinsightsmodel'] = 'This model does not generate insights';
 $string['noinsights'] = 'No insights reported';
 $string['nonewdata'] = 'No new data available';
+$string['nonewranges'] = 'No new predictions yet';
 $string['nonewtimeranges'] = 'No new time ranges, nothing to predict';
 $string['nopredictionsyet'] = 'No predictions available yet';
+$string['noranges'] = 'No predictions yet';
 $string['notrainingbasedassumptions'] = 'Models based on assumptions do not need training';
 $string['novaliddata'] = 'No valid data available';
 $string['novalidsamples'] = 'No valid samples available';
index e8928a9..72b71da 100644 (file)
@@ -728,6 +728,7 @@ $string['errorwhenconfirming'] = 'You are not confirmed yet because an error occ
 $string['eventcommentcreated'] = 'Comment created';
 $string['eventcommentdeleted'] = 'Comment deleted';
 $string['eventcommentsviewed'] = 'Comments viewed';
+$string['eventconfiglogcreated'] = 'Config log created';
 $string['eventcoursecategorycreated'] = 'Category created';
 $string['eventcoursecategorydeleted'] = 'Category deleted';
 $string['eventcoursecategoryupdated'] = 'Category updated';
diff --git a/lib/classes/event/config_log_created.php b/lib/classes/event/config_log_created.php
new file mode 100644 (file)
index 0000000..31366b1
--- /dev/null
@@ -0,0 +1,118 @@
+<?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/>.
+
+/**
+ * Config log created.
+ *
+ * @package    core
+ * @copyright  2017 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event class for when an admin config log is created.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - string name: name of config setting
+ *      - string plugin: name of plugin
+ *      - string oldvalue: previous value
+ *      - string value: new value
+ * }
+ *
+ * @package    core
+ * @copyright  2017 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class config_log_created extends base {
+
+    /**
+     * Initialise required event data properties.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'config_log';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventconfiglogcreated');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        $name = $this->other['name'];
+        $plugin = isset($this->other['plugin']) ? $this->other['plugin'] : 'core';
+        $value = isset($this->other['value']) ? $this->other['value'] : 'Not set';
+        $oldvalue = isset($this->other['oldvalue']) ? $this->other['oldvalue'] : 'Not set';
+        return "The user with id '$this->userid' changed the config setting '$name' for component '$plugin' " .
+               "from '$oldvalue' to '$value'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/admin/index.php');
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['name'])) {
+            throw new \coding_exception('The \'name\' value must be set in other.');
+        }
+        if (!array_key_exists('plugin', $this->other)) {
+            throw new \coding_exception('The \'plugin\' value must be set in other.');
+        }
+        if (!array_key_exists('oldvalue', $this->other)) {
+            throw new \coding_exception('The \'oldvalue\' value must be set in other.');
+        }
+        if (!array_key_exists('value', $this->other)) {
+            throw new \coding_exception('The \'value\' value must be set in other.');
+        }
+    }
+
+    public static function get_objectid_mapping() {
+        // Config log is not mappable.
+        return array('db' => 'config_log', 'restore' => base::NOT_MAPPED);
+    }
+
+    public static function get_other_mapping() {
+        return false;
+    }
+}
diff --git a/lib/classes/lock/installation_lock_factory.php b/lib/classes/lock/installation_lock_factory.php
new file mode 100644 (file)
index 0000000..5200cab
--- /dev/null
@@ -0,0 +1,127 @@
+<?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/>.
+
+/**
+ * Lock factory for use during installation.
+ *
+ * @package    core
+ * @category   lock
+ * @copyright  Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\lock;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Lock factory for use during installation.
+ *
+ * @package   core
+ * @category  lock
+ * @copyright  Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class installation_lock_factory implements lock_factory {
+
+    /**
+     * Create this lock factory.
+     *
+     * @param string $type - The type, e.g. cron, cache, session
+     */
+    public function __construct($type) {
+    }
+
+    /**
+     * Return information about the blocking behaviour of the lock type on this platform.
+     *
+     * @return boolean - False if attempting to get a lock will block indefinitely.
+     */
+    public function supports_timeout() {
+        return true;
+    }
+
+    /**
+     * This lock type will be automatically released when a process ends.
+     *
+     * @return boolean - True
+     */
+    public function supports_auto_release() {
+        return true;
+    }
+
+    /**
+     * This lock factory is only available during the initial installation.
+     * To use it at any other time would be potentially dangerous.
+     *
+     * @return boolean
+     */
+    public function is_available() {
+        return during_initial_install();
+    }
+
+    /**
+     * Multiple locks for the same resource cannot be held from a single process.
+     *
+     * @return boolean - False
+     */
+    public function supports_recursion() {
+        return false;
+    }
+
+    /**
+     * Get some info that might be useful for debugging.
+     * @return boolean - string
+     */
+    protected function get_debug_info() {
+        return 'host:' . php_uname('n') . ', pid:' . getmypid() . ', time:' . time();
+    }
+
+    /**
+     * Get a lock within the specified timeout or return false.
+     *
+     * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
+     * @param int $timeout - The number of seconds to wait for a lock before giving up.
+     * @param int $maxlifetime - Unused by this lock type.
+     * @return boolean - true if a lock was obtained.
+     */
+    public function get_lock($resource, $timeout, $maxlifetime = 86400) {
+        return new lock($resource, $this);
+    }
+
+    /**
+     * Release a lock that was previously obtained with @lock.
+     *
+     * @param lock $lock - A lock obtained from this factory.
+     * @return boolean - true if the lock is no longer held (including if it was never held).
+     */
+    public function release_lock(lock $lock) {
+        return true;
+    }
+
+    /**
+     * Extend a lock that was previously obtained with @lock.
+     *
+     * @param lock $lock - not used
+     * @param int $maxlifetime - not used
+     * @return boolean - true if the lock was extended.
+     */
+    public function extend_lock(lock $lock, $maxlifetime = 86400) {
+        // Not supported by this factory.
+        return false;
+    }
+
+}
index 43541a6..06fddf4 100644 (file)
@@ -48,7 +48,9 @@ class lock_config {
         global $CFG, $DB;
         $lockfactory = null;
 
-        if (isset($CFG->lock_factory) && $CFG->lock_factory != 'auto') {
+        if (during_initial_install()) {
+            $lockfactory = new \core\lock\installation_lock_factory($type);
+        } else if (isset($CFG->lock_factory) && $CFG->lock_factory != 'auto') {
             if (!class_exists($CFG->lock_factory)) {
                 // In this case I guess it is not safe to continue. Different cluster nodes could end up using different locking
                 // types because of an installation error.
index 28ad0cf..b1085ca 100644 (file)
@@ -371,7 +371,7 @@ class icon_system_fontawesome extends icon_system_font {
             'core:t/switch_whole' => 'fa-square-o',
             'core:t/unblock' => 'fa-commenting',
             'core:t/unlocked' => 'fa-unlock-alt',
-            'core:t/unlock' => 'fa-unlock',
+            'core:t/unlock' => 'fa-lock',
             'core:t/up' => 'fa-arrow-up',
             'core:t/user' => 'fa-user',
             'core:t/viewdetails' => 'fa-list',
index df48d6d..96b4dcd 100644 (file)
@@ -1573,13 +1573,28 @@ function add_to_config_log($name, $oldvalue, $value, $plugin) {
     global $USER, $DB;
 
     $log = new stdClass();
-    $log->userid       = during_initial_install() ? 0 :$USER->id; // 0 as user id during install
+    // Use 0 as user id during install.
+    $log->userid       = during_initial_install() ? 0 : $USER->id;
     $log->timemodified = time();
     $log->name         = $name;
     $log->oldvalue  = $oldvalue;
     $log->value     = $value;
     $log->plugin    = $plugin;
-    $DB->insert_record('config_log', $log);
+
+    $id = $DB->insert_record('config_log', $log);
+
+    $event = core\event\config_log_created::create(array(
+            'objectid' => $id,
+            'userid' => $log->userid,
+            'context' => \context_system::instance(),
+            'other' => array(
+                'name' => $log->name,
+                'oldvalue' => $log->oldvalue,
+                'value' => $log->value,
+                'plugin' => $log->plugin
+            )
+        ));
+    $event->trigger();
 }
 
 /**
index b83f318..768f7f2 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20170502" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20170721" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <FIELD NAME="target" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="indicators" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="timesplitting" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/>
-        <FIELD NAME="score" TYPE="number" LENGTH="10" DECIMALS="5" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="score" TYPE="number" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" DECIMALS="5"/>
         <FIELD NAME="info" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
         <FIELD NAME="dir" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <INDEX NAME="modelid" UNIQUE="false" FIELDS="modelid" COMMENT="Index on modelid"/>
       </INDEXES>
     </TABLE>
-
     <TABLE NAME="analytics_predictions" COMMENT="Predictions">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
         <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="predictionscore" TYPE="number" LENGTH="10" DECIMALS="5" NOTNULL="true" SEQUENCE="false"/>
+        <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>
         <INDEX NAME="modelidandanalysableidandtimesplitting" UNIQUE="false" FIELDS="modelid, analysableid, timesplitting" COMMENT="Index on modelid and analysableid and timesplitting"/>
       </INDEXES>
     </TABLE>
-    <TABLE NAME="analytics_predict_ranges" COMMENT="Time ranges already used for predictions.">
+    <TABLE NAME="analytics_predict_samples" COMMENT="Samples already used for predictions.">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
         <FIELD NAME="modelid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="analysableid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="timesplitting" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="rangeindex" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="sampleids" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
       </KEYS>
       <INDEXES>
-        <INDEX NAME="modelidandanalysableidandtimesplitting" UNIQUE="false" FIELDS="modelid, analysableid, timesplitting" COMMENT="Index on modelid and analysableid and timesplitting"/>
+        <INDEX NAME="modelidandanalysableidandtimesplittingandrangeindex" UNIQUE="false" FIELDS="modelid, analysableid, timesplitting, rangeindex" COMMENT="Index on modelid and analysableid and timesplitting"/>
       </INDEXES>
     </TABLE>
     <TABLE NAME="analytics_used_files" COMMENT="Files that have already been used for training and prediction.">
       </INDEXES>
     </TABLE>
   </TABLES>
-</XMLDB>
+</XMLDB>
\ No newline at end of file
index 36d4845..638044f 100644 (file)
@@ -2255,5 +2255,58 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2017072700.02);
     }
 
+    if ($oldversion < 2017080700.01) {
+
+        // Get the table by its previous name.
+        $table = new xmldb_table('analytics_predict_ranges');
+        if ($dbman->table_exists($table)) {
+
+            // We can only accept this because we are in master.
+            $DB->delete_records('analytics_predictions');
+            $DB->delete_records('analytics_used_files', array('action' => 'predicted'));
+            $DB->delete_records('analytics_predict_ranges');
+
+            // Define field sampleids to be added to analytics_predict_ranges (renamed below to analytics_predict_samples).
+            $field = new xmldb_field('sampleids', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null, 'rangeindex');
+
+            // Conditionally launch add field sampleids.
+            if (!$dbman->field_exists($table, $field)) {
+                $dbman->add_field($table, $field);
+            }
+
+            // Define field timemodified to be added to analytics_predict_ranges (renamed below to analytics_predict_samples).
+            $field = new xmldb_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'timecreated');
+
+            // Conditionally launch add field timemodified.
+            if (!$dbman->field_exists($table, $field)) {
+                $dbman->add_field($table, $field);
+            }
+
+            // Rename the table to its new name.
+            $dbman->rename_table($table, 'analytics_predict_samples');
+        }
+
+        $table = new xmldb_table('analytics_predict_samples');
+
+        $index = new xmldb_index('modelidandanalysableidandtimesplitting', XMLDB_INDEX_NOTUNIQUE,
+            array('modelid', 'analysableid', 'timesplitting'));
+
+        // Conditionally launch drop index.
+        if ($dbman->index_exists($table, $index)) {
+            $dbman->drop_index($table, $index);
+        }
+
+        $index = new xmldb_index('modelidandanalysableidandtimesplittingandrangeindex', XMLDB_INDEX_NOTUNIQUE,
+            array('modelid', 'analysableid', 'timesplitting', 'rangeindex'));
+
+        // Conditionally launch add index.
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2017080700.01);
+    }
+
     return true;
 }
diff --git a/lib/form/tests/behat/filetypes.feature b/lib/form/tests/behat/filetypes.feature
deleted file mode 100644 (file)
index 653e50d..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-@core_form
-Feature: There is a form element allowing to select filetypes
-  In order to test the filetypes field
-  As an admin
-  I need a test form that makes use of the filetypes field
-
-  Background:
-    Given the following "courses" exist:
-      | fullname | shortname | format |
-      | Course 1 | C1        | topics |
-    And the following "activities" exist:
-      | activity   | name | intro                                                              | course | idnumber |
-      | label      | L1   | <a href="../lib/form/tests/fixtures/filetypes.php">FixtureLink</a> | C1     | label1   |
-    And I log in as "admin"
-    And I am on "Course 1" course homepage
-    And I follow "FixtureLink"
-
-  Scenario: File types can be provided via direct input with JavaScript disabled
-    Given I set the field "Choose from all file types" to ".png .gif .jpg"
-    When I press "Save changes"
-    Then the field "Choose from all file types" matches value ".png .gif .jpg"
-
-  @javascript
-  Scenario: File types can be provided via direct input with JavaScript enabled
-    Given I set the field "Choose from all file types" to ".png .gif .jpg"
-    When I press "Save changes"
-    Then the field "Choose from all file types" matches value ".png .gif .jpg"
-
-  Scenario: File types are validated to be known, unless the field allows unknown be provided
-    Given I set the field "Choose from all file types" to ".pdf .doesnoexist"
-    And I set the field "Choose from a limited set" to "doc docx pdf rtf"
-    And I set the field "Unknown file types are allowed here" to ".neverminditdoesnotexist"
-    When I press "Save changes"
-    Then I should see "Unknown file types: .doesnoexist"
-    And I should see "These file types are not allowed here: .doc, .docx, .rtf"
-    And I should see "It is not allowed to select 'All file types' here"
-    And I should not see "Unknown file types: .neverminditdoesnotexist"
-
-  @javascript @_file_upload
-  Scenario: File manager element implicitly validates submitted files
-    # We can't directly upload the invalid file here as the upload repository would throw an exception.
-    # So instead we try to trick the filemanager, to be finally stopped by the implicit validation.
-    And I upload "lib/tests/fixtures/empty.txt" file to "Picky file manager" filemanager
-    And I follow "empty.txt"
-    And I set the field "Name" to "renamed.exe"
-    And I press "Update"
-    When I press "Save changes"
-    Then I should see "Some files (renamed.exe) cannot be uploaded. Only file types .txt are allowed."
diff --git a/lib/form/tests/behat/multi_select_dependencies.feature b/lib/form/tests/behat/multi_select_dependencies.feature
deleted file mode 100644 (file)
index 0abf192..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-@core_form @javascript @_bug_phantomjs
-Feature: Forms with a multi select field dependency
-  In order to test multi select field dependency
-  As an admin
-  I need forms field which depends on multiple select options
-
-  Scenario: Field should be enabled only when all select options are selected
-    # Get to the fixture page.
-    Given the following "courses" exist:
-      | fullname | shortname | format |
-      | Course 1 | C1        | topics |
-    And the following "activities" exist:
-      | activity   | name | intro                                                                               | course | idnumber |
-      | label      | L1   | <a href="../lib/form/tests/fixtures/multi_select_dependencies.php">FixtureLink</a> | C1     | label1   |
-    And I log in as "admin"
-    And I am on "Course 1" course homepage
-    When I follow "FixtureLink"
-    Then the "Enter your name" "field" should be disabled
-    And I set the field "Choose one or more directions" to "South,West"
-    Then the "Enter your name" "field" should be enabled
-    And I set the field "Choose one or more directions" to "West"
-    Then the "Enter your name" "field" should be disabled
-    And I set the field "Choose one or more directions" to "North,West"
-    Then the "Enter your name" "field" should be disabled
\ No newline at end of file
diff --git a/lib/form/tests/fixtures/filetypes.php b/lib/form/tests/fixtures/filetypes.php
deleted file mode 100644 (file)
index 8477743..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Fixture script for Behat test for testing the filetypes element.
- *
- * @package     core_form
- * @category    test
- * @copyright   2017 David Mudr├ík <david@moodle.com>
- * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-// No login check is expected here as this is for Behat tests only.
-// @codingStandardsIgnoreLine
-require(__DIR__.'/../../../../config.php');
-require_once($CFG->libdir.'/formslib.php');
-
-// Behat test fixture only.
-defined('BEHAT_SITE_RUNNING') || die('Only available on Behat test server');
-
-/**
- * Defines a test form to be used in automatic tests.
- *
- * @copyright 2017 David Mudrak <david@moodle.com>
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class test_form extends moodleform {
-
-    /**
-     * Defines the form fields.
-     */
-    public function definition() {
-        $mform = $this->_form;
-
-        $mform->addElement('filetypes', 'filetypes0', 'Choose from all file types');
-        $mform->setDefault('filetypes0', '.pdf');
-
-        $mform->addElement('filetypes', 'filetypes1', 'Choose from a limited set',
-            ['onlytypes' => array('.pdf', 'web_image', 'image')]);
-
-        $mform->addElement('filetypes', 'filetypes2', 'Choose without "all"',
-            ['allowall' => false]);
-
-        $mform->addElement('filetypes', 'filetypes3', 'Unknown file types are allowed here',
-            ['allowunknown' => true]);
-
-        $mform->addElement('filemanager', 'fileman1', 'Picky file manager', null, ['accepted_types' => '.txt']);
-
-        $this->add_action_buttons(false);
-    }
-}
-
-$PAGE->set_context(context_system::instance());
-$PAGE->set_url('/lib/form/tests/fixtures/filetypes.php');
-$PAGE->set_title('Filetypes element test page');
-
-$form = new test_form($PAGE->url);
-$formdata = $form->get_data();
-
-echo $OUTPUT->header();
-echo $OUTPUT->heading($PAGE->title);
-$form->display();
-echo $OUTPUT->footer();
\ No newline at end of file
diff --git a/lib/form/tests/fixtures/multi_select_dependencies.php b/lib/form/tests/fixtures/multi_select_dependencies.php
deleted file mode 100644 (file)
index a93dfd3..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Fixture for Behat test for testing multiple select dependencies.
- *
- * @package core_form
- * @copyright 2016 Rajesh Taneja <rajesh@moodle.com>
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-require(__DIR__ . '/../../../../config.php');
-require_once($CFG->libdir . '/formslib.php');
-
-// Behat test fixture only.
-defined('BEHAT_SITE_RUNNING') || die('Only available on Behat test server');
-
-/**
- * Form for testing multiple select dependencies.
- *
- * @package core_form
- * @copyright 2016 Rajesh Taneja <rajesh@moodle.com>
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class test_form extends moodleform {
-
-    /**
-     * Form definition.
-     */
-    public function definition() {
-
-        $mform = $this->_form;
-
-        $labels = array('North', 'Est', 'South', 'West');
-        $select = $mform->addElement('select', 'mselect_name', 'Choose one or more directions', $labels);
-        $select->setMultiple(true);
-
-        $mform->addElement('text', 'text_name', 'Enter your name');
-        $mform->setType('text_name', PARAM_RAW);
-
-        $mform->disabledIf('text_name', 'mselect_name[]', 'neq', array(2, 3));
-
-        $this->add_action_buttons($cancel = true, $submitlabel = null);
-    }
-}
-
-$PAGE->set_context(context_system::instance());
-$PAGE->set_url('/lib/form/tests/fixtures/multi_select_dependencies.php');
-$PAGE->set_title('multi_select_dependencies');
-
-$mform = new test_form(new moodle_url('/lib/form/tests/fixtures/multi_select_dependencies.php'));
-
-echo $OUTPUT->header();
-$mform->display();
-echo $OUTPUT->footer();
\ No newline at end of file
index eb57b0d..f2b30af 100644 (file)
@@ -727,8 +727,9 @@ class grade_grade extends grade_object {
      *
      * @param array $grade_grades all course grades of one user, & used for better internal caching
      * @param array $grade_items array of grade items, & used for better internal caching
-     * @return array This is an array of 3 arrays:
-     *      unknown => list of item ids that may be affected by hiding (with the calculated grade as the value)
+     * @return array This is an array of following arrays:
+     *      unknown => list of item ids that may be affected by hiding (with the ITEM ID as both the key and the value) - for BC with old gradereport plugins
+     *      unknowngrades => list of item ids that may be affected by hiding (with the calculated grade as the value)
      *      altered => list of item ids that are definitely affected by hiding (with the calculated grade as the value)
      *      alteredgrademax => for each item in altered or unknown, the new value of the grademax
      *      alteredgrademin => for each item in altered or unknown, the new value of the grademin
@@ -779,6 +780,7 @@ class grade_grade extends grade_object {
 
         if (!$hiddenfound) {
             return array('unknown' => array(),
+                         'unknowngrades' => array(),
                          'altered' => array(),
                          'alteredgrademax' => array(),
                          'alteredgrademin' => array(),
@@ -795,10 +797,10 @@ class grade_grade extends grade_object {
         for($i=0; $i<$max; $i++) {
             $found = false;
             foreach($todo as $key=>$do) {
-                $hidden_precursors = array_intersect($dependson[$do], $unknown);
+                $hidden_precursors = array_intersect($dependson[$do], array_keys($unknown));
                 if ($hidden_precursors) {
                     // this item depends on hidden grade indirectly
-                    $unknown[$do] = $do;
+                    $unknown[$do] = $grade_grades[$do]->finalgrade;
                     unset($todo[$key]);
                     $found = true;
                     continue;
@@ -828,7 +830,7 @@ class grade_grade extends grade_object {
                         ) {
                             // This is a grade item that is not a category or course and has been affected by grade hiding.
                             // I guess this means it is a calculation that needs to be recalculated.
-                            $unknown[$do] = $do;
+                            $unknown[$do] = $grade_grades[$do]->finalgrade;
                             unset($todo[$key]);
                             $found = true;
                             continue;
@@ -953,7 +955,8 @@ class grade_grade extends grade_object {
             }
         }
 
-        return array('unknown' => $unknown,
+        return array('unknown' => array_combine(array_keys($unknown), array_keys($unknown)), // Left for BC in case some gradereport plugins expect it.
+                     'unknowngrades' => $unknown,
                      'altered' => $altered,
                      'alteredgrademax' => $alteredgrademax,
                      'alteredgrademin' => $alteredgrademin,
index f6d581d..5db8d37 100644 (file)
@@ -111,6 +111,43 @@ class core_admintree_testcase extends advanced_testcase {
         $tree->add('root', new admin_category('bar', 'Bar'), '');
     }
 
+    /**
+     * Test that changes to config trigger events.
+     */
+    public function test_config_log_created_event() {
+        global $DB;
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $adminroot = new admin_root(true);
+        $adminroot->add('root', $one = new admin_category('one', 'One'));
+        $page = new admin_settingpage('page', 'Page');
+        $page->add(new admin_setting_configtext('text1', 'Text 1', '', ''));
+        $page->add(new admin_setting_configpasswordunmask('pass1', 'Password 1', '', ''));
+        $adminroot->add('one', $page);
+
+        $sink = $this->redirectEvents();
+        $data = array('s__text1' => 'sometext', 's__pass1' => '');
+        $this->save_config_data($adminroot, $data);
+
+        $events = $sink->get_events();
+        $sink->close();
+        $event = array_pop($events);
+        $this->assertInstanceOf('\core\event\config_log_created', $event);
+
+        $sink = $this->redirectEvents();
+        $data = array('s__text1'=>'other', 's__pass1'=>'nice password');
+        $count = $this->save_config_data($adminroot, $data);
+
+        $events = $sink->get_events();
+        $sink->close();
+        $event = array_pop($events);
+        $this->assertInstanceOf('\core\event\config_log_created', $event);
+        // Verify password was nuked.
+        $this->assertNotEquals($event->other['value'], 'nice password');
+
+    }
+
     /**
      * Testing whether a configexecutable setting is executable.
      */
index 66089a4..31b99d1 100644 (file)
@@ -337,15 +337,29 @@ class core_statslib_testcase extends advanced_testcase {
 
         $this->assertEquals($firstoldtime, stats_get_start_from('daily'));
 
+        $time = time() - 5;
         \core_tests\event\create_executed::create(array('context' => context_system::instance()))->trigger();
+        $DB->set_field('logstore_standard_log', 'timecreated', $time++, [
+                'eventname' => '\\core_tests\\event\\create_executed',
+            ]);
+
         \core_tests\event\read_executed::create(array('context' => context_system::instance()))->trigger();
+        $DB->set_field('logstore_standard_log', 'timecreated', $time++, [
+                'eventname' => '\\core_tests\\event\\read_executed',
+            ]);
+
         \core_tests\event\update_executed::create(array('context' => context_system::instance()))->trigger();
+        $DB->set_field('logstore_standard_log', 'timecreated', $time++, [
+                'eventname' => '\\core_tests\\event\\update_executed',
+            ]);
+
         \core_tests\event\delete_executed::create(array('context' => context_system::instance()))->trigger();
+        $DB->set_field('logstore_standard_log', 'timecreated', $time++, [
+                'eventname' => '\\core_tests\\event\\delete_executed',
+            ]);
 
-        // Fake the origin of events.
         $DB->set_field('logstore_standard_log', 'origin', 'web', array());
-
-        $logs = $DB->get_records('logstore_standard_log');
+        $logs = $DB->get_records('logstore_standard_log', null, 'timecreated ASC');
         $this->assertCount(4, $logs);
 
         $firstnew = reset($logs);
index bf81ed5..77e180a 100644 (file)
@@ -41,6 +41,7 @@ information provided here is intended especially for developers.
 * New optional parameter 'closeSuggestionsOnSelect' for the enhance() function for form-autocomplete. Setting this to true will
   close the suggestions popup immediately after an option has been selected. If not specified, it defaults to true for single-select
   elements and false for multiple-select elements.
+* user_can_view_profile() now also checks the moodle/user:viewalldetails capability.
 
 === 3.3.1 ===
 
index 65caaf7..ec4d368 100644 (file)
@@ -252,6 +252,8 @@ $string['grading'] = 'Grading';
 $string['gradingchangessaved'] = 'The grade changes were saved';
 $string['gradingduedate'] = 'Remind me to grade by';
 $string['gradingduedate_help'] = 'The expected date that marking of the submissions should be completed by. This date is used to prioritise dashboard notifications for teachers.';
+$string['gradingdueduedatevalidation'] = 'Remind me to grade by date cannot be earlier than the due date.';
+$string['gradingduefromdatevalidation'] = 'Remind me to grade by date cannot be earlier than the allow submissions from date.';
 $string['gradechangessaveddetail'] = 'The changes to the grade and feedback were saved';
 $string['gradingmethodpreview'] = 'Grading criteria';
 $string['gradingoptions'] = 'Options';
index 3a23272..7f88c07 100644 (file)
@@ -238,6 +238,14 @@ class mod_assign_mod_form extends moodleform_mod {
                 $errors['cutoffdate'] = get_string('cutoffdatefromdatevalidation', 'assign');
             }
         }
+        if ($data['gradingduedate']) {
+            if ($data['allowsubmissionsfromdate'] && $data['allowsubmissionsfromdate'] > $data['gradingduedate']) {
+                $errors['gradingduedate'] = get_string('gradingduefromdatevalidation', 'assign');
+            }
+            if ($data['duedate'] && $data['duedate'] > $data['gradingduedate']) {
+                $errors['gradingduedate'] = get_string('gradingdueduedatevalidation', 'assign');
+            }
+        }
         if ($data['blindmarking'] && $data['attemptreopenmethod'] == ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS) {
             $errors['attemptreopenmethod'] = get_string('reopenuntilpassincompatiblewithblindmarking', 'assign');
         }
index 5922e55..b3c7f17 100644 (file)
@@ -135,8 +135,7 @@ if (empty($result->posts)) {
     // In either case we need to decide whether we can show personal information
     // about the requested user to the current user so we will execute some checks
 
-    // TODO - Remove extra cap check once MDL-59172 is resolved.
-    $canviewuser = user_can_view_profile($user, null, $usercontext) || has_capability('moodle/user:viewalldetails', $usercontext);
+    $canviewuser = user_can_view_profile($user, null, $usercontext);
 
     // Prepare the page title
     $pagetitle = get_string('noposts', 'mod_forum');
index a743dc7..934e9f5 100644 (file)
@@ -92,7 +92,7 @@ if ($lesson->ongoing && !$reviewmode) {
     echo $lessonoutput->ongoing_score($lesson);
 }
 if (!$reviewmode) {
-    echo $result->feedback;
+    echo format_text($result->feedback, FORMAT_MOODLE, array('context' => $context));
 }
 
 // User is modifying attempts - save button and some instructions
index 8562aeb..7558f1b 100644 (file)
@@ -1616,16 +1616,22 @@ function lesson_check_updates_since(cm_info $cm, $from, $filter = array()) {
             $updates->userpagesviewed->itemids = array_keys($pagesviewed);
         }
 
-        $select = 'lessonid = ? AND completed > ? ' . $insql;
+        $select = 'lessonid = ? AND completed > ?';
+        if (!empty($insql)) {
+            $select .= ' AND userid ' . $insql;
+        }
         $grades = $DB->get_records_select('lesson_grades', $select, $params, '', 'id');
         if (!empty($grades)) {
             $updates->usergrades->updated = true;
             $updates->usergrades->itemids = array_keys($grades);
         }
 
-        $select = 'lessonid = ? AND (starttime > ? OR lessontime > ? OR timemodifiedoffline > ?) ' . $insql;
+        $select = 'lessonid = ? AND (starttime > ? OR lessontime > ? OR timemodifiedoffline > ?)';
         $params = array($cm->instance, $from, $from, $from);
-        $params = array_merge($params, $inparams);
+        if (!empty($insql)) {
+            $select .= ' AND userid ' . $insql;
+            $params = array_merge($params, $inparams);
+        }
         $timers = $DB->get_records_select('lesson_timer', $select, $params, '', 'id');
         if (!empty($timers)) {
             $updates->usertimers->updated = true;
index 7c7c379..8256004 100644 (file)
@@ -4091,13 +4091,8 @@ abstract class lesson_page extends lesson_base {
 
                 $result->feedback .= $OUTPUT->box(format_text($this->get_contents(), $this->properties->contentsformat, $options),
                         'generalbox boxaligncenter');
-                if (isset($result->studentanswerformat)) {
-                    // This is the student's answer so it should be cleaned.
-                    $studentanswer = format_text($result->studentanswer, $result->studentanswerformat,
-                            array('context' => $context, 'para' => true));
-                } else {
-                    $studentanswer = format_string($result->studentanswer);
-                }
+                $studentanswer = format_text($result->studentanswer, $result->studentanswerformat,
+                        array('context' => $context, 'para' => true));
                 $result->feedback .= '<div class="correctanswer generalbox"><em>'
                         . get_string("youranswer", "lesson").'</em> : ' . $studentanswer;
                 if (isset($result->responseformat)) {
@@ -4476,6 +4471,7 @@ abstract class lesson_page extends lesson_base {
         $result->response        = '';
         $result->newpageid       = 0;       // stay on the page
         $result->studentanswer   = '';      // use this to store student's answer(s) in order to display it on feedback page
+        $result->studentanswerformat = FORMAT_MOODLE;
         $result->userresponse    = null;
         $result->feedback        = '';
         $result->nodefaultresponse  = false; // Flag for redirecting when default feedback is turned off
index f0e8ef1..5cd91a2 100644 (file)
@@ -91,14 +91,27 @@ class mod_lesson_lib_testcase extends advanced_testcase {
 
         $this->resetAfterTest();
         $this->setAdminUser();
-        $course = $this->getDataGenerator()->create_course();
+        $course = new stdClass();
+        $course->groupmode = SEPARATEGROUPS;
+        $course->groupmodeforce = true;
+        $course = $this->getDataGenerator()->create_course($course);
 
         // Create user.
-        $student = self::getDataGenerator()->create_user();
+        $studentg1 = self::getDataGenerator()->create_user();
+        $teacherg1 = self::getDataGenerator()->create_user();
+        $studentg2 = self::getDataGenerator()->create_user();
 
         // User enrolment.
         $studentrole = $DB->get_record('role', array('shortname' => 'student'));
-        $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual');
+        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
+        $this->getDataGenerator()->enrol_user($studentg1->id, $course->id, $studentrole->id, 'manual');
+        $this->getDataGenerator()->enrol_user($teacherg1->id, $course->id, $teacherrole->id, 'manual');
+        $this->getDataGenerator()->enrol_user($studentg2->id, $course->id, $studentrole->id, 'manual');
+
+        $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        groups_add_member($group1, $studentg1);
+        groups_add_member($group2, $studentg2);
 
         $this->setCurrentTimeStart();
         $record = array(
@@ -136,8 +149,23 @@ class mod_lesson_lib_testcase extends advanced_testcase {
         $this->assertTrue($updates->answers->updated);
         $this->assertCount(2, $updates->answers->itemids);
 
-        // Now, do something in the lesson.
-        $this->setUser($student);
+        // Now, do something in the lesson with the two users.
+        $this->setUser($studentg1);
+        mod_lesson_external::launch_attempt($lesson->id);
+        $data = array(
+            array(
+                'name' => 'answerid',
+                'value' => $DB->get_field('lesson_answers', 'id', array('pageid' => $tfrecord->id, 'jumpto' => -1)),
+            ),
+            array(
+                'name' => '_qf__lesson_display_answer_form_truefalse',
+                'value' => 1,
+            )
+        );
+        mod_lesson_external::process_page($lesson->id, $tfrecord->id, $data);
+        mod_lesson_external::finish_attempt($lesson->id);
+
+        $this->setUser($studentg2);
         mod_lesson_external::launch_attempt($lesson->id);
         $data = array(
             array(
@@ -152,6 +180,7 @@ class mod_lesson_lib_testcase extends advanced_testcase {
         mod_lesson_external::process_page($lesson->id, $tfrecord->id, $data);
         mod_lesson_external::finish_attempt($lesson->id);
 
+        $this->setUser($studentg1);
         $updates = lesson_check_updates_since($cm, $onehourago);
 
         // Check question attempts, timers and new grades.
@@ -163,6 +192,33 @@ class mod_lesson_lib_testcase extends advanced_testcase {
 
         $this->assertTrue($updates->timers->updated);
         $this->assertCount(1, $updates->timers->itemids);
+
+        // Now, as teacher, check that I can see the two users (even in separate groups).
+        $this->setUser($teacherg1);
+        $updates = lesson_check_updates_since($cm, $onehourago);
+        $this->assertTrue($updates->userquestionattempts->updated);
+        $this->assertCount(2, $updates->userquestionattempts->itemids);
+
+        $this->assertTrue($updates->usergrades->updated);
+        $this->assertCount(2, $updates->usergrades->itemids);
+
+        $this->assertTrue($updates->usertimers->updated);
+        $this->assertCount(2, $updates->usertimers->itemids);
+
+        // Now, teacher can't access all groups.
+        groups_add_member($group1, $teacherg1);
+        assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $teacherrole->id, context_module::instance($cm->id));
+        accesslib_clear_all_caches_for_unit_testing();
+        $updates = lesson_check_updates_since($cm, $onehourago);
+        // I will see only the studentg1 updates.
+        $this->assertTrue($updates->userquestionattempts->updated);
+        $this->assertCount(1, $updates->userquestionattempts->itemids);
+
+        $this->assertTrue($updates->usergrades->updated);
+        $this->assertCount(1, $updates->usergrades->itemids);
+
+        $this->assertTrue($updates->usertimers->updated);
+        $this->assertCount(1, $updates->usertimers->itemids);
     }
 
     public function test_lesson_core_calendar_provide_event_action_open() {
index 56f25af..ca27416 100644 (file)
@@ -51,6 +51,7 @@ require_once($CFG->dirroot.'/mod/lti/lib.php');
 require_once($CFG->dirroot.'/mod/lti/locallib.php');
 
 $id = required_param('id', PARAM_INT); // Course Module ID.
+$triggerview = optional_param('triggerview', 1, PARAM_BOOL);
 
 $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST);
 $lti = $DB->get_record('lti', array('id' => $cm->instance), '*', MUST_EXIST);
@@ -62,7 +63,9 @@ require_login($course, true, $cm);
 require_capability('mod/lti:view', $context);
 
 // Completion and trigger events.
-lti_view($lti, $course, $cm, $context);
+if ($triggerview) {
+    lti_view($lti, $course, $cm, $context);
+}
 
 $lti->cmid = $cm->id;
 lti_launch_tool($lti);
index cc0c255..224b219 100644 (file)
@@ -53,6 +53,7 @@ require_once($CFG->dirroot.'/mod/lti/locallib.php');
 
 $id = optional_param('id', 0, PARAM_INT); // Course Module ID, or
 $l  = optional_param('l', 0, PARAM_INT);  // lti ID.
+$forceview = optional_param('forceview', 0, PARAM_BOOL);
 
 if ($l) {  // Two ways to specify the module.
     $lti = $DB->get_record('lti', array('id' => $l), '*', MUST_EXIST);
@@ -89,11 +90,16 @@ if ($launchcontainer == LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS) {
     $PAGE->set_pagelayout('frametop'); // Most frametops don't include footer, and pre-post blocks.
     $PAGE->blocks->show_only_fake_blocks(); // Disable blocks for layouts which do include pre-post blocks.
 } else if ($launchcontainer == LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW) {
-    redirect('launch.php?id=' . $cm->id);
+    if (!$forceview) {
+        $url = new moodle_url('/mod/lti/launch.php', array('id' => $cm->id));
+        redirect($url);
+    }
 } else {
     $PAGE->set_pagelayout('incourse');
 }
 
+lti_view($lti, $course, $cm, $context);
+
 $pagetitle = strip_tags($course->shortname.': '.format_string($lti->name));
 $PAGE->set_title($pagetitle);
 $PAGE->set_heading($course->fullname);
@@ -111,18 +117,20 @@ if ($lti->showdescriptionlaunch && $lti->intro) {
 }
 
 if ( $launchcontainer == LTI_LAUNCH_CONTAINER_WINDOW ) {
-    echo "<script language=\"javascript\">//<![CDATA[\n";
-    echo "window.open('launch.php?id=".$cm->id."','lti-".$cm->id."');";
-    echo "//]]\n";
-    echo "</script>\n";
-    echo "<p>".get_string("basiclti_in_new_window", "lti")."</p>\n";
+    if (!$forceview) {
+        echo "<script language=\"javascript\">//<![CDATA[\n";
+        echo "window.open('launch.php?id=" . $cm->id . "&triggerview=0','lti-" . $cm->id . "');";
+        echo "//]]\n";
+        echo "</script>\n";
+        echo "<p>".get_string("basiclti_in_new_window", "lti")."</p>\n";
+    }
     $url = new moodle_url('/mod/lti/launch.php', array('id' => $cm->id));
     echo html_writer::start_tag('p');
     echo html_writer::link($url, get_string("basiclti_in_new_window_open", "lti"), array('target' => '_blank'));
     echo html_writer::end_tag('p');
 } else {
     // Request the launch content with an iframe tag.
-    echo '<iframe id="contentframe" height="600px" width="100%" src="launch.php?id='.$cm->id.'"></iframe>';
+    echo '<iframe id="contentframe" height="600px" width="100%" src="launch.php?id='.$cm->id.'&triggerview=0"></iframe>';
 
     // Output script to make the iframe tag be as large as possible.
     $resize = '
index 6f5a0e2..c306f04 100644 (file)
     label {
         margin: 0;
     }
+
+    .header {
+        text-align: left;
+    }
 }
 
 #page-mod-quiz-edit {
index c9f37d4..024e920 100644 (file)
     label {
         margin: 0;
     }
+    .header {
+        text-align: left;
+    }
 }
 #page-mod-quiz-edit {
     div.questionbankwindow div.header {
index e93a190..066dc47 100644 (file)
@@ -9138,6 +9138,9 @@ a.ygtvspacer:hover {
 #categoryquestions label {
   margin: 0;
 }
+#categoryquestions .header {
+  text-align: left;
+}
 #page-mod-quiz-edit div.questionbankwindow div.header {
   margin: 0;
 }
index 83044f2..b8b4a63 100644 (file)
@@ -1143,7 +1143,7 @@ function user_can_view_profile($user, $course = null, $usercontext = null) {
         $usercontext = context_user::instance($user->id);
     }
     // Number 3.
-    if (has_capability('moodle/user:viewdetails', $usercontext)) {
+    if (has_capability('moodle/user:viewdetails', $usercontext) || has_capability('moodle/user:viewalldetails', $usercontext)) {
         return true;
     }
 
index 7a85f72..b69391e 100644 (file)
@@ -23,7 +23,7 @@ Feature: Bulk enrolments
   Scenario: Bulk edit enrolments
     When I log in as "admin"
     And I am on "Course 1" course homepage
-    And I follow "Participants"
+    And I navigate to course participants
     And I press "Select all"
     And I set the field "With selected users..." to "Edit selected user enrolments"
     And I set the field "Alter status" to "Suspended"
@@ -36,7 +36,7 @@ Feature: Bulk enrolments
   Scenario: Bulk delete enrolments
     When I log in as "admin"
     And I am on "Course 1" course homepage
-    And I follow "Participants"
+    And I navigate to course participants
     And I press "Select all"
     And I set the field "With selected users..." to "Delete selected user enrolments"
     And I press "Unenrol users"
index 0683cba..3e88074 100644 (file)
@@ -576,6 +576,15 @@ class core_userliblib_testcase extends advanced_testcase {
         $this->setUser($user5);
         $this->assertTrue(user_can_view_profile($user4));
 
+        // Test the user:viewalldetails cap check using the course creator role which, by default, can't see student profiles.
+        $this->setUser($user7);
+        $this->assertFalse(user_can_view_profile($user4));
+        assign_capability('moodle/user:viewalldetails', CAP_ALLOW, $coursecreatorrole->id, context_system::instance()->id, true);
+        reload_all_capabilities();
+        $this->assertTrue(user_can_view_profile($user4));
+        unassign_capability('moodle/user:viewalldetails', $coursecreatorrole->id, $coursecontext->id);
+        reload_all_capabilities();
+
         $CFG->coursecontact = null;
 
         // Visitor (Not a guest user, userid=0).
index dc4cc84..b44ec9d 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2017080400.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2017080700.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.