Merge branch 'MDL-59382-master-4' of git://github.com/ryanwyllie/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Thu, 3 Aug 2017 01:31:44 +0000 (09:31 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Thu, 3 Aug 2017 01:31:44 +0000 (09:31 +0800)
209 files changed:
admin/registration/forms.php
admin/registration/index.php
admin/registration/register.php
admin/renderer.php
admin/search.php
admin/settings/top.php
admin/tests/behat/manage_tokens.feature [new file with mode: 0644]
admin/tool/analytics/classes/output/models_list.php
admin/tool/analytics/model.php
admin/tool/behat/renderer.php
admin/tool/behat/tests/behat/data_generators.feature
admin/tool/monitor/tests/behat/rule.feature
admin/webservice/tokens.php
analytics/classes/model.php
auth/ldap/auth.php
auth/ldap/classes/task/sync_roles.php [new file with mode: 0644]
auth/ldap/db/tasks.php
auth/ldap/db/upgrade.php
auth/ldap/lang/en/auth_ldap.php
auth/ldap/lang/en/deprecated.txt [new file with mode: 0644]
auth/ldap/locallib.php [new file with mode: 0644]
auth/ldap/settings.php
auth/ldap/upgrade.txt
auth/ldap/version.php
availability/condition/completion/classes/frontend.php
availability/condition/completion/lang/en/availability_completion.php
availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-debug.js
availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-min.js
availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form.js
availability/condition/completion/yui/src/form/js/form.js
availability/condition/grade/tests/behat/availability_grade.feature
availability/tests/behat/edit_availability.feature
backup/moodle2/restore_stepslib.php
backup/moodle2/tests/restore_stepslib_date_test.php [new file with mode: 0644]
backup/util/plan/restore_step.class.php
backup/util/ui/tests/behat/restore_moodle2_courses.feature
blocks/activity_modules/tests/behat/block_activity_modules.feature
blocks/activity_results/tests/behat/addunsupportedactivity.feature
blocks/blog_recent/tests/behat/block_blog_recent_course.feature
blocks/calendar_month/tests/behat/block_calendar_month.feature
blocks/community/forms.php
blocks/completionstatus/tests/behat/block_completionstatus_activity_completion.feature
completion/tests/behat/bulk_edit_activity_completion.feature
completion/tests/behat/default_activity_completion.feature
course/classes/output/activity_navigation.php [new file with mode: 0644]
course/lib.php
course/modedit.php
course/renderer.php
course/templates/activity_navigation.mustache [new file with mode: 0644]
course/tests/behat/navigate_course_list.feature
course/tests/behat/paged_course_navigation.feature
enrol/cohort/lib.php
enrol/database/lib.php
enrol/flatfile/lib.php
enrol/locallib.php
enrol/lti/lib.php
enrol/manual/lib.php
enrol/meta/lib.php
enrol/meta/tests/behat/enrol_meta.feature
enrol/paypal/lib.php
enrol/self/lib.php
enrol/tests/behat/enrol_user.feature
grade/tests/behat/grade_minmax.feature
lang/en/admin.php
lang/en/deprecated.txt
lang/en/form.php
lang/en/hub.php
lang/en/moodle.php
lang/en/role.php
lang/en/webservice.php
lib/adminlib.php
lib/amd/build/form-autocomplete.min.js
lib/amd/src/form-autocomplete.js
lib/blocklib.php
lib/classes/oauth2/api.php
lib/db/access.php
lib/db/install.xml
lib/db/upgrade.php
lib/ddl/mysql_sql_generator.php
lib/dml/mariadb_native_moodle_database.php
lib/dml/mysqli_native_moodle_database.php
lib/enrollib.php
lib/form/filemanager.php
lib/form/filepicker.php
lib/moodlelib.php
lib/oauthlib.php
lib/outputrenderers.php
lib/phpunit/classes/restore_date_testcase.php [new file with mode: 0644]
lib/tests/behat/alpha_chooser.feature
lib/upgrade.txt
media/player/youtube/classes/plugin.php
media/player/youtube/tests/player_test.php
message/output/airnotifier/lang/en/message_airnotifier.php
message/output/airnotifier/requestaccesskey.php
mod/assign/backup/moodle2/restore_assign_stepslib.php
mod/assign/locallib.php
mod/assign/submission/file/lang/en/assignsubmission_file.php
mod/assign/submission/file/lang/en/deprecated.txt [new file with mode: 0644]
mod/assign/submission/file/locallib.php
mod/assign/submission/file/settings.php
mod/assign/submission/file/tests/behat/file_type_restriction.feature
mod/assign/submission/file/tests/locallib_test.php
mod/assign/tests/restore_date_test.php [new file with mode: 0644]
mod/book/backup/moodle2/restore_book_stepslib.php
mod/book/lib.php
mod/book/tests/behat/edit_tags.feature
mod/chat/backup/moodle2/restore_chat_stepslib.php
mod/chat/lib.php
mod/chat/tests/restore_date_test.php [new file with mode: 0644]
mod/choice/backup/moodle2/restore_choice_stepslib.php
mod/choice/lib.php
mod/choice/tests/restore_date_test.php [new file with mode: 0644]
mod/data/backup/moodle2/restore_data_stepslib.php
mod/data/classes/external.php
mod/data/classes/external/content_exporter.php
mod/data/lib.php
mod/data/locallib.php
mod/data/tests/behat/completion_condition_entries.feature
mod/data/tests/externallib_test.php
mod/data/tests/restore_date_test.php [new file with mode: 0644]
mod/data/upgrade.txt
mod/feedback/backup/moodle1/lib.php
mod/feedback/backup/moodle2/restore_feedback_stepslib.php
mod/feedback/lib.php
mod/feedback/tests/behat/anonymous.feature
mod/feedback/tests/behat/coursemapping.feature
mod/feedback/tests/behat/multipleattempt.feature
mod/feedback/tests/restore_date_test.php [new file with mode: 0644]
mod/folder/backup/moodle2/restore_folder_stepslib.php
mod/folder/lib.php
mod/folder/tests/restore_date_test.php [new file with mode: 0644]
mod/forum/backup/moodle2/restore_forum_stepslib.php
mod/forum/lib.php
mod/forum/tests/behat/posts_ordering_blog.feature
mod/forum/tests/restore_date_test.php [new file with mode: 0644]
mod/glossary/backup/moodle2/restore_glossary_stepslib.php
mod/glossary/lib.php
mod/glossary/tests/behat/edit_tags.feature
mod/glossary/tests/restore_date_test.php [new file with mode: 0644]
mod/imscp/backup/moodle2/restore_imscp_stepslib.php
mod/imscp/lib.php
mod/imscp/tests/restore_date_test.php [new file with mode: 0644]
mod/label/backup/moodle2/restore_label_stepslib.php
mod/label/lib.php
mod/lesson/backup/moodle2/restore_lesson_stepslib.php
mod/lesson/lib.php
mod/lesson/tests/behat/duplicate_lesson_page.feature
mod/lesson/tests/behat/import_fillintheblank_question.feature
mod/lesson/tests/behat/import_images.feature
mod/lesson/tests/behat/questions_images.feature
mod/lesson/tests/restore_date_test.php [new file with mode: 0644]
mod/lti/backup/moodle2/restore_lti_stepslib.php
mod/page/backup/moodle2/restore_page_stepslib.php
mod/page/lib.php
mod/quiz/backup/moodle2/restore_quiz_stepslib.php
mod/quiz/lib.php
mod/quiz/mod_form.php
mod/quiz/tests/behat/completion_condition_attempts_used.feature
mod/quiz/tests/behat/completion_condition_passing_grade.feature
mod/quiz/tests/restore_date_test.php [new file with mode: 0644]
mod/resource/backup/moodle2/restore_resource_stepslib.php
mod/resource/lib.php
mod/resource/tests/restore_date_test.php [new file with mode: 0644]
mod/resource/view.php
mod/scorm/backup/moodle2/restore_scorm_stepslib.php
mod/scorm/lib.php
mod/scorm/tests/restore_date_test.php [new file with mode: 0644]
mod/survey/backup/moodle2/restore_survey_stepslib.php
mod/survey/lib.php
mod/survey/tests/restore_date_test.php [new file with mode: 0644]
mod/upgrade.txt
mod/url/backup/moodle2/restore_url_stepslib.php
mod/url/lib.php
mod/url/view.php
mod/wiki/backup/moodle2/restore_wiki_stepslib.php
mod/wiki/lib.php
mod/wiki/tests/restore_date_test.php [new file with mode: 0644]
mod/workshop/backup/moodle2/restore_workshop_stepslib.php
mod/workshop/lib.php
mod/workshop/tests/restore_date_test.php [new file with mode: 0644]
theme/boost/scss/moodle/search.scss
theme/boost/templates/columns1.mustache
theme/boost/templates/columns2.mustache
theme/bootstrapbase/layout/columns1.php
theme/bootstrapbase/layout/columns3.php
theme/bootstrapbase/less/moodle/modules.less
theme/bootstrapbase/style/moodle.css
theme/clean/layout/columns1.php
theme/clean/layout/columns3.php
theme/upgrade.txt
user/action_redir.php
user/amd/build/unified_filter.min.js [new file with mode: 0644]
user/amd/build/unified_filter_datasource.min.js [new file with mode: 0644]
user/amd/src/unified_filter.js [new file with mode: 0644]
user/amd/src/unified_filter_datasource.js [new file with mode: 0644]
user/classes/output/unified_filter.php [new file with mode: 0644]
user/classes/participants_table.php
user/index.php
user/lib.php
user/renderer.php
user/templates/unified_filter.mustache [new file with mode: 0644]
user/tests/behat/bulk_editenrolment.feature [new file with mode: 0644]
user/tests/behat/course_preference.feature
user/tests/behat/filter_participants.feature [new file with mode: 0644]
user/tests/behat/set_default_homepage.feature
user/tests/userlib_test.php
version.php
webservice/classes/token_table.php [new file with mode: 0644]
webservice/lib.php

index 8142999..4cb0e40 100644 (file)
@@ -128,7 +128,7 @@ class hub_selector_form extends moodleform {
 
         //remove moodle.org from the hub list
         foreach ($hubs as $key => $hub) {
-            if ($hub['url'] == HUB_MOODLEORGHUBURL) {
+            if ($hub['url'] == HUB_MOODLEORGHUBURL || $hub['url'] == HUB_OLDMOODLEORGHUBURL) {
                 unset($hubs[$key]);
             }
         }
index 8b33014..b9aeb18 100644 (file)
@@ -181,13 +181,8 @@ if (empty($cancel) and $unregistration and !$confirm) {
     echo $OUTPUT->header();
 
     //check if the site is registered on Moodle.org and display a message about registering on MOOCH
-    $registered = $DB->count_records('registration_hubs', array('huburl' => HUB_MOODLEORGHUBURL, 'confirmed' => 1));
-    if (empty($registered)) {
-        $warningmsg = get_string('registermoochtips', 'hub');
-        $warningmsg .= $renderer->single_button(new moodle_url('register.php', array('huburl' => HUB_MOODLEORGHUBURL
-                    , 'hubname' => 'Moodle.org')), get_string('register', 'admin'));
-        echo $renderer->box($warningmsg, 'buttons mdl-align generalbox adminwarning');
-    }
+    $adminrenderer = $PAGE->get_renderer('core', 'admin');
+    echo $adminrenderer->warn_if_not_registered();
 
     //do not check sesskey if confirm = false because this script is linked into email message
     if (!empty($errormessage)) {
index 895ae5c..43d2e42 100644 (file)
@@ -189,17 +189,19 @@ if (!empty($error)) {
 
 // Some Moodle.org registration explanation.
 if ($huburl == HUB_MOODLEORGHUBURL) {
+    $notificationtype = \core\output\notification::NOTIFY_ERROR;
     if (!empty($registeredhub->token)) {
         if ($registeredhub->timemodified == 0) {
             $registrationmessage = get_string('pleaserefreshregistrationunknown', 'admin');
         } else {
             $lastupdated = userdate($registeredhub->timemodified, get_string('strftimedate', 'langconfig'));
             $registrationmessage = get_string('pleaserefreshregistration', 'admin', $lastupdated);
+            $notificationtype = \core\output\notification::NOTIFY_INFO;
         }
     } else {
         $registrationmessage = get_string('registrationwarning', 'admin');
     }
-    echo $OUTPUT->notification($registrationmessage);
+    echo $OUTPUT->notification($registrationmessage, $notificationtype);
 
     echo $OUTPUT->heading(get_string('registerwithmoodleorg', 'admin'));
     $renderer = $PAGE->get_renderer('core', 'register');
index 2d6c362..63cc1ad 100644 (file)
@@ -782,17 +782,36 @@ class core_admin_renderer extends plugin_renderer_base {
 
         if (!$registered) {
 
-            $registerbutton = $this->single_button(new moodle_url('/admin/registration/register.php',
-                    array('huburl' =>  HUB_MOODLEORGHUBURL, 'hubname' => 'Moodle.org')),
+            if (has_capability('moodle/site:config', context_system::instance())) {
+                $registerbutton = $this->single_button(new moodle_url('/admin/registration/register.php',
+                    array('huburl' =>  HUB_MOODLEORGHUBURL, 'hubname' => 'Moodle.net')),
                     get_string('register', 'admin'));
+                $str = 'registrationwarning';
+            } else {
+                $registerbutton = '';
+                $str = 'registrationwarningcontactadmin';
+            }
 
-            return $this->warning( get_string('registrationwarning', 'admin')
-                    . '&nbsp;' . $this->help_icon('registration', 'admin') . $registerbutton );
+            return $this->warning( get_string($str, 'admin')
+                    . '&nbsp;' . $this->help_icon('registration', 'admin') . $registerbutton ,
+                'error alert alert-danger');
         }
 
         return '';
     }
 
+    /**
+     * Return an admin page warning if site is not registered with moodle.org
+     *
+     * @return string
+     */
+    public function warn_if_not_registered() {
+        global $CFG;
+        require_once($CFG->dirroot . '/' . $CFG->admin . '/registration/lib.php');
+        $registrationmanager = new registration_manager();
+        return $this->registration_warning($registrationmanager->get_registeredhub(HUB_MOODLEORGHUBURL) ? true : false);
+    }
+
     /**
      * Helper method to render the information about the available Moodle update
      *
index 7dd8e2c..fb38431 100644 (file)
@@ -38,6 +38,12 @@ if ($data = data_submitted() and confirm_sesskey() and isset($data->action) and
 // to modify them
 echo $OUTPUT->header($focus);
 
+// Display a warning if site is not registered.
+if (empty($query)) {
+    $adminrenderer = $PAGE->get_renderer('core', 'admin');
+    echo $adminrenderer->warn_if_not_registered();
+}
+
 echo $OUTPUT->heading(get_string('administrationsite'));
 
 if ($errormsg !== '') {
index 5d4495c..c78c909 100644 (file)
@@ -11,7 +11,7 @@ $hassiteconfig = has_capability('moodle/site:config', $systemcontext);
 $ADMIN->add('root', new admin_externalpage('adminnotifications', new lang_string('notifications'), "$CFG->wwwroot/$CFG->admin/index.php"));
 
 $ADMIN->add('root', new admin_externalpage('registrationmoodleorg', new lang_string('registration', 'admin'),
-        "$CFG->wwwroot/$CFG->admin/registration/register.php?huburl=" . HUB_MOODLEORGHUBURL . "&hubname=Moodle.org&sesskey=" . sesskey()));
+        "$CFG->wwwroot/$CFG->admin/registration/register.php?huburl=" . HUB_MOODLEORGHUBURL . "&hubname=Moodle.net&sesskey=" . sesskey()));
 $ADMIN->add('root', new admin_externalpage('registrationhub', new lang_string('registerwith', 'hub'),
         "$CFG->wwwroot/$CFG->admin/registration/register.php", 'moodle/site:config', true));
 $ADMIN->add('root', new admin_externalpage('registrationhubs', new lang_string('hubs', 'admin'),
diff --git a/admin/tests/behat/manage_tokens.feature b/admin/tests/behat/manage_tokens.feature
new file mode 100644 (file)
index 0000000..d30e230
--- /dev/null
@@ -0,0 +1,26 @@
+@core @core_admin
+Feature: Manage tokens
+  In order to manage webservice usage
+  As an admin
+  I need to be able to create and delete tokens
+
+  Background:
+    Given the following "users" exist:
+    | username  | password  | firstname | lastname |
+    | testuser  | testuser  | Joe | Bloggs |
+    | testuser2 | testuser2 | TestFirstname | TestLastname |
+    And I log in as "admin"
+    And I am on site homepage
+
+  @javascript
+  Scenario: Add & delete a token
+    Given I navigate to "Plugins > Web services > Manage tokens" in site administration
+    And I follow "Add"
+    And I set the field "User" to "Joe Bloggs"
+    And I set the field "IP restriction" to "127.0.0.1"
+    When I press "Save changes"
+    Then I should see "Joe Bloggs"
+    And I should see "127.0.0.1"
+    And I follow "Delete"
+    And I press "Delete"
+    And I should not see "Joe Bloggs"
index 181b48d..36f1feb 100644 (file)
@@ -117,6 +117,20 @@ class models_list implements \renderable, \templatable {
                 $actionsmenu->add($icon);
             }
 
+            // 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';
+            }
+            $url = new \moodle_url('model.php', array('action' => $action, 'id' => $model->get_id()));
+            $icon = new \action_menu_link_secondary($url, new \pix_icon($icontype, $text), $text);
+            $actionsmenu->add($icon);
+
             // Evaluate machine-learning-based models.
             if ($model->get_indicators() && !$model->is_static()) {
                 $url = new \moodle_url('model.php', array('action' => 'evaluate', 'id' => $model->get_id()));
index 2c64179..9ef3c3d 100644 (file)
@@ -51,6 +51,13 @@ switch ($action) {
     case 'log':
         $title = get_string('viewlog', 'tool_analytics');
         break;
+    case 'enable':
+        $title = get_string('enable');
+        break;
+    case 'disable':
+        $title = get_string('disable');
+        break;
+
     default:
         throw new moodle_exception('errorunknownaction', 'analytics');
 }
@@ -63,6 +70,14 @@ $PAGE->set_heading($title);
 
 switch ($action) {
 
+    case 'enable':
+        $model->enable();
+        redirect(new \moodle_url('/admin/tool/analytics/index.php'));
+
+    case 'disable':
+        $model->update(0, false, false);
+        redirect(new \moodle_url('/admin/tool/analytics/index.php'));
+
     case 'edit':
 
         if ($model->is_static()) {
index 8667fa9..9bcda1f 100644 (file)
@@ -24,9 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-global $CFG;
-require_once($CFG->libdir . '/behat/classes/behat_selectors.php');
-
 /**
  * Renderer for behat tool web features
  *
@@ -44,6 +41,8 @@ class tool_behat_renderer extends plugin_renderer_base {
      * @return string HTML code
      */
     public function render_stepsdefinitions($stepsdefinitions, $form) {
+        global $CFG;
+        require_once($CFG->libdir . '/behat/classes/behat_selectors.php');
 
         $html = $this->generic_info();
 
index d8a325b..0e62104 100644 (file)
@@ -221,7 +221,7 @@ Feature: Set up contextual data for tests
     And I should see "Test workshop name"
     And I follow "Test assignment name"
     And I should see "Test assignment description"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I follow "Test assignment name with scale"
     And I follow "Edit settings"
     And the field "Type" matches value "Scale"
@@ -312,7 +312,6 @@ Feature: Set up contextual data for tests
       | fullname | course | gradecategory |
       | Grade sub category 2 | C1 | Grade category 1 |
     When I log in as "admin"
-    And I am on course index
     And I am on "Course 1" course homepage
     And I navigate to "View > Grader report" in the course gradebook
     Then I should see "Grade category 1"
index a38a75d..de818df 100644 (file)
@@ -17,7 +17,6 @@ Feature: tool_monitor_rule
     And I log in as "admin"
     And I navigate to "Event monitoring rules" node in "Site administration > Reports"
     And I click on "Enable" "link"
-    And I am on site homepage
     And I am on "Course 1" course homepage
     And I navigate to "Event monitoring rules" node in "Course administration > Reports"
     And I press "Add a new rule"
index 60ea271..f15f7c2 100644 (file)
@@ -101,7 +101,11 @@ switch ($action) {
         break;
 
     case 'delete':
-        $token = $webservicemanager->get_created_by_user_ws_token($USER->id, $tokenid);
+        $token = $webservicemanager->get_token_by_id_with_details($tokenid);
+
+        if ($token->creatorid != $USER->id) {
+            require_capability("moodle/webservice:managealltokens", context_system::instance());
+        }
 
         //Delete the token
         if ($confirm and confirm_sesskey()) {
index 79352cb..985d655 100644 (file)
@@ -395,20 +395,30 @@ class model {
      * Updates the model.
      *
      * @param int|bool $enabled
-     * @param \core_analytics\local\indicator\base[] $indicators
-     * @param string $timesplittingid
+     * @param \core_analytics\local\indicator\base[]|false $indicators False to respect current indicators
+     * @param string|false $timesplittingid False to respect current time splitting method
      * @return void
      */
-    public function update($enabled, $indicators, $timesplittingid = '') {
+    public function update($enabled, $indicators = false, $timesplittingid = '') {
         global $USER, $DB;
 
         \core_analytics\manager::check_can_manage_models();
 
         $now = time();
 
-        $indicatorclasses = self::indicator_classes($indicators);
+        if ($indicators !== false) {
+            $indicatorclasses = self::indicator_classes($indicators);
+            $indicatorsstr = json_encode($indicatorclasses);
+        } else {
+            // Respect current value.
+            $indicatorsstr = $this->model->indicators;
+        }
+
+        if ($timesplittingid === false) {
+            // Respect current value.
+            $timesplittingid = $this->model->timesplitting;
+        }
 
-        $indicatorsstr = json_encode($indicatorclasses);
         if ($this->model->timesplitting !== $timesplittingid ||
                 $this->model->indicators !== $indicatorsstr) {
             // We update the version of the model so different time splittings are not mixed up.
@@ -900,7 +910,7 @@ class model {
     /**
      * Enabled the model using the provided time splitting method.
      *
-     * @param string $timesplittingid
+     * @param string|false $timesplittingid False to respect the current time splitting method.
      * @return void
      */
     public function enable($timesplittingid = false) {
index 7f59282..dcfc2c5 100644 (file)
@@ -79,6 +79,7 @@ if (!defined('LDAP_OPT_DIAGNOSTIC_MESSAGE')) {
 require_once($CFG->libdir.'/authlib.php');
 require_once($CFG->libdir.'/ldaplib.php');
 require_once($CFG->dirroot.'/user/lib.php');
+require_once($CFG->dirroot.'/auth/ldap/locallib.php');
 
 /**
  * LDAP authentication plugin.
@@ -865,14 +866,6 @@ class auth_plugin_ldap extends auth_plugin_base {
             if (!empty($users)) {
                 print_string('userentriestoupdate', 'auth_ldap', count($users));
 
-                $sitecontext = context_system::instance();
-                if (!empty($this->config->creators) and !empty($this->config->memberattribute)
-                  and $roles = get_archetype_roles('coursecreator')) {
-                    $creatorrole = array_shift($roles);      // We can only use one, let's use the first one
-                } else {
-                    $creatorrole = false;
-                }
-
                 $transaction = $DB->start_delegated_transaction();
                 $xcount = 0;
                 $maxxcount = 100;
@@ -885,14 +878,8 @@ class auth_plugin_ldap extends auth_plugin_base {
                     echo "\n";
                     $xcount++;
 
-                    // Update course creators if needed
-                    if ($creatorrole !== false) {
-                        if ($this->iscreator($user->username)) {
-                            role_assign($creatorrole->id, $user->id, $sitecontext->id, $this->roleauth);
-                        } else {
-                            role_unassign($creatorrole->id, $user->id, $sitecontext->id, $this->roleauth);
-                        }
-                    }
+                    // Update system roles, if needed.
+                    $this->sync_roles($user);
                 }
                 $transaction->allow_commit();
                 unset($users); // free mem
@@ -914,14 +901,6 @@ class auth_plugin_ldap extends auth_plugin_base {
         if (!empty($add_users)) {
             print_string('userentriestoadd', 'auth_ldap', count($add_users));
 
-            $sitecontext = context_system::instance();
-            if (!empty($this->config->creators) and !empty($this->config->memberattribute)
-              and $roles = get_archetype_roles('coursecreator')) {
-                $creatorrole = array_shift($roles);      // We can only use one, let's use the first one
-            } else {
-                $creatorrole = false;
-            }
-
             $transaction = $DB->start_delegated_transaction();
             foreach ($add_users as $user) {
                 $user = $this->get_userinfo_asobj($user->username);
@@ -955,10 +934,8 @@ class auth_plugin_ldap extends auth_plugin_base {
                     set_user_preference('auth_forcepasswordchange', 1, $id);
                 }
 
-                // Add course creators if needed
-                if ($creatorrole !== false and $this->iscreator($user->username)) {
-                    role_assign($creatorrole->id, $id, $sitecontext->id, $this->roleauth);
-                }
+                // Add roles if needed.
+                $this->sync_roles($user);
 
             }
             $transaction->allow_commit();
@@ -1102,8 +1079,12 @@ class auth_plugin_ldap extends auth_plugin_base {
      *
      * @param mixed $username    username (without system magic quotes)
      * @return mixed result      null if course creators is not configured, boolean otherwise.
+     *
+     * @deprecated since Moodle 3.4 MDL-30634 - please do not use this function any more.
      */
     function iscreator($username) {
+        debugging('iscreator() is deprecated. Please use auth_plugin_ldap::is_role() instead.', DEBUG_DEVELOPER);
+
         if (empty($this->config->creators) or empty($this->config->memberattribute)) {
             return null;
         }
@@ -1128,6 +1109,40 @@ class auth_plugin_ldap extends auth_plugin_base {
         return $creator;
     }
 
+    /**
+     * Check if user has LDAP group membership.
+     *
+     * Returns true if user should be assigned role.
+     *
+     * @param mixed $username username (without system magic quotes).
+     * @param array $role Array of role's shortname, localname, and settingname for the config value.
+     * @return mixed result null if role/LDAP context is not configured, boolean otherwise.
+     */
+    private function is_role($username, $role) {
+        if (empty($this->config->{$role['settingname']}) or empty($this->config->memberattribute)) {
+            return null;
+        }
+
+        $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding);
+
+        $ldapconnection = $this->ldap_connect();
+
+        if ($this->config->memberattribute_isdn) {
+            if (!($userid = $this->ldap_find_userdn($ldapconnection, $extusername))) {
+                return false;
+            }
+        } else {
+            $userid = $extusername;
+        }
+
+        $groupdns = explode(';', $this->config->{$role['settingname']});
+        $isrole = ldap_isgroupmember($ldapconnection, $userid, $groupdns, $this->config->memberattribute);
+
+        $this->ldap_close();
+
+        return $isrole;
+    }
+
     /**
      * Called when the user record is updated.
      *
@@ -1792,25 +1807,29 @@ class auth_plugin_ldap extends auth_plugin_base {
     }
 
     /**
-     * Sync roles for this user
+     * Sync roles for this user.
      *
-     * @param $user object user object (without system magic quotes)
+     * @param object $user The user to sync (without system magic quotes).
      */
     function sync_roles($user) {
-        $iscreator = $this->iscreator($user->username);
-        if ($iscreator === null) {
-            return; // Nothing to sync - creators not configured
-        }
+        global $DB;
 
-        if ($roles = get_archetype_roles('coursecreator')) {
-            $creatorrole = array_shift($roles);      // We can only use one, let's use the first one
-            $systemcontext = context_system::instance();
+        $roles = get_ldap_assignable_role_names(2); // Admin user.
 
-            if ($iscreator) { // Following calls will not create duplicates
-                role_assign($creatorrole->id, $user->id, $systemcontext->id, $this->roleauth);
+        foreach ($roles as $role) {
+            $isrole = $this->is_role($user->username, $role);
+            if ($isrole === null) {
+                continue; // Nothing to sync - role/LDAP contexts not configured.
+            }
+
+            // Sync user.
+            $systemcontext = context_system::instance();
+            if ($isrole) {
+                // Following calls will not create duplicates.
+                role_assign($role['id'], $user->id, $systemcontext->id, $this->roleauth);
             } else {
-                // Unassign only if previously assigned by this plugin!
-                role_unassign($creatorrole->id, $user->id, $systemcontext->id, $this->roleauth);
+                // Unassign only if previously assigned by this plugin.
+                role_unassign($role['id'], $user->id, $systemcontext->id, $this->roleauth);
             }
         }
     }
diff --git a/auth/ldap/classes/task/sync_roles.php b/auth/ldap/classes/task/sync_roles.php
new file mode 100644 (file)
index 0000000..37ea23c
--- /dev/null
@@ -0,0 +1,61 @@
+<?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/>.
+
+/**
+ * A scheduled task for LDAP roles sync.
+ *
+ * @package    auth_ldap
+ * @author     David Balch <david.balch@conted.ox.ac.uk>
+ * @copyright  2017 The Chancellor Masters and Scholars of the University of Oxford {@link http://www.tall.ox.ac.uk}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace auth_ldap\task;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A scheduled task class for LDAP roles sync.
+ *
+ * @author     David Balch <david.balch@conted.ox.ac.uk>
+ * @copyright  2017 The Chancellor Masters and Scholars of the University of Oxford {@link http://www.tall.ox.ac.uk}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class sync_roles extends \core\task\scheduled_task {
+
+    /**
+     * Get a descriptive name for this task (shown to admins).
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('syncroles', 'auth_ldap');
+    }
+
+    /**
+     * Synchronise role assignments from LDAP.
+     */
+    public function execute() {
+        global $DB;
+        if (is_enabled_auth('ldap')) {
+            $auth = get_auth_plugin('ldap');
+            $users = $DB->get_records('user', array('auth' => 'ldap'));
+            foreach ($users as $user) {
+                $auth->sync_roles($user);
+            }
+        }
+    }
+
+}
index ba8c63c..0fe49cd 100644 (file)
 defined('MOODLE_INTERNAL') || die();
 
 $tasks = array(
+    array(
+        'classname' => 'auth_ldap\task\sync_roles',
+        'blocking' => 0,
+        'minute' => '0',
+        'hour' => '0',
+        'day' => '*',
+        'month' => '*',
+        'dayofweek' => '*',
+        'disabled' => 1
+    ),
     array(
         'classname' => 'auth_ldap\task\sync_task',
         'blocking' => 0,
index 3e1c58f..a6f7ced 100644 (file)
@@ -48,5 +48,23 @@ function xmldb_auth_ldap_upgrade($oldversion) {
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2017080100) {
+        // The "auth_ldap/coursecreators" setting was replaced with "auth_ldap/coursecreatorcontext" (created
+        // dynamically from system-assignable roles) - so migrate any existing value to the first new slot.
+        if ($ldapcontext = get_config('auth_ldap', 'creators')) {
+            // Get info about the role that the old coursecreators setting would apply.
+            $creatorrole = get_archetype_roles('coursecreator');
+            $creatorrole = array_shift($creatorrole); // We can only use one, let's use the first.
+
+            // Create new setting.
+            set_config($creatorrole->shortname . 'context', $ldapcontext, 'auth_ldap');
+
+            // Delete old setting.
+            set_config('creators', null, 'auth_ldap');
+
+            upgrade_plugin_savepoint(true, 2017080100, 'auth', 'ldap');
+        }
+    }
+
     return true;
 }
index 8efc99a..7a693b5 100644 (file)
@@ -36,8 +36,6 @@ $string['auth_ldap_contexts_key'] = 'Contexts';
 $string['auth_ldap_create_context'] = 'If you enable user creation with email confirmation, specify the context where users are created. This context should be different from other users to prevent security issues. You don\'t need to add this context to ldap_context-variable, Moodle will search for users from this context automatically.<br /><b>Note!</b> You have to modify the method user_create() in file auth/ldap/auth.php to make user creation work';
 $string['auth_ldap_create_context_key'] = 'Context for new users';
 $string['auth_ldap_create_error'] = 'Error creating user in LDAP.';
-$string['auth_ldap_creators'] = 'List of groups or contexts whose members are allowed to create new courses. Separate multiple groups with \';\'. Usually something like \'cn=teachers,ou=staff,o=myorg\'';
-$string['auth_ldap_creators_key'] = 'Creators';
 $string['auth_ldapdescription'] = 'This method provides authentication against an external LDAP server.
                                   If the given username and password are valid, Moodle creates a new user
                                   entry in its database. This module can read user attributes from LDAP and prefill
@@ -80,6 +78,8 @@ $string['auth_ldap_passtype_key'] = 'Password format';
 $string['auth_ldap_passwdexpire_settings'] = 'LDAP password expiration settings';
 $string['auth_ldap_preventpassindb'] = 'Select yes to prevent passwords from being stored in Moodle\'s DB.';
 $string['auth_ldap_preventpassindb_key'] = 'Prevent password caching';
+$string['auth_ldap_rolecontext'] = '{$a->localname} context';
+$string['auth_ldap_rolecontext_help'] = 'LDAP context used to select for <i>{$a->localname}</i> mapping. Separate multiple groups with \';\'. Usually something like "cn={$a->shortname},ou=staff,o=myorg".';
 $string['auth_ldap_search_sub'] = 'Search users from subcontexts.';
 $string['auth_ldap_search_sub_key'] = 'Search subcontexts';
 $string['auth_ldap_server_settings'] = 'LDAP server settings';
@@ -141,7 +141,9 @@ $string['pluginname'] = 'LDAP server';
 $string['pluginnotenabled'] = 'Plugin not enabled!';
 $string['renamingnotallowed'] = 'User renaming not allowed in LDAP';
 $string['rootdseerror'] = 'Error querying rootDSE for Active Directory';
+$string['syncroles'] = 'Synchronise system roles from LDAP';
 $string['synctask'] = 'LDAP users sync job';
+$string['systemrolemapping'] = 'System role mapping';
 $string['start_tls'] = 'Use regular LDAP service (port 389) with TLS encryption';
 $string['start_tls_key'] = 'Use TLS';
 $string['updateremfail'] = 'Error updating LDAP record. Error code: {$a->errno}; Error string: {$a->errstring}<br/>Key ({$a->key}) - old moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
@@ -158,3 +160,7 @@ $string['userentriestorevive'] = "User entries to be revived: {\$a}\n";
 $string['userentriestoupdate'] = "User entries to be updated: {\$a}\n";
 $string['usernotfound'] = 'User not found in LDAP';
 $string['useracctctrlerror'] = 'Error getting userAccountControl for {$a}';
+
+// Deprecated since Moodle 3.4.
+$string['auth_ldap_creators'] = 'List of groups or contexts whose members are allowed to create new courses. Separate multiple groups with \';\'. Usually something like \'cn=teachers,ou=staff,o=myorg\'';
+$string['auth_ldap_creators_key'] = 'Creators';
diff --git a/auth/ldap/lang/en/deprecated.txt b/auth/ldap/lang/en/deprecated.txt
new file mode 100644 (file)
index 0000000..fa73d1e
--- /dev/null
@@ -0,0 +1,2 @@
+auth_ldap_creators,auth_ldap
+auth_ldap_creators_key,auth_ldap
diff --git a/auth/ldap/locallib.php b/auth/ldap/locallib.php
new file mode 100644 (file)
index 0000000..5688064
--- /dev/null
@@ -0,0 +1,53 @@
+<?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/>.
+
+/**
+ * Internal library of functions for module auth_ldap
+ *
+ * @package    auth_ldap
+ * @author     David Balch <david.balch@conted.ox.ac.uk>
+ * @copyright  2017 The Chancellor Masters and Scholars of the University of Oxford {@link http://www.tall.ox.ac.uk/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Get a list of system roles assignable by the current or a specified user, including their localised names.
+ *
+ * @param integer|object $user A user id or object. By default (null) checks the permissions of the current user.
+ * @return array $roles, each role as an array with id, shortname, localname, and settingname for the config value.
+ */
+function get_ldap_assignable_role_names($user = null) {
+    $roles = array();
+
+    if ($assignableroles = get_assignable_roles(context_system::instance(), ROLENAME_SHORT, false, $user)) {
+        $systemroles = role_fix_names(get_all_roles(), context_system::instance(), ROLENAME_ORIGINAL);
+        foreach ($assignableroles as $shortname) {
+            foreach ($systemroles as $systemrole) {
+                if ($systemrole->shortname == $shortname) {
+                    $roles[] = array('id' => $systemrole->id,
+                                     'shortname' => $shortname,
+                                     'localname' => $systemrole->localname,
+                                     'settingname' => $shortname . 'context');
+                    break;
+                }
+            }
+        }
+    }
+
+    return $roles;
+}
index 5f62569..af1ddf7 100644 (file)
@@ -226,14 +226,17 @@ if ($ADMIN->fulltree) {
                 get_string('auth_ldap_create_context_key', 'auth_ldap'),
                 get_string('auth_ldap_create_context', 'auth_ldap'), '', PARAM_RAW_TRIMMED));
 
-        // Course Creators Header.
-        $settings->add(new admin_setting_heading('auth_ldap/coursecreators',
-                new lang_string('coursecreators'), ''));
-
-        // Course creators field mapping.
-        $settings->add(new admin_setting_configtext('auth_ldap/creators',
-                get_string('auth_ldap_creators_key', 'auth_ldap'),
-                get_string('auth_ldap_creators', 'auth_ldap'), '', PARAM_RAW_TRIMMED));
+        // System roles mapping header.
+        $settings->add(new admin_setting_heading('auth_ldap/systemrolemapping',
+                                        new lang_string('systemrolemapping', 'auth_ldap'), ''));
+
+        // Create system role mapping field for each assignable system role.
+        $roles = get_ldap_assignable_role_names();
+        foreach ($roles as $role) {
+            $settings->add(new admin_setting_configtext('auth_ldap/' . $role['settingname'],
+                    get_string('auth_ldap_rolecontext', 'auth_ldap', $role),
+                    get_string('auth_ldap_rolecontext_help', 'auth_ldap', $role), '', PARAM_RAW_TRIMMED));
+        }
 
         // User Account Sync.
         $settings->add(new admin_setting_heading('auth_ldap/syncusers',
index d5cb801..239e8ef 100644 (file)
@@ -1,5 +1,10 @@
 This files describes API changes in the auth_ldap code.
 
+=== 3.4 ===
+
+* The "auth_ldap/coursecreators" setting was replaced with dynamically generated "auth_ldap/<role>context" settings,
+  migrating any existing value to a new setting in this style.
+
 === 3.3 ===
 
 * The config.html file was migrated to use the admin settings API.
index 5e96509..93ce0e7 100644 (file)
@@ -25,6 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2017051500;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2017080100;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2017050500;        // Requires this Moodle version
 $plugin->component = 'auth_ldap';       // Full name of the plugin (used for diagnostics)
index 8b425d7..7427328 100644 (file)
@@ -65,8 +65,9 @@ class frontend extends \core_availability\frontend {
                 // Add each course-module if it has completion turned on and is not
                 // the one currently being edited.
                 if ($othercm->completion && (empty($cm) || $cm->id != $id) && !$othercm->deletioninprogress) {
-                    $cms[] = (object)array('id' => $id, 'name' =>
-                            format_string($othercm->name, true, array('context' => $context)));
+                    $cms[] = (object)array('id' => $id,
+                        'name' => format_string($othercm->name, true, array('context' => $context)),
+                        'completiongradeitemnumber' => $othercm->completiongradeitemnumber);
                 }
             }
 
index aa41e64..938cbab 100644 (file)
@@ -24,6 +24,7 @@
 
 $string['description'] = 'Require students to complete (or not complete) another activity.';
 $string['error_selectcmid'] = 'You must select an activity for the completion condition.';
+$string['error_selectcmidpassfail'] = 'You must select an activity with "Require grade" completion condition set.';
 $string['label_cm'] = 'Activity or resource';
 $string['label_completion'] = 'Required completion status';
 $string['missing'] = '(Missing activity)';
index 4885548..5212364 100644 (file)
Binary files a/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-debug.js and b/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-debug.js differ
index 36efbb5..10be26c 100644 (file)
Binary files a/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-min.js and b/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-min.js differ
index 4885548..5212364 100644 (file)
Binary files a/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form.js and b/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form.js differ
index e7b418e..77c9cf1 100644 (file)
@@ -76,4 +76,14 @@ M.availability_completion.form.fillErrors = function(errors, node) {
     if (cmid === 0) {
         errors.push('availability_completion:error_selectcmid');
     }
+    var e = parseInt(node.one('select[name=e]').get('value'), 10);
+    if (((e === 2) || (e === 3))) {
+        this.cms.forEach(function(cm) {
+            if (cm.id === cmid) {
+                if (cm.completiongradeitemnumber === null) {
+                    errors.push('availability_completion:error_selectcmidpassfail');
+                }
+            }
+        });
+    }
 };
index a984525..546e378 100644 (file)
@@ -96,7 +96,7 @@ Feature: availability_grade
     And I click on "Add submission" "button"
     And I set the field "Online text" to "Q"
     And I click on "Save changes" "button"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
 
     # None of the pages should appear (check assignment though).
     Then I should not see "P2" in the "region-main" "region"
index ce0392e..3c35227 100644 (file)
@@ -36,7 +36,7 @@ Feature: edit_availability
     And I add a "Page" to section "1"
     Then "Restrict access" "fieldset" should not exist
 
-    Given I follow "C1"
+    Given I am on "Course 1" course homepage
     When I edit the section "1"
     Then "Restrict access" "fieldset" should not exist
 
@@ -47,7 +47,7 @@ Feature: edit_availability
     And I add a "Page" to section "1"
     Then "Restrict access" "fieldset" should exist
 
-    Given I follow "C1"
+    Given I am on "Course 1" course homepage
     When I edit the section "1"
     Then "Restrict access" "fieldset" should exist
 
index b826709..dfcf545 100644 (file)
@@ -203,9 +203,7 @@ class restore_gradebook_structure_step extends restore_structure_step {
         $data->scaleid   = $this->get_mappingid('scale', $data->scaleid, NULL);
         $data->outcomeid = $this->get_mappingid('outcome', $data->outcomeid, NULL);
 
-        $data->locktime     = $this->apply_date_offset($data->locktime);
-        $data->timecreated  = $this->apply_date_offset($data->timecreated);
-        $data->timemodified = $this->apply_date_offset($data->timemodified);
+        $data->locktime = $this->apply_date_offset($data->locktime);
 
         $coursecategory = $newitemid = null;
         //course grade item should already exist so updating instead of inserting
@@ -264,10 +262,6 @@ class restore_gradebook_structure_step extends restore_structure_step {
         if (!empty($data->userid)) {
             $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
             $data->locktime     = $this->apply_date_offset($data->locktime);
-            // TODO: Ask, all the rest of locktime/exported... work with time... to be rolled?
-            $data->overridden = $this->apply_date_offset($data->overridden);
-            $data->timecreated  = $this->apply_date_offset($data->timecreated);
-            $data->timemodified = $this->apply_date_offset($data->timemodified);
 
             $gradeexists = $DB->record_exists('grade_grades', array('userid' => $data->userid, 'itemid' => $data->itemid));
             if ($gradeexists) {
@@ -292,9 +286,6 @@ class restore_gradebook_structure_step extends restore_structure_step {
         $data->course = $this->get_courseid();
         $data->courseid = $data->course;
 
-        $data->timecreated  = $this->apply_date_offset($data->timecreated);
-        $data->timemodified = $this->apply_date_offset($data->timemodified);
-
         $newitemid = null;
         //no parent means a course level grade category. That may have been created when the course was created
         if(empty($data->parent)) {
@@ -1576,7 +1567,7 @@ class restore_section_structure_step extends restore_structure_step {
         $section = new stdclass();
         $section->course  = $this->get_courseid();
         $section->section = $data->number;
-        $section->timemodified = isset($data->timemodified) ? $this->apply_date_offset($data->timemodified) : 0;
+        $section->timemodified = $data->timemodified ?? 0;
         // Section doesn't exist, create it with all the info from backup
         if (!$secrec = $DB->get_record('course_sections', ['course' => $this->get_courseid(), 'section' => $data->number])) {
             $section->name = $data->name;
@@ -2534,8 +2525,8 @@ class restore_badges_structure_step extends restore_structure_step {
         $params = array(
                 'name'           => $data->name,
                 'description'    => $data->description,
-                'timecreated'    => $this->apply_date_offset($data->timecreated),
-                'timemodified'   => $this->apply_date_offset($data->timemodified),
+                'timecreated'    => $data->timecreated,
+                'timemodified'   => $data->timemodified,
                 'usercreated'    => $data->usercreated,
                 'usermodified'   => $data->usermodified,
                 'issuername'     => $data->issuername,
@@ -2550,7 +2541,7 @@ class restore_badges_structure_step extends restore_structure_step {
                 'attachment'     => $data->attachment,
                 'notification'   => $data->notification,
                 'status'         => BADGE_STATUS_INACTIVE,
-                'nextcron'       => $this->apply_date_offset($data->nextcron)
+                'nextcron'       => $data->nextcron
         );
 
         $newid = $DB->insert_record('badge', $params);
@@ -2727,7 +2718,7 @@ class restore_calendarevents_structure_step extends restore_structure_step {
                 'visible'        => $data->visible,
                 'uuid'           => $data->uuid,
                 'sequence'       => $data->sequence,
-                'timemodified'   => $this->apply_date_offset($data->timemodified),
+                'timemodified'   => $data->timemodified,
                 'priority'       => isset($data->priority) ? $data->priority : null);
         if ($this->name == 'activity_calendar') {
             $params['instance'] = $this->task->get_activityid();
@@ -2959,7 +2950,7 @@ class restore_course_completion_structure_step extends restore_structure_step {
                 'userid' => $data->userid,
                 'course' => $data->course,
                 'criteriaid' => $data->criteriaid,
-                'timecompleted' => $this->apply_date_offset($data->timecompleted)
+                'timecompleted' => $data->timecompleted
             );
             if (isset($data->gradefinal)) {
                 $params['gradefinal'] = $data->gradefinal;
@@ -2989,9 +2980,9 @@ class restore_course_completion_structure_step extends restore_structure_step {
             $params = array(
                 'userid' => $data->userid,
                 'course' => $data->course,
-                'timeenrolled' => $this->apply_date_offset($data->timeenrolled),
-                'timestarted' => $this->apply_date_offset($data->timestarted),
-                'timecompleted' => $this->apply_date_offset($data->timecompleted),
+                'timeenrolled' => $data->timeenrolled,
+                'timestarted' => $data->timestarted,
+                'timecompleted' => $data->timecompleted,
                 'reaggregate' => $data->reaggregate
             );
 
@@ -3693,8 +3684,6 @@ class restore_activity_grades_structure_step extends restore_structure_step {
         $data->idnumber     = $idnumber;
         $data->scaleid      = $this->get_mappingid('scale', $data->scaleid);
         $data->outcomeid    = $this->get_mappingid('outcome', $data->outcomeid);
-        $data->timecreated  = $this->apply_date_offset($data->timecreated);
-        $data->timemodified = $this->apply_date_offset($data->timemodified);
 
         $gradeitem = new grade_item($data, false);
         $gradeitem->insert('restore');
@@ -3720,8 +3709,6 @@ class restore_activity_grades_structure_step extends restore_structure_step {
         if (!empty($data->userid)) {
             $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
             $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
-            // TODO: Ask, all the rest of locktime/exported... work with time... to be rolled?
-            $data->overridden = $this->apply_date_offset($data->overridden);
 
             $grade = new grade_grade($data, false);
             $grade->insert('restore');
@@ -4288,7 +4275,6 @@ class restore_userscompletion_structure_step extends restore_structure_step {
 
         $data->coursemoduleid = $this->task->get_moduleid();
         $data->userid = $this->get_mappingid('user', $data->userid);
-        $data->timemodified = $this->apply_date_offset($data->timemodified);
 
         // Find the existing record
         $existing = $DB->get_record('course_modules_completion', array(
@@ -5204,7 +5190,6 @@ abstract class restore_questions_activity_structure_step extends restore_activit
         if (!property_exists($data, 'variant')) {
             $data->variant = 1;
         }
-        $data->timemodified    = $this->apply_date_offset($data->timemodified);
 
         if (!property_exists($data, 'maxfraction')) {
             $data->maxfraction = 1;
@@ -5239,7 +5224,6 @@ abstract class restore_questions_activity_structure_step extends restore_activit
         unset($data->response);
 
         $data->questionattemptid = $this->get_new_parentid($nameprefix . 'question_attempt');
-        $data->timecreated = $this->apply_date_offset($data->timecreated);
         $data->userid      = $this->get_mappingid('user', $data->userid);
 
         // Everything ready, insert and create mapping (needed by question_sessions)
diff --git a/backup/moodle2/tests/restore_stepslib_date_test.php b/backup/moodle2/tests/restore_stepslib_date_test.php
new file mode 100644 (file)
index 0000000..1bcce5f
--- /dev/null
@@ -0,0 +1,413 @@
+<?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/>.
+
+/**
+ * Restore date tests.
+ *
+ * @package    core_backup
+ * @copyright  2017 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php");
+require_once($CFG->libdir . "/badgeslib.php");
+require_once($CFG->dirroot . '/mod/assign/tests/base_test.php');
+
+/**
+ * Restore date tests.
+ *
+ * @package    core_backup
+ * @copyright  2017 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class restore_stepslib_date_testcase extends restore_date_testcase {
+
+    /**
+     * Restoring a manual grade item does not result in the timecreated or
+     * timemodified dates being changed.
+     */
+    public function test_grade_item_date_restore() {
+
+        $course = $this->getDataGenerator()->create_course(['startdate' => time()]);
+
+        $params = new stdClass();
+        $params->courseid = $course->id;
+        $params->fullname = 'unittestgradecalccategory';
+        $params->aggregation = GRADE_AGGREGATE_MEAN;
+        $params->aggregateonlygraded = 0;
+        $gradecategory = new grade_category($params, false);
+        $gradecategory->insert();
+
+        $gradecategory->load_grade_item();
+
+        $gradeitems = new grade_item();
+        $gradeitems->courseid = $course->id;
+        $gradeitems->categoryid = $gradecategory->id;
+        $gradeitems->itemname = 'manual grade_item';
+        $gradeitems->itemtype = 'manual';
+        $gradeitems->itemnumber = 0;
+        $gradeitems->needsupdate = false;
+        $gradeitems->gradetype = GRADE_TYPE_VALUE;
+        $gradeitems->grademin = 0;
+        $gradeitems->grademax = 10;
+        $gradeitems->iteminfo = 'Manual grade item used for unit testing';
+        $gradeitems->timecreated = time();
+        $gradeitems->timemodified = time();
+
+        $gradeitems->aggregationcoef = GRADE_AGGREGATE_SUM;
+
+        $gradeitems->insert();
+
+        $gradeitemparams = [
+            'itemtype' => 'manual',
+            'itemname' => $gradeitems->itemname,
+            'courseid' => $course->id,
+        ];
+
+        $gradeitem = grade_item::fetch($gradeitemparams);
+
+        // Do backup and restore.
+
+        $newcourseid = $this->backup_and_restore($course);
+        $newcourse = get_course($newcourseid);
+        $newgradeitemparams = [
+            'itemtype' => 'manual',
+            'itemname' => $gradeitems->itemname,
+            'courseid' => $course->id,
+        ];
+
+        $newgradeitem = grade_item::fetch($newgradeitemparams);
+        $this->assertEquals($gradeitem->timecreated, $newgradeitem->timecreated);
+        $this->assertEquals($gradeitem->timemodified, $newgradeitem->timemodified);
+    }
+
+    /**
+     * The course section timemodified date does not get rolled forward
+     * when the course is restored.
+     */
+    public function test_course_section_date_restore() {
+        global $DB;
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course(['startdate' => time()]);
+        // Get the second course section.
+        $section = $DB->get_record('course_sections', ['course' => $course->id, 'section' => '1']);
+        // Do a backup and restore.
+        $newcourseid = $this->backup_and_restore($course);
+        $newcourse = get_course($newcourseid);
+
+        $newsection = $DB->get_record('course_sections', ['course' => $newcourse->id, 'section' => '1']);
+        // Compare dates.
+        $this->assertEquals($section->timemodified, $newsection->timemodified);
+    }
+
+    /**
+     * Test that the timecreated and timemodified dates are not rolled forward when restoring
+     * badge data.
+     */
+    public function test_badge_date_restore() {
+        global $DB, $USER;
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course(['startdate' => time()]);
+        // Create a badge.
+        $fordb = new stdClass();
+        $fordb->id = null;
+        $fordb->name = "Test badge";
+        $fordb->description = "Testing badges";
+        $fordb->timecreated = time();
+        $fordb->timemodified = time();
+        $fordb->usercreated = $USER->id;
+        $fordb->usermodified = $USER->id;
+        $fordb->issuername = "Test issuer";
+        $fordb->issuerurl = "http://issuer-url.domain.co.nz";
+        $fordb->issuercontact = "issuer@example.com";
+        $fordb->expiredate = time();
+        $fordb->expireperiod = null;
+        $fordb->type = BADGE_TYPE_COURSE;
+        $fordb->courseid = $course->id;
+        $fordb->messagesubject = "Test message subject";
+        $fordb->message = "Test message body";
+        $fordb->attachment = 1;
+        $fordb->notification = 0;
+        $fordb->status = BADGE_STATUS_INACTIVE;
+        $fordb->nextcron = time();
+
+        $this->badgeid = $DB->insert_record('badge', $fordb, true);
+        // Do a backup and restore.
+        $newcourseid = $this->backup_and_restore($course);
+        $newcourse = get_course($newcourseid);
+
+        $badges = badges_get_badges(BADGE_TYPE_COURSE, $newcourseid);
+
+        // Compare dates.
+        $badge = array_shift($badges);
+        $this->assertEquals($fordb->timecreated, $badge->timecreated);
+        $this->assertEquals($fordb->timemodified, $badge->timemodified);
+        $this->assertEquals($fordb->nextcron, $badge->nextcron);
+        // Expire date should be moved forward.
+        $this->assertNotEquals($fordb->expiredate, $badge->expiredate);
+    }
+
+    /**
+     * Test that course calendar events timemodified field is not rolled forward
+     * when restoring the course.
+     */
+    public function test_calendarevents_date_restore() {
+        global $USER, $DB;
+        // Create course.
+        $course = $this->getDataGenerator()->create_course(['startdate' => time()]);
+        // Create calendar event.
+        $starttime = time();
+        $event = [
+                'name' => 'Start of assignment',
+                'description' => '',
+                'format' => 1,
+                'courseid' => $course->id,
+                'groupid' => 0,
+                'userid' => $USER->id,
+                'modulename' => 0,
+                'instance' => 0,
+                'eventtype' => 'course',
+                'timestart' => $starttime,
+                'timeduration' => 86400,
+                'visible' => 1
+        ];
+        $calendarevent = calendar_event::create($event, false);
+
+        // Backup and restore.
+        $newcourseid = $this->backup_and_restore($course);
+        $newcourse = get_course($newcourseid);
+
+        $newevent = $DB->get_record('event', ['courseid' => $newcourseid, 'eventtype' => 'course']);
+        // Compare dates.
+        $this->assertEquals($calendarevent->timemodified, $newevent->timemodified);
+        $this->assertNotEquals($calendarevent->timestart, $newevent->timestart);
+    }
+
+    /**
+     * Testing that the timeenrolled, timestarted, and timecompleted fields are not rolled forward / back
+     * when doing a course restore.
+     */
+    public function test_course_completion_date_restore() {
+        global $DB;
+
+        // Create course with course completion enabled.
+        $course = $this->getDataGenerator()->create_course(['startdate' => time(), 'enablecompletion' => 1]);
+
+        // Enrol a user in the course.
+        $user = $this->getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', ['shortname' => 'student']);
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id);
+        // Complete the course with a user.
+        $ccompletion = new completion_completion(['course' => $course->id,
+                                                  'userid' => $user->id,
+                                                  'timeenrolled' => time(),
+                                                  'timestarted' => time()
+                                                ]);
+        // Now, mark the course as completed.
+        $ccompletion->mark_complete();
+        $this->assertEquals('100', \core_completion\progress::get_course_progress_percentage($course, $user->id));
+
+        // Back up and restore.
+        $newcourseid = $this->backup_and_restore($course);
+        $newcourse = get_course($newcourseid);
+
+        $newcompletion = completion_completion::fetch(['course' => $newcourseid, 'userid' => $user->id]);
+
+        // Compare dates.
+        $this->assertEquals($ccompletion->timeenrolled, $newcompletion->timeenrolled);
+        $this->assertEquals($ccompletion->timestarted, $newcompletion->timestarted);
+        $this->assertEquals($ccompletion->timecompleted, $newcompletion->timecompleted);
+    }
+
+    /**
+     * Testing that the grade grade date information is not changed in the gradebook when a course
+     * restore is performed.
+     */
+    public function test_grade_grade_date_restore() {
+        global $USER, $DB;
+        // Testing the restore of an overridden grade.
+        list($course, $assign) = $this->create_course_and_module('assign', []);
+        $cm = $DB->get_record('course_modules', ['course' => $course->id, 'instance' => $assign->id]);
+        $assignobj = new testable_assign(context_module::instance($cm->id), $cm, $course);
+        $submission = $assignobj->get_user_submission($USER->id, true);
+        $grade = $assignobj->get_user_grade($USER->id, true);
+        $grade->grade = 75;
+        $assignobj->update_grade($grade);
+
+        // Find the grade item.
+        $gradeitemparams = [
+            'itemtype' => 'mod',
+            'iteminstance' => $assign->id,
+            'itemmodule' => 'assign',
+            'courseid' => $course->id,
+        ];
+        $gradeitem = grade_item::fetch($gradeitemparams);
+
+        // Next the grade grade.
+        $gradegrade = grade_grade::fetch(['itemid' => $gradeitem->id, 'userid' => $USER->id]);
+        $gradegrade->set_overridden(true);
+
+        // Back up and restore.
+        $newcourseid = $this->backup_and_restore($course);
+        $newcourse = get_course($newcourseid);
+
+        // Find assignment.
+        $assignid = $DB->get_field('assign', 'id', ['course' => $newcourseid]);
+        // Find grade item.
+        $newgradeitemparams = [
+            'itemtype' => 'mod',
+            'iteminstance' => $assignid,
+            'itemmodule' => 'assign',
+            'courseid' => $newcourse->id,
+        ];
+
+        $newgradeitem = grade_item::fetch($newgradeitemparams);
+        // Find grade grade.
+        $newgradegrade = grade_grade::fetch(['itemid' => $newgradeitem->id, 'userid' => $USER->id]);
+        // Compare dates.
+        $this->assertEquals($gradegrade->timecreated, $newgradegrade->timecreated);
+        $this->assertEquals($gradegrade->timemodified, $newgradegrade->timemodified);
+        $this->assertEquals($gradegrade->overridden, $newgradegrade->overridden);
+    }
+
+    /**
+     * Checking that the user completion of an activity relating to the timemodified field does not change
+     * when doing a course restore.
+     */
+    public function test_usercompletion_date_restore() {
+        global $USER, $DB;
+        // More completion...
+        $course = $this->getDataGenerator()->create_course(['startdate' => time(), 'enablecompletion' => 1]);
+        $assign = $this->getDataGenerator()->create_module('assign', [
+                'course' => $course->id,
+                'completion' => COMPLETION_TRACKING_AUTOMATIC, // Show activity as complete when conditions are met.
+                'completionusegrade' => 1 // Student must receive a grade to complete this activity.
+            ]);
+        $cm = $DB->get_record('course_modules', ['course' => $course->id, 'instance' => $assign->id]);
+        $assignobj = new testable_assign(context_module::instance($cm->id), $cm, $course);
+        $submission = $assignobj->get_user_submission($USER->id, true);
+        $grade = $assignobj->get_user_grade($USER->id, true);
+        $grade->grade = 75;
+        $assignobj->update_grade($grade);
+
+        $coursemodulecompletion = $DB->get_record('course_modules_completion', ['coursemoduleid' => $cm->id]);
+
+        // Back up and restore.
+        $newcourseid = $this->backup_and_restore($course);
+        $newcourse = get_course($newcourseid);
+
+        // Find assignment.
+        $assignid = $DB->get_field('assign', 'id', ['course' => $newcourseid]);
+        $cm = $DB->get_record('course_modules', ['course' => $newcourse->id, 'instance' => $assignid]);
+        $newcoursemodulecompletion = $DB->get_record('course_modules_completion', ['coursemoduleid' => $cm->id]);
+
+        $this->assertEquals($coursemodulecompletion->timemodified, $newcoursemodulecompletion->timemodified);
+    }
+
+    /**
+     * Ensuring that the timemodified field of the question attempt steps table does not change when
+     * a course restore is done.
+     */
+    public function test_question_attempt_steps_date_restore() {
+        global $DB;
+
+        $course = $this->getDataGenerator()->create_course(['startdate' => time()]);
+        // Make a quiz.
+        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
+
+        $quiz = $quizgenerator->create_instance(array('course' => $course->id, 'questionsperpage' => 0, 'grade' => 100.0,
+                                                      'sumgrades' => 2));
+
+        $cm = $DB->get_record('course_modules', ['course' => $course->id, 'instance' => $quiz->id]);
+
+        // Create a couple of questions.
+        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
+
+        $cat = $questiongenerator->create_question_category();
+        $saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
+        $numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
+
+        // Add them to the quiz.
+        quiz_add_quiz_question($saq->id, $quiz);
+        quiz_add_quiz_question($numq->id, $quiz);
+
+        // Make a user to do the quiz.
+        $user1 = $this->getDataGenerator()->create_user();
+
+        $quizobj = quiz::create($quiz->id, $user1->id);
+
+        // Start the attempt.
+        $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
+        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
+
+        $timenow = time();
+        $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $user1->id);
+
+        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
+
+        quiz_attempt_save_started($quizobj, $quba, $attempt);
+
+        // Process some responses from the student.
+        $attemptobj = quiz_attempt::create($attempt->id);
+
+        $prefix1 = $quba->get_field_prefix(1);
+        $prefix2 = $quba->get_field_prefix(2);
+
+        $tosubmit = array(1 => array('answer' => 'frog'),
+                          2 => array('answer' => '3.14'));
+
+        $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
+
+        // Finish the attempt.
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_finish($timenow, false);
+
+        $questionattemptstepdates = [];
+        $originaliterator = $quba->get_attempt_iterator();
+        foreach ($originaliterator as $questionattempt) {
+            $questionattemptstepdates[] = ['originaldate' => $questionattempt->get_last_action_time()];
+        }
+
+        // Back up and restore.
+        $newcourseid = $this->backup_and_restore($course);
+        $newcourse = get_course($newcourseid);
+
+        // Get the quiz for this new restored course.
+        $quizdata = $DB->get_record('quiz', ['course' => $newcourseid]);
+        $quizobj = quiz::create($quizdata->id, $user1->id);
+
+        $questionusage = $DB->get_record('question_usages', [
+                'component' => 'mod_quiz',
+                'contextid' => $quizobj->get_context()->id
+            ]);
+
+        $newquba = question_engine::load_questions_usage_by_activity($questionusage->id);
+
+        $restorediterator = $newquba->get_attempt_iterator();
+        $i = 0;
+        foreach ($restorediterator as $restoredquestionattempt) {
+            $questionattemptstepdates[$i]['restoredate'] = $restoredquestionattempt->get_last_action_time();
+            $i++;
+        }
+
+        foreach ($questionattemptstepdates as $dates) {
+            $this->assertEquals($dates['originaldate'], $dates['restoredate']);
+        }
+    }
+}
\ No newline at end of file
index c72f339..da9a103 100644 (file)
@@ -51,6 +51,8 @@ abstract class restore_step extends base_step {
      * Note we are using one static cache here, but *by restoreid*, so it's ok for concurrence/multiple
      * executions in the same request
      *
+     * Note: The policy is to roll date only for configurations and not for user data. see MDL-9367.
+     *
      * @param int $value Time value (seconds since epoch), or empty for nothing
      * @return int Time value after applying the date offset, or empty for nothing
      */
index a2886be..d34b8d5 100644 (file)
@@ -169,8 +169,7 @@ Feature: Restore Moodle 2 course backups
     When I backup "Course 1" course using this options:
       | Initial |  Include enrolled users | 0 |
       | Confirmation | Filename | test_backup.mbz |
-    And I am on site homepage
-    And I follow "Course 2"
+    And I am on "Course 2" course homepage
     And I navigate to "Restore" node in "Course administration"
     And I merge "test_backup.mbz" backup into the current course after deleting it's contents using this options:
       | Schema | Overwrite course configuration | Yes |
@@ -199,8 +198,7 @@ Feature: Restore Moodle 2 course backups
     When I backup "Course 1" course using this options:
       | Initial |  Include enrolled users | 0 |
       | Confirmation | Filename | test_backup.mbz |
-    And I am on site homepage
-    And I follow "Course 2"
+    And I am on "Course 2" course homepage
     And I navigate to "Restore" node in "Course administration"
     And I merge "test_backup.mbz" backup into the current course after deleting it's contents using this options:
       | Schema | Overwrite course configuration | No |
@@ -229,8 +227,7 @@ Feature: Restore Moodle 2 course backups
     When I backup "Course 1" course using this options:
       | Initial |  Include enrolled users | 0 |
       | Confirmation | Filename | test_backup.mbz |
-    And I am on site homepage
-    And I follow "Course 4"
+    And I am on "Course 4" course homepage
     And I navigate to "Restore" node in "Course administration"
     And I merge "test_backup.mbz" backup into the current course after deleting it's contents using this options:
       | Schema | Overwrite course configuration | No |
index 2f24252..63e6dba 100644 (file)
@@ -109,7 +109,6 @@ Feature: Block activity modules
       | workshop   | Test workshop name     | Test workshop description     | C1     | workshop1   |
 
     When I log in as "admin"
-    And I am on course index
     And I am on "Course 1" course homepage with editing mode on
     And I add the "Activities" block
     And I click on "Assignments" "link" in the "Activities" "block"
index 5103f23..982510d 100644 (file)
@@ -22,7 +22,7 @@ Feature: The activity results block doesn't display student scores for unsupport
       | Assignment name | Test assignment |
       | Description | Offline text |
       | assignsubmission_file_enabled | 0 |
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I add the "Activity results" block
     And I configure the "Activity results" block
     And I set the following fields to these values:
index 67ee460..8814cc4 100644 (file)
@@ -32,7 +32,7 @@ Feature: Students can use the recent blog entries block to view recent entries o
     And I press "Save changes"
     Then I should see "S1 First Blog"
     And I should see "This is my awesome blog!"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I should see "S1 First Blog"
     And I follow "S1 First Blog"
     And I should see "This is my awesome blog!"
@@ -47,7 +47,7 @@ Feature: Students can use the recent blog entries block to view recent entries o
       | Blog entry body | This is my awesome blog! |
     And I press "Save changes"
     And I wait "1" seconds
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I follow "Add an entry about this course"
     # Blog 2 of 5
     And I set the following fields to these values:
@@ -57,7 +57,7 @@ Feature: Students can use the recent blog entries block to view recent entries o
     And I wait "1" seconds
     And I should see "S1 Second Blog"
     And I should see "This is my awesome blog!"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I follow "Add an entry about this course"
     # Blog 3 of 5
     And I set the following fields to these values:
@@ -67,7 +67,7 @@ Feature: Students can use the recent blog entries block to view recent entries o
     And I wait "1" seconds
     And I should see "S1 Third Blog"
     And I should see "This is my awesome blog!"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I follow "Add an entry about this course"
     # Blog 4 of 5
     And I set the following fields to these values:
@@ -77,7 +77,7 @@ Feature: Students can use the recent blog entries block to view recent entries o
     And I wait "1" seconds
     And I should see "S1 Fourth Blog"
     And I should see "This is my awesome blog!"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I follow "Add an entry about this course"
     # Blog 5 of 5
     And I set the following fields to these values:
@@ -86,7 +86,7 @@ Feature: Students can use the recent blog entries block to view recent entries o
     And I press "Save changes"
     And I should see "S1 Fifth Blog"
     And I should see "This is my awesome blog!"
-    When I follow "C1"
+    When I am on "Course 1" course homepage
     And I should not see "S1 First Blog"
     And I should see "S1 Second Blog"
     And I should see "S1 Third Blog"
index c01b8fa..8753167 100644 (file)
@@ -81,7 +81,6 @@ Feature: Enable the calendar block in a course and test it's functionality
     And I create a calendar event with form data:
       | id_eventtype | User |
       | id_name | User Event |
-    When I am on homepage
     And I am on "Course 1" course homepage
     And I follow "Hide course events"
     And I hover over today in the calendar
@@ -96,8 +95,7 @@ Feature: Enable the calendar block in a course and test it's functionality
     And I create a calendar event with form data:
       | id_eventtype | User |
       | id_name | User Event |
-    When I am on homepage
-    And I am on "Course 1" course homepage
+    When I am on "Course 1" course homepage
     And I hover over today in the calendar
     Then I should see "User Event"
 
@@ -113,8 +111,7 @@ Feature: Enable the calendar block in a course and test it's functionality
     And I create a calendar event with form data:
       | id_eventtype | User |
       | id_name | User Event |
-    When I am on homepage
-    And I am on "Course 1" course homepage
+    When I am on "Course 1" course homepage
     And I follow "Hide user events"
     And I hover over today in the calendar
     Then I should not see "User Event"
index c515911..a61b273 100644 (file)
@@ -109,6 +109,13 @@ class community_hub_search_form extends moodleform {
             $mform->addElement('static', 'errorhub', '', $error);
         }
 
+        // Hubdirectory returns old URL for the moodle.net hub, substitute it.
+        foreach ($hubs as $key => $hub) {
+            if ($hub['url'] === HUB_OLDMOODLEORGHUBURL) {
+                $hubs[$key]['url'] = HUB_MOODLEORGHUBURL;
+            }
+        }
+
         //display list of registered on hub
         $registrationmanager = new registration_manager();
         $registeredhubs = $registrationmanager->get_registered_on_hubs();
index a92c042..d41fe98 100644 (file)
@@ -60,7 +60,7 @@ Feature: Enable Block Completion in a course using activity completion
     When I log in as "student1"
     And I am on "Course 1" course homepage
     And I follow "Test page name"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     Then I should see "Status: Pending" in the "Course completion status" "block"
     And I should see "0 of 1" in the "Activity completion" "table_row"
     And I trigger cron
index c4d0352..4d72d35 100644 (file)
@@ -27,9 +27,7 @@ Feature: Allow teachers to bulk edit activity completion rules in a course.
       | assign | C1 | a3 | Test assignment three | Submit something! | 150 |
       | assign | C1 | a4 | Test assignment four | Submit nothing! | 150 |
     And I log in as "teacher1"
-    And I am on site homepage
-    And I follow "Course 1"
-    And I turn editing mode on
+    And I am on "Course 1" course homepage with editing mode on
     And I navigate to "Edit settings" in current page administration
     And I set the following fields to these values:
       | Enable completion tracking | Yes |
index 13aeb24..cc32275 100644 (file)
@@ -24,9 +24,7 @@ Feature: Allow teachers to edit the default activity completion rules in a cours
       | activity | course | idnumber | name | intro | grade |
       | assign | C1 | a1 | Test assignment one | Submit something! | 300 |
     And I log in as "teacher1"
-    And I am on site homepage
-    And I follow "Course 1"
-    And I turn editing mode on
+    And I am on "Course 1" course homepage with editing mode on
     And I navigate to "Edit settings" in current page administration
     And I set the following fields to these values:
       | Enable completion tracking | Yes |
diff --git a/course/classes/output/activity_navigation.php b/course/classes/output/activity_navigation.php
new file mode 100644 (file)
index 0000000..3fbf4e9
--- /dev/null
@@ -0,0 +1,129 @@
+<?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/>.
+
+/**
+ * File containing the class activity navigation renderable.
+ *
+ * @package    core_course
+ * @copyright  2017 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_course\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+use renderable;
+use templatable;
+use url_select;
+
+/**
+ * The class activity navigation renderable.
+ *
+ * @package    core_course
+ * @copyright  2017 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class activity_navigation implements renderable, templatable {
+
+    /**
+     * @var \action_link The action link object for the prev link.
+     */
+    public $prevlink = null;
+
+    /**
+     * @var \action_link The action link object for the next link.
+     */
+    public $nextlink = null;
+
+    /**
+     * @var url_select The url select object for the activity selector menu.
+     */
+    public $activitylist = null;
+
+    /**
+     * Constructor.
+     *
+     * @param \cm_info|null $prevmod The previous module to display, null if none.
+     * @param \cm_info|null $nextmod The next module to display, null if none.
+     * @param array $activitylist The list of activity URLs (as key) and names (as value) for the activity dropdown menu.
+     */
+    public function __construct($prevmod, $nextmod, $activitylist = array()) {
+        global $OUTPUT;
+
+        // Check if there is a previous module to display.
+        if ($prevmod) {
+            $linkurl = new \moodle_url($prevmod->url, array('forceview' => 1));
+            $linkname = $prevmod->get_formatted_name();
+            if (!$prevmod->visible) {
+                $linkname .= ' ' . get_string('hiddenwithbrackets');
+            }
+
+            $attributes = [
+                'classes' => 'btn btn-link',
+                'id' => 'prev-activity-link',
+                'title' => $linkname,
+            ];
+            $this->prevlink = new \action_link($linkurl, $OUTPUT->larrow() . ' ' . $linkname, null, $attributes);
+        }
+
+        // Check if there is a next module to display.
+        if ($nextmod) {
+            $linkurl = new \moodle_url($nextmod->url, array('forceview' => 1));
+            $linkname = $nextmod->get_formatted_name();
+            if (!$nextmod->visible) {
+                $linkname .= ' ' . get_string('hiddenwithbrackets');
+            }
+
+            $attributes = [
+                'classes' => 'btn btn-link',
+                'id' => 'next-activity-link',
+                'title' => $linkname,
+            ];
+            $this->nextlink = new \action_link($linkurl, $linkname . ' ' . $OUTPUT->rarrow(), null, $attributes);
+        }
+
+        // Render the activity list dropdown menu if available.
+        if (!empty($activitylist)) {
+            $select = new url_select($activitylist, '', array('' => get_string('jumpto')));
+            $select->set_label(get_string('jumpto'), array('class' => 'sr-only'));
+            $select->attributes = array('id' => 'jump-to-activity');
+            $this->activitylist = $select;
+        }
+    }
+
+    /**
+     * Export this data so it can be used as the context for a mustache template.
+     *
+     * @param \renderer_base $output Renderer base.
+     * @return \stdClass
+     */
+    public function export_for_template(\renderer_base $output) {
+        $data = new \stdClass();
+        if ($this->prevlink) {
+            $data->prevlink = $this->prevlink->export_for_template($output);
+        }
+
+        if ($this->nextlink) {
+            $data->nextlink = $this->nextlink->export_for_template($output);
+        }
+
+        if ($this->activitylist) {
+            $data->activitylist = $this->activitylist->export_for_template($output);
+        }
+
+        return $data;
+    }
+}
index 9a68ee3..8b22a9a 100644 (file)
@@ -3479,31 +3479,33 @@ function duplicate_module($course, $cm) {
         }
     }
 
+    $rc->destroy();
+
+    if (empty($CFG->keeptempdirectoriesonbackup)) {
+        fulldelete($backupbasepath);
+    }
+
     // If we know the cmid of the new course module, let us move it
     // right below the original one. otherwise it will stay at the
     // end of the section.
     if ($newcmid) {
-        $info = get_fast_modinfo($course);
-        $newcm = $info->get_cm($newcmid);
         $section = $DB->get_record('course_sections', array('id' => $cm->section, 'course' => $cm->course));
-        moveto_module($newcm, $section, $cm);
-        moveto_module($cm, $section, $newcm);
+        $modarray = explode(",", trim($section->sequence));
+        $cmindex = array_search($cm->id, $modarray);
+        if ($cmindex !== false && $cmindex < count($modarray) - 1) {
+            $newcm = get_coursemodule_from_id($cm->modname, $newcmid, $cm->course);
+            moveto_module($newcm, $section, $modarray[$cmindex + 1]);
+        }
 
         // Update calendar events with the duplicated module.
         // The following line is to be removed in MDL-58906.
         course_module_update_calendar_events($newcm->modname, null, $newcm);
 
         // Trigger course module created event. We can trigger the event only if we know the newcmid.
+        $newcm = get_fast_modinfo($cm->course)->get_cm($newcmid);
         $event = \core\event\course_module_created::create_from_cm($newcm);
         $event->trigger();
     }
-    rebuild_course_cache($cm->course);
-
-    $rc->destroy();
-
-    if (empty($CFG->keeptempdirectoriesonbackup)) {
-        fulldelete($backupbasepath);
-    }
 
     return isset($newcm) ? $newcm : null;
 }
index 1a87fb8..f048bcd 100644 (file)
@@ -157,11 +157,11 @@ if ($mform->is_cancelled()) {
     }
 
     if (isset($fromform->submitbutton)) {
+        $url = new moodle_url("/mod/$module->name/view.php", array('id' => $fromform->coursemodule, 'forceview' => 1));
         if (empty($fromform->showgradingmanagement)) {
-            redirect("$CFG->wwwroot/mod/$module->name/view.php?id=$fromform->coursemodule");
+            redirect($url);
         } else {
-            $returnurl = new moodle_url("/mod/$module->name/view.php", array('id' => $fromform->coursemodule));
-            redirect($fromform->gradingman->get_management_url($returnurl));
+            redirect($fromform->gradingman->get_management_url($url));
         }
     } else {
         redirect(course_get_url($course, $cw->section, array('sr' => $sectionreturn)));
index accae17..a2d222b 100644 (file)
@@ -2100,6 +2100,19 @@ class core_course_renderer extends plugin_renderer_base {
                 set_attributes(array('class' => 'frontpage-category-names'));
         return $this->coursecat_tree($chelper, coursecat::get(0));
     }
+
+    /**
+     * Renders the activity navigation.
+     *
+     * Defer to template.
+     *
+     * @param \core_course\output\activity_navigation $page
+     * @return string html for the page
+     */
+    public function render_activity_navigation(\core_course\output\activity_navigation $page) {
+        $data = $page->export_for_template($this->output);
+        return $this->output->render_from_template('core_course/activity_navigation', $data);
+    }
 }
 
 /**
diff --git a/course/templates/activity_navigation.mustache b/course/templates/activity_navigation.mustache
new file mode 100644 (file)
index 0000000..e6439d3
--- /dev/null
@@ -0,0 +1,85 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core_course/activity_navigation
+
+    Displays the activity navigation
+
+    Context variables required for this template:
+    * prevlink Object - The action link data for the previous activity link. Corresponds with the core/action_link context.
+    * nextlink Object - The action link data for the next activity link. Corresponds with the core/action_link context.
+    * activitylist Object - The data for the activity selector menu. Corresponds with the core/url_select context.
+
+    Example context (json):
+    {
+        "prevlink": {
+            "disabled": false,
+            "url": "#",
+            "id": "test-id-1",
+            "classes": "btn btn-link",
+            "attributes": [
+                {
+                    "name": "title",
+                    "value": "Activity A"
+                }
+            ],
+            "text": "◄ Activity A"
+        },
+        "nextlink": {
+            "disabled": false,
+            "url": "#",
+            "id": "test-id-2",
+            "classes": "btn btn-link",
+            "attributes": [
+                {
+                    "name": "title",
+                    "value": "Activity C"
+                }
+            ],
+            "text": "Activity C ►"
+        },
+        "activitylist": {
+            "id": "url_select_test",
+            "action": "#",
+            "options": [
+                {"name": "Jump to...", "value": "#0"},
+                {"name": "Activity A", "value": "#1"},
+                {"name": "Activity B", "value": "#2"},
+                {"name": "Activity C", "value": "#3"}
+            ]
+        }
+    }
+}}
+<div class="m-t-2 m-b-1">
+{{< core/columns-1to1to1}}
+    {{$column1}}
+        <div class="pull-left">
+            {{#prevlink}}{{> core/action_link }}{{/prevlink}}
+        </div>
+    {{/column1}}
+    {{$column2}}
+        <div class="mdl-align">
+            {{#activitylist}}{{> core/url_select }}{{/activitylist}}
+        </div>
+    {{/column2}}
+    {{$column3}}
+        <div class="pull-right">
+            {{#nextlink}}{{> core/action_link }}{{/nextlink}}
+        </div>
+    {{/column3}}
+{{/ core/columns-1to1to1}}
+</div>
index acd8219..4ae4483 100644 (file)
@@ -26,7 +26,7 @@ Feature: Browse course list and return back from enrolment page
     Then I should see "Courses" in the ".breadcrumb-nav" "css_element"
     And I click on "Courses" "link" in the ".breadcrumb-nav" "css_element"
     And I follow "Sample category"
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I press "Continue"
     And I should see "Sample category" in the ".breadcrumb-nav" "css_element"
 
index 0f929bd..3e88929 100644 (file)
@@ -12,9 +12,9 @@ Feature: Course paged mode
     And I log in as "admin"
     And I am on "Course 1" course homepage
     Then I click on <section2> "link" in the <section2> "section"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I click on <section3> "link" in the <section3> "section"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I click on <section1> "link" in the <section1> "section"
     And I should see <section1> in the "div.single-section" "css_element"
     And I should see <section2> in the ".single-section span.mdl-right" "css_element"
@@ -45,9 +45,9 @@ Feature: Course paged mode
     And I log in as "admin"
     And I am on "Course 1" course homepage
     Then I click on <section2> "link" in the <section2> "section"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I click on <section3> "link" in the <section3> "section"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I click on <section1> "link" in the <section1> "section"
     And I should see <section1> in the "div.single-section" "css_element"
     And I should see <section2> in the ".single-section span.mdl-right" "css_element"
index 7c1eef2..9e16980 100644 (file)
@@ -237,8 +237,8 @@ class enrol_cohort_plugin extends enrol_plugin {
         if ($this->allow_unenrol_user($instance, $ue) && has_capability('enrol/cohort:unenrol', $context)) {
             $url = new moodle_url('/enrol/unenroluser.php', $params);
             $actionparams = array('class' => 'unenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_UNENROL);
-            $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url,
-                $actionparams);
+            $actions[] = new user_enrolment_action(new pix_icon('t/delete', get_string('unenrol', 'enrol')),
+                get_string('unenrol', 'enrol'), $url, $actionparams);
         }
         return $actions;
     }
index 890942a..c9b3686 100644 (file)
@@ -98,8 +98,8 @@ class enrol_database_plugin extends enrol_plugin {
         if ($this->allow_unenrol_user($instance, $ue) && has_capability('enrol/database:unenrol', $context)) {
             $url = new moodle_url('/enrol/unenroluser.php', $params);
             $actionparams = array('class' => 'unenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_UNENROL);
-            $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url,
-                $actionparams);
+            $actions[] = new user_enrolment_action(new pix_icon('t/delete', get_string('unenrol', 'enrol')),
+                get_string('unenrol', 'enrol'), $url, $actionparams);
         }
         return $actions;
     }
index 4985848..fa4b6b8 100644 (file)
@@ -133,13 +133,14 @@ class enrol_flatfile_plugin extends enrol_plugin {
         if ($this->allow_unenrol_user($instance, $ue) && has_capability("enrol/flatfile:unenrol", $context)) {
             $url = new moodle_url('/enrol/unenroluser.php', $params);
             $actionparams = array('class' => 'unenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_UNENROL);
-            $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url,
-                $actionparams);
+            $actions[] = new user_enrolment_action(new pix_icon('t/delete', get_string('unenrol', 'enrol')),
+                get_string('unenrol', 'enrol'), $url, $actionparams);
         }
         if ($this->allow_manage($instance) && has_capability("enrol/flatfile:manage", $context)) {
             $url = new moodle_url('/enrol/editenrolment.php', $params);
             $actionparams = array('class' => 'editenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_EDIT);
-            $actions[] = new user_enrolment_action(new pix_icon('t/edit', ''), get_string('edit'), $url, $actionparams);
+            $actions[] = new user_enrolment_action(new pix_icon('t/edit', get_string('editenrolment', 'enrol')),
+                get_string('editenrolment', 'enrol'), $url, $actionparams);
         }
         return $actions;
     }
index 4699b3a..f8b7ca4 100644 (file)
@@ -521,13 +521,12 @@ class course_enrolment_manager {
     /**
      * Returns all of the enrolment instances for this course.
      *
-     * NOTE: since 2.4 it includes instances of disabled plugins too.
-     *
+     * @param bool $onlyenabled Whether to return data from enabled enrolment instance names only.
      * @return array
      */
-    public function get_enrolment_instances() {
+    public function get_enrolment_instances($onlyenabled = false) {
         if ($this->_instances === null) {
-            $this->_instances = enrol_get_instances($this->course->id, false);
+            $this->_instances = enrol_get_instances($this->course->id, $onlyenabled);
         }
         return $this->_instances;
     }
@@ -535,13 +534,12 @@ class course_enrolment_manager {
     /**
      * Returns the names for all of the enrolment instances for this course.
      *
-     * NOTE: since 2.4 it includes instances of disabled plugins too.
-     *
+     * @param bool $onlyenabled Whether to return data from enabled enrolment instance names only.
      * @return array
      */
-    public function get_enrolment_instance_names() {
+    public function get_enrolment_instance_names($onlyenabled = false) {
         if ($this->_inames === null) {
-            $instances = $this->get_enrolment_instances();
+            $instances = $this->get_enrolment_instances($onlyenabled);
             $plugins = $this->get_enrolment_plugins(false);
             foreach ($instances as $key=>$instance) {
                 if (!isset($plugins[$instance->enrol])) {
index 0edfbfb..3e6cb2e 100644 (file)
@@ -393,13 +393,14 @@ class enrol_lti_plugin extends enrol_plugin {
         if ($this->allow_unenrol_user($instance, $ue) && has_capability("enrol/lti:unenrol", $context)) {
             $url = new moodle_url('/enrol/unenroluser.php', $params);
             $actionparams = array('class' => 'unenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_UNENROL);
-            $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url,
-                $actionparams);
+            $actions[] = new user_enrolment_action(new pix_icon('t/delete', get_string('unenrol', 'enrol')),
+                get_string('unenrol', 'enrol'), $url, $actionparams);
         }
         if ($this->allow_manage($instance) && has_capability("enrol/lti:manage", $context)) {
             $url = new moodle_url('/enrol/editenrolment.php', $params);
             $actionparams = array('class' => 'editenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_EDIT);
-            $actions[] = new user_enrolment_action(new pix_icon('t/edit', ''), get_string('edit'), $url, $actionparams);
+            $actions[] = new user_enrolment_action(new pix_icon('t/edit', get_string('editenrolment', 'enrol')),
+                get_string('editenrolment', 'enrol'), $url, $actionparam);
         }
         return $actions;
     }
index c71c246..676901d 100644 (file)
@@ -377,13 +377,14 @@ class enrol_manual_plugin extends enrol_plugin {
         if ($this->allow_unenrol_user($instance, $ue) && has_capability("enrol/manual:unenrol", $context)) {
             $url = new moodle_url('/enrol/unenroluser.php', $params);
             $actionparams = array('class' => 'unenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_UNENROL);
-            $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url,
-                $actionparams);
+            $actions[] = new user_enrolment_action(new pix_icon('t/delete', get_string('unenrol', 'enrol')),
+                get_string('unenrol', 'enrol'), $url, $actionparams);
         }
         if ($this->allow_manage($instance) && has_capability("enrol/manual:manage", $context)) {
             $url = new moodle_url('/enrol/editenrolment.php', $params);
             $actionparams = array('class' => 'editenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_EDIT);
-            $actions[] = new user_enrolment_action(new pix_icon('t/edit', ''), get_string('edit'), $url, $actionparams);
+            $actions[] = new user_enrolment_action(new pix_icon('t/edit', get_string('editenrolment', 'enrol')),
+                get_string('editenrolment', 'enrol'), $url, $actionparams);
         }
         return $actions;
     }
index 0b6bfd0..3aafd8a 100644 (file)
@@ -111,8 +111,8 @@ class enrol_meta_plugin extends enrol_plugin {
         if ($this->allow_unenrol_user($instance, $ue) && has_capability('enrol/meta:unenrol', $context)) {
             $url = new moodle_url('/enrol/unenroluser.php', $params);
             $actionparams = array('class' => 'unenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_UNENROL);
-            $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url,
-                $actionparams);
+            $actions[] = new user_enrolment_action(new pix_icon('t/delete', get_string('unenrol', 'enrol')),
+                get_string('unenrol', 'enrol'), $url, $actionparams);
         }
         return $actions;
     }
index 4478229..52d1d19 100644 (file)
@@ -92,12 +92,11 @@ Feature: Enrolments are synchronised with meta courses
     And I set the field "targetid" to "1"
     And I click on "Continue" "button" in the ".bcs-new-course" "css_element"
     And I press "Next"
-    And I set the field "Course name" to "Course 4"
+    And I set the field "Course name" to "Course 5"
     And I press "Next"
     And I press "Perform restore"
     And I trigger cron
-    And I am on course index
-    And I follow "Course 4"
+    And I am on "Course 5 copy 1" course homepage
     And I navigate to "Enrolment methods" node in "Course administration > Users"
     Then I should see "Course meta link (Course 1)"
     And I should see "Course meta link (Course 2)"
index 7ec6722..f2cab89 100644 (file)
@@ -292,13 +292,14 @@ class enrol_paypal_plugin extends enrol_plugin {
         if ($this->allow_unenrol($instance) && has_capability("enrol/paypal:unenrol", $context)) {
             $url = new moodle_url('/enrol/unenroluser.php', $params);
             $actionparams = array('class' => 'unenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_UNENROL);
-            $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url,
-                $actionparams);
+            $actions[] = new user_enrolment_action(new pix_icon('t/delete', get_string('unenrol', 'enrol')),
+                get_string('unenrol', 'enrol'), $url, $actionparams);
         }
         if ($this->allow_manage($instance) && has_capability("enrol/paypal:manage", $context)) {
             $url = new moodle_url('/enrol/editenrolment.php', $params);
             $actionparams = array('class' => 'editenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_EDIT);
-            $actions[] = new user_enrolment_action(new pix_icon('t/edit', ''), get_string('edit'), $url, $actionparams);
+            $actions[] = new user_enrolment_action(new pix_icon('t/edit', get_string('editenrolment', 'enrol')),
+                get_string('editenrolment', 'enrol'), $url, $actionparams);
         }
         return $actions;
     }
index 3519f76..a1bfd9b 100644 (file)
@@ -537,13 +537,14 @@ class enrol_self_plugin extends enrol_plugin {
         if ($this->allow_unenrol($instance) && has_capability("enrol/self:unenrol", $context)) {
             $url = new moodle_url('/enrol/unenroluser.php', $params);
             $actionparams = array('class' => 'unenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_UNENROL);
-            $actions[] = new user_enrolment_action(new pix_icon('t/delete', ''), get_string('unenrol', 'enrol'), $url,
-                $actionparams);
+            $actions[] = new user_enrolment_action(new pix_icon('t/delete', get_string('unenrol', 'enrol')),
+                get_string('unenrol', 'enrol'), $url, $actionparams);
         }
         if ($this->allow_manage($instance) && has_capability("enrol/self:manage", $context)) {
             $url = new moodle_url('/enrol/editenrolment.php', $params);
             $actionparams = array('class' => 'editenrollink', 'rel' => $ue->id, 'data-action' => ENROL_ACTION_EDIT);
-            $actions[] = new user_enrolment_action(new pix_icon('t/edit', ''), get_string('edit'), $url, $actionparams);
+            $actions[] = new user_enrolment_action(new pix_icon('t/edit', get_string('editenrolment', 'enrol')),
+                get_string('editenrolment', 'enrol'), $url, $actionparams);
         }
         return $actions;
     }
index aa88075..294ee58 100644 (file)
@@ -12,7 +12,6 @@ Feature: User can be enrolled into a course
       | fullname   | shortname |
       | Course 001 | C001      |
     And I log in as "admin"
-    And I am on course index
     And I am on "Course 001" course homepage
 
   Scenario: User can be enrolled without javascript
index 13f1b75..b1535e0 100644 (file)
@@ -24,8 +24,7 @@ Feature: We can choose what min or max grade to use when aggregating grades.
     And I log in as "admin"
     And I set the following administration settings values:
       | grade_minmaxtouse | Min and max grades as specified in grade item settings |
-    And I am on site homepage
-    And I follow "C1"
+    And I am on "C1" 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:
@@ -66,7 +65,7 @@ Feature: We can choose what min or max grade to use when aggregating grades.
       | Aggregation          | Natural |
     And I log out
     And I log in as "teacher1"
-    And I follow "C1"
+    And I am on "C1" course homepage
     And I navigate to "View > Grader report" in the course gradebook
     And I turn editing mode on
     And I give the grade "75.00" to the user "Student 1" for the grade item "MI 1"
index 84e3c02..13267b8 100644 (file)
@@ -944,6 +944,7 @@ $string['registerwithmoodleorg'] = 'Register your site';
 $string['registration'] = 'Registration';
 $string['registration_help'] = 'It is recommended that you register your site in order to receive security alerts and access to Moodle.net, our course sharing platform.';
 $string['registrationwarning'] = 'Your site is not yet registered.';
+$string['registrationwarningcontactadmin'] = 'Your site is not yet registered. Please notify your administrator.';
 $string['releasenoteslink'] = 'For information about this version of Moodle, please see the online <a target="_blank" href="{$a}">Release Notes</a>';
 $string['rememberusername'] = 'Remember username';
 $string['rememberusername_desc'] = 'Enable if you want to store permanent cookies with usernames during user login. Permanent cookies may be considered a privacy issue if used without consent.';
index bbdb91a..55a0e70 100644 (file)
@@ -60,3 +60,4 @@ error:badjson,core_badges
 error:backpackloginfailed,core_badges
 signinwithyouremail,core_badges
 sectionusedefaultname,core
+registermoochtips,core_hub
index 2b9b10a..b899919 100644 (file)
@@ -42,6 +42,7 @@ $string['err_numeric'] = 'You must enter a number here.';
 $string['err_rangelength'] = 'You must enter between {$a->format[0]} and {$a->format[1]} characters here.';
 $string['err_required'] = 'You must supply a value here.';
 $string['err_wrongfileextension'] = 'Some files ({$a->wrongfiles}) cannot be uploaded. Only file types {$a->whitelist} are allowed.';
+$string['filesofthesetypes'] = 'Accepted files types:';
 $string['filetypesany'] = 'All file types';
 $string['filetypesnotall'] = 'It is not allowed to select \'All file types\' here';
 $string['filetypesnotwhitelisted'] = 'These file types are not allowed here: {$a}';
index 3bf13d5..0d168b8 100644 (file)
@@ -164,7 +164,6 @@ $string['registeredmoodleorg'] = 'Moodle ({$a})';
 $string['registeredon'] = 'Where your site is registered';
 $string['registereduserdevices'] = 'Number of users with registered mobile devices ({$a})';
 $string['registeredactiveuserdevices'] = 'Number of active users with registered mobile devices which are receiving notifications ({$a})';
-$string['registermoochtips'] = 'Register your site with Moodle to get security alerts and access to Moodle.net, our course sharing platform.';
 $string['registersite'] = 'Register with {$a}';
 $string['registerwith'] = 'Register with a hub';
 $string['registrationconfirmed'] = 'Site registration confirmed';
@@ -268,3 +267,6 @@ $string['wrongurlformat'] = 'Bad URL format';
 $string['xmlrpcdisabledcommunity'] = 'The XML-RPC extension is not enabled on the server. You can not search and download courses.';
 $string['xmlrpcdisabledpublish'] = 'The XML-RPC extension is not enabled on the server. You can not publish courses or manage published courses.';
 $string['xmlrpcdisabledregistration'] = 'The XML-RPC extension is not enabled on the server. You will not be able to unregister or update your registration until you enable it.';
+
+// Deprecated since Moodle 3.4.
+$string['registermoochtips'] = 'Register your site with Moodle to get security alerts and access to Moodle.net, our course sharing platform.';
index 5edcbfc..e8928a9 100644 (file)
@@ -931,6 +931,7 @@ $string['hiddensections'] = 'Hidden sections';
 $string['hiddensections_help'] = 'This setting determines whether hidden sections are displayed to students in collapsed form (perhaps for a course in weekly format to indicate holidays) or are completely hidden.';
 $string['hiddensectionscollapsed'] = 'Hidden sections are shown in collapsed form';
 $string['hiddensectionsinvisible'] = 'Hidden sections are completely invisible';
+$string['hiddenwithbrackets'] = '(hidden)';
 $string['hide'] = 'Hide';
 $string['hideadvancedsettings'] = 'Hide advanced settings';
 $string['hidechartdata'] = 'Hide chart data';
@@ -982,6 +983,9 @@ $string['chooseuser'] = 'Choose a user';
 $string['courseduration'] = 'Course duration';
 $string['courseduration_desc'] = 'The course duration is used to calculate the default course end date. The course end date is only used for reports. Users can still enter the course after the end date.';
 $string['eventcontentviewed'] = 'Content viewed';
+$string['filter'] = 'Filter';
+$string['filteroption'] = '{$a->criteria}: {$a->value}';
+$string['filters'] = 'Filters';
 $string['icqnumber'] = 'ICQ number';
 $string['icon'] = 'Icon';
 $string['idnumber'] = 'ID number';
@@ -1343,6 +1347,7 @@ $string['nocomments'] = 'No comments';
 $string['nodstpresets'] = 'The administrator has not enabled Daylight Savings Time support.';
 $string['nofilesselected'] = 'No files have been selected to restore';
 $string['nofilesyet'] = 'No files have been uploaded to your course yet';
+$string['nofiltersapplied'] = 'No filters applied';
 $string['nograde'] = 'No grade';
 $string['nohelpforactivityorresource'] = 'There is currently no help associated with this resource or activity';
 $string['nochange'] = 'No change';
@@ -2003,6 +2008,7 @@ $string['userdescription'] = 'Description';
 $string['userdescription_help'] = 'This box enables you to enter some text about yourself which will then be displayed on your profile page for others to view.';
 $string['userdetails'] = 'User details';
 $string['userfiles'] = 'User files';
+$string['userfilterplaceholder'] = 'Search keyword or select filter';
 $string['userlist'] = 'User list';
 $string['usermenu'] = 'User menu';
 $string['username'] = 'Username';
index 45870a2..2bfc5df 100644 (file)
@@ -455,9 +455,9 @@ $string['useshowadvancedtochange'] = 'Use \'Show advanced\' to change';
 $string['viewingdefinitionofrolex'] = 'Viewing the definition of role \'{$a}\'';
 $string['viewrole'] = 'View role details';
 $string['webservice:createtoken'] = 'Create a web service token';
+$string['webservice:managealltokens'] = 'Manage all users\' web services';
 $string['webservice:createmobiletoken'] = 'Create a web service token for mobile access';
 $string['whydoesuserhavecap'] = 'Why does {$a->fullname} have capability {$a->capability} in context {$a->context}?';
 $string['whydoesusernothavecap'] = 'Why does {$a->fullname} not have capability {$a->capability} in context {$a->context}?';
 $string['xroleassignments'] = '{$a}\'s role assignments';
 $string['xuserswiththerole'] = 'Users with the role "{$a->role}"';
-
index ebfe938..057f9f6 100644 (file)
@@ -131,6 +131,7 @@ $string['norequiredcapability'] = 'No required capability';
 $string['notoken'] = 'The token list is empty.';
 $string['onesystemcontrolling'] = 'Allow an external system to control Moodle';
 $string['onesystemcontrollingdescription'] = 'The following steps help you to set up the Moodle web services to allow an external system to interact with Moodle. This includes setting up a token (security key) authentication method.';
+$string['onlyseecreatedtokens'] = 'Only tokens you own or created can be seen. You can still delete other tokens.';
 $string['operation'] = 'Operation';
 $string['optional'] = 'Optional';
 $string['passwordisexpired'] = 'Password is expired.';
index 5386669..410130e 100644 (file)
@@ -9524,84 +9524,29 @@ class admin_setting_managewebservicetokens extends admin_setting {
      * @return string
      */
     public function output_html($data, $query='') {
-        global $CFG, $OUTPUT, $DB, $USER;
+        global $CFG, $OUTPUT;
 
-        // display strings
-        $stroperation = get_string('operation', 'webservice');
-        $strtoken = get_string('token', 'webservice');
-        $strservice = get_string('service', 'webservice');
-        $struser = get_string('user');
-        $strcontext = get_string('context', 'webservice');
-        $strvaliduntil = get_string('validuntil', 'webservice');
-        $striprestriction = get_string('iprestriction', 'webservice');
+        require_once($CFG->dirroot . '/webservice/classes/token_table.php');
+        $baseurl = new moodle_url('/' . $CFG->admin . '/settings.php?section=webservicetokens');
 
         $return = $OUTPUT->box_start('generalbox webservicestokenui');
 
-        $table = new html_table();
-        $table->head  = array($strtoken, $struser, $strservice, $striprestriction, $strvaliduntil, $stroperation);
-        $table->colclasses = array('leftalign', 'leftalign', 'leftalign', 'centeralign', 'centeralign', 'centeralign');
-        $table->id = 'webservicetokens';
-        $table->attributes['class'] = 'admintable generaltable';
+        if (has_capability('moodle/webservice:managealltokens', context_system::instance())) {
+            $return .= \html_writer::div(get_string('onlyseecreatedtokens', 'webservice'));
+        }
+
+        $table = new \webservice\token_table('webservicetokens');
+        $table->define_baseurl($baseurl);
+        $table->attributes['class'] = 'admintable generaltable'; // Any need changing?
         $table->data  = array();
+        ob_start();
+        $table->out(10, false);
+        $tablehtml = ob_get_contents();
+        ob_end_clean();
+        $return .= $tablehtml;
 
         $tokenpageurl = "$CFG->wwwroot/$CFG->admin/webservice/tokens.php?sesskey=" . sesskey();
 
-        //TODO: in order to let the administrator delete obsolete token, split this request in multiple request or use LEFT JOIN
-
-        //here retrieve token list (including linked users firstname/lastname and linked services name)
-        $sql = "SELECT t.id, t.token, u.id AS userid, u.firstname, u.lastname, s.name, t.iprestriction, t.validuntil, s.id AS serviceid
-                  FROM {external_tokens} t, {user} u, {external_services} s
-                 WHERE t.creatorid=? AND t.tokentype = ? AND s.id = t.externalserviceid AND t.userid = u.id";
-        $tokens = $DB->get_records_sql($sql, array($USER->id, EXTERNAL_TOKEN_PERMANENT));
-        if (!empty($tokens)) {
-            foreach ($tokens as $token) {
-                //TODO: retrieve context
-
-                $delete = "<a href=\"".$tokenpageurl."&amp;action=delete&amp;tokenid=".$token->id."\">";
-                $delete .= get_string('delete')."</a>";
-
-                $validuntil = '';
-                if (!empty($token->validuntil)) {
-                    $validuntil = userdate($token->validuntil, get_string('strftimedatetime', 'langconfig'));
-                }
-
-                $iprestriction = '';
-                if (!empty($token->iprestriction)) {
-                    $iprestriction = $token->iprestriction;
-                }
-
-                $userprofilurl = new moodle_url('/user/profile.php?id='.$token->userid);
-                $useratag = html_writer::start_tag('a', array('href' => $userprofilurl));
-                $useratag .= $token->firstname." ".$token->lastname;
-                $useratag .= html_writer::end_tag('a');
-
-                //check user missing capabilities
-                require_once($CFG->dirroot . '/webservice/lib.php');
-                $webservicemanager = new webservice();
-                $usermissingcaps = $webservicemanager->get_missing_capabilities_by_users(
-                        array(array('id' => $token->userid)), $token->serviceid);
-
-                if (!is_siteadmin($token->userid) and
-                        array_key_exists($token->userid, $usermissingcaps)) {
-                    $missingcapabilities = implode(', ',
-                            $usermissingcaps[$token->userid]);
-                    if (!empty($missingcapabilities)) {
-                        $useratag .= html_writer::tag('div',
-                                        get_string('usermissingcaps', 'webservice',
-                                                $missingcapabilities)
-                                        . '&nbsp;' . $OUTPUT->help_icon('missingcaps', 'webservice'),
-                                        array('class' => 'missingcaps'));
-                    }
-                }
-
-                $table->data[] = array($token->token, $useratag, $token->name, $iprestriction, $validuntil, $delete);
-            }
-
-            $return .= html_writer::table($table);
-        } else {
-            $return .= get_string('notoken', 'webservice');
-        }
-
         $return .= $OUTPUT->box_end();
         // add a token to the table
         $return .= "<a href=\"".$tokenpageurl."&amp;action=create\">";
index 5e301a0..233e46e 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
index b43080a..17e7adf 100644 (file)
@@ -451,7 +451,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
         // Notifiy that the selection changed.
         notifyChange(originalSelect);
 
-        if (!options.multiple) {
+        if (options.closeSuggestionsOnSelect) {
             // Clear the input element.
             inputElement.val('');
             // Close the list of suggestions.
@@ -718,9 +718,11 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
          * @param {Boolean} caseSensitive - If search has to be made case sensitive.
          * @param {Boolean} showSuggestions - If suggestions should be shown
          * @param {String} noSelectionString - Text to display when there is no selection
+         * @param {Boolean} closeSuggestionsOnSelect - Whether to close the suggestions immediately after making a selection.
          * @return {Promise}
          */
-        enhance: function(selector, tags, ajax, placeholder, caseSensitive, showSuggestions, noSelectionString) {
+        enhance: function(selector, tags, ajax, placeholder, caseSensitive, showSuggestions, noSelectionString,
+                          closeSuggestionsOnSelect) {
             // Set some default values.
             var options = {
                 selector: selector,
@@ -769,6 +771,13 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
             };
             options.multiple = originalSelect.attr('multiple');
 
+            if (typeof closeSuggestionsOnSelect !== "undefined") {
+                options.closeSuggestionsOnSelect = closeSuggestionsOnSelect;
+            } else {
+                // If not specified, this will close suggestions by default for single-select elements only.
+                options.closeSuggestionsOnSelect = !options.multiple;
+            }
+
             var originalLabel = $('[for=' + state.selectId + ']');
             // Create the new markup and insert it after the select.
             var suggestions = [];
index acdaab3..62b8bf5 100644 (file)
@@ -728,6 +728,8 @@ class block_manager {
         $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = bi.id AND ctx.contextlevel = :contextlevel)";
 
         $systemcontext = context_system::instance();
+        list($bpcontext, $bpcontextidparams) = $DB->get_in_or_equal(array($context->id, $systemcontext->id),
+                SQL_PARAMS_NAMED, 'bpcontextid');
         $params = array(
             'contextlevel' => CONTEXT_BLOCK,
             'subpage1' => $this->page->subpage,
@@ -761,7 +763,7 @@ class block_manager {
                 FROM {block_instances} bi
                 JOIN {block} b ON bi.blockname = b.name
                 LEFT JOIN {block_positions} bp ON bp.blockinstanceid = bi.id
-                                                  AND bp.contextid = :contextid1
+                                                  AND bp.contextid $bpcontext
                                                   AND bp.pagetype = :pagetype
                                                   AND bp.subpage = :subpage1
                 $ccjoin
@@ -779,7 +781,8 @@ class block_manager {
                     COALESCE(bp.weight, bi.defaultweight),
                     bi.id";
 
-        $allparams = $params + $parentcontextparams + $pagetypepatternparams + $requiredbythemeparams + $requiredbythemenotparams;
+        $allparams = $params + $parentcontextparams + $pagetypepatternparams + $requiredbythemeparams;
+        $allparams = $allparams + $requiredbythemenotparams + $bpcontextidparams;
         $blockinstances = $DB->get_recordset_sql($sql, $allparams);
 
         $this->birecordsbyregion = $this->prepare_per_region_arrays();
index 966ea3f..a27e605 100644 (file)
@@ -752,7 +752,7 @@ class api {
         $record->issuerid = $issuer->get('id');
         $record->refreshtoken = $refreshtoken;
         $record->grantedscopes = $scopes;
-        $record->email = $userinfo['email'];
+        $record->email = isset($userinfo['email']) ? $userinfo['email'] : '';
         $record->username = $userinfo['username'];
 
         $systemaccount = new system_account(0, $record);
index 808d0b9..86c450c 100644 (file)
@@ -1845,6 +1845,13 @@ $capabilities = array(
             'manager' => CAP_ALLOW
         )
     ),
+    'moodle/webservice:managealltokens' => array(
+
+        'riskbitmask' => RISK_CONFIG | RISK_DATALOSS | RISK_PERSONAL,
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_SYSTEM,
+        'archetypes' => array()
+    ),
     'moodle/webservice:createmobiletoken' => array(
 
         'riskbitmask' => RISK_SPAM | RISK_PERSONAL,
index e947ca7..b83f318 100644 (file)
         <FIELD NAME="issuerid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The id of the oauth 2 identity issuer"/>
         <FIELD NAME="refreshtoken" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="The refresh token used to request access tokens."/>
         <FIELD NAME="grantedscopes" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="The scopes that this system account has been granted access to."/>
-        <FIELD NAME="email" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="The email that was connected to this issuer."/>
+        <FIELD NAME="email" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="The email that was connected to this issuer."/>
         <FIELD NAME="username" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="The username that was connected as a system account to this issue."/>
       </FIELDS>
       <KEYS>
index ca24cd9..36d4845 100644 (file)
@@ -2214,5 +2214,46 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2017072000.02);
     }
 
+    if ($oldversion < 2017072700.01) {
+        // Changing nullability of field email on table oauth2_system_account to null.
+        $table = new xmldb_table('oauth2_system_account');
+        $field = new xmldb_field('email', XMLDB_TYPE_TEXT, null, null, null, null, null, 'grantedscopes');
+
+        // Launch change of nullability for field email.
+        $dbman->change_field_notnull($table, $field);
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2017072700.01);
+    }
+
+    if ($oldversion < 2017072700.02) {
+
+        // If the site was previously registered with http://hub.moodle.org change the registration to
+        // point to https://moodle.net - this is the correct hub address using https protocol.
+        $oldhuburl = "http://hub.moodle.org";
+        $newhuburl = "https://moodle.net";
+        $cleanoldhuburl = preg_replace('/[^A-Za-z0-9_-]/i', '', $oldhuburl);
+        $cleannewhuburl = preg_replace('/[^A-Za-z0-9_-]/i', '', $newhuburl);
+
+        // Update existing registration.
+        $DB->execute("UPDATE {registration_hubs} SET hubname = ?, huburl = ? WHERE huburl = ?",
+            ['Moodle.net', $newhuburl, $oldhuburl]);
+
+        // Update settings of existing registration.
+        $sqlnamelike = $DB->sql_like('name', '?');
+        $entries = $DB->get_records_sql("SELECT * FROM {config_plugins} where plugin=? and " . $sqlnamelike,
+            ['hub', '%' . $DB->sql_like_escape('_' . $cleanoldhuburl)]);
+        foreach ($entries as $entry) {
+            $newname = substr($entry->name, 0, -strlen($cleanoldhuburl)) . $cleannewhuburl;
+            $DB->update_record('config_plugins', ['id' => $entry->id, 'name' => $newname]);
+        }
+
+        // Update published courses.
+        $DB->execute('UPDATE {course_published} SET huburl = ? WHERE huburl = ?', [$newhuburl, $oldhuburl]);
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2017072700.02);
+    }
+
     return true;
 }
index b3ecee8..efc610c 100644 (file)
@@ -329,13 +329,10 @@ class mysql_sql_generator extends sql_generator {
      * @return array of sql statements
      */
     public function getCreateTempTableSQL($xmldb_table) {
-        $engine = $this->mdb->get_dbengine();
         // Do we know collation?
         $collation = $this->mdb->get_dbcollation();
         $this->temptables->add_temptable($xmldb_table->getName());
 
-        $rowformat = $this->mdb->get_row_format_sql($engine, $collation);
-
         $sqlarr = parent::getCreateTableSQL($xmldb_table);
 
         // Let's inject the extra MySQL tweaks.
@@ -347,7 +344,7 @@ class mysql_sql_generator extends sql_generator {
                     if (strpos($collation, 'utf8_') === 0) {
                         $sqlarr[$i] .= " DEFAULT CHARACTER SET utf8";
                     }
-                    $sqlarr[$i] .= " DEFAULT COLLATE $collation $rowformat";
+                    $sqlarr[$i] .= " DEFAULT COLLATE $collation ROW_FORMAT=DYNAMIC";
                 }
             }
         }
index ffc7796..48debee 100644 (file)
@@ -88,6 +88,12 @@ class mariadb_native_moodle_database extends mysqli_native_moodle_database {
         return array('description'=>$this->mysqli->server_info, 'version'=>$version);
     }
 
+    protected function has_breaking_change_quoted_defaults() {
+        $version = $this->get_server_info()['version'];
+        // Breaking change since 10.2.7: MDEV-13132.
+        return version_compare($version, '10.2.7', '>=');
+    }
+
     /**
      * It is time to require transactions everywhere.
      *
index 15926ee..fee8b73 100644 (file)
@@ -715,7 +715,7 @@ class mysqli_native_moodle_database extends moodle_database {
                 $rawcolumn->numeric_scale            = null;
                 $rawcolumn->is_nullable              = $rawcolumn->null; unset($rawcolumn->null);
                 $rawcolumn->column_default           = $rawcolumn->default; unset($rawcolumn->default);
-                $rawcolumn->column_key               = $rawcolumn->key; unset($rawcolumn->default);
+                $rawcolumn->column_key               = $rawcolumn->key; unset($rawcolumn->key);
 
                 if (preg_match('/(enum|varchar)\((\d+)\)/i', $rawcolumn->column_type, $matches)) {
                     $rawcolumn->data_type = $matches[1];
@@ -783,6 +783,14 @@ class mysqli_native_moodle_database extends moodle_database {
         return $structure;
     }
 
+    /**
+     * Indicates whether column information retrieved from `information_schema.columns` has default values quoted or not.
+     * @return boolean True when default values are quoted (breaking change); otherwise, false.
+     */
+    protected function has_breaking_change_quoted_defaults() {
+        return false;
+    }
+
     /**
      * Returns moodle column info for raw column from information schema.
      * @param stdClass $rawcolumn
@@ -794,7 +802,11 @@ class mysqli_native_moodle_database extends moodle_database {
         $info->name           = $rawcolumn->column_name;
         $info->type           = $rawcolumn->data_type;
         $info->meta_type      = $this->mysqltype2moodletype($rawcolumn->data_type);
-        $info->default_value  = $rawcolumn->column_default;
+        if ($this->has_breaking_change_quoted_defaults()) {
+            $info->default_value = trim($rawcolumn->column_default, "'");
+        } else {
+            $info->default_value = $rawcolumn->column_default;
+        }
         $info->has_default    = !is_null($rawcolumn->column_default);
         $info->not_null       = ($rawcolumn->is_nullable === 'NO');
         $info->primary_key    = ($rawcolumn->column_key === 'PRI');
index 93f67dc..7f81f17 100644 (file)
@@ -1209,15 +1209,16 @@ function is_enrolled(context $context, $user = null, $withcapability = '', $only
  * @param int $group optional, 0 indicates no current group, otherwise the group id
  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
  * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
+ * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
  * @return \core\dml\sql_join Contains joins, wheres, params
  */
 function get_enrolled_with_capabilities_join(context $context, $prefix = '', $capability = '', $group = 0,
-        $onlyactive = false, $onlysuspended = false) {
+        $onlyactive = false, $onlysuspended = false, $enrolid = 0) {
     $uid = $prefix . 'u.id';
     $joins = array();
     $wheres = array();
 
-    $enrolledjoin = get_enrolled_join($context, $uid, $onlyactive, $onlysuspended);
+    $enrolledjoin = get_enrolled_join($context, $uid, $onlyactive, $onlysuspended, $enrolid);
     $joins[] = $enrolledjoin->joins;
     $wheres[] = $enrolledjoin->wheres;
     $params = $enrolledjoin->params;
@@ -1253,9 +1254,11 @@ function get_enrolled_with_capabilities_join(context $context, $prefix = '', $ca
  * @param int $groupid 0 means ignore groups, any other value limits the result by group id
  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
  * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
+ * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
  * @return array list($sql, $params)
  */
-function get_enrolled_sql(context $context, $withcapability = '', $groupid = 0, $onlyactive = false, $onlysuspended = false) {
+function get_enrolled_sql(context $context, $withcapability = '', $groupid = 0, $onlyactive = false, $onlysuspended = false,
+                          $enrolid = 0) {
 
     // Use unique prefix just in case somebody makes some SQL magic with the result.
     static $i = 0;
@@ -1263,7 +1266,7 @@ function get_enrolled_sql(context $context, $withcapability = '', $groupid = 0,
     $prefix = 'eu' . $i . '_';
 
     $capjoin = get_enrolled_with_capabilities_join(
-            $context, $prefix, $withcapability, $groupid, $onlyactive, $onlysuspended);
+            $context, $prefix, $withcapability, $groupid, $onlyactive, $onlysuspended, $enrolid);
 
     $sql = "SELECT DISTINCT {$prefix}u.id
               FROM {user} {$prefix}u
@@ -1285,9 +1288,10 @@ function get_enrolled_sql(context $context, $withcapability = '', $groupid = 0,
  * @param string $useridcolumn User id column used the calling query, e.g. u.id
  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
  * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
+ * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
  * @return \core\dml\sql_join Contains joins, wheres, params
  */
-function get_enrolled_join(context $context, $useridcolumn, $onlyactive = false, $onlysuspended = false) {
+function get_enrolled_join(context $context, $useridcolumn, $onlyactive = false, $onlysuspended = false, $enrolid = 0) {
     // Use unique prefix just in case somebody makes some SQL magic with the result.
     static $i = 0;
     $i++;
@@ -1315,7 +1319,18 @@ function get_enrolled_join(context $context, $useridcolumn, $onlyactive = false,
     if (!$isfrontpage) {
         $where1 = "{$prefix}ue.status = :{$prefix}active AND {$prefix}e.status = :{$prefix}enabled";
         $where2 = "{$prefix}ue.timestart < :{$prefix}now1 AND ({$prefix}ue.timeend = 0 OR {$prefix}ue.timeend > :{$prefix}now2)";
-        $ejoin = "JOIN {enrol} {$prefix}e ON ({$prefix}e.id = {$prefix}ue.enrolid AND {$prefix}e.courseid = :{$prefix}courseid)";
+
+        $enrolconditions = array(
+            "{$prefix}e.id = {$prefix}ue.enrolid",
+            "{$prefix}e.courseid = :{$prefix}courseid",
+        );
+        if ($enrolid) {
+            $enrolconditions[] = "{$prefix}e.id = :{$prefix}enrolid";
+            $params[$prefix . 'enrolid'] = $enrolid;
+        }
+        $enrolconditionssql = implode(" AND ", $enrolconditions);
+        $ejoin = "JOIN {enrol} {$prefix}e ON ($enrolconditionssql)";
+
         $params[$prefix.'courseid'] = $coursecontext->instanceid;
 
         if (!$onlysuspended) {
@@ -1329,8 +1344,16 @@ function get_enrolled_join(context $context, $useridcolumn, $onlyactive = false,
             // Consider multiple enrols where one is not suspended or plain role_assign.
             $enrolselect = "SELECT DISTINCT {$prefix}ue.userid FROM {user_enrolments} {$prefix}ue $ejoin WHERE $where1 AND $where2";
             $joins[] = "JOIN {user_enrolments} {$prefix}ue1 ON {$prefix}ue1.userid = $useridcolumn";
-            $joins[] = "JOIN {enrol} {$prefix}e1 ON ({$prefix}e1.id = {$prefix}ue1.enrolid
-                    AND {$prefix}e1.courseid = :{$prefix}_e1_courseid)";
+            $enrolconditions = array(
+                "{$prefix}e1.id = {$prefix}ue1.enrolid",
+                "{$prefix}e1.courseid = :{$prefix}_e1_courseid",
+            );
+            if ($enrolid) {
+                $enrolconditions[] = "{$prefix}e1.id = :{$prefix}e1.enrolid";
+                $params[$prefix . 'e1.enrolid'] = $enrolid;
+            }
+            $enrolconditionssql = implode(" AND ", $enrolconditions);
+            $joins[] = "JOIN {enrol} {$prefix}e1 ON ($enrolconditionssql)";
             $params["{$prefix}_e1_courseid"] = $coursecontext->instanceid;
             $wheres[] = "$useridcolumn NOT IN ($enrolselect)";
         }
index 93ff4cf..71686d0 100644 (file)
@@ -300,6 +300,14 @@ class MoodleQuickForm_filemanager extends HTML_QuickForm_element implements temp
         // label element needs 'for' attribute work
         $html .= html_writer::empty_tag('input', array('value' => '', 'id' => 'id_'.$elname, 'type' => 'hidden'));
 
+        if (!empty($options->accepted_types) && $options->accepted_types != '*') {
+            $html .= html_writer::tag('p', get_string('filesofthesetypes', 'form'));
+            $util = new \core_form\filetypes_util();
+            $filetypes = $options->accepted_types;
+            $filetypedescriptions = $util->describe_file_types($filetypes);
+            $html .= $OUTPUT->render_from_template('core_form/filetypes-descriptions', $filetypedescriptions);
+        }
+
         return $html;
     }
 
index c1485b2..a376459 100644 (file)
@@ -184,6 +184,14 @@ class MoodleQuickForm_filepicker extends HTML_QuickForm_input implements templat
         $html .= "<div><object type='text/html' data='$nonjsfilepicker' height='160' width='600' style='border:1px solid #000'></object></div>";
         $html .= '</noscript>';
 
+        if (!empty($options->accepted_types) && $options->accepted_types != '*') {
+            $html .= html_writer::tag('p', get_string('filesofthesetypes', 'form'));
+            $util = new \core_form\filetypes_util();
+            $filetypes = $options->accepted_types;
+            $filetypedescriptions = $util->describe_file_types($filetypes);
+            $html .= $OUTPUT->render_from_template('core_form/filetypes-descriptions', $filetypedescriptions);
+        }
+
         return $html;
     }
 
index 96d13f4..9e5ede0 100644 (file)
@@ -488,13 +488,14 @@ define('HOMEPAGE_USER', 2);
 /**
  * Hub directory url (should be moodle.org)
  */
-define('HUB_HUBDIRECTORYURL', "http://hubdirectory.moodle.org");
+define('HUB_HUBDIRECTORYURL', "https://hubdirectory.moodle.org");
 
 
 /**
- * Moodle.org url (should be moodle.org)
+ * Moodle.net url (should be moodle.net)
  */
-define('HUB_MOODLEORGHUBURL', "http://hub.moodle.org");
+define('HUB_MOODLEORGHUBURL', "https://moodle.net");
+define('HUB_OLDMOODLEORGHUBURL', "http://hub.moodle.org");
 
 /**
  * Moodle mobile app service name
index 8f3e795..f918112 100644 (file)
@@ -557,6 +557,10 @@ abstract class oauth2_client extends curl {
 
         $r = json_decode($response);
 
+        if (is_null($r)) {
+            throw new moodle_exception("Could not decode JSON token response");
+        }
+
         if (!empty($r->error)) {
             throw new moodle_exception($r->error . ' ' . $r->error_description);
         }
@@ -610,6 +614,9 @@ abstract class oauth2_client extends curl {
             }
         }
 
+        // Force JSON format content in response.
+        $this->setHeader('Accept: application/json');
+
         $response = parent::request($murl->out(false), $options);
 
         $this->resetHeader();
index 08e917b..57a2b12 100644 (file)
@@ -813,6 +813,85 @@ class core_renderer extends renderer_base {
         return '<div role="main">'.$this->unique_main_content_token.'</div>';
     }
 
+    /**
+     * Returns standard navigation between activities in a course.
+     *
+     * @return string the navigation HTML.
+     */
+    public function activity_navigation() {
+        // First we should check if we want to add navigation.
+        $context = $this->page->context;
+        if (($this->page->pagelayout !== 'incourse' && $this->page->pagelayout !== 'frametop')
+            || $context->contextlevel != CONTEXT_MODULE) {
+            return '';
+        }
+
+        // If the activity is in stealth mode, show no links.
+        if ($this->page->cm->is_stealth()) {
+            return '';
+        }
+
+        // Get a list of all the activities in the course.
+        $course = $this->page->cm->get_course();
+        $modules = get_fast_modinfo($course->id)->get_cms();
+
+        // Put the modules into an array in order by the position they are shown in the course.
+        $mods = [];
+        $activitylist = [];
+        foreach ($modules as $module) {
+            // Only add activities the user can access, aren't in stealth mode and have a url (eg. mod_label does not).
+            if (!$module->uservisible || $module->is_stealth() || empty($module->url)) {
+                continue;
+            }
+            $mods[$module->id] = $module;
+
+            // No need to add the current module to the list for the activity dropdown menu.
+            if ($module->id == $this->page->cm->id) {
+                continue;
+            }
+            // Module name.
+            $modname = $module->get_formatted_name();
+            // Display the hidden text if necessary.
+            if (!$module->visible) {
+                $modname .= ' ' . get_string('hiddenwithbrackets');
+            }
+            // Module URL.
+            $linkurl = new moodle_url($module->url, array('forceview' => 1));
+            // Add module URL (as key) and name (as value) to the activity list array.
+            $activitylist[$linkurl->out(false)] = $modname;
+        }
+
+        $nummods = count($mods);
+
+        // If there is only one mod then do nothing.
+        if ($nummods == 1) {
+            return '';
+        }
+
+        // Get an array of just the course module ids used to get the cmid value based on their position in the course.
+        $modids = array_keys($mods);
+
+        // Get the position in the array of the course module we are viewing.
+        $position = array_search($this->page->cm->id, $modids);
+
+        $prevmod = null;
+        $nextmod = null;
+
+        // Check if we have a previous mod to show.
+        if ($position > 0) {
+            $prevmod = $mods[$modids[$position - 1]];
+        }
+
+        // Check if we have a next mod to show.
+        if ($position < ($nummods - 1)) {
+            $nextmod = $mods[$modids[$position + 1]];
+        }
+
+        $activitynav = new \core_course\output\activity_navigation($prevmod, $nextmod, $activitylist);
+        $renderer = $this->page->get_renderer('core', 'course');
+        return $renderer->render($activitynav);
+    }
+
     /**
      * The standard tags (typically script tags that are not needed earlier) that
      * should be output after everything else. Designed to be called in theme layout.php files.
@@ -3176,7 +3255,7 @@ EOD;
             array('role' => 'button', 'tabindex' => 0));
         $formattrs = array('class' => 'search-input-form', 'action' => $CFG->wwwroot . '/search/index.php');
         $inputattrs = array('type' => 'text', 'name' => 'q', 'placeholder' => get_string('search', 'search'),
-            'size' => 13, 'tabindex' => -1, 'id' => 'id_q_' . $id);
+            'size' => 13, 'tabindex' => -1, 'id' => 'id_q_' . $id, 'class' => 'form-control');
 
         $contents = html_writer::tag('label', get_string('enteryoursearchquery', 'search'),
             array('for' => 'id_q_' . $id, 'class' => 'accesshide')) . html_writer::tag('input', '', $inputattrs);
diff --git a/lib/phpunit/classes/restore_date_testcase.php b/lib/phpunit/classes/restore_date_testcase.php
new file mode 100644 (file)
index 0000000..bc80ae7
--- /dev/null
@@ -0,0 +1,163 @@
+<?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/>.
+
+/**
+ * Restore dates test case.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2017 onwards Ankit Agarwal
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
+
+
+/**
+ * Advanced PHPUnit test case customised for testing restore dates in Moodle.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2017 onwards Ankit Agarwal
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class restore_date_testcase extends advanced_testcase {
+    /**
+     * @var int Course start date.
+     */
+    protected $startdate;
+
+    /**
+     * @var int Course restore date.
+     */
+    protected $restorestartdate;
+
+    /**
+     * Setup.
+     */
+    public function setUp() {
+        global $CFG;
+
+        parent::setUp();
+        $this->resetAfterTest();
+        $this->setAdminUser();
+        $this->startdate = strtotime('1 Jan 2017 00:00 GMT');
+        $this->restorestartdate = strtotime('1 Feb 2017 00:00 GMT');
+        $CFG->enableavailability = true;
+    }
+
+    /**
+     * Backs a course up and restores it.
+     *
+     * @param stdClass $course Course object to backup
+     * @param int $newdate If non-zero, specifies custom date for new course
+     * @return int ID of newly restored course
+     */
+    protected function backup_and_restore($course, $newdate = 0) {
+        global $USER, $CFG;
+
+        // Turn off file logging, otherwise it can't delete the file (Windows).
+        $CFG->backup_file_logger_level = backup::LOG_NONE;
+
+        // Do backup with default settings.
+        set_config('backup_general_users', 1, 'backup');
+        $bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
+            backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_GENERAL,
+            $USER->id);
+        $bc->execute_plan();
+        $results = $bc->get_results();
+        $file = $results['backup_destination'];
+        $fp = get_file_packer('application/vnd.moodle.backup');
+        $filepath = $CFG->dataroot . '/temp/backup/test-restore-course';
+        $file->extract_to_pathname($fp, $filepath);
+        $bc->destroy();
+
+        // Do restore to new course with default settings.
+        $newcourseid = restore_dbops::create_new_course(
+            $course->fullname, $course->shortname . '_2', $course->category);
+        $rc = new restore_controller('test-restore-course', $newcourseid,
+            backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
+            backup::TARGET_NEW_COURSE);
+
+        if (empty($newdate)) {
+            $newdate = $this->restorestartdate;
+        }
+
+        $rc->get_plan()->get_setting('course_startdate')->set_value($newdate);
+        $this->assertTrue($rc->execute_precheck());
+        $rc->execute_plan();
+        $rc->destroy();
+
+        return $newcourseid;
+    }
+
+    /**
+     * Helper method to create a course and a module.
+     *
+     * @param string $modulename
+     * @param array|stdClass $record
+     * @return array
+     */
+    protected function create_course_and_module($modulename, $record = []) {
+        // Create a course with specific start date.
+        $record = (array)$record;
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course(['startdate' => $this->startdate]);
+        $record = array_merge(['course' => $course->id], $record);
+        $module = $this->getDataGenerator()->create_module($modulename, $record);
+        return [$course, $module];
+    }
+
+    /**
+     * Verify that the given properties are not rolled.
+     *
+     * @param stdClass $oldinstance
+     * @param stdClass $newinstance
+     * @param [] $props
+     */
+    protected function assertFieldsNotRolledForward($oldinstance, $newinstance, $props) {
+        foreach ($props as $prop) {
+            $this->assertEquals($oldinstance->$prop, $newinstance->$prop, "'$prop' should not roll forward.");
+        }
+    }
+
+    /**
+     * Verify that the given properties are rolled.
+     *
+     * @param stdClass $oldinstance
+     * @param stdClass $newinstance
+     * @param [] $props
+     */
+    protected function assertFieldsRolledForward($oldinstance, $newinstance, $props) {
+        $diff = $this->get_diff();
+        foreach ($props as $prop) {
+            $this->assertEquals(($oldinstance->$prop + $diff), $newinstance->$prop, "'$prop' doesn't roll as expected.");
+        }
+    }
+
+    /**
+     * Get time diff between start date and restore date in seconds.
+     *
+     * @return mixed
+     */
+    protected function get_diff() {
+        return ($this->restorestartdate - $this->startdate);
+    }
+
+}
\ No newline at end of file
index dbfbbc1..ed67575 100644 (file)
@@ -64,7 +64,6 @@ Feature: Initials bar
       | student23 | C1     | student        |
       | student24 | C1     | student        |
 
-  @javascript
   Scenario: Filter users on assignment submission page
     Given the following "activities" exist:
       | activity | course | idnumber | name           | intro                       | assignsubmission_onlinetext_enabled | assignsubmission_file_enabled |
@@ -123,7 +122,6 @@ Feature: Initials bar
     And I should see "Bstudent Astudent"
     And I should see "Cstudent Cstudent"
 
-  @javascript
   Scenario: Filter users on view gradebook page
     Given the following "activities" exist:
       | activity | course | idnumber | name           | intro                       | assignsubmission_onlinetext_enabled | assignsubmission_file_enabled |
@@ -184,11 +182,16 @@ Feature: Initials bar
     And I should see "Bstudent Astudent"
     And I should see "Cstudent Cstudent"
 
-  @javascript
   Scenario: Filter users on course participants page
     Given the following "activities" exist:
       | activity | course | idnumber | name           | intro                       | assignsubmission_onlinetext_enabled | assignsubmission_file_enabled |
       | assign   | C1     | assign1  | TestAssignment | Test assignment description | 0                                   | 0                             |
+    And I log in as "student1"
+    And I am on "Course 1" course homepage
+    And I log out
+    And I log in as "student2"
+    And I am on "Course 1" course homepage
+    And I log out
     And I log in as "teacher"
     And I am on "Course 1" course homepage
     And I follow "Participants"
index ea0eace..bf81ed5 100644 (file)
@@ -32,6 +32,15 @@ information provided here is intended especially for developers.
   These attributes enable enrol actions to be rendered via modals. If not added, clicking on the enrolment action buttons will still
   redirect the user to the appropriate enrolment action page. Though optional, it is recommended to add these attributes for a
   better user experience when performing enrol actions.
+* New optional parameter $enrolid for the following functions:
+  - get_enrolled_join()
+  - get_enrolled_sql()
+  - get_enrolled_with_capabilities_join()
+  Setting this parameter to a non-zero value will add a condition to the query such that only users that were enrolled
+  with this enrolment method will be returned.
+* 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.
 
 === 3.3.1 ===
 
index 8cf80a1..dd14789 100644 (file)
@@ -159,7 +159,7 @@ OET;
         $shortlink = '((youtu|y2u)\.be/)';
 
         // Initial part of link.
-        $start = '~^https?://(www\.)?(' . $link . '|' . $shortlink . ')';
+        $start = '~^https?://((www|m)\.)?(' . $link . '|' . $shortlink . ')';
         // Middle bit: Video key value.
         $middle = '([a-z0-9\-_]+)';
         return $start . $middle . core_media_player_external::END_LINK_REGEX_PART;
index ffaf52e..5c9d851 100644 (file)
@@ -70,6 +70,9 @@ class media_youtube_testcase extends advanced_testcase {
         $url = new moodle_url('http://www.youtube.com/v/vyrwMmsufJc');
         $t = $manager->embed_url($url);
         $this->assertContains('</iframe>', $t);
+        $url = new moodle_url('http://m.youtube.com/watch?v=vyrwMmsufJc');
+        $t = $manager->embed_url($url);
+        $this->assertContains('</iframe>', $t);
 
         // Format: youtube video within playlist.
         $url = new moodle_url('https://www.youtube.com/watch?v=dv2f_xfmbD8&index=4&list=PLxcO_MFWQBDcyn9xpbmx601YSDlDcTcr0');
index 19c888f..7896464 100644 (file)
@@ -41,7 +41,7 @@ $string['nodevices'] = 'No registered devices. Devices will automatically appear
 $string['nopermissiontomanagedevices'] = 'You don\'t have permission to manage devices.';
 $string['notconfigured'] = 'The Airnotifier server hasn\'t been configured so Airnotifier messages cannot be sent';
 $string['pluginname'] = 'Mobile';
-$string['sitemustberegistered'] = 'In order to use the public Airnotifier instance you must register your site with Moodle.org';
+$string['sitemustberegistered'] = 'In order to use the public Airnotifier instance you must register your site with Moodle.net';
 $string['showhide'] = 'Enable/disable the device.';
 $string['requestaccesskey'] = 'Request access key';
 $string['unknowndevice'] = 'Unknown device';
index 0b5ff28..3d1991f 100644 (file)
@@ -23,7 +23,6 @@
  */
 
 require('../../../config.php');
-require_once($CFG->dirroot . '/' . $CFG->admin . '/registration/lib.php');
 
 define('AIRNOTIFIER_PUBLICURL', 'https://messages.moodle.net');
 
@@ -50,10 +49,10 @@ $msg = "";
 // If we are requesting a key to the official message system, verify first that this site is registered.
 // This check is also done in Airnotifier.
 if (strpos($CFG->airnotifierurl, AIRNOTIFIER_PUBLICURL) !== false ) {
-    $registrationmanager = new registration_manager();
-    if (!$registrationmanager->get_registeredhub(HUB_MOODLEORGHUBURL)) {
-        $msg = get_string('sitemustberegistered', 'message_airnotifier');
-        $msg .= $OUTPUT->continue_button($returl);
+    $adminrenderer = $PAGE->get_renderer('core', 'admin');
+    $msg = $adminrenderer->warn_if_not_registered();
+    if ($msg) {
+        $msg .= html_writer::div(get_string('sitemustberegistered', 'message_airnotifier'));
 
         echo $OUTPUT->header();
         echo $OUTPUT->box($msg, 'generalbox');
index 64bc4b5..981ad27 100644 (file)
@@ -87,7 +87,8 @@ class restore_assign_activity_structure_step extends restore_activity_structure_
         $oldid = $data->id;
         $data->course = $this->get_courseid();
 
-        $data->timemodified = $this->apply_date_offset($data->timemodified);
+        // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
+        // See MDL-9367.
         $data->allowsubmissionsfromdate = $this->apply_date_offset($data->allowsubmissionsfromdate);
         $data->duedate = $this->apply_date_offset($data->duedate);
 
@@ -159,8 +160,6 @@ class restore_assign_activity_structure_step extends restore_activity_structure_
 
         $data->assignment = $this->get_new_parentid('assign');
 
-        $data->timemodified = $this->apply_date_offset($data->timemodified);
-        $data->timecreated = $this->apply_date_offset($data->timecreated);
         if ($data->userid > 0) {
             $data->userid = $this->get_mappingid('user', $data->userid);
         }
@@ -220,8 +219,6 @@ class restore_assign_activity_structure_step extends restore_activity_structure_
 
         $data->assignment = $this->get_new_parentid('assign');
 
-        $data->timemodified = $this->apply_date_offset($data->timemodified);
-        $data->timecreated = $this->apply_date_offset($data->timecreated);
         $data->userid = $this->get_mappingid('user', $data->userid);
         $data->grader = $this->get_mappingid('user', $data->grader);
 
index 5a16442..003229b 100644 (file)
@@ -1035,6 +1035,8 @@ class assign {
                        WHERE assignid =? AND cutoffdate <> 0",
                 array($data->timeshift, $this->get_instance()->id));
 
+            // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
+            // See MDL-9367.
             shift_course_mod_dates('assign',
                                     array('duedate', 'allowsubmissionsfromdate', 'cutoffdate'),
                                     $data->timeshift,
index c501bf8..45dd3ff 100644 (file)
@@ -24,7 +24,7 @@
 
 
 $string['acceptedfiletypes'] = 'Accepted file types';
-$string['acceptedfiletypes_help'] = 'Accepted file types can be restricted by entering a comma-separated list of mimetypes, e.g. video/mp4, audio/mp3, image/png, image/jpeg, or file extensions including a dot, e.g. .png, .jpg. If the field is left empty, then all file types are allowed.';
+$string['acceptedfiletypes_help'] = 'Accepted file types can be restricted by entering a list of file extensions. If the field is left empty, then all file types are allowed.';
 $string['configmaxbytes'] = 'Maximum file size';
 $string['countfiles'] = '{$a} files';
 $string['default'] = 'Enabled by default';
@@ -34,8 +34,6 @@ $string['enabled'] = 'File submissions';
 $string['enabled_help'] = 'If enabled, students are able to upload one or more files as their submission.';
 $string['eventassessableuploaded'] = 'A file has been uploaded.';
 $string['file'] = 'File submissions';
-$string['filesofthesetypes'] = 'Files of these types may be added to the submission:';
-$string['filetypewithexts'] = '{$a->name} &mdash; {$a->extlist}';
 $string['maxbytes'] = 'Maximum file size';
 $string['maxfiles'] = 'Maximum files per submission';
 $string['maxfiles_help'] = 'If file submissions are enabled, each assignment can be set to accept up to this number of files for their submission.';
@@ -43,8 +41,11 @@ $string['maxfilessubmission'] = 'Maximum number of uploaded files';
 $string['maxfilessubmission_help'] = 'If file submissions are enabled, each student will be able to upload up to this number of files for their submission.';
 $string['maximumsubmissionsize'] = 'Maximum submission size';
 $string['maximumsubmissionsize_help'] = 'Files uploaded by students may be up to this size.';
-$string['nonexistentfiletypes'] = 'The following file types were not recognised: {$a}';
 $string['numfilesforlog'] = 'The number of file(s) : {$a} file(s).';
 $string['pluginname'] = 'File submissions';
 $string['siteuploadlimit'] = 'Site upload limit';
 $string['submissionfilearea'] = 'Uploaded submission files';
+// Deprecated since Moodle 3.4.
+$string['filesofthesetypes'] = 'Files of these types may be added to the submission:';
+$string['filetypewithexts'] = '{$a->name} &mdash; {$a->extlist}';
+$string['nonexistentfiletypes'] = 'The following file types were not recognised: {$a}';
diff --git a/mod/assign/submission/file/lang/en/deprecated.txt b/mod/assign/submission/file/lang/en/deprecated.txt
new file mode 100644 (file)
index 0000000..c3ca9ee
--- /dev/null
@@ -0,0 +1,3 @@
+filesofthesetypes,assignsubmission_file
+filetypewithexts,assignsubmission_file
+nonexistentfiletypes,assignsubmission_file
index d96b293..8a82199 100644 (file)
@@ -113,23 +113,10 @@ class assign_submission_file extends assign_submission_plugin {
                            'notchecked');
 
         $name = get_string('acceptedfiletypes', 'assignsubmission_file');
-        $mform->addElement('text', 'assignsubmission_file_filetypes', $name, array('size' => '60'));
+        $mform->addElement('filetypes', 'assignsubmission_file_filetypes', $name);
         $mform->addHelpButton('assignsubmission_file_filetypes', 'acceptedfiletypes', 'assignsubmission_file');
-        $mform->setType('assignsubmission_file_filetypes', PARAM_RAW);
         $mform->setDefault('assignsubmission_file_filetypes', $defaultfiletypes);
         $mform->disabledIf('assignsubmission_file_filetypes', 'assignsubmission_file_enabled', 'notchecked');
-        $mform->addFormRule(function ($values, $files) {
-            if (empty($values['assignsubmission_file_filetypes'])) {
-                return true;
-            }
-            $nonexistent = $this->get_nonexistent_file_types($values['assignsubmission_file_filetypes']);
-            if (empty($nonexistent)) {
-                return true;
-            } else {
-                $a = join(' ', $nonexistent);
-                return ["assignsubmission_file_filetypes" => get_string('nonexistentfiletypes', 'assignsubmission_file', $a)];
-            }
-        });
     }
 
     /**
@@ -160,7 +147,7 @@ class assign_submission_file extends assign_submission_plugin {
         $fileoptions = array('subdirs' => 1,
                                 'maxbytes' => $this->get_config('maxsubmissionsizebytes'),
                                 'maxfiles' => $this->get_config('maxfilesubmissions'),
-                                'accepted_types' => $this->get_accepted_types(),
+                                'accepted_types' => $this->get_configured_typesets(),
                                 'return_types' => (FILE_INTERNAL | FILE_CONTROLLED_LINK));
         if ($fileoptions['maxbytes'] == 0) {
             // Use module default.
@@ -178,6 +165,7 @@ class assign_submission_file extends assign_submission_plugin {
      * @return bool
      */
     public function get_form_elements($submission, MoodleQuickForm $mform, stdClass $data) {
+        global $OUTPUT;
 
         if ($this->get_config('maxfilesubmissions') <= 0) {
             return false;
@@ -195,34 +183,6 @@ class assign_submission_file extends assign_submission_plugin {
                                                   $submissionid);
         $mform->addElement('filemanager', 'files_filemanager', $this->get_name(), null, $fileoptions);
 
-        if (!empty($this->get_config('filetypeslist'))) {
-            $text = html_writer::tag('p', get_string('filesofthesetypes', 'assignsubmission_file'));
-            $text .= html_writer::start_tag('ul');
-
-            $typesets = $this->get_configured_typesets();
-            foreach ($typesets as $type) {
-                $a = new stdClass();
-                $extensions = file_get_typegroup('extension', $type);
-                $typetext = html_writer::tag('li', $type);
-                // Only bother checking if it's a mimetype or group if it has extensions in the group.
-                if (!empty($extensions)) {
-                    if (strpos($type, '/') !== false) {
-                        $a->name = get_mimetype_description($type);
-                        $a->extlist = implode(' ', $extensions);
-                        $typetext = html_writer::tag('li', get_string('filetypewithexts', 'assignsubmission_file', $a));
-                    } else if (get_string_manager()->string_exists("group:$type", 'mimetypes')) {
-                        $a->name = get_string("group:$type", 'mimetypes');
-                        $a->extlist = implode(' ', $extensions);
-                        $typetext = html_writer::tag('li', get_string('filetypewithexts', 'assignsubmission_file', $a));
-                    }
-                }
-                $text .= $typetext;
-            }
-
-            $text .= html_writer::end_tag('ul');
-            $mform->addElement('static', '', '', $text);
-        }
-
         return true;
     }
 
@@ -639,56 +599,9 @@ class assign_submission_file extends assign_submission_plugin {
     private function get_configured_typesets() {
         $typeslist = (string)$this->get_config('filetypeslist');
 
-        $sets = $this->get_typesets($typeslist);
-
-        return $sets;
-    }
+        $util = new \core_form\filetypes_util();
+        $sets = $util->normalize_file_types($typeslist);
 
-    /**
-     * Get the type sets passed.
-     *
-     * @param string $types The space , ; separated list of types
-     * @return array('groupname', 'mime/type', ...)
-     */
-    private function get_typesets($types) {
-        $sets = array();
-        if (!empty($types)) {
-            $sets = preg_split('/[\s,;:"\']+/', $types, null, PREG_SPLIT_NO_EMPTY);
-        }
         return $sets;
     }
-
-
-    /**
-     * Return the accepted types list for the file manager component.
-     *
-     * @return array|string
-     */
-    private function get_accepted_types() {
-        $acceptedtypes = $this->get_configured_typesets();
-
-        if (!empty($acceptedtypes)) {
-            return $acceptedtypes;
-        }
-
-        return '*';
-    }
-
-    /**
-     * List the nonexistent file types that need to be removed.
-     *
-     * @param string $types space , or ; separated types
-     * @return array A list of the nonexistent file types.
-     */
-    private function get_nonexistent_file_types($types) {
-        $nonexistent = [];
-        foreach ($this->get_typesets($types) as $type) {
-            // If there's no extensions under that group, it doesn't exist.
-            $extensions = file_get_typegroup('extension', [$type]);
-            if (empty($extensions)) {
-                $nonexistent[$type] = true;
-            }
-        }
-        return array_keys($nonexistent);
-    }
 }
index a895e62..9ed9b5e 100644 (file)
@@ -32,9 +32,9 @@ $settings->add(new admin_setting_configtext('assignsubmission_file/maxfiles',
                    new lang_string('maxfiles', 'assignsubmission_file'),
                    new lang_string('maxfiles_help', 'assignsubmission_file'), 20, PARAM_INT));
 
-$settings->add(new admin_setting_configtext('assignsubmission_file/filetypes',
+$settings->add(new admin_setting_filetypes('assignsubmission_file/filetypes',
                    new lang_string('defaultacceptedfiletypes', 'assignsubmission_file'),
-                   new lang_string('acceptedfiletypes_help', 'assignsubmission_file'), '', PARAM_RAW, 60));
+                   new lang_string('acceptedfiletypes_help', 'assignsubmission_file'), ''));
 
 if (isset($CFG->maxbytes)) {
 
index 34f1c07..9fd2ad4 100644 (file)
@@ -30,25 +30,32 @@ Feature: In an assignment, limit submittable file types
     And I navigate to "Edit settings" in current page administration
     When I set the field "Accepted file types" to "image/png;doesntexist;.anything;unreal/mimetype;nodot"
     And I press "Save and display"
-    And I should see "The following file types were not recognised: doesntexist .anything unreal/mimetype nodot"
+    And I should see "Unknown file types: .doesntexist, .anything, unreal/mimetype, .nodot"
     And I set the field "Accepted file types" to "image/png;spreadsheet"
     And I press "Save and display"
     And I navigate to "Edit settings" in current page administration
-    Then the field "Accepted file types" matches value "image/png;spreadsheet"
+    And the field "Accepted file types" matches value "image/png,spreadsheet"
+    And I set the field "Accepted file types" to ""
+    And I press "Choose"
+    And I set the field "Image files" to "1"
+    And I press "Save changes"
+    And I press "Save and display"
+    And I navigate to "Edit settings" in current page administration
+    Then the field "Accepted file types" matches value "image"
 
   @javascript @_file_upload
   Scenario: Uploading permitted file types for an assignment
     Given the following "activities" exist:
       | activity | course | idnumber | name                 | intro                       | duedate    | assignsubmission_onlinetext_enabled | assignsubmission_file_enabled | assignsubmission_file_maxfiles | assignsubmission_file_maxsizebytes | assignsubmission_file_filetypes |
-      | assign   | C1     | assign1  | Test assignment name | Test assignment description | 1388534400 | 0                                   | 1                             | 3                              | 0                                  | image/png;spreadsheet;.xml;.txt  |
+      | assign   | C1     | assign1  | Test assignment name | Test assignment description | 1388534400 | 0                                   | 1                             | 3                              | 0                                  | image/png,spreadsheet,.xml,.txt  |
     And I log in as "student1"
     And I am on "Course 1" course homepage
     And I follow "Test assignment name"
     When I press "Add submission"
     And I should see "Files of these types may be added to the submission"
-    And I should see "Image (PNG) — .png"
-    And I should see "Spreadsheet files — .csv .gsheet .ods .ots .xls .xlsx .xlsm"
-    And I should see ".txt"
+    And I should see "Image (PNG)"
+    And I should see "Spreadsheet files"
+    And I should see "Text file"
     And I upload "lib/tests/fixtures/gd-logo.png" file to "File submissions" filemanager
     And I upload "lib/tests/fixtures/tabfile.csv" file to "File submissions" filemanager
     And I upload "lib/tests/fixtures/empty.txt" file to "File submissions" filemanager
index 618a787..2291d62 100644 (file)
@@ -136,70 +136,4 @@ class assignsubmission_file_locallib_testcase extends advanced_testcase {
             'Without file' => [null, true]
         ];
     }
-
-    /**
-     * Data provider for testing test_get_nonexistent_file_types.
-     *
-     * @return array
-     */
-    public function get_nonexistent_file_types_provider() {
-        return [
-            'Nonexistent extensions are not allowed' => [
-                'filetypes' => '.rat',
-                'expected' => ['.rat']
-            ],
-            'Multiple nonexistent extensions are not allowed' => [
-                'filetypes' => '.ricefield .rat',
-                'expected' => ['.ricefield', '.rat']
-            ],
-            'Existent extension is allowed' => [
-                'filetypes' => '.xml',
-                'expected' => []
-            ],
-            'Existent group is allowed' => [
-                'filetypes' => 'web_file',
-                'expected' => []
-            ],
-            'Nonexistent group is not allowed' => [
-                'filetypes' => '©ç√√ß∂å√©åß©√',
-                'expected' => ['©ç√√ß∂å√©åß©√']
-            ],
-            'Existent mimetype is allowed' => [
-                'filetypes' => 'application/xml',
-                'expected' => []
-            ],
-            'Nonexistent mimetype is not allowed' => [
-                'filetypes' => 'ricefield/rat',
-                'expected' => ['ricefield/rat']
-            ],
-            'Multiple nonexistent mimetypes are not allowed' => [
-                'filetypes' => 'ricefield/rat cam/ball',
-                'expected' => ['ricefield/rat', 'cam/ball']
-            ],
-            'Missing dot in extension is not allowed' => [
-                'filetypes' => 'png',
-                'expected' => ['png']
-            ],
-            'Some existent some not' => [
-                'filetypes' => '.txt application/xml web_file ©ç√√ß∂å√©åß©√ .png ricefield/rat document png',
-                'expected' => ['©ç√√ß∂å√©åß©√', 'ricefield/rat', 'png']
-            ]
-        ];
-    }
-
-    /**
-     * Test get_nonexistent_file_types().
-     * @dataProvider get_nonexistent_file_types_provider
-     * @param string $filetypes The filetypes to check
-     * @param array $expected The expected result. The list of non existent file types.
-     */
-    public function test_get_nonexistent_file_types($filetypes, $expected) {
-        $this->resetAfterTest();
-        $method = new ReflectionMethod(assign_submission_file::class, 'get_nonexistent_file_types');
-        $method->setAccessible(true);
-        $plugin = $this->assign->get_submission_plugin_by_type('file');
-        $nonexistentfiletypes = $method->invokeArgs($plugin, [$filetypes]);
-        $this->assertSame($expected, $nonexistentfiletypes);
-    }
-
 }
diff --git a/mod/assign/tests/restore_date_test.php b/mod/assign/tests/restore_date_test.php
new file mode 100644 (file)
index 0000000..c6c50b9
--- /dev/null
@@ -0,0 +1,92 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Restore date tests.
+ *
+ * @package    mod_assign
+ * @copyright  2017 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php");
+require_once($CFG->dirroot . '/mod/assign/tests/base_test.php');
+
+/**
+ * Restore date tests.
+ *
+ * @package    mod_assign
+ * @copyright  2017 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_assign_restore_date_testcase extends restore_date_testcase {
+
+    /**
+     * Test restore dates.
+     */
+    public function test_restore_dates() {
+        global $DB, $USER;
+
+        $record = ['cutoffdate' => 100, 'allowsubmissionsfromdate' => 100, 'duedate' => 100, 'timemodified' => 100];
+        list($course, $assign) = $this->create_course_and_module('assign', $record);
+        $cm = $DB->get_record('course_modules', ['course' => $course->id, 'instance' => $assign->id]);
+        $assignobj = new testable_assign(context_module::instance($cm->id), $cm, $course);
+        $submission = $assignobj->get_user_submission($USER->id, true);
+        $grade = $assignobj->get_user_grade($USER->id, true);
+
+        // User override.
+        $override = (object)[
+            'assignid' => $assign->id,
+            'groupid' => 0,
+            'userid' => $USER->id,
+            'sortorder' => 1,
+            'allowsubmissionsfromdate' => 100,
+            'duedate' => 200,
+            'cutoffdate' => 300
+        ];
+        $DB->insert_record('assign_overrides', $override);
+
+        // Do backup and restore.
+        $newcourseid = $this->backup_and_restore($course);
+        $newassign = $DB->get_record('assign', ['course' => $newcourseid]);
+
+        $this->assertFieldsNotRolledForward($assign, $newassign, ['timemodified']);
+        $props = ['allowsubmissionsfromdate', 'duedate', 'cutoffdate'];
+        $this->assertFieldsRolledForward($assign, $newassign, $props);
+
+        $newsubmission = $DB->get_record('assign_submission', ['assignment' => $newassign->id]);
+        $newoverride = $DB->get_record('assign_overrides', ['assignid' => $newassign->id]);
+        $newgrade = $DB->get_record('assign_grades', ['assignment' => $newassign->id]);
+
+        // Assign submission time checks.
+        $this->assertEquals($submission->timecreated, $newsubmission->timecreated);
+        $this->assertEquals($submission->timemodified, $newsubmission->timemodified);
+
+        // Assign override time checks.
+        $diff = $this->get_diff();
+        $this->assertEquals($override->duedate + $diff, $newoverride->duedate);
+        $this->assertEquals($override->cutoffdate + $diff, $newoverride->cutoffdate);
+        $this->assertEquals($override->allowsubmissionsfromdate + $diff, $newoverride->allowsubmissionsfromdate);
+
+        // Assign grade time checks.
+        $this->assertEquals($grade->timecreated, $newgrade->timecreated);
+        $this->assertEquals($grade->timemodified, $newgrade->timemodified);
+
+    }
+}
index 076a24f..8987706 100644 (file)
@@ -51,6 +51,9 @@ class restore_book_activity_structure_step extends restore_activity_structure_st
         $oldid = $data->id;
         $data->course = $this->get_courseid();
 
+        // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
+        // See MDL-9367.
+
         $newitemid = $DB->insert_record('book', $data);
         $this->apply_activity_instance($newitemid);
     }
index c3bd4d0..ffc7eb7 100644 (file)
@@ -164,6 +164,9 @@ function book_print_recent_activity($course, $viewfullnames, $timestart) {
  * @return array status array
  */
 function book_reset_userdata($data) {
+    // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
+    // See MDL-9367.
+
     return array();
 }
 
index 8a23482..938b07f 100644 (file)
@@ -17,8 +17,7 @@ Feature: Edited book chapters handle tags correctly
       | teacher1 | C1 | editingteacher |
       | student1 | C1 | student |
     And I log in as "teacher1"
-    And I am on "Course 1" course homepage
-    And I turn editing mode on
+    And I am on "Course 1" course homepage with editing mode on
     And I add a "Book" to section "1" and I fill the form with:
       | Name | Test book |
       | Description | A book about dreams! |
index 582227f..95be95a 100644 (file)
@@ -51,8 +51,9 @@ class restore_chat_activity_structure_step extends restore_activity_structure_st
         $oldid = $data->id;
         $data->course = $this->get_courseid();
 
+        // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
+        // See MDL-9367.
         $data->chattime = $this->apply_date_offset($data->chattime);
-        $data->timemodified = $this->apply_date_offset($data->timemodified);
 
         // Insert the chat record.
         $newitemid = $DB->insert_record('chat', $data);
@@ -69,7 +70,6 @@ class restore_chat_activity_structure_step extends restore_activity_structure_st
         $data->userid = $this->get_mappingid('user', $data->userid);
         $data->groupid = $this->get_mappingid('group', $data->groupid);
         $data->message = $data->message_text;
-        $data->timestamp = $this->apply_date_offset($data->timestamp);
 
         $newitemid = $DB->insert_record('chat_messages', $data);
         $this->set_mapping('chat_message', $oldid, $newitemid); // Because of decode.
index 7a91799..ef91dc1 100644 (file)
@@ -1257,6 +1257,8 @@ function chat_reset_userdata($data) {
 
     // Updating dates - shift may be negative too.
     if ($data->timeshift) {
+        // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
+        // See MDL-9367.
         shift_course_mod_dates('chat', array('chattime'), $data->timeshift, $data->courseid);
         $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false);
     }
diff --git a/mod/chat/tests/restore_date_test.php b/mod/chat/tests/restore_date_test.php
new file mode 100644 (file)
index 0000000..94043a4
--- /dev/null
@@ -0,0 +1,68 @@
+<?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/>.
+
+/**
+ * Restore date tests.
+ *
+ * @package    mod_chat
+ * @copyright  2017 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php");
+
+/**
+ * Restore date tests.
+ *
+ * @package    mod_chat
+ * @copyright  2017 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_chat_restore_date_testcase extends restore_date_testcase {
+
+    public function test_restore_dates() {
+        global $DB;
+
+        list($course, $chat) = $this->create_course_and_module('chat');
+        $result = mod_chat_external::login_user($chat->id);
+        $result = external_api::clean_returnvalue(mod_chat_external::login_user_returns(), $result);
+        $chatsid = $result['chatsid'];
+
+        $result = mod_chat_external::send_chat_message($chatsid, 'hello!');
+        $result = external_api::clean_returnvalue(mod_chat_external::send_chat_message_returns(), $result);
+        $message = $DB->get_record('chat_messages', ['id' => $result['messageid']]);
+        $timestamp = 1000;
+        $DB->set_field('chat_messages', 'timestamp', $timestamp);
+
+        // Do backup and restore.
+        $newcourseid = $this->backup_and_restore($course);
+        $newchat = $DB->get_record('chat', ['course' => $newcourseid]);
+
+        $this->assertFieldsNotRolledForward($chat, $newchat, ['timemodified']);
+        $props = ['chattime'];
+        $this->assertFieldsRolledForward($chat, $newchat, $props);
+
+        $newmessages = $DB->get_records('chat_messages', ['chatid' => $newchat->id]);
+
+        foreach ($newmessages as $message) {
+            $this->assertEquals($timestamp, $message->timestamp);
+        }
+
+    }
+}
index bcba1a1..0b0703c 100644 (file)
@@ -53,9 +53,10 @@ class restore_choice_activity_structure_step extends restore_activity_structure_
         $oldid = $data->id;
         $data->course = $this->get_courseid();
 
+        // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
+        // See MDL-9367.
         $data->timeopen = $this->apply_date_offset($data->timeopen);
         $data->timeclose = $this->apply_date_offset($data->timeclose);
-        $data->timemodified = $this->apply_date_offset($data->timemodified);
 
         // insert the choice record
         $newitemid = $DB->insert_record('choice', $data);
@@ -70,7 +71,6 @@ class restore_choice_activity_structure_step extends restore_activity_structure_
         $oldid = $data->id;
 
         $data->choiceid = $this->get_new_parentid('choice');
-        $data->timemodified = $this->apply_date_offset($data->timemodified);
 
         $newitemid = $DB->insert_record('choice_options', $data);
         $this->set_mapping('choice_option', $oldid, $newitemid);
@@ -84,7 +84,6 @@ class restore_choice_activity_structure_step extends restore_activity_structure_
         $data->choiceid = $this->get_new_parentid('choice');
         $data->optionid = $this->get_mappingid('choice_option', $data->optionid);
         $data->userid = $this->get_mappingid('user', $data->userid);
-        $data->timemodified = $this->apply_date_offset($data->timemodified);
 
         $newitemid = $DB->insert_record('choice_answers', $data);
         // No need to save this mapping as far as nothing depend on it
index 124e081..2689910 100644 (file)
@@ -754,6 +754,8 @@ function choice_reset_userdata($data) {
 
     /// updating dates - shift may be negative too
     if ($data->timeshift) {
+        // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
+        // See MDL-9367.
         shift_course_mod_dates('choice', array('timeopen', 'timeclose'), $data->timeshift, $data->courseid);
         $status[] = array('component'=>$componentstr, 'item'=>get_string('datechanged'), 'error'=>false);
     }
diff --git a/mod/choice/tests/restore_date_test.php b/mod/choice/tests/restore_date_test.php
new file mode 100644 (file)
index 0000000..8c49f47
--- /dev/null
@@ -0,0 +1,71 @@
+<?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/>.
+
+/**
+ * Restore date tests.
+ *
+ * @package    mod_choice
+ * @copyright  2017 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php");
+
+/**
+ * Restore date tests.
+ *
+ * @package    mod_choice
+ * @copyright  2017 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_choice_restore_date_testcase extends restore_date_testcase {
+
+    public function test_restore_dates() {
+        global $DB, $USER;
+
+        $time = 100000;
+        $record = ['timeopen' => $time, 'timeclose' => $time];
+        list($course, $choice) = $this->create_course_and_module('choice', $record);
+
+        $options = $DB->get_records('choice_options', ['choiceid' => $choice->id]);
+        $DB->set_field('choice_options', 'timemodified', $time);
+        $option = reset($options);
+        $cm = $DB->get_record('course_modules', ['id' => $choice->cmid]);
+        choice_user_submit_response($option->id, $choice, $USER->id, $course, $cm);
+        $answer = $DB->get_record('choice_answers', ['choiceid' => $choice->id]);
+
+        // Do backup and restore.
+        $newcourseid = $this->backup_and_restore($course);
+        $newchoice = $DB->get_record('choice', ['course' => $newcourseid]);
+        $newoptions = $DB->get_records('choice_options', ['choiceid' => $newchoice->id]);
+
+        $this->assertFieldsNotRolledForward($choice, $newchoice, ['timemodified']);
+        $props = ['timeopen', 'timeclose'];
+        $this->assertFieldsRolledForward($choice, $newchoice, $props);
+
+        // Options check.
+        foreach ($newoptions as $newoption) {
+            $this->assertEquals($time, $newoption->timemodified);
+        }
+
+        // Answers check.
+        $newanswer = $DB->get_record('choice_answers', ['choiceid' => $newchoice->id]);
+        $this->assertEquals($answer->timemodified, $newanswer->timemodified);
+    }
+}
index dd71cf2..7ac138b 100644 (file)
@@ -55,14 +55,14 @@ class restore_data_activity_structure_step extends restore_activity_structure_st
         $oldid = $data->id;
         $data->course = $this->get_courseid();
 
+        // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
+        // See MDL-9367.
         $data->timeavailablefrom = $this->apply_date_offset($data->timeavailablefrom);
         $data->timeavailableto = $this->apply_date_offset($data->timeavailableto);
         $data->timeviewfrom = $this->apply_date_offset($data->timeviewfrom);
         $data->timeviewto = $this->apply_date_offset($data->timeviewto);
         $data->assesstimestart = $this->apply_date_offset($data->assesstimestart);
         $data->assesstimefinish = $this->apply_date_offset($data->assesstimefinish);
-        // Added in 3.1, hence conditional.
-        $data->timemodified = isset($data->timemodified) ? $this->apply_date_offset($data->timemodified) : time();
 
         if ($data->scale < 0) { // scale found, get mapping
             $data->scale = -($this->get_mappingid('scale', abs($data->scale)));
@@ -98,9 +98,6 @@ class restore_data_activity_structure_step extends restore_activity_structure_st
         $data = (object)$data;
         $oldid = $data->id;
 
-        $data->timecreated = $this->apply_date_offset($data->timecreated);
-        $data->timemodified = $this->apply_date_offset($data->timemodified);
-
         $data->userid = $this->get_mappingid('user', $data->userid);
         $data->groupid = $this->get_mappingid('group', $data->groupid);
         $data->dataid = $this->get_new_parentid('data');
@@ -137,8 +134,6 @@ class restore_data_activity_structure_step extends restore_activity_structure_st
         }
         $data->rating = $data->value;
         $data->userid = $this->get_mappingid('user', $data->userid);
-        $data->timecreated = $this->apply_date_offset($data->timecreated);
-        $data->timemodified = $this->apply_date_offset($data->timemodified);
 
         // We need to check that component and ratingarea are both set here.
         if (empty($data->component)) {
index e71c98c..0cd9457 100644 (file)
@@ -596,7 +596,7 @@ class mod_data_external extends external_api {
 
         $params = array('databaseid' => $databaseid);
         $params = self::validate_parameters(self::get_fields_parameters(), $params);
-        $warnings = array();
+        $fields = $warnings = array();
 
         list($database, $course, $cm, $context) = self::validate_database($params['databaseid']);
 
@@ -807,7 +807,9 @@ class mod_data_external extends external_api {
                 'entries' => new external_multiple_structure(
                     record_exporter::get_read_structure()
                 ),
-                'totalcount' => new external_value(PARAM_INT, 'Total count of records.'),
+                'totalcount' => new external_value(PARAM_INT, 'Total count of records returned by the search.'),
+                'maxcount' => new external_value(PARAM_INT, 'Total count of records that the user could see in the database
+                    (if all the search criterias were removed).', VALUE_OPTIONAL),
                 'listviewcontents' => new external_value(PARAM_RAW, 'The list view contents as is rendered in the site.',
                                                             VALUE_OPTIONAL),
                 'warnings' => new external_warnings()
index f7806eb..3e41e9e 100644 (file)
@@ -70,12 +70,12 @@ class content_exporter extends exporter {
                 'null' => NULL_ALLOWED,
             ),
             'content3' => array(
-                'type' => PARAM_BOOL,
+                'type' => PARAM_RAW,
                 'description' => 'Contents.',
                 'null' => NULL_ALLOWED,
             ),
             'content4' => array(
-                'type' => PARAM_BOOL,
+                'type' => PARAM_RAW,
                 'description' => 'Contents.',
                 'null' => NULL_ALLOWED,
             ),
index 2d50f84..498a5c0 100644 (file)
@@ -2872,7 +2872,10 @@ function data_reset_userdata($data) {
 
     // updating dates - shift may be negative too
     if ($data->timeshift) {
-        shift_course_mod_dates('data', array('timeavailablefrom', 'timeavailableto', 'timeviewfrom', 'timeviewto'), $data->timeshift, $data->courseid);
+        // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
+        // See MDL-9367.
+        shift_course_mod_dates('data', array('timeavailablefrom', 'timeavailableto',
+            'timeviewfrom', 'timeviewto', 'assesstimestart', 'assesstimefinish'), $data->timeshift, $data->courseid);
         $status[] = array('component'=>$componentstr, 'item'=>get_string('datechanged'), 'error'=>false);
     }
 
index 89624ef..93be6ac 100644 (file)
@@ -1006,15 +1006,17 @@ function data_search_entries($data, $cm, $context, $mode, $currentgroup, $search
 
     $recordids = data_get_all_recordids($data->id, $initialselect, $initialparams);
     $newrecordids = data_get_advance_search_ids($recordids, $searcharray, $data->id);
-    $totalcount = count($newrecordids);
     $selectdata = $where . $groupselect . $approveselect;
 
     if (!empty($advanced)) {
         $advancedsearchsql = data_get_advanced_search_sql($sort, $data, $newrecordids, $selectdata, $sortorder);
         $sqlselect = $advancedsearchsql['sql'];
         $allparams = array_merge($allparams, $advancedsearchsql['params']);
+        $totalcount = count($newrecordids);
     } else {
         $sqlselect  = "SELECT $what $fromsql $sortorder";
+        $sqlcountselect  = "SELECT $count $fromsql";
+        $totalcount = $DB->count_records_sql($sqlcountselect, $allparams);
     }
 
     // Work out the paging numbers and counts.
index d9da1db..4f74d2c 100644 (file)
@@ -17,8 +17,7 @@ Feature: Set entries required as a completion condition for a data item
       | teacher1 | C1 | editingteacher |
       | student1 | C1 | student |
     And I log in as "teacher1"
-    And I follow "C1"
-    And I turn editing mode on
+    And I am on "Course 1" course homepage with editing mode on
     And I add a "Database" to section "1" and I fill the form with:
       | Name | Test database name |
       | Description | Test database description |
@@ -26,38 +25,38 @@ Feature: Set entries required as a completion condition for a data item
       | completionview | 0 |
       | completionentriesenabled | checked |
       | completionentries        | 2 |
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I add a "Text input" field to "Test database name" database and I fill the form with:
       | Field name | Test field name |
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I log out
     When I log in as "student1"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I add an entry to "Test database name" database with:
       | Test field name | Student original entry |
     And I press "Save and view"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I log out
     And I log in as "teacher1"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     #One entry is not enough to mark as complete
     And "Student 1" user has not completed "Test database name" activity
     And I log out
     When I log in as "student1"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     And I add an entry to "Test database name" database with:
       | Test field name | Student second entry |
     And I press "Save and view"
     And I log out
     And I log in as "teacher1"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     Then "Student 1" user has completed "Test database name" activity
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I follow "Test database name"
     And I navigate to "Edit settings" in current page administration
     And I press "Unlock completion"
     And I set the field "completionentries" to "1"
     And I press "Save and display"
-    And I follow "C1"
+    And I am on "Course 1" course homepage
     Then "Student 1" user has completed "Test database name" activity
     And I log out
index 1b61d09..8220817 100644 (file)
@@ -659,6 +659,18 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         }
     }
 
+    /**
+     * Test get_fields_database_without_fields.
+     */
+    public function test_get_fields_database_without_fields() {
+
+        $this->setUser($this->student1);
+        $result = mod_data_external::get_fields($this->database->id);
+        $result = external_api::clean_returnvalue(mod_data_external::get_fields_returns(), $result);
+
+        $this->assertEmpty($result['fields']);
+    }
+
     /**
      * Test search_entries.
      */
@@ -666,12 +678,25 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         global $DB;
         list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
 
-        // First do a normal text search as student 1. I should see my two group entries.
         $this->setUser($this->student1);
+        // Empty search, it should return all the visible entries.
+        $result = mod_data_external::search_entries($this->database->id, 0, false);
+        $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
+        $this->assertCount(2, $result['entries']);
+        $this->assertEquals(2, $result['totalcount']);
+
+        // Search for something that does not exists.
+        $result = mod_data_external::search_entries($this->database->id, 0, false, 'abc');
+        $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+        $this->assertEquals(0, $result['totalcount']);
+
+        // Search by text matching all the entries.
         $result = mod_data_external::search_entries($this->database->id, 0, false, 'text');
         $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
         $this->assertCount(2, $result['entries']);
         $this->assertEquals(2, $result['totalcount']);
+        $this->assertEquals(2, $result['maxcount']);
 
         // Now as the other student I should receive my not approved entry. Apply ordering here.
         $this->setUser($this->student2);
@@ -679,6 +704,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
         $this->assertCount(3, $result['entries']);
         $this->assertEquals(3, $result['totalcount']);
+        $this->assertEquals(3, $result['maxcount']);
         // The not approved one should be the first.
         $this->assertEquals($entry13, $result['entries'][0]['id']);
 
@@ -688,6 +714,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
         $this->assertCount(1, $result['entries']);
         $this->assertEquals(1, $result['totalcount']);
+        $this->assertEquals(1, $result['maxcount']);
         $this->assertEquals($this->student3->id, $result['entries'][0]['userid']);
 
         // Same normal text search as teacher.
@@ -696,6 +723,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
         $this->assertCount(4, $result['entries']);  // I can see all groups and non approved.
         $this->assertEquals(4, $result['totalcount']);
+        $this->assertEquals(4, $result['maxcount']);
 
         // Pagination.
         $this->setUser($this->teacher);
@@ -703,6 +731,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
         $this->assertCount(2, $result['entries']);  // Only 2 per page.
         $this->assertEquals(4, $result['totalcount']);
+        $this->assertEquals(4, $result['maxcount']);
 
         // Now advanced search or not dinamic fields (user firstname for example).
         $this->setUser($this->student1);
@@ -713,6 +742,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
         $this->assertCount(1, $result['entries']);
         $this->assertEquals(1, $result['totalcount']);
+        $this->assertEquals(2, $result['maxcount']);
         $this->assertEquals($this->student2->id, $result['entries'][0]['userid']);  // I only found mine!
 
         // Advanced search for fields.
@@ -724,6 +754,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
         $this->assertCount(2, $result['entries']);  // Found two entries matching this.
         $this->assertEquals(2, $result['totalcount']);
+        $this->assertEquals(2, $result['maxcount']);
 
         // Combined search.
         $field2 = $DB->get_record('data_fields', array('type' => 'number'));
@@ -736,6 +767,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
         $this->assertCount(1, $result['entries']);  // Only one matching everything.
         $this->assertEquals(1, $result['totalcount']);
+        $this->assertEquals(2, $result['maxcount']);
 
         // Combined search (no results).
         $field2 = $DB->get_record('data_fields', array('type' => 'number'));
@@ -747,6 +779,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
         $this->assertCount(0, $result['entries']);  // Only one matching everything.
         $this->assertEquals(0, $result['totalcount']);
+        $this->assertEquals(2, $result['maxcount']);
     }
 
     /**
diff --git a/mod/data/tests/restore_date_test.php b/mod/data/tests/restore_date_test.php
new file mode 100644 (file)
index 0000000..fcf751b
--- /dev/null
@@ -0,0 +1,93 @@
+<?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/>.
+
+/**
+ * Restore date tests.
+ *
+ * @package    mod_data
+ * @copyright  2017 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php");
+require_once($CFG->dirroot . '/rating/lib.php');
+
+/**
+ * Restore date tests.
+ *
+ * @package    mod_data
+ * @copyright  2017 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_data_restore_date_testcase extends restore_date_testcase {
+
+    /**
+     * Test restore dates.
+     */
+    public function test_restore_dates() {
+        global $DB, $USER;
+
+        $gg = $this->getDataGenerator()->get_plugin_generator('mod_data');
+        $record = ['assesstimefinish' => 100, 'assesstimestart' => 100, 'ratingtime' => 1, 'assessed' => 2, 'scale' => 1,
+                   'timeavailablefrom' => 100, 'timeavailableto' => 100, 'timeviewfrom' => 100, 'timeviewto' => 100];
+        list($course, $data) = $this->create_course_and_module('data', $record);
+
+        // Data field/record.
+        $timestamp = 996699;
+        $diff = $this->get_diff();
+        $record = new StdClass();
+        $record->name = 'field-1';
+        $record->type = 'text';
+        $field = $gg->create_field($record, $data);
+        $datarecordid = $gg->create_entry($data, [$field->field->id => 'NERDS NERDS EVERYWHERE, NO BRAIN TO THINK']);
+        $datarecord = $DB->get_record('data_records', ['id' => $datarecordid]);
+
+        // Ratings.
+        $ratingoptions = new stdClass;
+        $ratingoptions->context = context_module::instance($data->cmid);
+        $ratingoptions->ratingarea = 'entry';
+        $ratingoptions->component = 'mod_data';
+        $ratingoptions->itemid  = $datarecord->id;
+        $ratingoptions->scaleid = 2;
+        $ratingoptions->userid  = $USER->id;
+        $rating = new rating($ratingoptions);
+        $rating->update_rating(2);
+        $rating = $DB->get_record('rating', ['itemid' => $datarecord->id]);
+
+        // Do backup and restore.
+        $newcourseid = $this->backup_and_restore($course);
+        $newdata = $DB->get_record('data', ['course' => $newcourseid]);
+
+        $this->assertFieldsNotRolledForward($data, $newdata, ['timemodified']);
+        $props = ['assesstimefinish', 'assesstimestart', 'timeavailablefrom', 'timeavailableto', 'timeviewfrom', 'timeviewto'];
+        $this->assertFieldsRolledForward($data, $newdata, $props);
+
+        $newdatarecord = $DB->get_record('data_records', ['dataid' => $newdata->id]);
+        $newcm = $DB->get_record('course_modules', ['course' => $newcourseid, 'instance' => $newdata->id]);
+
+        // Data record time checks.
+        $this->assertEquals($datarecord->timecreated, $newdatarecord->timecreated);
+        $this->assertEquals($datarecord->timemodified, $newdatarecord->timemodified);
+
+        // Rating test.
+        $newrating = $DB->get_record('rating', ['contextid' => context_module::instance($newcm->id)->id]);
+        $this->assertEquals($rating->timecreated, $newrating->timecreated);
+        $this->assertEquals($rating->timemodified, $newrating->timemodified);
+    }
+}
index 7ee17a8..92ede9d 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /mod/data - plugins,
 information provided here is intended especially for developers.
 
+=== 3.4 ===
+* External function mod_data_external::search_entries() now returns the maxcount field: Total count of records that the user could
+    see in the database (if all the search criterias were removed).
+
 === 3.3.2 ===
 * data_refresh_events() Now takes two additional parameters to refine the update to a specific instance. This function
   now optionally takes the module instance object or ID, and the course module object or ID. Please try to send the full
index 320e847..fb859ca 100644 (file)
@@ -93,6 +93,8 @@ class moodle1_mod_feedback_handler extends moodle1_mod_handler {
         $this->moduleid = $cminfo['id'];
         $contextid      = $this->converter->get_contextid(CONTEXT_MODULE, $this->moduleid);
 
+
+
         // get a fresh new file manager for this instance
         $this->fileman = $this->converter->get_file_manager($contextid, 'mod_feedback');
 
index f190b56..1de792d 100644 (file)
@@ -53,9 +53,10 @@ class restore_feedback_activity_structure_step extends restore_activity_structur
         $oldid = $data->id;
         $data->course = $this->get_courseid();
 
+        // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
+        // See MDL-9367.
         $data->timeopen = $this->apply_date_offset($data->timeopen);
         $data->timeclose = $this->apply_date_offset($data->timeclose);
-        $data->timemodified = $this->apply_date_offset($data->timemodified);
 
         // insert the feedback record
         $newitemid = $DB->insert_record('feedback', $data);
@@ -84,7 +85,6 @@ class restore_feedback_activity_structure_step extends restore_activity_structur
         $oldid = $data->id;
         $data->feedback = $this->get_new_parentid('feedback');
         $data->userid = $this->get_mappingid('user', $data->userid);
-        $data->timemodified = $this->apply_date_offset($data->timemodified);
         if ($this->task->is_samesite() && !empty($data->courseid)) {
             $data->courseid = $data->courseid;
         } else if ($this->get_courseid() == SITEID) {
index ce93172..0946089 100644 (file)
@@ -695,6 +695,8 @@ function feedback_reset_userdata($data) {
 
     // Updating dates - shift may be negative too.
     if ($data->timeshift) {
+        // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
+        // See MDL-9367.
         $shifterror = !shift_course_mod_dates('feedback', array('timeopen', 'timeclose'), $data->timeshift, $data->courseid);
         $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => $shifterror);
     }
index 849b945..cfe497c 100644 (file)
@@ -247,7 +247,7 @@ Feature: Anonymous feedback
 
   Scenario: Collecting new non-anonymous feedback from a previously anonymous feedback activity
     When I log in as "teacher"
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I follow "Course feedback"
     And I navigate to "Edit settings" in current page administration
     And I set the following fields to these values:
@@ -260,7 +260,7 @@ Feature: Anonymous feedback
       | Maximum characters accepted | 200                    |
     And I log out
     When I log in as "user1"
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I follow "Course feedback"
     And I follow "Answer the questions..."
     And I set the following fields to these values:
@@ -269,7 +269,7 @@ Feature: Anonymous feedback
     And I log out
     # Switch to non-anon responses.
     And I log in as "teacher"
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I follow "Course feedback"
     And I navigate to "Edit settings" in current page administration
     And I set the following fields to these values:
@@ -278,7 +278,7 @@ Feature: Anonymous feedback
     And I log out
     # Now leave a non-anon feedback as user1
     When I log in as "user1"
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I follow "Course feedback"
     And I follow "Answer the questions..."
     And I set the following fields to these values:
@@ -287,7 +287,7 @@ Feature: Anonymous feedback
     And I log out
     # Now check the responses are correct.
     When I log in as "teacher"
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I follow "Course feedback"
     And I follow "Show responses"
     And I should see "Anonymous entries (1)"
index 12fba55..d4a0315 100644 (file)
@@ -81,7 +81,7 @@ Feature: Mapping courses in a feedback
       | this is a simple multiple choice | option d |
     And I press "Submit your answers"
     And I press "Continue"
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I click on "Course feedback" "link" in the "Feedback" "block"
     And I follow "Answer the questions..."
     And I should not see "Acceptance test site" in the ".feedback_form" "css_element"
index aa226bf..4087dd6 100644 (file)
@@ -25,7 +25,7 @@ Feature: Non anonymous feedback with multiple submissions
 
   Scenario: Completing a feedback second time
     When I log in as "teacher"
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I follow "Course feedback"
     And I click on "Edit questions" "link" in the "[role=main]" "css_element"
     And I add a "Short text answer" question to the feedback with:
@@ -39,7 +39,7 @@ Feature: Non anonymous feedback with multiple submissions
       | Maximum characters accepted | 200        |
     And I log out
     And I log in as "user1"
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I follow "Course feedback"
     And I follow "Answer the questions..."
     And I set the following fields to these values:
@@ -50,7 +50,7 @@ Feature: Non anonymous feedback with multiple submissions
     And I press "Submit your answers"
     And I log out
     And I log in as "user1"
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     And I follow "Course feedback"
     And I follow "Answer the questions..."
     Then the field "first" matches value "111"
diff --git a/mod/feedback/tests/restore_date_test.php b/mod/feedback/tests/restore_date_test.php
new file mode 100644 (file)
index 0000000..9642bfa
--- /dev/null
@@ -0,0 +1,64 @@
+<?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/>.
+
+/**
+ * Restore date tests.
+ *
+ * @package    mod_feedback
+ * @copyright  2017 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php");
+
+/**
+ * Restore date tests.
+ *
+ * @package    mod_feedback
+ * @copyright  2017 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_feedback_restore_date_testcase extends restore_date_testcase {
+
+    public function test_restore_dates() {
+        global $DB, $USER;
+
+        $time = 10000;
+        list($course, $feedback) = $this->create_course_and_module('feedback', ['timeopen' => $time, 'timeclose' => $time]);
+
+        // Create response.
+        $response = new stdClass();
+        $response->feedback = $feedback->id;
+        $response->userid = $USER->id;
+        $response->anonymous_response = FEEDBACK_ANONYMOUS_NO;
+        $response->timemodified = $time;
+        $completedid = $DB->insert_record('feedback_completed', $response);
+        $response = $DB->get_record('feedback_completed', array('id' => $completedid), '*', MUST_EXIST);
+
+        // Do backup and restore.
+        $newcourseid = $this->backup_and_restore($course);
+        $newfeedback = $DB->get_record('feedback', ['course' => $newcourseid]);
+        $newresponse = $DB->get_record('feedback_completed', ['feedback' => $newfeedback->id]);
+
+        $this->assertFieldsNotRolledForward($feedback, $newfeedback, ['timemodified']);
+        $props = ['timeopen', 'timeclose'];
+        $this->assertFieldsRolledForward($feedback, $newfeedback, $props);
+        $this->assertEquals($response->timemodified, $newresponse->timemodified);
+    }
+}
index 450f882..7fc2559 100644 (file)
@@ -46,7 +46,9 @@ class restore_folder_activity_structure_step extends restore_activity_structure_
         $data = (object)$data;
         $oldid = $data->id;
         $data->course = $this->get_courseid();
-        $data->timemodified = $this->apply_date_offset($data->timemodified);
+
+        // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
+        // See MDL-9367.
 
         // If showexpanded is not set, apply site default.
         if (!isset($data->showexpanded)) {
index f07e63f..4c76c55 100644 (file)
@@ -65,6 +65,10 @@ function folder_get_extra_capabilities() {
  * @return array status array
  */
 function folder_reset_userdata($data) {
+
+    // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
+    // See MDL-9367.
+
     return array();
 }
 
diff --git a/mod/folder/tests/restore_date_test.php b/mod/folder/tests/restore_date_test.php
new file mode 100644 (file)
index 0000000..b18dd6d
--- /dev/null
@@ -0,0 +1,49 @@
+<?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/>.
+
+/**
+ * Restore date tests.
+ *
+ * @package    mod_folder
+ * @copyright  2017 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php");
+
+/**
+ * Restore date tests.
+ *
+ * @package    mod_folder
+ * @copyright  2017 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_folder_restore_date_testcase extends restore_date_testcase {
+
+    public function test_restore_dates() {
+        global $DB;
+
+        list($course, $folder) = $this->create_course_and_module('folder');
+
+        // Do backup and restore.
+        $newcourseid = $this->backup_and_restore($course);
+        $newfolder = $DB->get_record('folder', ['course' => $newcourseid]);
+        $this->assertFieldsNotRolledForward($folder, $newfolder, ['timemodified']);
+    }
+}
index c026123..f7521c0 100644 (file)
@@ -60,6 +60,8 @@ class restore_forum_activity_structure_step extends restore_activity_structure_s
         $oldid = $data->id;
         $data->course = $this->get_courseid();
 
+        // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
+        // See MDL-9367.
         $data->assesstimestart = $this->apply_date_offset($data->assesstimestart);
         $data->assesstimefinish = $this->apply_date_offset($data->assesstimefinish);
         if ($data->scale < 0) { // scale found, get mapping
@@ -83,7 +85,6 @@ class restore_forum_activity_structure_step extends restore_activity_structure_s
         $data->course = $this->get_courseid();
 
         $data->forum = $this->get_new_parentid('forum');
-        $data->timemodified = $this->apply_date_offset($data->timemodified);
         $data->timestart = $this->apply_date_offset($data->timestart);
         $data->timeend = $this->apply_date_offset($data->timeend);
         $data->userid = $this->get_mappingid('user', $data->userid);
@@ -101,8 +102,6 @@ class restore_forum_activity_structure_step extends restore_activity_structure_s
         $oldid = $data->id;
 
         $data->discussion = $this->get_new_parentid('forum_discussion');
-        $data->created = $this->apply_date_offset($data->created);
-        $data->modified = $this->apply_date_offset($data->modified);
         $data->userid = $this->get_mappingid('user', $data->userid);
         // If post has parent, map it (it has been already restored)
         if (!empty($data->parent)) {
@@ -148,8 +147,6 @@ class restore_forum_activity_structure_step extends restore_activity_structure_s
         }
         $data->rating = $data->value;
         $data->userid = $this->get_mappingid('user', $data->userid);
-        $data->timecreated = $this->apply_date_offset($data->timecreated);
-        $data->timemodified = $this->apply_date_offset($data->timemodified);
 
         // We need to check that component and ratingarea are both set here.
         if (empty($data->component)) {
index 929af39..0f080a9 100644 (file)
@@ -3788,14 +3788,15 @@ function forum_print_discussion_header(&$post, $forum, $group = -1, $datestring
     $postuserfields = explode(',', user_picture::fields());
     $postuser = username_load_fields_from_object($postuser, $post, null, $postuserfields);
     $postuser->id = $post->userid;
-    echo '<td class="picture">';
+    echo '<td class="author">';
+    echo '<span class="picture">';
     echo $OUTPUT->user_picture($postuser, array('courseid'=>$forum->course));
-    echo "</td>\n";
-
+    echo '</span>';
+    echo '<span class="name">';
     // User name
     $fullname = fullname($postuser, has_capability('moodle/site:viewfullnames', $modcontext));
-    echo '<td class="author">';
     echo '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$post->userid.'&amp;course='.$forum->course.'">'.$fullname.'</a>';
+    echo '</span>';
     echo "</td>\n";
 
     // Group picture
@@ -5520,7 +5521,7 @@ function forum_print_latest_discussions($course, $forum, $maxdiscussions = -1, $
         echo '<thead>';
         echo '<tr>';
         echo '<th class="header topic" scope="col">'.get_string('discussion', 'forum').'</th>';
-        echo '<th class="header author" colspan="2" scope="col">'.get_string('startedby', 'forum').'</th>';
+        echo '<th class="header author" scope="col">'.get_string('startedby', 'forum').'</th>';
         if ($groupmode > 0) {
             echo '<th class="header group" scope="col">'.get_string('group').'</th>';
         }
@@ -7117,6 +7118,8 @@ function forum_reset_userdata($data) {
 
     /// updating dates - shift may be negative too
     if ($data->timeshift) {
+        // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
+        // See MDL-9367.
         shift_course_mod_dates('forum', array('assesstimestart', 'assesstimefinish'), $data->timeshift, $data->courseid);
         $status[] = array('component'=>$componentstr, 'item'=>get_string('datechanged'), 'error'=>false);
     }
index 91c5105..95a227b 100644 (file)
@@ -67,7 +67,6 @@ Feature: Blog posts are always displayed in reverse chronological order
       | Message | Reply to the first post |
     And I press "Post to forum"
     And I wait to be redirected
-    And I am on site homepage
     And I am on "Course 1" course homepage
     And I follow "Course blog forum"
     #
diff --git a/mod/forum/tests/restore_date_test.php b/mod/forum/tests/restore_date_test.php
new file mode 100644 (file)
index 0000000..64e447c
--- /dev/null
@@ -0,0 +1,110 @@
+<?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/>.
+
+/**
+ * Restore date tests.
+ *
+ * @package    mod_forum
+ * @copyright  2017 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php");
+require_once($CFG->dirroot . '/rating/lib.php');
+
+/**
+ * Restore date tests.
+ *
+ * @package    mod_forum
+ * @copyright  2017 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_forum_restore_date_testcase extends restore_date_testcase {
+
+    /**
+     * Test restore dates.
+     */
+    public function test_restore_dates() {
+        global $DB, $USER;
+
+        $gg = $this->getDataGenerator()->get_plugin_generator('mod_forum');
+        $record = ['assesstimefinish' => 100, 'assesstimestart' => 100, 'ratingtime' => 1, 'assessed' => 2, 'scale' => 1];
+        list($course, $forum) = $this->create_course_and_module('forum', $record);
+
+        // Forum Discussions/posts/ratings.
+        $timestamp = 996699;
+        $diff = $this->get_diff();
+        $record = new stdClass();
+        $record->course = $course->id;
+        $record->userid = $USER->id;
+        $record->forum = $forum->id;
+        $record->timestart = $record->timeend = $record->timemodified = $timestamp;
+        $discussion = $gg->create_discussion($record);
+
+        $record = new stdClass();
+        $record->discussion = $discussion->id;
+        $record->parent = $discussion->firstpost;
+        $record->userid = $USER->id;
+        $record->created = $record->modified = $timestamp;
+        $post = $gg->cre