Merge branch 'MDL-59438-master' of git://github.com/rezaies/moodle
authorDamyon Wiese <damyon@moodle.com>
Tue, 12 Dec 2017 04:56:19 +0000 (12:56 +0800)
committerDamyon Wiese <damyon@moodle.com>
Tue, 12 Dec 2017 05:05:22 +0000 (13:05 +0800)
57 files changed:
.gitattributes
admin/tool/analytics/classes/output/models_list.php
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/templates/models_list.mustache
admin/tool/behat/cli/util.php
admin/tool/httpsreplace/tests/httpsreplace_test.php
analytics/classes/model.php
analytics/tests/model_test.php
auth/ldap/lang/en/auth_ldap.php
auth/ldap/settings.php
cohort/classes/external/cohort_summary_exporter.php
competency/classes/api.php
competency/classes/competency.php
competency/classes/template_competency.php
competency/classes/user_competency_plan.php
competency/tests/external_test.php
completion/classes/external.php
completion/criteria/completion_criteria_activity.php
enrol/imsenterprise/tests/imsenterprise_test.php
files/classes/converter.php
files/tests/converter_test.php
grade/tests/edittreelib_test.php
grade/tests/importlib_test.php
grade/tests/querylib_test.php
grade/tests/report_graderlib_test.php
grade/tests/reportlib_test.php
grade/tests/reportuserlib_test.php
lib/accesslib.php
lib/authlib.php
lib/classes/access/get_user_capability_course_helper.php
lib/classes/hub/api.php
lib/classes/plugininfo/gradingform.php
lib/classes/task/delete_unconfirmed_users_task.php
lib/db/upgrade.php
lib/dml/moodle_database.php
lib/dml/oci_native_moodle_package.sql
lib/tests/accesslib_test.php
lib/tests/gradelib_test.php
mod/assign/feedback/editpdf/lang/en/assignfeedback_editpdf.php
mod/assign/feedback/editpdf/locallib.php
mod/assign/feedback/editpdf/settings.php
mod/assign/submission/file/locallib.php
mod/assign/submission/onlinetext/locallib.php
mod/data/lang/en/data.php
mod/data/lib.php
mod/data/tests/lib_test.php
mod/lesson/lang/en/lesson.php
mod/lesson/lib.php
mod/lesson/locallib.php
mod/lesson/tests/lib_test.php
mod/quiz/index.php
question/type/multianswer/questiontype.php
search/tests/behat/behat_search.php
theme/boost/scss/moodle/modules.scss
theme/bootstrapbase/less/moodle/modules.less
theme/bootstrapbase/style/moodle.css
version.php

index fb39fe0..8afacce 100644 (file)
@@ -1,4 +1,5 @@
 **/yui/build/** -diff
 **/amd/build/** -diff
+lib/dml/oci_native_moodle_package.sql text eol=lf
 theme/bootstrapbase/style/editor.css -diff
 theme/bootstrapbase/style/moodle.css -diff
index 0e8d4e8..cbd0a60 100644 (file)
@@ -119,6 +119,9 @@ class models_list implements \renderable, \templatable {
                     debugging("The time splitting method '{$modeldata->timesplitting}' should include a '{$identifier}_help'
                         string to describe its purpose.", DEBUG_DEVELOPER);
                 }
+            } else {
+                $helpicon = new \help_icon('timesplittingnotdefined', 'tool_analytics');
+                $modeldata->timesplittinghelp = $helpicon->export_for_template($output);
             }
 
             // Has this model generated predictions?.
@@ -207,19 +210,22 @@ class models_list implements \renderable, \templatable {
             }
 
             // Enable / disable.
-            if ($model->is_enabled()) {
-                $action = 'disable';
-                $text = get_string('disable');
-                $icontype = 't/block';
-            } else {
-                $action = 'enable';
-                $text = get_string('enable');
-                $icontype = 'i/checked';
+            if ($model->is_enabled() || !empty($modeldata->timesplitting)) {
+                // If there is no timesplitting method set, the model can not be enabled.
+                if ($model->is_enabled()) {
+                    $action = 'disable';
+                    $text = get_string('disable');
+                    $icontype = 't/block';
+                } else {
+                    $action = 'enable';
+                    $text = get_string('enable');
+                    $icontype = 'i/checked';
+                }
+                $urlparams['action'] = $action;
+                $url = new \moodle_url('model.php', $urlparams);
+                $icon = new \action_menu_link_secondary($url, new \pix_icon($icontype, $text), $text);
+                $actionsmenu->add($icon);
             }
-            $urlparams['action'] = $action;
-            $url = new \moodle_url('model.php', $urlparams);
-            $icon = new \action_menu_link_secondary($url, new \pix_icon($icontype, $text), $text);
-            $actionsmenu->add($icon);
 
             // Export training data.
             if (!$model->is_static() && $model->is_trained()) {
index 68ca347..47def2b 100644 (file)
@@ -85,6 +85,8 @@ $string['previouspage'] = 'Previous page';
 $string['samestartdate'] = 'Current start date is good';
 $string['sameenddate'] = 'Current end date is good';
 $string['target'] = 'Target';
+$string['timesplittingnotdefined'] = 'Time splitting is not defined.';
+$string['timesplittingnotdefined_help'] = 'You need to select a time-splitting method before enabling the model.';
 $string['trainandpredictmodel'] = 'Training model and calculating predictions';
 $string['trainingprocessfinished'] = 'Training process finished';
 $string['trainingresults'] = 'Training results';
index 8eb6211..efdc4ca 100644 (file)
                     {{/timesplitting}}
                     {{^timesplitting}}
                         {{#str}}notdefined, tool_analytics{{/str}}
+                        {{#timesplittinghelp}}
+                            {{>core/help_icon}}
+                        {{/timesplittinghelp}}
                     {{/timesplitting}}
                 </td>
                 <td>
index 8efa4ed..e88125b 100644 (file)
@@ -398,7 +398,7 @@ function print_combined_install_output($processes) {
     // Show process name in first row.
     foreach ($processes as $name => $process) {
         // If we don't have enough space to show full run name then show runX.
-        if ($lengthofprocessline < strlen($name + 2)) {
+        if ($lengthofprocessline < strlen($name) + 2) {
             $name = substr($name, -5);
         }
         // One extra padding as we are adding | separator for rest of the data.
index c10ea2f..e3599c6 100644 (file)
@@ -47,12 +47,12 @@ class httpsreplace_test extends \advanced_testcase {
             "Test image from another site should be replaced" => [
                 "content" => '<img src="' . $this->getExternalTestFileUrl('/test.jpg', false) . '">',
                 "outputregex" => '/UPDATE/',
-                "expectedcontent" => '<img src="' . $this->getExternalTestFileUrl('/test.jpg', true) . '">',
+                "expectedcontent" => '<img src="' . $this->get_converted_http_link('/test.jpg') . '">',
             ],
             "Test object from another site should be replaced" => [
                 "content" => '<object data="' . $this->getExternalTestFileUrl('/test.swf', false) . '">',
                 "outputregex" => '/UPDATE/',
-                "expectedcontent" => '<object data="' . $this->getExternalTestFileUrl('/test.swf', true) . '">',
+                "expectedcontent" => '<object data="' . $this->get_converted_http_link('/test.swf') . '">',
             ],
             "Test image from a site with international name should be replaced" => [
                 "content" => '<img src="http://中国互联网络信息中心.中国/logosy/201706/W01.png">',
@@ -82,7 +82,7 @@ class httpsreplace_test extends \advanced_testcase {
             "Search for params should be case insensitive" => [
                 "content" => '<object DATA="' . $this->getExternalTestFileUrl('/test.swf', false) . '">',
                 "outputregex" => '/UPDATE/',
-                "expectedcontent" => '<object DATA="' . $this->getExternalTestFileUrl('/test.swf', true) . '">',
+                "expectedcontent" => '<object DATA="' . $this->get_converted_http_link('/test.swf') . '">',
             ],
             "URL should be case insensitive" => [
                 "content" => '<object data="HTTP://some.site/path?query">',
@@ -93,7 +93,7 @@ class httpsreplace_test extends \advanced_testcase {
                 "content" => '<img alt="A picture" src="' . $this->getExternalTestFileUrl('/test.png', false) .
                     '" width="1”><p style="font-size: \'20px\'"></p>',
                 "outputregex" => '/UPDATE/',
-                "expectedcontent" => '<img alt="A picture" src="' . $this->getExternalTestFileUrl('/test.png', true) .
+                "expectedcontent" => '<img alt="A picture" src="' . $this->get_converted_http_link('/test.png') .
                     '" width="1”><p style="font-size: \'20px\'"></p>',
             ],
             "Broken URL should not be changed" => [
@@ -113,11 +113,25 @@ class httpsreplace_test extends \advanced_testcase {
                     $this->getExternalTestFileUrl('/test.jpg', false) . '"></a>',
                 "outputregex" => '/UPDATE/',
                 "expectedcontent" => '<a href="' . $this->getExternalTestFileUrl('/test.png', false) . '"><img src="' .
-                    $this->getExternalTestFileUrl('/test.jpg', true) . '"></a>',
+                    $this->get_converted_http_link('/test.jpg') . '"></a>',
             ],
         ];
     }
 
+    /**
+     * Convert the HTTP external test file URL to use HTTPS.
+     *
+     * Note: We *must not* use getExternalTestFileUrl with the True option
+     * here, becase it is reasonable to have only one of these set due to
+     * issues with SSL certificates.
+     *
+     * @param   string  $path Path to be rewritten
+     * @return  string
+     */
+    protected function get_converted_http_link($path) {
+        return preg_replace('/^http:/', 'https:', $this->getExternalTestFileUrl($path, false));
+    }
+
     /**
      * Test upgrade_http_links
      * @param string $content Example content that we'll attempt to replace.
@@ -152,7 +166,7 @@ class httpsreplace_test extends \advanced_testcase {
         // Get the http url, since the default test wwwroot is https.
         $wwwrootdomain = 'www.example.com';
         $wwwroothttp = preg_replace('/^https:/', 'http:', $CFG->wwwroot);
-        $testdomain = 'download.moodle.org';
+        $testdomain = $this->get_converted_http_link('');
         return [
             "Test image from an available site so shouldn't be reported" => [
                 "content" => '<img src="' . $this->getExternalTestFileUrl('/test.jpg', false) . '">',
index d881645..0bee650 100644 (file)
@@ -1013,6 +1013,10 @@ class model {
             if (!$this->is_static()) {
                 $this->model->trained = 0;
             }
+        } else if (empty($this->model->timesplitting)) {
+            // A valid timesplitting method needs to be supplied before a model can be enabled.
+            throw new \moodle_exception('invalidtimesplitting', 'analytics', '', $this->model->id);
+
         }
 
         // Purge pages with insights as this may change things.
index 2685b64..d5d0153 100644 (file)
@@ -226,7 +226,7 @@ class analytics_model_testcase extends advanced_testcase {
         $this->model->mark_as_trained();
         $this->assertEquals($originaluniqueid, $this->model->get_unique_id());
 
-        $this->model->enable();
+        $this->model->enable('\core\analytics\time_splitting\deciles');
         $this->assertEquals($originaluniqueid, $this->model->get_unique_id());
 
         // Wait 1 sec so the timestamp changes.
index fdb9541..843e1b0 100644 (file)
@@ -41,7 +41,7 @@ $string['auth_ldapdescription'] = 'This method provides authentication against a
                                   entry in its database. This module can read user attributes from LDAP and prefill
                                   wanted fields in Moodle.  For following logins only the username and
                                   password are checked.';
-$string['auth_ldap_expiration_desc'] = 'Select No to disable expired password checking or LDAP to read passwordexpiration time directly from LDAP';
+$string['auth_ldap_expiration_desc'] = 'Select \'{$a->no}\' to disable expired password checking or \'{$a->ldapserver}\' to read the password expiration time directly from the LDAP server';
 $string['auth_ldap_expiration_key'] = 'Expiration';
 $string['auth_ldap_expiration_warning_desc'] = 'Number of days before password expiration warning is issued.';
 $string['auth_ldap_expiration_warning_key'] = 'Expiration warning';
index af1ddf7..43fb8eb 100644 (file)
@@ -185,12 +185,24 @@ if ($ADMIN->fulltree) {
                 new lang_string('auth_ldap_passwdexpire_settings', 'auth_ldap'), ''));
 
         // Password Expiration.
+
+        // Create the description lang_string object.
+        $strno = get_string('no');
+        $strldapserver = get_string('pluginname', 'auth_ldap');
+        $langobject = new stdClass();
+        $langobject->no = $strno;
+        $langobject->ldapserver = $strldapserver;
+        $description = new lang_string('auth_ldap_expiration_desc', 'auth_ldap', $langobject);
+
+        // Now create the options.
         $expiration = array();
-        $expiration['0'] = 'no';
-        $expiration['1'] = 'LDAP';
+        $expiration['0'] = $strno;
+        $expiration['1'] = $strldapserver;
+
+        // Add the setting.
         $settings->add(new admin_setting_configselect('auth_ldap/expiration',
                 new lang_string('auth_ldap_expiration_key', 'auth_ldap'),
-                new lang_string('auth_ldap_expiration_desc', 'auth_ldap'), 0 , $expiration));
+                $description, 0 , $expiration));
 
         // Password Expiration warning.
         $settings->add(new admin_setting_configtext('auth_ldap/expiration_warning',
index 44d8e25..55e7529 100644 (file)
@@ -52,6 +52,16 @@ class cohort_summary_exporter extends \core\external\exporter {
                 'default' => '',
                 'null' => NULL_ALLOWED
             ),
+            'description' => array(
+                'type' => PARAM_TEXT,
+                'default' => '',
+                'null' => NULL_ALLOWED
+            ),
+            'descriptionformat' => array(
+                'type' => PARAM_INT,
+                'default' => FORMAT_HTML,
+                'null' => NULL_ALLOWED
+            ),
             'visible' => array(
                 'type' => PARAM_BOOL,
             )
index fc16c0c..9d04083 100644 (file)
@@ -165,7 +165,7 @@ class api {
         require_capability('moodle/competency:competencymanage', $competency->get_context());
 
         // Reset the sortorder, use reorder instead.
-        $competency->set('sortorder', null);
+        $competency->set('sortorder', 0);
         $competency->create();
 
         \core\event\competency_created::create_from_competency($competency)->trigger();
index c537508..9e417e9 100644 (file)
@@ -76,7 +76,7 @@ class competency extends persistent {
                 'default' => FORMAT_HTML
             ),
             'sortorder' => array(
-                'default' => null,
+                'default' => 0,
                 'type' => PARAM_INT
             ),
             'parentid' => array(
index 3e0c8f8..8c6df0b 100644 (file)
@@ -53,7 +53,7 @@ class template_competency extends persistent {
             ),
             'sortorder' => array(
                 'type' => PARAM_INT,
-                'default' => null,
+                'default' => 0,
             ),
         );
     }
index 3880a2a..5b53589 100644 (file)
@@ -66,7 +66,7 @@ class user_competency_plan extends persistent {
             ),
             'sortorder' => array(
                 'type' => PARAM_INT,
-                'default' => null,
+                'default' => 0,
             ),
         );
     }
index f8a09e0..f0b50c8 100644 (file)
@@ -281,8 +281,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
             'idnumber' => 'idnumber' . $number,
             'description' => 'description' . $number,
             'descriptionformat' => FORMAT_HTML,
-            'competencyframeworkid' => $frameworkid,
-            'sortorder' => 0
+            'competencyframeworkid' => $frameworkid
         );
         $result = external::create_competency($competency);
         return (object) external_api::clean_returnvalue(external::create_competency_returns(), $result);
@@ -294,8 +293,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
             'shortname' => 'shortname' . $number,
             'idnumber' => 'idnumber' . $number,
             'description' => 'description' . $number,
-            'descriptionformat' => FORMAT_HTML,
-            'sortorder' => 0
+            'descriptionformat' => FORMAT_HTML
         );
         $result = external::update_competency($competency);
         return external_api::clean_returnvalue(external::update_competency_returns(), $result);
index b610108..24384e9 100644 (file)
@@ -249,7 +249,7 @@ class core_completion_external extends external_api {
 
         $completion = new completion_info($course);
         $activities = $completion->get_activities();
-        $progresses = $completion->get_progress_all();
+        $progresses = $completion->get_progress_all('u.id = :uid', ['uid' => $params['userid']]);
         $userprogress = $progresses[$user->id];
 
         $results = array();
index 29c570b..592c84e 100644 (file)
@@ -279,7 +279,8 @@ class completion_criteria_activity extends completion_criteria {
             $details['requirement'][] = get_string('markingyourselfcomplete', 'completion');
         } elseif ($cm->completion == COMPLETION_TRACKING_AUTOMATIC) {
             if ($cm->completionview) {
-                $details['requirement'][] = get_string('viewingactivity', 'completion', $this->module);
+                $modulename = core_text::strtolower(get_string('modulename', $this->module));
+                $details['requirement'][] = get_string('viewingactivity', 'completion', $modulename);
             }
 
             if (!is_null($cm->completiongradeitemnumber)) {
index e3ed112..dd8d9a8 100644 (file)
@@ -206,7 +206,7 @@ class enrol_imsenterprise_testcase extends advanced_testcase {
         $this->set_xml_file(array($imsuser));
 
         $this->imsplugin->cron();
-        $this->assertEquals(1, $DB->get_field('user', 'deleted', array('id' => $user->id), '*', MUST_EXIST));
+        $this->assertEquals(1, $DB->get_field('user', 'deleted', array('id' => $user->id), MUST_EXIST));
     }
 
     /**
@@ -227,7 +227,7 @@ class enrol_imsenterprise_testcase extends advanced_testcase {
         $this->set_xml_file(array($imsuser));
 
         $this->imsplugin->cron();
-        $this->assertEquals(0, $DB->get_field('user', 'deleted', array('id' => $user->id), '*', MUST_EXIST));
+        $this->assertEquals(0, $DB->get_field('user', 'deleted', array('id' => $user->id), MUST_EXIST));
     }
 
     /**
index 32793d3..da8c5ba 100644 (file)
@@ -127,7 +127,7 @@ class converter {
         if ($status === conversion::STATUS_PENDING || $status === conversion::STATUS_FAILED) {
             // The current status is either pending or failed.
             // Attempt to pick up a new converter and convert the document.
-            $from = \core_filetypes::get_file_extension($file->get_mimetype());
+            $from = pathinfo($file->get_filename(), PATHINFO_EXTENSION);
             $converters = $this->get_document_converter_classes($from, $format);
             $currentconverter = $this->get_next_converter($converters, $conversion->get('converter'));
 
@@ -225,9 +225,9 @@ class converter {
             return false;
         }
 
-        $from = \core_filetypes::get_file_extension($file->get_mimetype());
+        $from = pathinfo($file->get_filename(), PATHINFO_EXTENSION);
         if (!$from) {
-            // No mime type could be found. Unable to determine converter.
+            // No file extension could be found. Unable to determine converter.
             return false;
         }
 
index 950d593..e345c1e 100644 (file)
@@ -341,7 +341,7 @@ class core_files_converter_testcase extends advanced_testcase {
     }
 
     /**
-     * Test the can_convert_storedfile_to function with a file with indistinguished mimetype.
+     * Test the can_convert_storedfile_to function with a file with a known mimetype and extension.
      */
     public function test_can_convert_storedfile_to_docx() {
         $returnvalue = (object) [];
@@ -352,8 +352,7 @@ class core_files_converter_testcase extends advanced_testcase {
 
         $types = \core_filetypes::get_types();
 
-        // A file with filename '.' is a directory.
-        $file = $this->get_stored_file('example content', 'example', [
+        $file = $this->get_stored_file('example content', 'example.docx', [
                 'mimetype' => $types['docx']['type'],
             ]);
 
index cb96fa0..b24608d 100644 (file)
@@ -17,7 +17,7 @@
 /**
  * Unit tests for grade/edit/tree/lib.php.
  *
- * @pacakge  core_grade
+ * @package  core_grades
  * @category phpunit
  * @author   Andrew Davis
  * @license  http://www.gnu.org/copyleft/gpl.html GNU Public License
index 0acea9e..234e6a4 100644 (file)
@@ -17,7 +17,7 @@
 /**
  * Unit tests for grade/import/lib.php.
  *
- * @package   core_grade
+ * @package   core_grades
  * @category  phpunit
  * @copyright 2015 Adrian Greeve <adrian@moodle.com>
  * @license   http://www.gnu.org/copyleft/gpl.html GNU Public License
index c284123..28e6cde 100644 (file)
@@ -17,7 +17,7 @@
 /**
  * Unit tests for grade quering
  *
- * @pacakge   core_grade
+ * @package   core_grades
  * @category  phpunit
  * @copyright 2011 Petr Skoda {@link http://skodak.org}
  * @license   http://www.gnu.org/copyleft/gpl.html GNU Public License
index d65e285..75f52c3 100644 (file)
@@ -17,7 +17,7 @@
 /**
  * Unit tests for grade/report/user/lib.php.
  *
- * @package  core_grade
+ * @package  core_grades
  * @category phpunit
  * @copyright 2012 Andrew Davis
  * @license  http://www.gnu.org/copyleft/gpl.html GNU Public License
index bade609..d8928e5 100644 (file)
@@ -17,7 +17,7 @@
 /**
  * Unit tests for grade/report/lib.php.
  *
- * @pacakge  core_grade
+ * @package  core_grades
  * @category phpunit
  * @author   Andrew Davis
  * @license  http://www.gnu.org/copyleft/gpl.html GNU Public License
index 7bc3289..94ae292 100644 (file)
@@ -17,7 +17,7 @@
 /**
  * Unit tests for grade/report/user/lib.php.
  *
- * @package  core_grade
+ * @package  core_grades
  * @category phpunit
  * @copyright 2012 Andrew Davis
  * @license  http://www.gnu.org/copyleft/gpl.html GNU Public License
index bd400e4..856a809 100644 (file)
@@ -319,8 +319,7 @@ function get_role_definitions_uncached(array $roleids) {
     $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
               FROM {role_capabilities} rc
               JOIN {context} ctx ON rc.contextid = ctx.id
-             WHERE rc.roleid $sql
-          ORDER BY ctx.path, rc.roleid, rc.capability";
+             WHERE rc.roleid $sql";
     $rs = $DB->get_recordset_sql($sql, $params);
 
     foreach ($rs as $rd) {
@@ -334,6 +333,15 @@ function get_role_definitions_uncached(array $roleids) {
     }
 
     $rs->close();
+
+    // Sometimes (e.g. get_user_capability_course_helper::get_capability_info_at_each_context)
+    // we process role definitinons in a way that requires we see parent contexts
+    // before child contexts. This sort ensures that works (and is faster than
+    // sorting in the SQL query).
+    foreach ($rdefs as $roleid => $rdef) {
+        ksort($rdefs[$roleid]);
+    }
+
     return $rdefs;
 }
 
index 44b3221..e1735fd 100644 (file)
@@ -1049,7 +1049,7 @@ function display_auth_lock_options($settings, $auth, $userfields, $helptext, $ma
             // We are mapping to a remote field here.
             // Mapping.
             $settings->add(new admin_setting_configtext("auth_{$auth}/field_map_{$field}",
-                    get_string('auth_fieldmapping', 'auth', $fieldname), '', '', PARAM_ALPHANUMEXT, 30));
+                    get_string('auth_fieldmapping', 'auth', $fieldname), '', '', PARAM_RAW, 30));
 
             // Update local.
             $settings->add(new admin_setting_configselect("auth_{$auth}/field_updatelocal_{$field}",
index 80b5b45..93dccfa 100644 (file)
@@ -39,7 +39,10 @@ class get_user_capability_course_helper {
      * an array of capability values at each relevant context for the given user and capability.
      *
      * This is organised by the effective context path (the one at which the capability takes
-     * effect) and then by role id.
+     * effect) and then by role id. Note, however, that the resulting array only has
+     * the information that will be needed later. If there are Prohibits present in some
+     * roles, then they cannot be overridden by other roles or role overrides in lower contexts,
+     * therefore, such information, if any, is absent from the results.
      *
      * @param int $userid User id
      * @param string $capability Capability e.g. 'moodle/course:view'
@@ -58,42 +61,101 @@ class get_user_capability_course_helper {
         }
         $rdefs = get_role_definitions(array_keys($roleids));
 
+        // A prohibit in any relevant role prevents the capability
+        // in that context and all subcontexts. We need to track that.
+        // Here, the array keys are the paths where there is a prohibit the values are the role id.
+        $prohibitpaths = [];
+
         // Get data for required capability at each context path where the user has a role that can
         // affect it.
-        $systemcontext = \context_system::instance();
         $pathroleperms = [];
-        foreach ($accessdata['ra'] as $userpath => $roles) {
+        foreach ($accessdata['ra'] as $rapath => $roles) {
+
             foreach ($roles as $roleid) {
                 // Get role definition for that role.
-                foreach ($rdefs[$roleid] as $rolepath => $caps) {
+                foreach ($rdefs[$roleid] as $rdefpath => $caps) {
                     // Ignore if this override/definition doesn't refer to the relevant cap.
                     if (!array_key_exists($capability, $caps)) {
                         continue;
                     }
 
-                    // Check path is /1 or matches a path the user has.
-                    if ($rolepath === '/' . $systemcontext->id) {
-                        // Note /1 is listed first in the array so this entry will be overridden
-                        // if there is an override for the role on this actual level.
-                        $effectivepath = $userpath;
-                    } else if (preg_match('~^' . $userpath . '($|/)~', $rolepath)) {
-                        $effectivepath = $rolepath;
+                    // Check a role definition or override above ra.
+                    if (self::path_is_above($rdefpath, $rapath)) {
+                        // Note that $rdefs is sorted by path, so if a more specific override
+                        // exists, it will be processed later and override this one.
+                        $effectivepath = $rapath;
+                    } else if (self::path_is_above($rapath, $rdefpath)) {
+                        $effectivepath = $rdefpath;
                     } else {
                         // Not inside an area where the user has the role, so ignore.
                         continue;
                     }
 
+                    // Check for already seen prohibits in higher context. Overrides can't change that.
+                    if (self::any_path_is_above($prohibitpaths, $effectivepath)) {
+                        continue;
+                    }
+
+                    // This is a releavant role assignment / permission combination. Save it.
                     if (!array_key_exists($effectivepath, $pathroleperms)) {
                         $pathroleperms[$effectivepath] = [];
                     }
                     $pathroleperms[$effectivepath][$roleid] = $caps[$capability];
+
+                    // Update $prohibitpaths if necessary.
+                    if ($caps[$capability] == CAP_PROHIBIT) {
+                        // First remove any lower-context prohibits that might have come from other roles.
+                        foreach ($prohibitpaths as $otherprohibitpath => $notused) {
+                            if (self::path_is_above($effectivepath, $otherprohibitpath)) {
+                                unset($prohibitpaths[$otherprohibitpath]);
+                            }
+                        }
+                        $prohibitpaths[$effectivepath] = $roleid;
+                    }
                 }
             }
         }
 
+        // Finally, if a later role had a higher-level prohibit that an earlier role,
+        // there may be more bits we can prune - but don't prune the prohibits!
+        foreach ($pathroleperms as $effectivepath => $roleperms) {
+            if ($roleid = self::any_path_is_above($prohibitpaths, $effectivepath)) {
+                unset($pathroleperms[$effectivepath]);
+                $pathroleperms[$effectivepath][$roleid] = CAP_PROHIBIT;
+            }
+        }
+
         return $pathroleperms;
     }
 
+    /**
+     * Test if a context path $otherpath is the same as, or underneath, $parentpath.
+     *
+     * @param string $parentpath the path of the parent context.
+     * @param string $otherpath the path of another context.
+     * @return bool true if $otherpath is underneath (or equal to) $parentpath.
+     */
+    protected static function path_is_above($parentpath, $otherpath) {
+        return preg_match('~^' . $parentpath . '($|/)~', $otherpath);
+    }
+
+    /**
+     * Test if a context path $otherpath is the same as, or underneath, any of $prohibitpaths.
+     *
+     * @param array $prohibitpaths array keys are context paths.
+     * @param string $otherpath the path of another context.
+     * @return int releavant $roleid if $otherpath is underneath (or equal to)
+     *      any of the $prohibitpaths, 0 otherwise (so, can be used as a bool).
+     */
+    protected static function any_path_is_above($prohibitpaths, $otherpath) {
+        foreach ($prohibitpaths as $prohibitpath => $roleid) {
+            if (self::path_is_above($prohibitpath, $otherpath)) {
+                return $roleid;
+            }
+        }
+        return 0;
+    }
+
     /**
      * Calculates a permission tree based on an array of information about role permissions.
      *
index fe6e112..2c66b58 100644 (file)
@@ -67,36 +67,7 @@ class api {
             registration::require_registration();
         }
 
-        if (extension_loaded('xmlrpc')) {
-            // Use XMLRPC protocol.
-            return self::call_xmlrpc($token, $function, $data);
-        } else {
-            // Use REST.
-            return self::call_rest($token, $function, $data);
-        }
-    }
-
-    /**
-     * Performs REST request to moodle.net (using GET method)
-     *
-     * @param string $token
-     * @param string $function
-     * @param array $data
-     * @return mixed
-     * @throws moodle_exception
-     */
-    protected static function call_xmlrpc($token, $function, array $data) {
-        global $CFG;
-        require_once($CFG->dirroot . "/webservice/xmlrpc/lib.php");
-
-        $serverurl = HUB_MOODLEORGHUBURL . "/local/hub/webservice/webservices.php";
-        $xmlrpcclient = new webservice_xmlrpc_client($serverurl, $token);
-        try {
-            return $xmlrpcclient->call($function, $data);
-        } catch (\Exception $e) {
-            // Function webservice_xmlrpc_client::call() can throw Exception, wrap it into moodle_exception.
-            throw new moodle_exception('errorws', 'hub', '', $e->getMessage());
-        }
+        return self::call_rest($token, $function, $data);
     }
 
     /**
index 77137af..8c01012 100644 (file)
@@ -31,6 +31,30 @@ defined('MOODLE_INTERNAL') || die();
 class gradingform extends base {
 
     public function is_uninstall_allowed() {
-        return false;
+        return true;
+    }
+
+    /**
+     * Pre-uninstall hook.
+     * This is intended for disabling of plugin, some DB table purging, etc.
+     */
+    public function uninstall_cleanup() {
+        global $DB;
+
+        // Find all definitions and templates.
+        $definitions = $DB->get_fieldset_select('grading_definitions', 'id', 'method = ?', [$this->name]);
+        if ($definitions) {
+            // Delete instances and definitions. Deleting instance will not delete grades because they were
+            // already pushed to the module and gradebook.
+            list($sqld, $paramsd) = $DB->get_in_or_equal($definitions);
+            $DB->delete_records_select('grading_instances', 'definitionid ' . $sqld, $paramsd);
+            $DB->delete_records_select('grading_definitions', 'id ' . $sqld, $paramsd);
+        }
+        // Delete templates for this grading method.
+        $DB->delete_records_select('grading_areas', 'component = ? AND activemethod = ?', array('core_grading', $this->name));
+        // Update the remaining grading areas to use simple grading method instead of this grading method.
+        $DB->execute('UPDATE {grading_areas} SET activemethod = NULL WHERE activemethod = ?', [$this->name]);
+
+        parent::uninstall_cleanup();
     }
 }
index d5c3939..b9aa9a3 100644 (file)
@@ -51,8 +51,8 @@ class delete_unconfirmed_users_task extends scheduled_task {
             $cuttime = $timenow - ($CFG->deleteunconfirmed * 3600);
             $rs = $DB->get_recordset_sql ("SELECT *
                                              FROM {user}
-                                            WHERE confirmed = 0 AND firstaccess > 0
-                                                  AND firstaccess < ? AND deleted = 0", array($cuttime));
+                                            WHERE confirmed = 0 AND timecreated > 0
+                                                  AND timecreated < ? AND deleted = 0", array($cuttime));
             foreach ($rs as $user) {
                 delete_user($user); // We MUST delete user properly first.
                 $DB->delete_records('user', array('id' => $user->id)); // This is a bloody hack, but it might work.
index 46adea2..31019f5 100644 (file)
@@ -1837,7 +1837,7 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2017111300.02);
     }
 
-    if ($oldversion < 2017120100.01) {
+    if ($oldversion < 2017121200.00) {
 
         // Define key subscriptionid (foreign) to be added to event.
         $table = new xmldb_table('event');
@@ -1856,7 +1856,7 @@ function xmldb_main_upgrade($oldversion) {
         }
 
         // Main savepoint reached.
-        upgrade_main_savepoint(true, 2017120100.01);
+        upgrade_main_savepoint(true, 2017121200.00);
     }
 
     return true;
index 1b72c00..a16b933 100644 (file)
@@ -2402,21 +2402,25 @@ abstract class moodle_database {
 
         // Enclose the column name by the proper quotes if it's a reserved word.
         $columnname = $this->get_manager()->generator->getEncQuoted($column->name);
+
+        $searchsql = $this->sql_like($columnname, '?');
+        $searchparam = '%'.$this->sql_like_escape($search).'%';
+
         $sql = "UPDATE {".$table."}
                        SET $columnname = REPLACE($columnname, ?, ?)
-                     WHERE $columnname IS NOT NULL";
+                     WHERE $searchsql";
 
         if ($column->meta_type === 'X') {
-            $this->execute($sql, array($search, $replace));
+            $this->execute($sql, array($search, $replace, $searchparam));
 
         } else if ($column->meta_type === 'C') {
             if (core_text::strlen($search) < core_text::strlen($replace)) {
                 $colsize = $column->max_length;
                 $sql = "UPDATE {".$table."}
                        SET $columnname = " . $this->sql_substr("REPLACE(" . $columnname . ", ?, ?)", 1, $colsize) . "
-                     WHERE $columnname IS NOT NULL";
+                     WHERE $searchsql";
             }
-            $this->execute($sql, array($search, $replace));
+            $this->execute($sql, array($search, $replace, $searchparam));
         }
     }
 
index 850fb5f..1d6dec9 100644 (file)
@@ -30,6 +30,8 @@
  *  - bit ops: To provide cross-db bitwise operations to be used by the
  *             sql_bitXXX() helper functions
  *  - one space hacks: One space empty string substitute hacks.
+ *
+ * Moodle will not parse this file correctly if it uses Windows line endings.
  */
 
 CREATE OR REPLACE PACKAGE MOODLELIB AS
index 9e27b4b..a4d16e5 100644 (file)
@@ -1707,6 +1707,25 @@ class core_accesslib_testcase extends advanced_testcase {
         $generator = $this->getDataGenerator();
         $cap = 'moodle/course:view';
 
+        // The structure being created here is this:
+        //
+        // All tests work with the single capability 'moodle/course:view'.
+        //
+        //             ROLE DEF/OVERRIDE                        ROLE ASSIGNS
+        //    Role:  Allow    Prohib    Empty   Def user      u1  u2  u3  u4   u5  u6  u7  u8
+        // System    ALLOW    PROHIBIT                            A   E   A+E
+        //   cat1                       ALLOW
+        //     C1                               (ALLOW)                            P
+        //     C2             ALLOW                                                    E   P
+        //     cat2                     PREVENT
+        //       C3                     ALLOW                                      E
+        //       C4
+        //   Misc.                                                             A
+        //     C5    PREVENT                                                       A
+        //     C6                       PROHIBIT
+        //
+        // Front-page and guest role stuff from the end of this test not included in the diagram.
+
         // Create a role which allows course:view and one that prohibits it, and one neither.
         $allowroleid = $generator->create_role();
         $prohibitroleid = $generator->create_role();
@@ -1720,6 +1739,7 @@ class core_accesslib_testcase extends advanced_testcase {
         $cat2 = $generator->create_category(['parent' => $cat1->id]);
 
         // Create six courses - two in cat1, two in cat2, and two in default category.
+        // Shortnames are used for a sorting test. Otherwise they are not significant.
         $c1 = $generator->create_course(['category' => $cat1->id, 'shortname' => 'Z']);
         $c2 = $generator->create_course(['category' => $cat1->id, 'shortname' => 'Y']);
         $c3 = $generator->create_course(['category' => $cat2->id, 'shortname' => 'X']);
@@ -1741,6 +1761,8 @@ class core_accesslib_testcase extends advanced_testcase {
                 context_course::instance($c6->id)->id);
         assign_capability($cap, CAP_ALLOW, $emptyroleid,
                 context_course::instance($c3->id)->id);
+        assign_capability($cap, CAP_ALLOW, $prohibitroleid,
+                context_course::instance($c2->id)->id);
 
         // User 1 has no roles except default user role.
         $u1 = $generator->create_user();
@@ -1800,6 +1822,22 @@ class core_accesslib_testcase extends advanced_testcase {
         $courses = get_user_capability_course($cap, $u6->id, true, '', 'id');
         $this->assert_course_ids([$c3->id], $courses);
 
+        // User 7 has empty role in C2.
+        $u7 = $generator->create_user();
+        role_assign($emptyroleid, $u7->id, context_course::instance($c2->id)->id);
+
+        // Should get C1 by the default user role override, and C2 by the cat1 level override.
+        $courses = get_user_capability_course($cap, $u7->id, true, '', 'id');
+        $this->assert_course_ids([$c1->id, $c2->id], $courses);
+
+        // User 8 has prohibit role as system context, to verify that prohibits can't be overridden.
+        $u8 = $generator->create_user();
+        role_assign($prohibitroleid, $u8->id, context_course::instance($c2->id)->id);
+
+        // Should get C1 by the default user role override, no other courses because the prohibit cannot be overridden.
+        $courses = get_user_capability_course($cap, $u8->id, true, '', 'id');
+        $this->assert_course_ids([$c1->id], $courses);
+
         // Admin user gets everything....
         $courses = get_user_capability_course($cap, get_admin()->id, true, '', 'id');
         $this->assert_course_ids([SITEID, $c1->id, $c2->id, $c3->id, $c4->id, $c5->id, $c6->id],
index 2b2744a..f178315 100644 (file)
@@ -17,7 +17,7 @@
 /**
  * Unit tests for /lib/gradelib.php.
  *
- * @package   core_grade
+ * @package   core_grades
  * @category  phpunit
  * @copyright 2012 Andrew Davis
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
index 6aec217..fe6a7ec 100644 (file)
@@ -39,6 +39,8 @@ $string['command'] = 'Command:';
 $string['commentcontextmenu'] = 'Comment context menu';
 $string['couldnotsavepage'] = 'Could not save page {$a}';
 $string['currentstamp'] = 'Stamp';
+$string['default'] = 'Enabled by default';
+$string['default_help'] = 'If set, this feedback method will be enabled by default for all new assignments.';
 $string['deleteannotation'] = 'Delete annotation';
 $string['deletecomment'] = 'Delete comment';
 $string['deletefeedback'] = 'Delete feedback PDF';
index c8bc5cf..5da7e4b 100644 (file)
@@ -332,12 +332,11 @@ class assign_feedback_editpdf extends assign_feedback_plugin {
     }
 
     /**
-     * Automatically enable or disable editpdf feedback plugin based on
-     * whether the ghostscript path is set correctly.
+     * Determine if ghostscript is available and working.
      *
      * @return bool
      */
-    public function is_enabled() {
+    public function is_available() {
         if ($this->enabledcache === null) {
             $testpath = assignfeedback_editpdf\pdf::test_gs_path(false);
             $this->enabledcache = ($testpath->status == assignfeedback_editpdf\pdf::GSPATH_OK);
@@ -345,12 +344,12 @@ class assign_feedback_editpdf extends assign_feedback_plugin {
         return $this->enabledcache;
     }
     /**
-     * Automatically hide the setting for the editpdf feedback plugin.
+     * Prevent enabling this plugin if ghostscript is not available.
      *
      * @return bool false
      */
     public function is_configurable() {
-        return false;
+        return $this->is_available();
     }
 
     /**
index 674a21f..64355fd 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+// Enabled by default.
+$settings->add(new admin_setting_configcheckbox('assignfeedback_editpdf/default',
+                   new lang_string('default', 'assignfeedback_editpdf'),
+                   new lang_string('default_help', 'assignfeedback_editpdf'), 1));
+
 // Stamp files setting.
 $name = 'assignfeedback_editpdf/stamps';
 $title = get_string('stamps','assignfeedback_editpdf');
index 8a82199..2a02a85 100644 (file)
@@ -260,7 +260,7 @@ class assign_submission_file extends assign_submission_plugin {
         $groupid = 0;
         // Get the group name as other fields are not transcribed in the logs and this information is important.
         if (empty($submission->userid) && !empty($submission->groupid)) {
-            $groupname = $DB->get_field('groups', 'name', array('id' => $submission->groupid), '*', MUST_EXIST);
+            $groupname = $DB->get_field('groups', 'name', array('id' => $submission->groupid), MUST_EXIST);
             $groupid = $submission->groupid;
         } else {
             $params['relateduserid'] = $submission->userid;
index d75de5a..1a03ee5 100644 (file)
@@ -239,7 +239,7 @@ class assign_submission_onlinetext extends assign_submission_plugin {
         $groupid = 0;
         // Get the group name as other fields are not transcribed in the logs and this information is important.
         if (empty($submission->userid) && !empty($submission->groupid)) {
-            $groupname = $DB->get_field('groups', 'name', array('id' => $submission->groupid), '*', MUST_EXIST);
+            $groupname = $DB->get_field('groups', 'name', array('id' => $submission->groupid), MUST_EXIST);
             $groupid = $submission->groupid;
         } else {
             $params['relateduserid'] = $submission->userid;
index 3e6851f..30db083 100644 (file)
@@ -62,6 +62,7 @@ $string['cannotadd'] = 'Can not add entries!';
 $string['cannotdeletepreset'] = 'Error deleting a preset!';
 $string['cannotoverwritepreset'] = 'Error overwriting preset';
 $string['cannotunziptopreset'] = 'Cannot unzip to the preset directory';
+$string['closebeforeopen'] = 'You have specified an end date before the start date.';
 $string['columns'] = 'columns';
 $string['comment'] = 'Comment';
 $string['commentdeleted'] = 'Comment deleted';
@@ -284,6 +285,7 @@ $string['numberrssarticles'] = 'Entries in the RSS feed';
 $string['numnotapproved'] = 'Pending';
 $string['numrecords'] = '{$a} entries';
 $string['ods'] = '<acronym title="OpenDocument Spreadsheet">ODS</acronym> (OpenOffice)';
+$string['openafterclose'] = 'You have specified an open date after the close date';
 $string['optionaldescription'] = 'Short description (optional)';
 $string['optionalfilename'] = 'Filename (optional)';
 $string['other'] = 'Other';
index 3f46920..c150d23 100644 (file)
@@ -4554,3 +4554,117 @@ function mod_data_get_completion_active_rule_descriptions($cm) {
     }
     return $descriptions;
 }
+
+/**
+ * This function calculates the minimum and maximum cutoff values for the timestart of
+ * the given event.
+ *
+ * It will return an array with two values, the first being the minimum cutoff value and
+ * the second being the maximum cutoff value. Either or both values can be null, which
+ * indicates there is no minimum or maximum, respectively.
+ *
+ * If a cutoff is required then the function must return an array containing the cutoff
+ * timestamp and error string to display to the user if the cutoff value is violated.
+ *
+ * A minimum and maximum cutoff return value will look like:
+ * [
+ *     [1505704373, 'The due date must be after the sbumission start date'],
+ *     [1506741172, 'The due date must be before the cutoff date']
+ * ]
+ *
+ * @param calendar_event $event The calendar event to get the time range for
+ * @param stdClass $instance The module instance to get the range from
+ * @return array
+ */
+function mod_data_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $instance) {
+    $mindate = null;
+    $maxdate = null;
+
+    if ($event->eventtype == DATA_EVENT_TYPE_OPEN) {
+        // The start time of the open event can't be equal to or after the
+        // close time of the database activity.
+        if (!empty($instance->timeavailableto)) {
+            $maxdate = [
+                $instance->timeavailableto,
+                get_string('openafterclose', 'data')
+            ];
+        }
+    } else if ($event->eventtype == DATA_EVENT_TYPE_CLOSE) {
+        // The start time of the close event can't be equal to or earlier than the
+        // open time of the database activity.
+        if (!empty($instance->timeavailablefrom)) {
+            $mindate = [
+                $instance->timeavailablefrom,
+                get_string('closebeforeopen', 'data')
+            ];
+        }
+    }
+
+    return [$mindate, $maxdate];
+}
+
+/**
+ * This function will update the data module according to the
+ * event that has been modified.
+ *
+ * It will set the timeopen or timeclose value of the data instance
+ * according to the type of event provided.
+ *
+ * @throws \moodle_exception
+ * @param \calendar_event $event
+ * @param stdClass $data The module instance to get the range from
+ */
+function mod_data_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $data) {
+    global $DB;
+
+    if (empty($event->instance) || $event->modulename != 'data') {
+        return;
+    }
+
+    if ($event->instance != $data->id) {
+        return;
+    }
+
+    if (!in_array($event->eventtype, [DATA_EVENT_TYPE_OPEN, DATA_EVENT_TYPE_CLOSE])) {
+        return;
+    }
+
+    $courseid = $event->courseid;
+    $modulename = $event->modulename;
+    $instanceid = $event->instance;
+    $modified = false;
+
+    $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid];
+    $context = context_module::instance($coursemodule->id);
+
+    // The user does not have the capability to modify this activity.
+    if (!has_capability('moodle/course:manageactivities', $context)) {
+        return;
+    }
+
+    if ($event->eventtype == DATA_EVENT_TYPE_OPEN) {
+        // If the event is for the data activity opening then we should
+        // set the start time of the data activity to be the new start
+        // time of the event.
+        if ($data->timeavailablefrom != $event->timestart) {
+            $data->timeavailablefrom = $event->timestart;
+            $data->timemodified = time();
+            $modified = true;
+        }
+    } else if ($event->eventtype == DATA_EVENT_TYPE_CLOSE) {
+        // If the event is for the data activity closing then we should
+        // set the end time of the data activity to be the new start
+        // time of the event.
+        if ($data->timeavailableto != $event->timestart) {
+            $data->timeavailableto = $event->timestart;
+            $modified = true;
+        }
+    }
+
+    if ($modified) {
+        $data->timemodified = time();
+        $DB->update_record('data', $data);
+        $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context);
+        $event->trigger();
+    }
+}
index 351879b..69e93f6 100644 (file)
@@ -1431,9 +1431,10 @@ class mod_data_lib_testcase extends advanced_testcase {
      * @param int $courseid
      * @param int $instanceid The data id.
      * @param string $eventtype The event type. eg. DATA_EVENT_TYPE_OPEN.
+     * @param int|null $timestart The start timestamp for the event
      * @return bool|calendar_event
      */
-    private function create_action_event($courseid, $instanceid, $eventtype) {
+    private function create_action_event($courseid, $instanceid, $eventtype, $timestart = null) {
         $event = new stdClass();
         $event->name = 'Calendar event';
         $event->modulename  = 'data';
@@ -1441,7 +1442,11 @@ class mod_data_lib_testcase extends advanced_testcase {
         $event->instance = $instanceid;
         $event->type = CALENDAR_EVENT_TYPE_ACTION;
         $event->eventtype = $eventtype;
-        $event->timestart = time();
+        if ($timestart) {
+            $event->timestart = $timestart;
+        } else {
+            $event->timestart = time();
+        }
 
         return calendar_event::create($event);
     }
@@ -1483,4 +1488,288 @@ class mod_data_lib_testcase extends advanced_testcase {
         $this->assertEquals(mod_data_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions);
         $this->assertEquals(mod_data_get_completion_active_rule_descriptions(new stdClass()), []);
     }
+
+    /**
+     * An unknown event type should not change the data instance.
+     */
+    public function test_mod_data_core_calendar_event_timestart_updated_unknown_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $datagenerator = $generator->get_plugin_generator('mod_data');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $data = $datagenerator->create_instance(['course' => $course->id]);
+        $data->timeavailablefrom = $timeopen;
+        $data->timeavailableto = $timeclose;
+        $DB->update_record('data', $data);
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'data',
+            'instance' => $data->id,
+            'eventtype' => DATA_EVENT_TYPE_OPEN . "SOMETHING ELSE",
+            'timestart' => 1,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        mod_data_core_calendar_event_timestart_updated($event, $data);
+        $data = $DB->get_record('data', ['id' => $data->id]);
+        $this->assertEquals($timeopen, $data->timeavailablefrom);
+        $this->assertEquals($timeclose, $data->timeavailableto);
+    }
+
+    /**
+     * A DATA_EVENT_TYPE_OPEN event should update the timeavailablefrom property of the data activity.
+     */
+    public function test_mod_data_core_calendar_event_timestart_updated_open_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $datagenerator = $generator->get_plugin_generator('mod_data');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $timemodified = 1;
+        $newtimeopen = $timeopen - DAYSECS;
+        $data = $datagenerator->create_instance(['course' => $course->id]);
+        $data->timeavailablefrom = $timeopen;
+        $data->timeavailableto = $timeclose;
+        $data->timemodified = $timemodified;
+        $DB->update_record('data', $data);
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'data',
+            'instance' => $data->id,
+            'eventtype' => DATA_EVENT_TYPE_OPEN,
+            'timestart' => $newtimeopen,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        // Trigger and capture the event when adding a contact.
+        $sink = $this->redirectEvents();
+        mod_data_core_calendar_event_timestart_updated($event, $data);
+        $triggeredevents = $sink->get_events();
+        $moduleupdatedevents = array_filter($triggeredevents, function($e) {
+            return is_a($e, 'core\event\course_module_updated');
+        });
+        $data = $DB->get_record('data', ['id' => $data->id]);
+
+        // Ensure the timeavailablefrom property matches the event timestart.
+        $this->assertEquals($newtimeopen, $data->timeavailablefrom);
+        // Ensure the timeavailableto isn't changed.
+        $this->assertEquals($timeclose, $data->timeavailableto);
+        // Ensure the timemodified property has been changed.
+        $this->assertNotEquals($timemodified, $data->timemodified);
+        // Confirm that a module updated event is fired when the module is changed.
+        $this->assertNotEmpty($moduleupdatedevents);
+    }
+
+    /**
+     * A DATA_EVENT_TYPE_CLOSE event should update the timeavailableto property of the data activity.
+     */
+    public function test_mod_data_core_calendar_event_timestart_updated_close_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $datagenerator = $generator->get_plugin_generator('mod_data');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $timemodified = 1;
+        $newtimeclose = $timeclose + DAYSECS;
+        $data = $datagenerator->create_instance(['course' => $course->id]);
+        $data->timeavailablefrom = $timeopen;
+        $data->timeavailableto = $timeclose;
+        $data->timemodified = $timemodified;
+        $DB->update_record('data', $data);
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'data',
+            'instance' => $data->id,
+            'eventtype' => DATA_EVENT_TYPE_CLOSE,
+            'timestart' => $newtimeclose,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        // Trigger and capture the event when adding a contact.
+        $sink = $this->redirectEvents();
+        mod_data_core_calendar_event_timestart_updated($event, $data);
+        $triggeredevents = $sink->get_events();
+        $moduleupdatedevents = array_filter($triggeredevents, function($e) {
+            return is_a($e, 'core\event\course_module_updated');
+        });
+        $data = $DB->get_record('data', ['id' => $data->id]);
+
+        // Ensure the timeavailableto property matches the event timestart.
+        $this->assertEquals($newtimeclose, $data->timeavailableto);
+        // Ensure the timeavailablefrom isn't changed.
+        $this->assertEquals($timeopen, $data->timeavailablefrom);
+        // Ensure the timemodified property has been changed.
+        $this->assertNotEquals($timemodified, $data->timemodified);
+        // Confirm that a module updated event is fired when the module is changed.
+        $this->assertNotEmpty($moduleupdatedevents);
+    }
+
+    /**
+     * An unknown event type should not have any limits.
+     */
+    public function test_mod_data_core_calendar_get_valid_event_timestart_range_unknown_event() {
+        global $CFG;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $data = new \stdClass();
+        $data->timeavailablefrom = $timeopen;
+        $data->timeavailableto = $timeclose;
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'data',
+            'instance' => 1,
+            'eventtype' => DATA_EVENT_TYPE_OPEN . "SOMETHING ELSE",
+            'timestart' => 1,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        list ($min, $max) = mod_data_core_calendar_get_valid_event_timestart_range($event, $data);
+        $this->assertNull($min);
+        $this->assertNull($max);
+    }
+
+    /**
+     * The open event should be limited by the data's timeclose property, if it's set.
+     */
+    public function test_mod_data_core_calendar_get_valid_event_timestart_range_open_event() {
+        global $CFG;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $data = new \stdClass();
+        $data->timeavailablefrom = $timeopen;
+        $data->timeavailableto = $timeclose;
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'data',
+            'instance' => 1,
+            'eventtype' => DATA_EVENT_TYPE_OPEN,
+            'timestart' => 1,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        // The max limit should be bounded by the timeclose value.
+        list ($min, $max) = mod_data_core_calendar_get_valid_event_timestart_range($event, $data);
+        $this->assertNull($min);
+        $this->assertEquals($timeclose, $max[0]);
+
+        // No timeclose value should result in no upper limit.
+        $data->timeavailableto = 0;
+        list ($min, $max) = mod_data_core_calendar_get_valid_event_timestart_range($event, $data);
+        $this->assertNull($min);
+        $this->assertNull($max);
+    }
+
+    /**
+     * The close event should be limited by the data's timeavailablefrom property, if it's set.
+     */
+    public function test_mod_data_core_calendar_get_valid_event_timestart_range_close_event() {
+        global $CFG;
+
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $data = new \stdClass();
+        $data->timeavailablefrom = $timeopen;
+        $data->timeavailableto = $timeclose;
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'data',
+            'instance' => 1,
+            'eventtype' => DATA_EVENT_TYPE_CLOSE,
+            'timestart' => 1,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        // The max limit should be bounded by the timeclose value.
+        list ($min, $max) = mod_data_core_calendar_get_valid_event_timestart_range($event, $data);
+        $this->assertEquals($timeopen, $min[0]);
+        $this->assertNull($max);
+
+        // No timeavailableto value should result in no upper limit.
+        $data->timeavailablefrom = 0;
+        list ($min, $max) = mod_data_core_calendar_get_valid_event_timestart_range($event, $data);
+        $this->assertNull($min);
+        $this->assertNull($max);
+    }
 }
index 4bc9b93..c425235 100644 (file)
@@ -391,6 +391,7 @@ $string['ongoing_help'] = 'If enabled, each page will display the student\'s cur
 $string['ongoingcustom'] = 'You have earned {$a->score} point(s) out of {$a->currenthigh} point(s) thus far.';
 $string['ongoingnormal'] = 'You have answered {$a->correct} correctly out of {$a->viewed} attempts.';
 $string['onpostperpage'] = 'Only one posting per grade';
+$string['openafterclose'] = 'You have specified an open date after the close date';
 $string['options'] = 'Options';
 $string['or'] = 'OR';
 $string['ordered'] = 'Ordered';
index 96931c5..fb55485 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+// Event types.
+define('LESSON_EVENT_TYPE_OPEN', 'open');
+define('LESSON_EVENT_TYPE_CLOSE', 'close');
+
 /* Do not include any libraries here! */
 
 /**
@@ -1746,3 +1750,117 @@ function mod_lesson_get_completion_active_rule_descriptions($cm) {
     }
     return $descriptions;
 }
+
+/**
+ * This function calculates the minimum and maximum cutoff values for the timestart of
+ * the given event.
+ *
+ * It will return an array with two values, the first being the minimum cutoff value and
+ * the second being the maximum cutoff value. Either or both values can be null, which
+ * indicates there is no minimum or maximum, respectively.
+ *
+ * If a cutoff is required then the function must return an array containing the cutoff
+ * timestamp and error string to display to the user if the cutoff value is violated.
+ *
+ * A minimum and maximum cutoff return value will look like:
+ * [
+ *     [1505704373, 'The due date must be after the start date'],
+ *     [1506741172, 'The due date must be before the cutoff date']
+ * ]
+ *
+ * @param calendar_event $event The calendar event to get the time range for
+ * @param stdClass $instance The module instance to get the range from
+ * @return array
+ */
+function mod_lesson_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $instance) {
+    $mindate = null;
+    $maxdate = null;
+
+    if ($event->eventtype == LESSON_EVENT_TYPE_OPEN) {
+        // The start time of the open event can't be equal to or after the
+        // close time of the lesson activity.
+        if (!empty($instance->deadline)) {
+            $maxdate = [
+                $instance->deadline,
+                get_string('openafterclose', 'lesson')
+            ];
+        }
+    } else if ($event->eventtype == LESSON_EVENT_TYPE_CLOSE) {
+        // The start time of the close event can't be equal to or earlier than the
+        // open time of the lesson activity.
+        if (!empty($instance->available)) {
+            $mindate = [
+                $instance->available,
+                get_string('closebeforeopen', 'lesson')
+            ];
+        }
+    }
+
+    return [$mindate, $maxdate];
+}
+
+/**
+ * This function will update the lesson module according to the
+ * event that has been modified.
+ *
+ * It will set the available or deadline value of the lesson instance
+ * according to the type of event provided.
+ *
+ * @throws \moodle_exception
+ * @param \calendar_event $event
+ * @param stdClass $lesson The module instance to get the range from
+ */
+function mod_lesson_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $lesson) {
+    global $DB;
+
+    if (empty($event->instance) || $event->modulename != 'lesson') {
+        return;
+    }
+
+    if ($event->instance != $lesson->id) {
+        return;
+    }
+
+    if (!in_array($event->eventtype, [LESSON_EVENT_TYPE_OPEN, LESSON_EVENT_TYPE_CLOSE])) {
+        return;
+    }
+
+    $courseid = $event->courseid;
+    $modulename = $event->modulename;
+    $instanceid = $event->instance;
+    $modified = false;
+
+    $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid];
+    $context = context_module::instance($coursemodule->id);
+
+    // The user does not have the capability to modify this activity.
+    if (!has_capability('moodle/course:manageactivities', $context)) {
+        return;
+    }
+
+    if ($event->eventtype == LESSON_EVENT_TYPE_OPEN) {
+        // If the event is for the lesson activity opening then we should
+        // set the start time of the lesson activity to be the new start
+        // time of the event.
+        if ($lesson->available != $event->timestart) {
+            $lesson->available = $event->timestart;
+            $lesson->timemodified = time();
+            $modified = true;
+        }
+    } else if ($event->eventtype == LESSON_EVENT_TYPE_CLOSE) {
+        // If the event is for the lesson activity closing then we should
+        // set the end time of the lesson activity to be the new start
+        // time of the event.
+        if ($lesson->deadline != $event->timestart) {
+            $lesson->deadline = $event->timestart;
+            $modified = true;
+        }
+    }
+
+    if ($modified) {
+        $lesson->timemodified = time();
+        $DB->update_record('lesson', $lesson);
+        $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context);
+        $event->trigger();
+    }
+}
index 5eb0341..d6fcf50 100644 (file)
@@ -61,10 +61,6 @@ define("LESSON_MAX_EVENT_LENGTH", "432000");
 /** Answer format is HTML */
 define("LESSON_ANSWER_HTML", "HTML");
 
-// Event types.
-define('LESSON_EVENT_TYPE_OPEN', 'open');
-define('LESSON_EVENT_TYPE_CLOSE', 'close');
-
 //////////////////////////////////////////////////////////////////////////////////////
 /// Any other lesson functions go here.  Each of them must have a name that
 /// starts with lesson_
@@ -3031,7 +3027,7 @@ class lesson extends lesson_base {
                         $this->add_message(get_string('numberofpagesviewednotice', 'lesson', $a));
                     }
 
-                    if (!$reviewmode && !$this->properties->retake) {
+                    if (!$reviewmode && $this->properties->ongoing) {
                         $this->add_message(get_string("numberofcorrectanswers", "lesson", $gradeinfo->earned), 'notify');
                         if ($this->properties->grade != GRADE_TYPE_NONE) {
                             $a = new stdClass;
index 5cd91a2..a122c0f 100644 (file)
@@ -441,4 +441,286 @@ class mod_lesson_lib_testcase extends advanced_testcase {
         $this->assertEquals(mod_lesson_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions);
         $this->assertEquals(mod_lesson_get_completion_active_rule_descriptions(new stdClass()), []);
     }
+
+    /**
+     * An unknown event type should not change the lesson instance.
+     */
+    public function test_mod_lesson_core_calendar_event_timestart_updated_unknown_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $lessongenerator = $generator->get_plugin_generator('mod_lesson');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $lesson = $lessongenerator->create_instance(['course' => $course->id]);
+        $lesson->available = $timeopen;
+        $lesson->deadline = $timeclose;
+        $DB->update_record('lesson', $lesson);
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'lesson',
+            'instance' => $lesson->id,
+            'eventtype' => LESSON_EVENT_TYPE_OPEN . "SOMETHING ELSE",
+            'timestart' => 1,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        mod_lesson_core_calendar_event_timestart_updated($event, $lesson);
+        $lesson = $DB->get_record('lesson', ['id' => $lesson->id]);
+        $this->assertEquals($timeopen, $lesson->available);
+        $this->assertEquals($timeclose, $lesson->deadline);
+    }
+
+    /**
+     * A LESSON_EVENT_TYPE_OPEN event should update the available property of the lesson activity.
+     */
+    public function test_mod_lesson_core_calendar_event_timestart_updated_open_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $lessongenerator = $generator->get_plugin_generator('mod_lesson');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $timemodified = 1;
+        $newtimeopen = $timeopen - DAYSECS;
+        $lesson = $lessongenerator->create_instance(['course' => $course->id]);
+        $lesson->available = $timeopen;
+        $lesson->deadline = $timeclose;
+        $lesson->timemodified = $timemodified;
+        $DB->update_record('lesson', $lesson);
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'lesson',
+            'instance' => $lesson->id,
+            'eventtype' => LESSON_EVENT_TYPE_OPEN,
+            'timestart' => $newtimeopen,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        // Trigger and capture the event when adding a contact.
+        $sink = $this->redirectEvents();
+        mod_lesson_core_calendar_event_timestart_updated($event, $lesson);
+        $triggeredevents = $sink->get_events();
+        $moduleupdatedevents = array_filter($triggeredevents, function($e) {
+            return is_a($e, 'core\event\course_module_updated');
+        });
+        $lesson = $DB->get_record('lesson', ['id' => $lesson->id]);
+
+        // Ensure the available property matches the event timestart.
+        $this->assertEquals($newtimeopen, $lesson->available);
+
+        // Ensure the deadline isn't changed.
+        $this->assertEquals($timeclose, $lesson->deadline);
+
+        // Ensure the timemodified property has been changed.
+        $this->assertNotEquals($timemodified, $lesson->timemodified);
+
+        // Confirm that a module updated event is fired when the module is changed.
+        $this->assertNotEmpty($moduleupdatedevents);
+    }
+
+    /**
+     * A LESSON_EVENT_TYPE_CLOSE event should update the deadline property of the lesson activity.
+     */
+    public function test_mod_lesson_core_calendar_event_timestart_updated_close_event() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $lessongenerator = $generator->get_plugin_generator('mod_lesson');
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $timemodified = 1;
+        $newtimeclose = $timeclose + DAYSECS;
+        $lesson = $lessongenerator->create_instance(['course' => $course->id]);
+        $lesson->available = $timeopen;
+        $lesson->deadline = $timeclose;
+        $lesson->timemodified = $timemodified;
+        $DB->update_record('lesson', $lesson);
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'lesson',
+            'instance' => $lesson->id,
+            'eventtype' => LESSON_EVENT_TYPE_CLOSE,
+            'timestart' => $newtimeclose,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+        // Trigger and capture the event when adding a contact.
+        $sink = $this->redirectEvents();
+        mod_lesson_core_calendar_event_timestart_updated($event, $lesson);
+        $triggeredevents = $sink->get_events();
+        $moduleupdatedevents = array_filter($triggeredevents, function($e) {
+            return is_a($e, 'core\event\course_module_updated');
+        });
+        $lesson = $DB->get_record('lesson', ['id' => $lesson->id]);
+        // Ensure the deadline property matches the event timestart.
+        $this->assertEquals($newtimeclose, $lesson->deadline);
+        // Ensure the available isn't changed.
+        $this->assertEquals($timeopen, $lesson->available);
+        // Ensure the timemodified property has been changed.
+        $this->assertNotEquals($timemodified, $lesson->timemodified);
+        // Confirm that a module updated event is fired when the module is changed.
+        $this->assertNotEmpty($moduleupdatedevents);
+    }
+
+    /**
+     * An unknown event type should not have any limits.
+     */
+    public function test_mod_lesson_core_calendar_get_valid_event_timestart_range_unknown_event() {
+        global $CFG;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $lesson = new \stdClass();
+        $lesson->available = $timeopen;
+        $lesson->deadline = $timeclose;
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'lesson',
+            'instance' => 1,
+            'eventtype' => LESSON_EVENT_TYPE_OPEN . "SOMETHING ELSE",
+            'timestart' => 1,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        list ($min, $max) = mod_lesson_core_calendar_get_valid_event_timestart_range($event, $lesson);
+        $this->assertNull($min);
+        $this->assertNull($max);
+    }
+
+    /**
+     * The open event should be limited by the lesson's deadline property, if it's set.
+     */
+    public function test_mod_lesson_core_calendar_get_valid_event_timestart_range_open_event() {
+        global $CFG;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $lesson = new \stdClass();
+        $lesson->available = $timeopen;
+        $lesson->deadline = $timeclose;
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'lesson',
+            'instance' => 1,
+            'eventtype' => LESSON_EVENT_TYPE_OPEN,
+            'timestart' => 1,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        // The max limit should be bounded by the timeclose value.
+        list ($min, $max) = mod_lesson_core_calendar_get_valid_event_timestart_range($event, $lesson);
+        $this->assertNull($min);
+        $this->assertEquals($timeclose, $max[0]);
+
+        // No timeclose value should result in no upper limit.
+        $lesson->deadline = 0;
+        list ($min, $max) = mod_lesson_core_calendar_get_valid_event_timestart_range($event, $lesson);
+        $this->assertNull($min);
+        $this->assertNull($max);
+    }
+
+    /**
+     * The close event should be limited by the lesson's available property, if it's set.
+     */
+    public function test_mod_lesson_core_calendar_get_valid_event_timestart_range_close_event() {
+        global $CFG;
+        require_once($CFG->dirroot . "/calendar/lib.php");
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $timeopen = time();
+        $timeclose = $timeopen + DAYSECS;
+        $lesson = new \stdClass();
+        $lesson->available = $timeopen;
+        $lesson->deadline = $timeclose;
+
+        // Create a valid event.
+        $event = new \calendar_event([
+            'name' => 'Test event',
+            'description' => '',
+            'format' => 1,
+            'courseid' => $course->id,
+            'groupid' => 0,
+            'userid' => 2,
+            'modulename' => 'lesson',
+            'instance' => 1,
+            'eventtype' => LESSON_EVENT_TYPE_CLOSE,
+            'timestart' => 1,
+            'timeduration' => 86400,
+            'visible' => 1
+        ]);
+
+        // The max limit should be bounded by the timeclose value.
+        list ($min, $max) = mod_lesson_core_calendar_get_valid_event_timestart_range($event, $lesson);
+        $this->assertEquals($timeopen, $min[0]);
+        $this->assertNull($max);
+
+        // No deadline value should result in no upper limit.
+        $lesson->available = 0;
+        list ($min, $max) = mod_lesson_core_calendar_get_valid_event_timestart_range($event, $lesson);
+        $this->assertNull($min);
+        $this->assertNull($max);
+    }
 }
index b8ff44b..93f2287 100644 (file)
@@ -43,20 +43,8 @@ $event->trigger();
 
 // Print the header.
 $strquizzes = get_string("modulenameplural", "quiz");
-$streditquestions = '';
-$editqcontexts = new question_edit_contexts($coursecontext);
-if ($editqcontexts->have_one_edit_tab_cap('questions')) {
-    $streditquestions =
-            "<form target=\"_parent\" method=\"get\" action=\"$CFG->wwwroot/question/edit.php\">
-               <div>
-               <input type=\"hidden\" name=\"courseid\" value=\"$course->id\" />
-               <input type=\"submit\" value=\"".get_string("editquestions", "quiz")."\" />
-               </div>
-             </form>";
-}
 $PAGE->navbar->add($strquizzes);
 $PAGE->set_title($strquizzes);
-$PAGE->set_button($streditquestions);
 $PAGE->set_heading($course->fullname);
 echo $OUTPUT->header();
 echo $OUTPUT->heading($strquizzes, 2);
index 998a568..9f51c6d 100644 (file)
@@ -46,7 +46,7 @@ class qtype_multianswer extends question_type {
 
         // Get relevant data indexed by positionkey from the multianswers table.
         $sequence = $DB->get_field('question_multianswer', 'sequence',
-                array('question' => $question->id), '*', MUST_EXIST);
+                array('question' => $question->id), MUST_EXIST);
 
         $wrappedquestions = $DB->get_records_list('question', 'id',
                 explode(',', $sequence), 'id ASC');
index b6d209b..206819d 100644 (file)
@@ -96,7 +96,7 @@ class behat_search extends behat_base {
 
             // Find the specified activity.
             $idnumber = $input['idnumber'];
-            $cmid = $DB->get_field('course_modules', 'id', ['idnumber' => $idnumber], '*', IGNORE_MISSING);
+            $cmid = $DB->get_field('course_modules', 'id', ['idnumber' => $idnumber], IGNORE_MISSING);
             if (!$cmid) {
                 throw new Exception('Cannot find activity with idnumber: ' . $idnumber);
             }
index 7c1976b..7f40642 100644 (file)
@@ -237,6 +237,12 @@ div#dock {
 .path-mod-lesson .invisiblefieldset.fieldsetfix {
     display: block;
 }
+.path-mod-lesson .answeroption .checkbox label p {
+    display: inline;
+}
+#page-mod-lesson-view .branchbuttoncontainer .singlebutton button[type="submit"] {
+    white-space: normal;
+}
 
 .path-mod-wiki .wiki_headingtitle,
 .path-mod-wiki .midpad,
index 796edf1..7a7a6b5 100644 (file)
@@ -400,6 +400,10 @@ div#dock {
 .path-mod-lesson .answeroptiongroup .felement label p:last-child {
     margin-bottom: 0;
 }
+.path-mod-lesson .answeroption .fradio label p,
+.path-mod-lesson .answeroption .fcheckbox label p {
+    display: inline;
+}
 
 /**
  * Style for view.php
@@ -407,6 +411,9 @@ div#dock {
 #page-mod-lesson-view .password-form .submitbutton {
     display: inline;
 }
+#page-mod-lesson-view .branchbuttoncontainer .singlebutton input[type="submit"] {
+    white-space: normal;
+}
 .path-mod-lesson .reviewessay {
     width: 40%;
     border: 1px solid #ddd;
index 940f2cc..2abeb79 100644 (file)
@@ -17571,12 +17571,19 @@ div#dock {
 .path-mod-lesson .answeroptiongroup .felement label p:last-child {
   margin-bottom: 0;
 }
+.path-mod-lesson .answeroption .fradio label p,
+.path-mod-lesson .answeroption .fcheckbox label p {
+  display: inline;
+}
 /**
  * Style for view.php
  **/
 #page-mod-lesson-view .password-form .submitbutton {
   display: inline;
 }
+#page-mod-lesson-view .branchbuttoncontainer .singlebutton input[type="submit"] {
+  white-space: normal;
+}
 .path-mod-lesson .reviewessay {
   width: 40%;
   border: 1px solid #ddd;
index c8d3af3..52dfcbd 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2017120800.01;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2017121200.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.