Merge branch 'MDL-59656_master' of git://github.com/markn86/moodle
authorDavid Monllao <davidm@moodle.com>
Mon, 7 Aug 2017 07:26:03 +0000 (09:26 +0200)
committerDavid Monllao <davidm@moodle.com>
Mon, 7 Aug 2017 07:26:03 +0000 (09:26 +0200)
287 files changed:
admin/cli/mysql_collation.php
admin/registration/forms.php
admin/registration/index.php
admin/registration/register.php
admin/renderer.php
admin/search.php
admin/settings/analytics.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/tests/plugin_test.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/calendar_upcoming/tests/behat/block_calendar_upcoming_course.feature
blocks/calendar_upcoming/tests/behat/block_calendar_upcoming_dashboard.feature
blocks/calendar_upcoming/tests/behat/block_calendar_upcoming_frontpage.feature
blocks/community/forms.php
blocks/completionstatus/tests/behat/block_completionstatus_activity_completion.feature
blocks/participants/tests/behat/block_participants_course.feature
calendar/amd/build/calendar.min.js
calendar/amd/build/calendar_events.min.js [deleted file]
calendar/amd/build/event_form.min.js [new file with mode: 0644]
calendar/amd/build/events.min.js [new file with mode: 0644]
calendar/amd/build/modal_event_form.min.js [new file with mode: 0644]
calendar/amd/build/repository.min.js [moved from calendar/amd/build/calendar_repository.min.js with 54% similarity]
calendar/amd/build/summary_modal.min.js
calendar/amd/src/calendar.js
calendar/amd/src/event_form.js [new file with mode: 0644]
calendar/amd/src/events.js [moved from calendar/amd/src/calendar_events.js with 77% similarity]
calendar/amd/src/modal_event_form.js [new file with mode: 0644]
calendar/amd/src/repository.js [moved from calendar/amd/src/calendar_repository.js with 75% similarity]
calendar/amd/src/summary_modal.js
calendar/classes/external/event_exporter.php
calendar/classes/local/event/forms/create.php [new file with mode: 0644]
calendar/classes/local/event/forms/update.php [new file with mode: 0644]
calendar/classes/local/event/mappers/create_update_form_mapper.php [new file with mode: 0644]
calendar/classes/local/event/mappers/create_update_form_mapper_interface.php [new file with mode: 0644]
calendar/classes/local/event/mappers/event_mapper.php
calendar/externallib.php
calendar/lib.php
calendar/renderer.php
calendar/templates/modal_event_form.mustache [new file with mode: 0644]
calendar/tests/behat/behat_calendar.php
calendar/tests/behat/calendar.feature
calendar/tests/behat/calendar_import.feature
calendar/tests/behat/calendar_lookahead.feature
calendar/tests/lib_test.php
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
course/tests/behat/rename_roles.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/add_to_group.feature
enrol/tests/behat/enrol_user.feature
enrol/tests/behat/manage_enrolments_from_participants.feature [deleted file]
grade/report/grader/lib.php
grade/report/lib.php
grade/tests/behat/grade_hidden_items.feature
grade/tests/behat/grade_minmax.feature
group/tests/behat/create_groups.feature
install/lang/de/error.php
install/lang/ru/install.php
install/lang/sr_cr/install.php
install/lang/sr_lt/install.php
lang/en/admin.php
lang/en/analytics.php
lang/en/calendar.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/build/modal.min.js
lib/amd/build/modal_factory.min.js
lib/amd/src/form-autocomplete.js
lib/amd/src/modal.js
lib/amd/src/modal_factory.js
lib/blocklib.php
lib/classes/event/config_log_created.php [new file with mode: 0644]
lib/classes/oauth2/api.php
lib/datalib.php
lib/db/access.php
lib/db/install.xml
lib/db/services.php
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/external/externallib.php
lib/form/filemanager.php
lib/form/filepicker.php
lib/form/templatable_form_element.php
lib/form/tests/behat/filetypes.feature [deleted file]
lib/form/tests/behat/multi_select_dependencies.feature [deleted file]
lib/form/tests/fixtures/filetypes.php [deleted file]
lib/form/tests/fixtures/multi_select_dependencies.php [deleted file]
lib/grade/grade_grade.php
lib/grouplib.php
lib/moodlelib.php
lib/oauthlib.php
lib/outputrenderers.php
lib/phpunit/classes/restore_date_testcase.php [new file with mode: 0644]
lib/tests/admintree_test.php
lib/tests/behat/alpha_chooser.feature
lib/tests/grouplib_test.php
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/continue.php
mod/lesson/lib.php
mod/lesson/locallib.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/lti/launch.php
mod/lti/view.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/question.scss
theme/boost/scss/moodle/search.scss
theme/boost/templates/columns1.mustache
theme/boost/templates/columns2.mustache
theme/boost/templates/core/modal.mustache
theme/boost/templates/core_form/element-template-inline.mustache
theme/bootstrapbase/layout/columns1.php
theme/bootstrapbase/layout/columns3.php
theme/bootstrapbase/less/moodle.less
theme/bootstrapbase/less/moodle/calendar.less
theme/bootstrapbase/less/moodle/modal.less
theme/bootstrapbase/less/moodle/modules.less
theme/bootstrapbase/less/moodle/question.less
theme/bootstrapbase/less/moodle/variables.less [new file with mode: 0644]
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/edit_user_roles.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 ae3c709..ba8a49e 100644 (file)
@@ -203,7 +203,7 @@ if (!empty($options['collation'])) {
                 $DB->change_database_structure($sql);
             } else {
                 echo "ERROR (unknown column type: $column->type)\n";
-                $error++;
+                $errors++;
                 continue;
             }
             echo "CONVERTED\n";
index 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 a1ac0b1..17724a3 100644 (file)
@@ -84,7 +84,7 @@ if ($hassiteconfig) {
             $timesplittingoptions[$key] = $timesplitting->get_name();
         }
         $settings->add(new admin_setting_configmultiselect('analytics/timesplittings',
-            new lang_string('enabledtimesplittings', 'analytics'), new lang_string('enabledtimesplittings_help', 'analytics'),
+            new lang_string('enabledtimesplittings', 'analytics'), new lang_string('timesplittingmethod_help', 'analytics'),
             $timesplittingdefaults, $timesplittingoptions)
         );
 
index 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..6cf1f28 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);
@@ -949,16 +928,14 @@ class auth_plugin_ldap extends auth_plugin_base {
 
                 $id = user_create_user($user, false);
                 echo "\t"; print_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)); echo "\n";
-                $euser = $DB->get_record('user', array('id' => $id));
+                $user = $DB->get_record('user', array('id' => $id));
 
                 if (!empty($this->config->forcechangepassword)) {
                     set_user_preference('auth_forcepasswordchange', 1, $id);
                 }
 
-                // 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 d50f67d..f746583 100644 (file)
@@ -110,7 +110,7 @@ class auth_ldap_plugin_testcase extends advanced_testcase {
         set_config('user_attribute', 'cn', 'auth_ldap');
         set_config('memberattribute', 'memberuid', 'auth_ldap');
         set_config('memberattribute_isdn', 0, 'auth_ldap');
-        set_config('creators', 'cn=creators,'.$topdn, 'auth_ldap');
+        set_config('coursecreatorcontext', 'cn=creators,'.$topdn, 'auth_ldap');
         set_config('removeuser', AUTH_REMOVEUSER_KEEP, 'auth_ldap');
 
         set_config('field_map_email', 'mail', 'auth_ldap');
index 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 db6f058..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"
@@ -141,7 +138,6 @@ Feature: Enable the calendar block in a course and test it's functionality
     And I add the "Calendar" block
     And I create a calendar event with form data:
       | id_eventtype | Group |
-      | id_groupid | Group 1 |
       | id_name | Group Event |
     And I log out
     Then I log in as "student1"
@@ -179,7 +175,6 @@ Feature: Enable the calendar block in a course and test it's functionality
     And I am on "Course 1" course homepage
     And I create a calendar event with form data:
       | id_eventtype | Group |
-      | id_groupid | Group 1 |
       | id_name | Group Event 1 |
     And I log out
     Then I log in as "student1"
index d517c5f..9e092bf 100644 (file)
@@ -4,7 +4,7 @@ Feature: Enable the upcoming events block in a course
   As a teacher
   I can view the event in the upcoming events block
 
-  Scenario: View a global event in the upcoming events block in a course
+  Background:
     Given the following "users" exist:
       | username | firstname | lastname | email | idnumber |
       | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
@@ -14,7 +14,10 @@ Feature: Enable the upcoming events block in a course
     And the following "course enrolments" exist:
       | user | course | role |
       | teacher1 | C1 | editingteacher |
-    When I log in as "admin"
+
+  @javascript
+  Scenario: View a global event in the calendar block
+    Given I log in as "admin"
     And I create a calendar event with form data:
       | id_eventtype | Site |
       | id_name | My Site Event |
index ceb1898..0b7c739 100644 (file)
@@ -3,12 +3,14 @@ Feature: View a upcoming site event on the dashboard
   In order to view a site event
   As a student
   I can view the event in the upcoming events block
-
-  Scenario: View a global event in the upcoming events block on the dashboard
+  Background:
     Given the following "users" exist:
       | username | firstname | lastname | email | idnumber |
       | student1 | Student | 1 | student1@example.com | S1 |
-    And I log in as "admin"
+
+  @javascript
+  Scenario: View a global event in the upcoming events block on the dashboard
+    Given I log in as "admin"
     And I create a calendar event with form data:
       | id_eventtype | Site |
       | id_name | My Site Event |
index a101952..448b710 100644 (file)
@@ -4,11 +4,14 @@ Feature: View a site event on the frontpage
   As a teacher
   I can view the event in the upcoming events block
 
-  Scenario: View a global event in the upcoming events block on the frontpage
+  Background:
     Given the following "users" exist:
       | username | firstname | lastname | email | idnumber |
       | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
-    And I log in as "admin"
+
+  @javascript
+  Scenario: View a global event in the upcoming events block on the frontpage
+    Given I log in as "admin"
     And I create a calendar event with form data:
       | id_eventtype | Site |
       | id_name | My Site 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 a850602..5fa4a0b 100644 (file)
@@ -29,7 +29,7 @@ Feature: People Block used in a course
     When I log in as "student1"
     And I am on "Course 1" course homepage
     And I click on "Participants" "link" in the "People" "block"
-    Then I should see "All participants" in the "#page-content" "css_element"
+    Then I should see "Participants" in the "#page-content" "css_element"
 
   Scenario: Student without permission can not view participants link
     Given the following "permission overrides" exist:
index d54e1ca..fab055d 100644 (file)
Binary files a/calendar/amd/build/calendar.min.js and b/calendar/amd/build/calendar.min.js differ
diff --git a/calendar/amd/build/calendar_events.min.js b/calendar/amd/build/calendar_events.min.js
deleted file mode 100644 (file)
index 5496507..0000000
Binary files a/calendar/amd/build/calendar_events.min.js and /dev/null differ
diff --git a/calendar/amd/build/event_form.min.js b/calendar/amd/build/event_form.min.js
new file mode 100644 (file)
index 0000000..49c3a88
Binary files /dev/null and b/calendar/amd/build/event_form.min.js differ
diff --git a/calendar/amd/build/events.min.js b/calendar/amd/build/events.min.js
new file mode 100644 (file)
index 0000000..938b804
Binary files /dev/null and b/calendar/amd/build/events.min.js differ
diff --git a/calendar/amd/build/modal_event_form.min.js b/calendar/amd/build/modal_event_form.min.js
new file mode 100644 (file)
index 0000000..78b804a
Binary files /dev/null and b/calendar/amd/build/modal_event_form.min.js differ
similarity index 54%
rename from calendar/amd/build/calendar_repository.min.js
rename to calendar/amd/build/repository.min.js
index bfb5d0d..27c0538 100644 (file)
Binary files a/calendar/amd/build/calendar_repository.min.js and b/calendar/amd/build/repository.min.js differ
index 3c917ad..9c0bb22 100644 (file)
Binary files a/calendar/amd/build/summary_modal.min.js and b/calendar/amd/build/summary_modal.min.js differ
index 5de6318..1843984 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * A javascript module to calendar events.
+ * This module is the highest level module for the calendar. It is
+ * responsible for initialising all of the components required for
+ * the calendar to run. It also coordinates the interaction between
+ * components by listening for and responding to different events
+ * triggered within the calendar UI.
  *
  * @module     core_calendar/calendar
  * @package    core_calendar
  * @copyright  2017 Simey Lameze <simey@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/ajax', 'core/str', 'core/templates', 'core/notification', 'core/custom_interaction_events',
-        'core/modal_factory', 'core_calendar/summary_modal', 'core/modal_events', 'core_calendar/calendar_repository'],
-    function($, Ajax, Str, Templates, Notification, CustomEvents, ModalFactory, SummaryModal, ModalEvents, CalendarRepository) {
-
-        var SELECTORS = {
-            ROOT: "[data-region='calendar']",
-            EVENT_LINK: "[data-action='view-event']",
-        };
-
-        /**
-         * Get the event type lang string.
-         *
-         * @param {String} eventType The event type.
-         * @return {promise} The lang string promise.
-         */
-        var getEventType = function(eventType) {
-            var lang = 'type' + eventType;
-            return Str.get_string(lang, 'core_calendar').then(function(langStr) {
-                return langStr;
-            });
-        };
-
-        /**
-         * Get the event source.
-         *
-         * @param {Object} subscription The event subscription object.
-         * @return {promise} The lang string promise.
-         */
-        var getEventSource = function(subscription) {
-            return Str.get_string('subsource', 'core_calendar', subscription).then(function(langStr) {
-                if (subscription.url) {
-                    return '<a href="' + subscription.url + '">' + langStr + '</a>';
-                }
-                return langStr;
-            });
-        };
-
-        /**
-         * Render the event summary modal.
-         *
-         * @param {Number} eventId The calendar event id.
-         */
-        var renderEventSummaryModal = function(eventId) {
-            // Calendar repository promise.
-            CalendarRepository.getEventById(eventId).then(function(getEventResponse) {
-                if (!getEventResponse.event) {
-                    throw new Error('Error encountered while trying to fetch calendar event with ID: ' + eventId);
-                }
-                var eventData = getEventResponse.event;
-                var eventTypePromise = getEventType(eventData.eventtype);
-
-                // If the calendar event has event source, get the source's language string/link.
-                if (eventData.displayeventsource) {
-                    eventData.subscription = JSON.parse(eventData.subscription);
-                    var eventSourceParams = {
-                        url: eventData.subscription.url,
-                        name: eventData.subscription.name
-                    };
-                    var eventSourcePromise = getEventSource(eventSourceParams);
-
-                    // Return event data with event type and event source info.
-                    return $.when(eventTypePromise, eventSourcePromise).then(function(eventType, eventSource) {
-                        eventData.eventtype = eventType;
-                        eventData.source = eventSource;
-                        return eventData;
-                    });
-                }
+define([
+            'jquery',
+            'core/ajax',
+            'core/str',
+            'core/templates',
+            'core/notification',
+            'core/custom_interaction_events',
+            'core/modal_events',
+            'core/modal_factory',
+            'core_calendar/modal_event_form',
+            'core_calendar/summary_modal',
+            'core_calendar/repository',
+            'core_calendar/events'
+        ],
+        function(
+            $,
+            Ajax,
+            Str,
+            Templates,
+            Notification,
+            CustomEvents,
+            ModalEvents,
+            ModalFactory,
+            ModalEventForm,
+            SummaryModal,
+            CalendarRepository,
+            CalendarEvents
+        ) {
+
+    var SELECTORS = {
+        ROOT: "[data-region='calendar']",
+        EVENT_LINK: "[data-action='view-event']",
+        NEW_EVENT_BUTTON: "[data-action='new-event-button']"
+    };
+
+    /**
+     * Get the event type lang string.
+     *
+     * @param {String} eventType The event type.
+     * @return {promise} The lang string promise.
+     */
+    var getEventType = function(eventType) {
+        var lang = 'type' + eventType;
+        return Str.get_string(lang, 'core_calendar').then(function(langStr) {
+            return langStr;
+        });
+    };
+
+    /**
+     * Get the event source.
+     *
+     * @param {Object} subscription The event subscription object.
+     * @return {promise} The lang string promise.
+     */
+    var getEventSource = function(subscription) {
+        return Str.get_string('subsource', 'core_calendar', subscription).then(function(langStr) {
+            if (subscription.url) {
+                return '<a href="' + subscription.url + '">' + langStr + '</a>';
+            }
+            return langStr;
+        });
+    };
 
-                // Return event data with event type info.
-                return eventTypePromise.then(function(eventType) {
+    /**
+     * Render the event summary modal.
+     *
+     * @param {Number} eventId The calendar event id.
+     */
+    var renderEventSummaryModal = function(eventId) {
+        // Calendar repository promise.
+        CalendarRepository.getEventById(eventId).then(function(getEventResponse) {
+            if (!getEventResponse.event) {
+                throw new Error('Error encountered while trying to fetch calendar event with ID: ' + eventId);
+            }
+            var eventData = getEventResponse.event;
+            var eventTypePromise = getEventType(eventData.eventtype);
+
+            // If the calendar event has event source, get the source's language string/link.
+            if (eventData.displayeventsource) {
+                eventData.subscription = JSON.parse(eventData.subscription);
+                var eventSourceParams = {
+                    url: eventData.subscription.url,
+                    name: eventData.subscription.name
+                };
+                var eventSourcePromise = getEventSource(eventSourceParams);
+
+                // Return event data with event type and event source info.
+                return $.when(eventTypePromise, eventSourcePromise).then(function(eventType, eventSource) {
                     eventData.eventtype = eventType;
+                    eventData.source = eventSource;
                     return eventData;
                 });
+            }
 
-            }).then(function(eventData) {
-                // Build the modal parameters from the event data.
-                var modalParams = {
-                    title: eventData.name,
-                    type: SummaryModal.TYPE,
-                    body: Templates.render('core_calendar/event_summary_body', eventData)
-                };
-                if (!eventData.caneditevent) {
-                    modalParams.footer = '';
+            // Return event data with event type info.
+            return eventTypePromise.then(function(eventType) {
+                eventData.eventtype = eventType;
+                return eventData;
+            });
+
+        }).then(function(eventData) {
+            // Build the modal parameters from the event data.
+            var modalParams = {
+                title: eventData.name,
+                type: SummaryModal.TYPE,
+                body: Templates.render('core_calendar/event_summary_body', eventData)
+            };
+            if (!eventData.caneditevent) {
+                modalParams.footer = '';
+            }
+            // Create the modal.
+            return ModalFactory.create(modalParams);
+
+        }).done(function(modal) {
+            // Handle hidden event.
+            modal.getRoot().on(ModalEvents.hidden, function() {
+                // Destroy when hidden.
+                modal.destroy();
+            });
+
+            // Finally, render the modal!
+            modal.show();
+
+        }).fail(Notification.exception);
+    };
+
+    /**
+     * Create the event form modal for creating new events and
+     * editing existing events.
+     *
+     * @method registerEventFormModal
+     * @param {object} root The calendar root element
+     * @return {object} The create modal promise
+     */
+    var registerEventFormModal = function(root) {
+        var newEventButton = root.find(SELECTORS.NEW_EVENT_BUTTON);
+        var contextId = newEventButton.attr('data-context-id');
+
+        return ModalFactory.create(
+            {
+                type: ModalEventForm.TYPE,
+                large: true,
+                templateContext: {
+                    contextid: contextId
                 }
-                // Create the modal.
-                return ModalFactory.create(modalParams);
-
-            }).done(function(modal) {
-                // Handle hidden event.
-                modal.getRoot().on(ModalEvents.hidden, function() {
-                    // Destroy when hidden.
-                    modal.destroy();
-                });
+            },
+            newEventButton
+        );
+    };
 
-                // Finally, render the modal!
-                modal.show();
+    /**
+     * Listen to and handle any calendar events fired by the calendar UI.
+     *
+     * @method registerCalendarEventListeners
+     * @param {object} root The calendar root element
+     * @param {object} eventFormModalPromise A promise reolved with the event form modal
+     */
+    var registerCalendarEventListeners = function(root, eventFormModalPromise) {
+        var body = $('body');
+
+        // TODO: Replace these with actual logic to update
+        // the UI without having to force a page reload.
+        body.on(CalendarEvents.created, function() {
+            window.location.reload();
+        });
+        body.on(CalendarEvents.deleted, function() {
+            window.location.reload();
+        });
+        body.on(CalendarEvents.updated, function() {
+            window.location.reload();
+        });
 
-            }).fail(Notification.exception);
-        };
-
-        /**
-         * Register event listeners for the module.
-         */
-        var registerEventListeners = function() {
-            // Bind click events to event links.
-            $(SELECTORS.EVENT_LINK).click(function(e) {
-                e.preventDefault();
-                var eventId = $(this).attr('data-event-id');
-                renderEventSummaryModal(eventId);
+        eventFormModalPromise.then(function(modal) {
+            // When something within the calendar tells us the user wants
+            // to edit an event then show the event form modal.
+            body.on(CalendarEvents.editEvent, function(e, eventId) {
+                modal.setEventId(eventId);
+                modal.show();
             });
-        };
 
-        return {
-            init: function() {
-                registerEventListeners();
-            }
-        };
-    });
+            return;
+        });
+    };
+
+    /**
+     * Register event listeners for the module.
+     */
+    var registerEventListeners = function() {
+        var root = $(SELECTORS.ROOT);
+
+        // Bind click events to event links.
+        $(SELECTORS.EVENT_LINK).click(function(e) {
+            e.preventDefault();
+            var eventId = $(this).attr('data-event-id');
+            renderEventSummaryModal(eventId);
+        });
+
+        var eventFormPromise = registerEventFormModal(root);
+        registerCalendarEventListeners(root, eventFormPromise);
+    };
+
+    return {
+        init: function() {
+            registerEventListeners();
+        }
+    };
+});
diff --git a/calendar/amd/src/event_form.js b/calendar/amd/src/event_form.js
new file mode 100644 (file)
index 0000000..e62ee63
--- /dev/null
@@ -0,0 +1,282 @@
+// 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 javascript module to enhance the event form.
+ *
+ * @module     core_calendar/event_form
+ * @package    core_calendar
+ * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/templates'], function($, Templates) {
+
+    var SELECTORS = {
+        EVENT_TYPE: '[name="eventtype"]',
+        EVENT_COURSE_ID: '[name="courseid"]',
+        EVENT_GROUP_COURSE_ID: '[name="groupcourseid"]',
+        EVENT_GROUP_ID: '[name="groupid"]',
+        FORM_GROUP: '.form-group',
+        SELECT_OPTION: 'option',
+        ADVANCED_ELEMENT: '.fitem.advanced',
+        FIELDSET_ADVANCED_ELEMENTS: 'fieldset.containsadvancedelements',
+        MORELESS_TOGGLE: '.moreless-actions'
+    };
+
+    var EVENT_TYPES = {
+        USER: 'user',
+        SITE: 'site',
+        COURSE: 'course',
+        GROUP: 'group'
+    };
+
+    var EVENTS = {
+        SHOW_ADVANCED: 'event_form-show-advanced',
+        HIDE_ADVANCED: 'event_form-hide-advanced',
+        ADVANCED_SHOWN: 'event_form-advanced-shown',
+        ADVANCED_HIDDEN: 'event_form-advanced-hidden',
+    };
+
+    /**
+     * Find the old show more / show less toggle added by the mform and destroy it.
+     * We are handling the visibility of the advanced fields with the more/less button
+     * in the footer of the modal that this form is rendered within.
+     *
+     * @method destroyOldMoreLessToggle
+     * @param {object} formElement The root form element
+     */
+    var destroyOldMoreLessToggle = function(formElement) {
+        formElement.find(SELECTORS.FIELDSET_ADVANCED_ELEMENTS).removeClass('containsadvancedelements');
+        var element = formElement.find(SELECTORS.MORELESS_TOGGLE);
+        Templates.replaceNode(element, '', '');
+    };
+
+    /**
+     * Find each of the advanced form elements and make them visible.
+     *
+     * This function triggers the ADVANCED_SHOWN event for any other
+     * component to handle (e.g. the event form modal).
+     *
+     * @method destroyOldMoreLessToggle
+     * @param {object} formElement The root form element
+     */
+    var showAdvancedElements = function(formElement) {
+        formElement.find(SELECTORS.ADVANCED_ELEMENT).removeClass('hidden');
+        formElement.trigger(EVENTS.ADVANCED_SHOWN);
+    };
+
+    /**
+     * Find each of the advanced form elements and hide them.
+     *
+     * This function triggers the ADVANCED_HIDDEN event for any other
+     * component to handle (e.g. the event form modal).
+     *
+     * @method hideAdvancedElements
+     * @param {object} formElement The root form element
+     */
+    var hideAdvancedElements = function(formElement) {
+        formElement.find(SELECTORS.ADVANCED_ELEMENT).addClass('hidden');
+        formElement.trigger(EVENTS.ADVANCED_HIDDEN);
+    };
+
+    /**
+     * Listen for any events telling this module to show or hide it's
+     * advanced elements.
+     *
+     * This function listens for SHOW_ADVANCED and HIDE_ADVANCED.
+     *
+     * @method listenForShowHideEvents
+     * @param {object} formElement The root form element
+     */
+    var listenForShowHideEvents = function(formElement) {
+        formElement.on(EVENTS.SHOW_ADVANCED, function() {
+            showAdvancedElements(formElement);
+        });
+
+        formElement.on(EVENTS.HIDE_ADVANCED, function() {
+            hideAdvancedElements(formElement);
+        });
+    };
+
+    /**
+     * Parse the group id select element in the event form and pull out
+     * the course id from the value to allow us to toggle other select
+     * elements based on the course id for the group a user selects.
+     *
+     * This is a little hacky but I couldn't find a better way to pass
+     * the course id for each group id with the limitations of mforms.
+     *
+     * The group id options are rendered with a value like:
+     * "<courseid>-<groupid>"
+     * E.g.
+     * For a group with id 10 in a course with id 3 the value of the
+     * option will be 3-10.
+     *
+     * @method parseGroupSelect
+     * @param {object} formElement The root form element
+     */
+    var parseGroupSelect = function(formElement) {
+        formElement.find(SELECTORS.EVENT_GROUP_ID)
+            .find(SELECTORS.SELECT_OPTION)
+            .each(function(index, element) {
+                element = $(element);
+                var value = element.attr('value');
+                var splits = value.split('-');
+                var courseId = splits[0];
+
+                element.attr('data-course-id', courseId);
+            });
+    };
+
+    /**
+     * Toggle the visibility of the secondary select elements based on
+     * the event type the user has selected.
+     *
+     * There are 3 secondary select elements within the form:
+     *      - course: a list of all courses a user can add course events to
+     *      - group course: a list of all courses a user can add group events to.
+     *                      this list can be different from the course list above.
+     *      - group: a list of all groups a user can add an event to. This list will
+     *               be filtered further based on the group course selected.
+     *
+     *  There are 4 event types:
+     *      - user: none of the secondary selects should be visible.
+     *      - site: none of the secondary selects should be visible.
+     *      - course: "course" select should be visible and both "group course"
+     *                and "group" should be hidden.
+     *      - group: "group course" and "group" should be visible and "course"
+     *               should be hidden.
+     *
+     * @method hideTypeSubSelects
+     * @param {object} formElement The root form element
+     */
+    var hideTypeSubSelects = function(formElement) {
+        var typeSelect = formElement.find(SELECTORS.EVENT_TYPE);
+        var eventType = typeSelect.val();
+        var courseIdSelect = formElement.find(SELECTORS.EVENT_COURSE_ID)
+            .closest(SELECTORS.FORM_GROUP)
+            .removeClass('hidden');
+        var groupCourseIdSelect = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID)
+            .closest(SELECTORS.FORM_GROUP)
+            .removeClass('hidden');
+        var groupIdSelect = formElement.find(SELECTORS.EVENT_GROUP_ID)
+            .closest(SELECTORS.FORM_GROUP)
+            .removeClass('hidden');
+
+        // Hide the unreleated selectors for the given event type.
+        switch (eventType) {
+            case EVENT_TYPES.COURSE:
+                groupCourseIdSelect.addClass('hidden');
+                groupIdSelect.addClass('hidden');
+                break;
+            case EVENT_TYPES.GROUP:
+                courseIdSelect.addClass('hidden');
+                break;
+            default:
+                courseIdSelect.addClass('hidden');
+                groupCourseIdSelect.addClass('hidden');
+                groupIdSelect.addClass('hidden');
+        }
+    };
+
+    /**
+     * Listen for when the user changes the event type select in the
+     * form and then toggle the visibility of the appropriate secondary
+     * select elements.
+     *
+     * See: hideTypeSubSelects.
+     *
+     * @method addTypeSelectListeners
+     * @param {object} formElement The root form element
+     */
+    var addTypeSelectListeners = function(formElement) {
+        var typeSelect = formElement.find(SELECTORS.EVENT_TYPE);
+
+        typeSelect.on('change', function() {
+            hideTypeSubSelects(formElement);
+        });
+    };
+
+    /**
+     * Listen for when the user changes the group course when configuring
+     * a group event and filter the options in the group select to only
+     * show the groups available within the course the user has selected.
+     *
+     * @method addCourseGroupSelectListeners
+     * @param {object} formElement The root form element
+     */
+    var addCourseGroupSelectListeners = function(formElement) {
+        var courseGroupSelect = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID);
+        var groupSelect = formElement.find(SELECTORS.EVENT_GROUP_ID);
+        var groupSelectOptions = groupSelect.find(SELECTORS.SELECT_OPTION);
+        var filterGroupSelectOptions = function() {
+            var selectedCourseId = courseGroupSelect.val();
+            var selectedIndex = null;
+
+            groupSelectOptions.each(function(index, element) {
+                element = $(element);
+
+                if (element.attr('data-course-id') == selectedCourseId) {
+                    element.removeClass('hidden');
+                    element.prop('disabled', false);
+
+                    if (selectedIndex === null) {
+                        selectedIndex = index;
+                    }
+                } else {
+                    element.addClass('hidden');
+                    element.prop('disabled', true);
+                }
+            });
+
+            groupSelect.prop('selectedIndex', selectedIndex);
+        };
+
+        courseGroupSelect.on('change', filterGroupSelectOptions);
+        filterGroupSelectOptions();
+    };
+
+    /**
+     * Initialise all of the form enhancementds.
+     *
+     * @method init
+     * @param {string} formId The value of the form's id attribute
+     * @param {bool} hasError If the form has errors rendered form the server.
+     */
+    var init = function(formId, hasError) {
+        var formElement = $('#' + formId);
+
+        listenForShowHideEvents(formElement);
+        destroyOldMoreLessToggle(formElement);
+        hideTypeSubSelects(formElement);
+        parseGroupSelect(formElement);
+        addTypeSelectListeners(formElement);
+        addCourseGroupSelectListeners(formElement);
+
+        // If we know that the form has been rendered with server side
+        // errors then we need to display all of the elements in the form
+        // in case one of those elements has the error.
+        if (hasError) {
+            showAdvancedElements(formElement);
+        } else {
+            hideAdvancedElements(formElement);
+        }
+    };
+
+    return {
+        init: init,
+        events: EVENTS,
+    };
+});
similarity index 77%
rename from calendar/amd/src/calendar_events.js
rename to calendar/amd/src/events.js
index bb88d81..80ec8ca 100644 (file)
@@ -14,9 +14,9 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Contain the events a modal can fire.
+ * Contain the events the calendar component can fire.
  *
- * @module     core_calendar/calendar_events
+ * @module     core_calendar/events
  * @class      calendar_events
  * @package    core_calendar
  * @copyright  2017 Simey Lameze <simey@moodle.com>
@@ -24,6 +24,9 @@
  */
 define([], function() {
     return {
-        deleted: 'calendar-events:deleted'
+        created: 'calendar-events:created',
+        deleted: 'calendar-events:deleted',
+        updated: 'calendar-events:updated',
+        editEvent: 'calendar-events:edit_event'
     };
 });
diff --git a/calendar/amd/src/modal_event_form.js b/calendar/amd/src/modal_event_form.js
new file mode 100644 (file)
index 0000000..bad5ca4
--- /dev/null
@@ -0,0 +1,433 @@
+// 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/>.
+
+/**
+ * Contain the logic for the quick add or update event modal.
+ *
+ * @module     calendar/modal_quick_add_event
+ * @class      modal_quick_add_event
+ * @package    core
+ * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([
+            'jquery',
+            'core/event',
+            'core/str',
+            'core/notification',
+            'core/templates',
+            'core/custom_interaction_events',
+            'core/modal',
+            'core/modal_registry',
+            'core/fragment',
+            'core_calendar/events',
+            'core_calendar/repository',
+            'core_calendar/event_form'
+        ],
+        function(
+            $,
+            Event,
+            Str,
+            Notification,
+            Templates,
+            CustomEvents,
+            Modal,
+            ModalRegistry,
+            Fragment,
+            CalendarEvents,
+            Repository,
+            EventForm
+        ) {
+
+    var registered = false;
+    var SELECTORS = {
+        MORELESS_BUTTON: '[data-action="more-less-toggle"]',
+        SAVE_BUTTON: '[data-action="save"]',
+        LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]',
+    };
+
+    /**
+     * Constructor for the Modal.
+     *
+     * @param {object} root The root jQuery element for the modal
+     */
+    var ModalEventForm = function(root) {
+        Modal.call(this, root);
+        this.eventId = null;
+        this.reloadingBody = false;
+        this.reloadingTitle = false;
+        this.saveButton = this.getFooter().find(SELECTORS.SAVE_BUTTON);
+        this.moreLessButton = this.getFooter().find(SELECTORS.MORELESS_BUTTON);
+    };
+
+    ModalEventForm.TYPE = 'core_calendar-modal_event_form';
+    ModalEventForm.prototype = Object.create(Modal.prototype);
+    ModalEventForm.prototype.constructor = ModalEventForm;
+
+    /**
+     * Set the event id to the given value.
+     *
+     * @method setEventId
+     * @param {int} id The event id
+     */
+    ModalEventForm.prototype.setEventId = function(id) {
+        this.eventId = id;
+    };
+
+    /**
+     * Retrieve the current event id, if any.
+     *
+     * @method getEventId
+     * @return {int|null} The event id
+     */
+    ModalEventForm.prototype.getEventId = function() {
+        return this.eventId;
+    };
+
+    /**
+     * Check if the modal has an event id.
+     *
+     * @method hasEventId
+     * @return {bool}
+     */
+    ModalEventForm.prototype.hasEventId = function() {
+        return this.eventId !== null;
+    };
+
+    /**
+     * Get the form element from the modal.
+     *
+     * @method getForm
+     * @return {object}
+     */
+    ModalEventForm.prototype.getForm = function() {
+        return this.getBody().find('form');
+    };
+
+    /**
+     * Disable the buttons in the footer.
+     *
+     * @method disableButtons
+     */
+    ModalEventForm.prototype.disableButtons = function() {
+        this.saveButton.prop('disabled', true);
+        this.moreLessButton.prop('disabled', true);
+    };
+
+    /**
+     * Enable the buttons in the footer.
+     *
+     * @method enableButtons
+     */
+    ModalEventForm.prototype.enableButtons = function() {
+        this.saveButton.prop('disabled', false);
+        this.moreLessButton.prop('disabled', false);
+    };
+
+    /**
+     * Set the more/less button in the footer to the "more"
+     * state.
+     *
+     * @method setMoreButton
+     */
+    ModalEventForm.prototype.setMoreButton = function() {
+        this.moreLessButton.attr('data-collapsed', 'true');
+        Str.get_string('more', 'calendar').then(function(string) {
+            this.moreLessButton.text(string);
+            return;
+        }.bind(this));
+    };
+
+    /**
+     * Set the more/less button in the footer to the "less"
+     * state.
+     *
+     * @method setLessButton
+     */
+    ModalEventForm.prototype.setLessButton = function() {
+        this.moreLessButton.attr('data-collapsed', 'false');
+        Str.get_string('less', 'calendar').then(function(string) {
+            this.moreLessButton.text(string);
+            return;
+        }.bind(this));
+    };
+
+    /**
+     * Toggle the more/less button in the footer from the current
+     * state to it's opposite state.
+     *
+     * @method toggleMoreLessButton
+     */
+    ModalEventForm.prototype.toggleMoreLessButton = function() {
+        var form = this.getForm();
+
+        if (this.moreLessButton.attr('data-collapsed') == 'true') {
+            form.trigger(EventForm.events.SHOW_ADVANCED);
+            this.setLessButton();
+        } else {
+            form.trigger(EventForm.events.HIDE_ADVANCED);
+            this.setMoreButton();
+        }
+    };
+
+    /**
+     * Reload the title for the modal to the appropriate value
+     * depending on whether we are creating a new event or
+     * editing an existing event.
+     *
+     * @method reloadTitleContent
+     * @return {object} A promise resolved with the new title text
+     */
+    ModalEventForm.prototype.reloadTitleContent = function() {
+        if (this.reloadingTitle) {
+            return this.titlePromise;
+        }
+
+        this.reloadingTitle = true;
+
+        if (this.hasEventId()) {
+            this.titlePromise = Str.get_string('editevent', 'calendar');
+        } else {
+            this.titlePromise = Str.get_string('newevent', 'calendar');
+        }
+
+        this.titlePromise.then(function(string) {
+            this.setTitle(string);
+            return string;
+        }.bind(this))
+        .always(function() {
+            this.reloadingTitle = false;
+            return;
+        }.bind(this));
+
+        return this.titlePromise;
+    };
+
+    /**
+     * Send a request to the server to get the event_form in a fragment
+     * and render the result in the body of the modal.
+     *
+     * If serialised form data is provided then it will be sent in the
+     * request to the server to have the form rendered with the data. This
+     * is used when the form had a server side error and we need the server
+     * to re-render it for us to display the error to the user.
+     *
+     * @method reloadBodyContent
+     * @param {string} formData The serialised form data
+     * @param {bool} hasError True if we know the form data is erroneous
+     * @return {object} A promise resolved with the fragment html and js from
+     */
+    ModalEventForm.prototype.reloadBodyContent = function(formData, hasError) {
+        if (this.reloadingBody) {
+            return this.bodyPromise;
+        }
+
+        this.reloadingBody = true;
+        this.disableButtons();
+
+        var contextId = this.saveButton.attr('data-context-id');
+        var args = {};
+
+        if (this.hasEventId()) {
+            args.eventid = this.getEventId();
+        }
+
+        if (typeof formData !== 'undefined') {
+            args.formdata = formData;
+        }
+
+        args.haserror = (typeof hasError == 'undefined') ? false : hasError;
+
+        this.bodyPromise = Fragment.loadFragment('calendar', 'event_form', contextId, args);
+
+        this.setBody(this.bodyPromise);
+
+        this.bodyPromise.then(function() {
+            this.enableButtons();
+            return;
+        }.bind(this))
+        .catch(Notification.exception)
+        .always(function() {
+            this.reloadingBody = false;
+            return;
+        }.bind(this));
+
+        return this.bodyPromise;
+    };
+
+    /**
+     * Reload both the title and body content.
+     *
+     * @method reloadAllContent
+     * @return {object} promise
+     */
+    ModalEventForm.prototype.reloadAllContent = function() {
+        return $.when(this.reloadTitleContent(), this.reloadBodyContent());
+    };
+
+    /**
+     * Kick off a reload the modal content before showing it. This
+     * is to allow us to re-use the same modal for creating and
+     * editing different events within the page.
+     *
+     * We do the reload when showing the modal rather than hiding it
+     * to save a request to the server if the user closes the modal
+     * and never re-opens it.
+     *
+     * @method show
+     */
+    ModalEventForm.prototype.show = function() {
+        this.reloadAllContent();
+        Modal.prototype.show.call(this);
+    };
+
+    /**
+     * Clear the event id from the modal when it's closed so
+     * that it is loaded fresh next time it's displayed.
+     *
+     * The event id will be set by the calling code if it wants
+     * to edit a specific event.
+     *
+     * @method hide
+     */
+    ModalEventForm.prototype.hide = function() {
+        Modal.prototype.hide.call(this);
+        this.setEventId(null);
+    };
+
+    /**
+     * Get the serialised form data.
+     *
+     * @method getFormData
+     * @return {string} serialised form data
+     */
+    ModalEventForm.prototype.getFormData = function() {
+        return this.getForm().serialize();
+    };
+
+    /**
+     * Send the form data to the server to create or update
+     * an event.
+     *
+     * If there is a server side validation error then we re-request the
+     * rendered form (with the data) from the server in order to get the
+     * server side errors to display.
+     *
+     * On success the modal is hidden and the page is reloaded so that the
+     * new event will display.
+     *
+     * @method save
+     * @return {object} A promise
+     */
+    ModalEventForm.prototype.save = function() {
+        var loadingContainer = this.saveButton.find(SELECTORS.LOADING_ICON_CONTAINER);
+
+        loadingContainer.removeClass('hidden');
+        this.disableButtons();
+
+        var formData = this.getFormData();
+        // Send the form data to the server for processing.
+        return Repository.submitCreateUpdateForm(formData)
+            .then(function(response) {
+                if (response.validationerror) {
+                    // If there was a server side validation error then
+                    // we need to re-request the rendered form from the server
+                    // in order to display the error for the user.
+                    return this.reloadBodyContent(formData, true);
+                } else {
+                    // No problemo! Our work here is done.
+                    this.hide();
+
+                    // Trigger the appropriate calendar event so that the view can
+                    // be updated.
+                    if (this.hasEventId()) {
+                        $('body').trigger(CalendarEvents.updated, [response.event]);
+                    } else {
+                        $('body').trigger(CalendarEvents.created, [response.event]);
+                    }
+                }
+
+                return;
+            }.bind(this))
+            .always(function() {
+                // Regardless of success or error we should always stop
+                // the loading icon and re-enable the buttons.
+                loadingContainer.addClass('hidden');
+                this.enableButtons();
+            }.bind(this))
+            .catch(Notification.exception);
+    };
+
+    /**
+     * Set up all of the event handling for the modal.
+     *
+     * @method registerEventListeners
+     */
+    ModalEventForm.prototype.registerEventListeners = function() {
+        // Apply parent event listeners.
+        Modal.prototype.registerEventListeners.call(this);
+
+        // When the user clicks the save button we trigger the form submission. We need to
+        // trigger an actual submission because there is some JS code in the form that is
+        // listening for this event and doing some stuff (e.g. saving draft areas etc).
+        this.getModal().on(CustomEvents.events.activate, SELECTORS.SAVE_BUTTON, function(e, data) {
+            this.getForm().submit();
+            data.originalEvent.preventDefault();
+            e.stopPropagation();
+        }.bind(this));
+
+        // Catch the submit event before it is actually processed by the browser and
+        // prevent the submission. We'll take it from here.
+        this.getModal().on('submit', function(e) {
+            this.save();
+
+            // Stop the form from actually submitting and prevent it's
+            // propagation because we have already handled the event.
+            e.preventDefault();
+            e.stopPropagation();
+        }.bind(this));
+
+        // Toggle the state of the more/less button in the footer.
+        this.getModal().on(CustomEvents.events.activate, SELECTORS.MORELESS_BUTTON, function(e, data) {
+            this.toggleMoreLessButton();
+
+            data.originalEvent.preventDefault();
+            e.stopPropagation();
+        }.bind(this));
+
+        // When the event form tells us that the advanced fields are shown
+        // then the more/less button should be set to less to allow the user
+        // to hide the advanced fields.
+        this.getModal().on(EventForm.events.ADVANCED_SHOWN, function() {
+            this.setLessButton();
+        }.bind(this));
+
+        // When the event form tells us that the advanced fields are hidden
+        // then the more/less button should be set to more to allow the user
+        // to show the advanced fields.
+        this.getModal().on(EventForm.events.ADVANCED_HIDDEN, function() {
+            this.setMoreButton();
+        }.bind(this));
+    };
+
+    // Automatically register with the modal registry the first time this module is imported so that you can create modals
+    // of this type using the modal factory.
+    if (!registered) {
+        ModalRegistry.register(ModalEventForm.TYPE, ModalEventForm, 'calendar/modal_event_form');
+        registered = true;
+    }
+
+    return ModalEventForm;
+});
similarity index 75%
rename from calendar/amd/src/calendar_repository.js
rename to calendar/amd/src/repository.js
index 2de4c41..0f96079 100644 (file)
@@ -16,7 +16,7 @@
 /**
  * A javascript module to handle calendar ajax actions.
  *
- * @module     core_calendar/calendar_repository
+ * @module     core_calendar/repository
  * @class      repository
  * @package    core_calendar
  * @copyright  2017 Simey Lameze <lameze@moodle.com>
@@ -65,8 +65,27 @@ define(['jquery', 'core/ajax'], function($, Ajax) {
         return Ajax.call([request])[0];
     };
 
+    /**
+     * Submit the form data for the event form.
+     *
+     * @method submitCreateUpdateForm
+     * @param {string} formdata The URL encoded values from the form
+     * @return {promise} Resolved with the new or edited event
+     */
+    var submitCreateUpdateForm = function(formdata) {
+        var request = {
+            methodname: 'core_calendar_submit_create_update_form',
+            args: {
+                formdata: formdata
+            }
+        };
+
+        return Ajax.call([request])[0];
+    };
+
     return {
         getEventById: getEventById,
-        deleteEvent: deleteEvent
+        deleteEvent: deleteEvent,
+        submitCreateUpdateForm: submitCreateUpdateForm
     };
 });
index e344d62..b758f45 100644 (file)
@@ -22,8 +22,8 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 define(['jquery', 'core/str', 'core/notification', 'core/custom_interaction_events', 'core/modal',
-    'core/modal_registry', 'core/modal_factory', 'core/modal_events', 'core_calendar/calendar_repository',
-    'core_calendar/calendar_events'],
+    'core/modal_registry', 'core/modal_factory', 'core/modal_events', 'core_calendar/repository',
+    'core_calendar/events'],
     function($, Str, Notification, CustomEvents, Modal, ModalRegistry, ModalFactory, ModalEvents, CalendarRepository,
              CalendarEvents) {
 
@@ -32,7 +32,6 @@ define(['jquery', 'core/str', 'core/notification', 'core/custom_interaction_even
         ROOT: "[data-region='summary-modal-container']",
         EDIT_BUTTON: '[data-action="edit"]',
         DELETE_BUTTON: '[data-action="delete"]',
-        EVENT_LINK: '[data-action="event-link"]'
     };
 
     /**
@@ -43,11 +42,11 @@ define(['jquery', 'core/str', 'core/notification', 'core/custom_interaction_even
     var ModalEventSummary = function(root) {
         Modal.call(this, root);
 
-        if (!this.getFooter().find(SELECTORS.EDIT_BUTTON).length) {
+        if (!this.getEditButton().length) {
             Notification.exception({message: 'No edit button found'});
         }
 
-        if (!this.getFooter().find(SELECTORS.DELETE_BUTTON).length) {
+        if (!this.getDeleteButton().length) {
             Notification.exception({message: 'No delete button found'});
         }
     };
@@ -56,6 +55,48 @@ define(['jquery', 'core/str', 'core/notification', 'core/custom_interaction_even
     ModalEventSummary.prototype = Object.create(Modal.prototype);
     ModalEventSummary.prototype.constructor = ModalEventSummary;
 
+    /**
+     * Get the edit button element from the footer. The button is cached
+     * as it's not expected to change.
+     *
+     * @method getEditButton
+     * @return {object} button element
+     */
+    ModalEventSummary.prototype.getEditButton = function() {
+        if (typeof this.editButton == 'undefined') {
+            this.editButton = this.getFooter().find(SELECTORS.EDIT_BUTTON);
+        }
+
+        return this.editButton;
+    };
+
+    /**
+     * Get the delete button element from the footer. The button is cached
+     * as it's not expected to change.
+     *
+     * @method getDeleteButton
+     * @return {object} button element
+     */
+    ModalEventSummary.prototype.getDeleteButton = function() {
+        if (typeof this.deleteButton == 'undefined') {
+            this.deleteButton = this.getFooter().find(SELECTORS.DELETE_BUTTON);
+        }
+
+        return this.deleteButton;
+    };
+
+    /**
+     * Get the id for the event being shown in this modal. This value is
+     * not cached because it will change depending on which event is
+     * being displayed.
+     *
+     * @method getEventId
+     * @return {int}
+     */
+    ModalEventSummary.prototype.getEventId = function() {
+        return this.getBody().find(SELECTORS.ROOT).attr('data-event-id');
+    };
+
     /**
      * Set up all of the event handling for the modal.
      *
@@ -64,28 +105,53 @@ define(['jquery', 'core/str', 'core/notification', 'core/custom_interaction_even
     ModalEventSummary.prototype.registerEventListeners = function() {
         // Apply parent event listeners.
         Modal.prototype.registerEventListeners.call(this);
-        var confirmPromise = ModalFactory.create({
-            type: ModalFactory.types.CONFIRM,
-        }, this.getFooter().find(SELECTORS.DELETE_BUTTON)).then(function(modal) {
-            Str.get_string('confirm').then(function(languagestring) {
-                modal.setTitle(languagestring);
-            }.bind(this)).catch(Notification.exception);
+
+        var confirmPromise = ModalFactory.create(
+            {
+                type: ModalFactory.types.CONFIRM
+            },
+            this.getDeleteButton()
+        ).then(function(modal) {
             modal.getRoot().on(ModalEvents.yes, function() {
-                var eventId = this.getBody().find(SELECTORS.ROOT).attr('data-event-id');
-                CalendarRepository.deleteEvent(eventId).done(function() {
-                    modal.getRoot().trigger(CalendarEvents.deleted, eventId);
-                    window.location.reload();
-                }).fail(Notification.exception);
+                var eventId = this.getEventId();
+
+                CalendarRepository.deleteEvent(eventId)
+                    .then(function() {
+                        $('body').trigger(CalendarEvents.deleted, [eventId]);
+                        this.hide();
+                    }.bind(this))
+                    .catch(Notification.exception);
             }.bind(this));
+
             return modal;
         }.bind(this));
 
+        // We have to wait for the modal to finish rendering in order to ensure that
+        // the data-event-title property is available to use as the modal title.
         this.getRoot().on(ModalEvents.bodyRendered, function() {
             var eventTitle = this.getBody().find(SELECTORS.ROOT).attr('data-event-title');
             confirmPromise.then(function(modal) {
                 modal.setBody(Str.get_string('confirmeventdelete', 'core_calendar', eventTitle));
             });
         }.bind(this));
+
+        CustomEvents.define(this.getEditButton(), [
+            CustomEvents.events.activate
+        ]);
+
+        this.getEditButton().on(CustomEvents.events.activate, function(e, data) {
+            // When the edit button is clicked we fire an event for the calendar UI to handle.
+            // We don't care how the UI chooses to handle it.
+            $('body').trigger(CalendarEvents.editEvent, [this.getEventId()]);
+            // There is nothing else for us to do so let's hide.
+            this.hide();
+
+            // We've handled this event so no need to propagate it.
+            e.preventDefault();
+            e.stopPropagation();
+            data.originalEvent.preventDefault();
+            data.originalEvent.stopPropagation();
+        }.bind(this));
     };
 
     // Automatically register with the modal registry the first time this module is imported so that you can create modals
index baa73f6..ec2d938 100644 (file)
@@ -71,7 +71,7 @@ class event_exporter extends exporter {
         $data->timestart = $starttimestamp;
         $data->timeduration = $endtimestamp - $starttimestamp;
         $data->timesort = $event->get_times()->get_sort_time()->getTimestamp();
-        $data->visible = $event->is_visible();
+        $data->visible = $event->is_visible() ? 1 : 0;
         $data->timemodified = $event->get_times()->get_modified_time()->getTimestamp();
 
         if ($repeats = $event->get_repeats()) {
diff --git a/calendar/classes/local/event/forms/create.php b/calendar/classes/local/event/forms/create.php
new file mode 100644 (file)
index 0000000..3e8ed78
--- /dev/null
@@ -0,0 +1,277 @@
+<?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/>.
+
+/**
+ * The mform for creating a calendar event. Based on the old event form.
+ *
+ * @package    core_calendar
+ * @copyright 2017 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_calendar\local\event\forms;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot.'/lib/formslib.php');
+
+/**
+ * The mform class for creating a calendar event.
+ *
+ * @copyright 2017 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class create extends \moodleform {
+    /**
+     * The form definition
+     */
+    public function definition () {
+        global $PAGE;
+
+        $mform = $this->_form;
+        $haserror = !empty($this->_customdata['haserror']);
+        $eventtypes = calendar_get_all_allowed_types();
+
+        $mform->setDisableShortforms();
+        $mform->disable_form_change_checker();
+
+        // Empty string so that the element doesn't get rendered.
+        $mform->addElement('header', 'general', '');
+
+        $this->add_default_hidden_elements($mform);
+
+        // Event name field.
+        $mform->addElement('text', 'name', get_string('eventname', 'calendar'), 'size="50"');
+        $mform->addRule('name', get_string('required'), 'required', null, 'client');
+        $mform->setType('name', PARAM_TEXT);
+
+        // Event time start field.
+        $mform->addElement('date_time_selector', 'timestart', get_string('date'));
+
+        // Add the select elements for the available event types.
+        $this->add_event_type_elements($mform, $eventtypes);
+
+        // Start of advanced elements.
+        // Advanced elements are not visible to the user by default.
+        // They are displayed through the user of a show more / less button.
+        $mform->addElement('editor', 'description', get_string('eventdescription', 'calendar'), ['rows' => 3]);
+        $mform->setType('description', PARAM_RAW);
+        $mform->setAdvanced('description');
+
+        // Add the variety of elements allowed for selecting event duration.
+        $this->add_event_duration_elements($mform);
+
+        // Add the form elements for repeating events.
+        $this->add_event_repeat_elements($mform);
+
+        // Add the javascript required to enhance this mform.
+        // Including the show/hide of advanced elements and the display of the correct select elements for event types.
+        $PAGE->requires->js_call_amd('core_calendar/event_form', 'init', [$mform->getAttribute('id'), $haserror]);
+    }
+
+    /**
+     * A bit of custom validation for this form
+     *
+     * @param array $data An assoc array of field=>value
+     * @param array $files An array of files
+     * @return array
+     */
+    public function validation($data, $files) {
+        global $DB, $CFG;
+
+        $errors = parent::validation($data, $files);
+        $coursekey = isset($data['groupcourseid']) ? 'groupcourseid' : 'courseid';
+
+        if (isset($data[$coursekey]) && $data[$coursekey] > 0) {
+            if ($course = $DB->get_record('course', ['id' => $data[$coursekey]])) {
+                if ($data['timestart'] < $course->startdate) {
+                    $errors['timestart'] = get_string('errorbeforecoursestart', 'calendar');
+                }
+            } else {
+                $errors[$coursekey] = get_string('invalidcourse', 'error');
+            }
+        }
+
+        if ($data['duration'] == 1 && $data['timestart'] > $data['timedurationuntil']) {
+            $errors['durationgroup'] = get_string('invalidtimedurationuntil', 'calendar');
+        } else if ($data['duration'] == 2 && (trim($data['timedurationminutes']) == '' || $data['timedurationminutes'] < 1)) {
+            $errors['durationgroup'] = get_string('invalidtimedurationminutes', 'calendar');
+        }
+
+        return $errors;
+    }
+
+    /**
+     * Add the list of hidden elements that should appear in this form each
+     * time. These elements will never be visible to the user.
+     *
+     * @param MoodleQuickForm $mform
+     */
+    protected function add_default_hidden_elements($mform) {
+        global $USER;
+
+        // Add some hidden fields.
+        $mform->addElement('hidden', 'id');
+        $mform->setType('id', PARAM_INT);
+        $mform->setDefault('id', 0);
+
+        $mform->addElement('hidden', 'userid');
+        $mform->setType('userid', PARAM_INT);
+        $mform->setDefault('userid', $USER->id);
+
+        $mform->addElement('hidden', 'modulename');
+        $mform->setType('modulename', PARAM_INT);
+        $mform->setDefault('modulename', '');
+
+        $mform->addElement('hidden', 'instance');
+        $mform->setType('instance', PARAM_INT);
+        $mform->setDefault('instance', 0);
+
+        $mform->addElement('hidden', 'visible');
+        $mform->setType('visible', PARAM_INT);
+        $mform->setDefault('visible', 1);
+    }
+
+    /**
+     * Add the appropriate elements for the available event types.
+     *
+     * If the only event type available is 'user' then we add a hidden
+     * element because there is nothing for the user to choose.
+     *
+     * If more than one type is available then we add the elements as
+     * follows:
+     *      - Always add the event type selector
+     *      - Elements per type:
+     *          - course: add an additional select element with each
+     *                    course as an option.
+     *          - group: add a select element for the course (different
+     *                   from the above course select) and a select
+     *                   element for the group.
+     *
+     * @param MoodleQuickForm $mform
+     * @param array $eventtypes The available event types for the user
+     */
+    protected function add_event_type_elements($mform, $eventtypes) {
+        $options = [];
+
+        if (isset($eventtypes['user'])) {
+            $options['user'] = get_string('user');
+        }
+        if (isset($eventtypes['group'])) {
+            $options['group'] = get_string('group');
+        }
+        if (isset($eventtypes['course'])) {
+            $options['course'] = get_string('course');
+        }
+        if (isset($eventtypes['site'])) {
+            $options['site'] = get_string('site');
+        }
+
+        // If we only have one event type and it's 'user' event then don't bother
+        // rendering the select boxes because there is no choice for the user to
+        // make.
+        if (count(array_keys($eventtypes)) == 1 && isset($eventtypes['user'])) {
+            $mform->addElement('hidden', 'eventtype');
+            $mform->setType('eventtype', PARAM_TEXT);
+            $mform->setDefault('eventtype', 'user');
+
+            // Render a static element to tell the user what type of event will
+            // be created.
+            $mform->addElement('static', 'staticeventtype', get_string('eventkind', 'calendar'), $options['user']);
+            return;
+        } else {
+            $mform->addElement('select', 'eventtype', get_string('eventkind', 'calendar'), $options);
+        }
+
+        if (isset($eventtypes['course'])) {
+            $courseoptions = [];
+            foreach ($eventtypes['course'] as $course) {
+                $courseoptions[$course->id] = format_string($course->fullname, true,
+                    ['context' => \context_course::instance($course->id)]);
+            }
+
+            $mform->addElement('select', 'courseid', get_string('course'), $courseoptions);
+            $mform->disabledIf('courseid', 'eventtype', 'noteq', 'course');
+        }
+
+        if (isset($eventtypes['group'])) {
+            $courseoptions = [];
+            foreach ($eventtypes['groupcourses'] as $course) {
+                $courseoptions[$course->id] = format_string($course->fullname, true,
+                    ['context' => \context_course::instance($course->id)]);
+            }
+
+            $mform->addElement('select', 'groupcourseid', get_string('course'), $courseoptions);
+            $mform->disabledIf('groupcourseid', 'eventtype', 'noteq', 'group');
+
+            $groupoptions = [];
+            foreach ($eventtypes['group'] as $group) {
+                // We are formatting it this way in order to provide the javascript both
+                // the course and group ids so that it can enhance the form for the user.
+                $index = "{$group->courseid}-{$group->id}";
+                $groupoptions[$index] = format_string($group->name, true,
+                    ['context' => \context_course::instance($group->courseid)]);
+            }
+
+            $mform->addElement('select', 'groupid', get_string('group'), $groupoptions);
+            $mform->disabledIf('groupid', 'eventtype', 'noteq', 'group');
+        }
+    }
+
+    /**
+     * Add the various elements to express the duration options available
+     * for an event.
+     *
+     * @param MoodleQuickForm $mform
+     */
+    protected function add_event_duration_elements($mform) {
+        $group = [];
+        $group[] = $mform->createElement('radio', 'duration', null, get_string('durationnone', 'calendar'), 0);
+        $group[] = $mform->createElement('radio', 'duration', null, get_string('durationuntil', 'calendar'), 1);
+        $group[] = $mform->createElement('date_time_selector', 'timedurationuntil', '');
+        $group[] = $mform->createElement('radio', 'duration', null, get_string('durationminutes', 'calendar'), 2);
+        $group[] = $mform->createElement('text', 'timedurationminutes', get_string('durationminutes', 'calendar'));
+
+        $mform->addGroup($group, 'durationgroup', get_string('eventduration', 'calendar'), '<br />', false);
+        $mform->setAdvanced('durationgroup');
+
+        $mform->disabledIf('timedurationuntil',         'duration', 'noteq', 1);
+        $mform->disabledIf('timedurationuntil[day]',    'duration', 'noteq', 1);
+        $mform->disabledIf('timedurationuntil[month]',  'duration', 'noteq', 1);
+        $mform->disabledIf('timedurationuntil[year]',   'duration', 'noteq', 1);
+        $mform->disabledIf('timedurationuntil[hour]',   'duration', 'noteq', 1);
+        $mform->disabledIf('timedurationuntil[minute]', 'duration', 'noteq', 1);
+
+        $mform->setType('timedurationminutes', PARAM_INT);
+        $mform->disabledIf('timedurationminutes', 'duration', 'noteq', 2);
+
+        $mform->setDefault('duration', 0);
+    }
+
+    /**
+     * Add the repeat elements for the form when creating a new event.
+     *
+     * @param MoodleQuickForm $mform
+     */
+    protected function add_event_repeat_elements($mform) {
+        $mform->addElement('checkbox', 'repeat', get_string('repeatevent', 'calendar'), null);
+        $mform->addElement('text', 'repeats', get_string('repeatweeksl', 'calendar'), 'maxlength="10" size="10"');
+        $mform->setType('repeats', PARAM_INT);
+        $mform->setDefault('repeats', 1);
+        $mform->disabledIf('repeats', 'repeat', 'notchecked');
+        $mform->setAdvanced('repeat');
+        $mform->setAdvanced('repeats');
+    }
+}
diff --git a/calendar/classes/local/event/forms/update.php b/calendar/classes/local/event/forms/update.php
new file mode 100644 (file)
index 0000000..94b0a26
--- /dev/null
@@ -0,0 +1,57 @@
+<?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/>.
+
+/**
+ * The mform for updating a calendar event. Based on the old event form.
+ *
+ * @package    core_calendar
+ * @copyright 2017 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_calendar\local\event\forms;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot.'/lib/formslib.php');
+
+/**
+ * The mform class for updating a calendar event.
+ *
+ * @copyright 2017 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class update extends create {
+    /**
+     * Add the repeat elements for the form when editing an existing event.
+     *
+     * @param MoodleQuickForm $mform
+     */
+    protected function add_event_repeat_elements($mform) {
+        $event = $this->_customdata['event'];
+
+        $mform->addElement('hidden', 'repeatid');
+        $mform->setType('repeatid', PARAM_INT);
+
+        $group = [];
+        $group[] = $mform->createElement('radio', 'repeateditall', null, get_string('repeateditall', 'calendar',
+                $event->eventrepeats), 1);
+        $group[] = $mform->createElement('radio', 'repeateditall', null, get_string('repeateditthis', 'calendar'), 0);
+        $mform->addGroup($group, 'repeatgroup', get_string('repeatedevents', 'calendar'), '<br />', false);
+
+        $mform->setDefault('repeateditall', 1);
+        $mform->setAdvanced('repeatgroup');
+    }
+}
diff --git a/calendar/classes/local/event/mappers/create_update_form_mapper.php b/calendar/classes/local/event/mappers/create_update_form_mapper.php
new file mode 100644 (file)
index 0000000..bedcccc
--- /dev/null
@@ -0,0 +1,118 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Event create form and update form mapper.
+ *
+ * @package    core_calendar
+ * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_calendar\local\event\mappers;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/calendar/lib.php');
+
+/**
+ * Event create form and update form mapper class.
+ *
+ * This class will perform the necessary data transformations to take
+ * a legacy event and build the appropriate data structure for both the
+ * create and update event forms.
+ *
+ * It will also do the reverse transformation
+ * and take the returned form data and provide a data structure that can
+ * be used to set legacy event properties.
+ *
+ * @copyright 2017 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class create_update_form_mapper implements create_update_form_mapper_interface {
+
+    /**
+     * Generate the appropriate data for the form from a legacy event.
+     *
+     * @param \calendar_event $legacyevent
+     * @return stdClass
+     */
+    public function from_legacy_event_to_data(\calendar_event $legacyevent) {
+        $legacyevent->count_repeats();
+        $data = $legacyevent->properties(true);
+        $data->timedurationuntil = $legacyevent->timestart + $legacyevent->timeduration;
+        $data->duration = (empty($legacyevent->timeduration)) ? 0 : 1;
+
+        if ($legacyevent->eventtype == 'group') {
+            // Set up the correct value for the to display on the form.
+            $data->groupid = "{$legacyevent->courseid}-{$legacyevent->groupid}";
+            $data->groupcourseid = $legacyevent->courseid;
+        }
+
+        return $data;
+    }
+
+    /**
+     * Generate the appropriate calendar_event properties from the form data.
+     *
+     * @param \stdClass $data
+     * @return stdClass
+     */
+    public function from_data_to_event_properties(\stdClass $data) {
+        $properties = clone($data);
+
+        // Undo the form definition work around to allow us to have two different
+        // course selectors present depending on which event type the user selects.
+        if (isset($data->groupcourseid)) {
+            $properties->courseid = $data->groupcourseid;
+            unset($properties->groupcourseid);
+        }
+
+        // Pull the group id back out of the value. The form saves the value
+        // as "<courseid>-<groupid>" to allow the javascript to work correctly.
+        if (isset($data->groupid)) {
+            list($courseid, $groupid) = explode('-', $data->groupid);
+            $properties->groupid = $groupid;
+        }
+
+        // Default course id if none is set.
+        if (!isset($properties->courseid)) {
+            $properties->courseid = 0;
+        }
+
+        // Decode the form fields back into valid event property.
+        $properties->timeduration = $this->get_time_duration_from_form_data($data);
+
+        return $properties;
+    }
+
+    /**
+     * A helper function to calculate the time duration for an event based on
+     * the event_form data.
+     *
+     * @param \stdClass $data event_form data
+     * @return int
+     */
+    private function get_time_duration_from_form_data(\stdClass $data) {
+        if ($data->duration == 1) {
+            return $data->timedurationuntil - $data->timestart;
+        } else if ($data->duration == 2) {
+            return $data->timedurationminutes * MINSECS;
+        } else {
+            return 0;
+        }
+    }
+}
diff --git a/calendar/classes/local/event/mappers/create_update_form_mapper_interface.php b/calendar/classes/local/event/mappers/create_update_form_mapper_interface.php
new file mode 100644 (file)
index 0000000..6b38c7c
--- /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/>.
+
+/**
+ * Create update form mapper interface.
+ *
+ * @package    core_calendar
+ * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_calendar\local\event\mappers;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/calendar/lib.php');
+
+/**
+ * Interface for a create_update_form_mapper class
+ *
+ * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface create_update_form_mapper_interface {
+    /**
+     * Generate the appropriate data for the form from a legacy event.
+     *
+     * @param \calendar_event $legacyevent
+     * @return stdClass
+     */
+    public function from_legacy_event_to_data(\calendar_event $legacyevent);
+
+    /**
+     * Generate the appropriate calendar_event properties from the form data.
+     *
+     * @param \stdClass $data
+     * @return stdClass
+     */
+    public function from_data_to_event_properties(\stdClass $data);
+}
index a4191f2..8e544dd 100644 (file)
@@ -54,28 +54,34 @@ class event_mapper implements event_mapper_interface {
 
     public function from_legacy_event_to_event(\calendar_event $legacyevent) {
         $coalesce = function($property) use ($legacyevent) {
-            return property_exists($legacyevent, $property) ? $legacyevent->{$property} : null;
+            try {
+                return $legacyevent->$property;
+            } catch (\coding_exception $e) {
+                // The magic setter throews an exception if the
+                // property doesn't exist.
+                return null;
+            }
         };
 
         return $this->factory->create_instance(
             (object)[
-                $coalesce('id'),
-                $coalesce('name'),
-                $coalesce('description'),
-                $coalesce('format'),
-                $coalesce('courseid'),
-                $coalesce('groupid'),
-                $coalesce('userid'),
-                $coalesce('repeatid'),
-                $coalesce('modulename'),
-                $coalesce('instance'),
-                $coalesce('type'),
-                $coalesce('timestart'),
-                $coalesce('timeduration'),
-                $coalesce('timemodified'),
-                $coalesce('timesort'),
-                $coalesce('visible'),
-                $coalesce('subscription')
+                'id' => $coalesce('id'),
+                'name' => $coalesce('name'),
+                'description' => $coalesce('description'),
+                'format' => $coalesce('format'),
+                'courseid' => $coalesce('courseid'),
+                'groupid' => $coalesce('groupid'),
+                'userid' => $coalesce('userid'),
+                'repeatid' => $coalesce('repeatid'),
+                'modulename' => $coalesce('modulename'),
+                'instance' => $coalesce('instance'),
+                'eventtype' => $coalesce('eventtype'),
+                'timestart' => $coalesce('timestart'),
+                'timeduration' => $coalesce('timeduration'),
+                'timemodified' => $coalesce('timemodified'),
+                'timesort' => $coalesce('timesort'),
+                'visible' => $coalesce('visible'),
+                'subscriptionid' => $coalesce('subscriptionid')
             ]
         );
     }
index 6fd68d4..037f332 100644 (file)
@@ -30,6 +30,11 @@ defined('MOODLE_INTERNAL') || die;
 require_once("$CFG->libdir/externallib.php");
 
 use \core_calendar\local\api as local_api;
+use \core_calendar\local\event\container as event_container;
+use \core_calendar\local\event\forms\create as create_event_form;
+use \core_calendar\local\event\forms\update as update_event_form;
+use \core_calendar\local\event\mappers\create_update_form_mapper;
+use \core_calendar\external\event_exporter;
 use \core_calendar\external\events_exporter;
 use \core_calendar\external\events_grouped_by_course_exporter;
 use \core_calendar\external\events_related_objects_cache;
@@ -779,4 +784,94 @@ class core_calendar_external extends external_api {
             )
         );
     }
+
+    /**
+     * Returns description of method parameters.
+     *
+     * @return external_function_parameters.
+     */
+    public static function submit_create_update_form_parameters() {
+        return new external_function_parameters(
+            [
+                'formdata' => new external_value(PARAM_RAW, 'The data from the event form'),
+            ]
+        );
+    }
+
+    /**
+     * Handles the event form submission.
+     *
+     * @param string $formdata The event form data in a URI encoded param string
+     * @return array The created or modified event
+     * @throws moodle_exception
+     */
+    public static function submit_create_update_form($formdata) {
+        global $CFG, $USER, $PAGE;
+        require_once($CFG->dirroot."/calendar/lib.php");
+
+        // Parameter validation.
+        $params = self::validate_parameters(self::submit_create_update_form_parameters(), ['formdata' => $formdata]);
+        $context = \context_user::instance($USER->id);
+        $data = [];
+
+        self::validate_context($context);
+        parse_str($params['formdata'], $data);
+
+        if (!empty($data['id'])) {
+            $eventid = clean_param($data['id'], PARAM_INT);
+            $legacyevent = calendar_event::load($eventid);
+            $legacyevent->count_repeats();
+            $formoptions = ['event' => $legacyevent];
+            $mform = new update_event_form(null, $formoptions, 'post', '', null, true, $data);
+        } else {
+            $legacyevent = null;
+            $mform = new create_event_form(null, null, 'post', '', null, true, $data);
+        }
+
+        if ($validateddata = $mform->get_data()) {
+            $formmapper = new create_update_form_mapper();
+            $properties = $formmapper->from_data_to_event_properties($validateddata);
+
+            if (is_null($legacyevent)) {
+                $legacyevent = new \calendar_event($properties);
+                // Need to do this in order to initialise the description
+                // property which then triggers the update function below
+                // to set the appropriate default properties on the event.
+                $properties = $legacyevent->properties(true);
+            }
+
+            $legacyevent->update($properties);
+
+            $eventmapper = event_container::get_event_mapper();
+            $event = $eventmapper->from_legacy_event_to_event($legacyevent);
+            $cache = new events_related_objects_cache([$event]);
+            $relatedobjects = [
+                'context' => $cache->get_context($event),
+                'course' => $cache->get_course($event),
+            ];
+            $exporter = new event_exporter($event, $relatedobjects);
+            $renderer = $PAGE->get_renderer('core_calendar');
+
+            return [ 'event' => $exporter->export($renderer) ];
+        } else {
+            return [ 'validationerror' => true ];
+        }
+    }
+
+    /**
+     * Returns description of method result value.
+     *
+     * @return external_description.
+     */
+    public static function  submit_create_update_form_returns() {
+        $eventstructure = event_exporter::get_read_structure();
+        $eventstructure->required = VALUE_OPTIONAL;
+
+        return new external_single_structure(
+            array(
+                'event' => $eventstructure,
+                'validationerror' => new external_value(PARAM_BOOL, 'Invalid form data', VALUE_DEFAULT, false),
+            )
+        );
+    }
 }
index 94e617b..45d1297 100644 (file)
@@ -2686,8 +2686,9 @@ function calendar_set_event_type_display($type, $display = null, $user = null) {
  *
  * @param stdClass $allowed list of allowed edit for event  type
  * @param stdClass|int $course object of a course or course id
+ * @param array $groups array of groups for the given course
  */
-function calendar_get_allowed_types(&$allowed, $course = null) {
+function calendar_get_allowed_types(&$allowed, $course = null, $groups = null) {
     global $USER, $DB;
 
     $allowed = new \stdClass();
@@ -2695,6 +2696,23 @@ function calendar_get_allowed_types(&$allowed, $course = null) {
     $allowed->groups = false;
     $allowed->courses = false;
     $allowed->site = has_capability('moodle/calendar:manageentries', \context_course::instance(SITEID));
+    $getgroupsfunc = function($course, $context, $user) use ($groups) {
+        if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) {
+            if (has_capability('moodle/site:accessallgroups', $context)) {
+                return is_null($groups) ? groups_get_all_groups($course->id) : $groups;
+            } else {
+                if (is_null($groups)) {
+                    return groups_get_all_groups($course->id, $user->id);
+                } else {
+                    return array_filter($groups, function($group) use ($user) {
+                        return isset($group->members[$user->id]);
+                    });
+                }
+            }
+        }
+
+        return false;
+    };
 
     if (!empty($course)) {
         if (!is_object($course)) {
@@ -2706,27 +2724,84 @@ function calendar_get_allowed_types(&$allowed, $course = null) {
 
             if (has_capability('moodle/calendar:manageentries', $coursecontext)) {
                 $allowed->courses = array($course->id => 1);
-
-                if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) {
-                    if (has_capability('moodle/site:accessallgroups', $coursecontext)) {
-                        $allowed->groups = groups_get_all_groups($course->id);
-                    } else {
-                        $allowed->groups = groups_get_all_groups($course->id, $USER->id);
-                    }
-                }
+                $allowed->groups = $getgroupsfunc($course, $coursecontext, $USER);
             } else if (has_capability('moodle/calendar:managegroupentries', $coursecontext)) {
-                if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) {
-                    if (has_capability('moodle/site:accessallgroups', $coursecontext)) {
-                        $allowed->groups = groups_get_all_groups($course->id);
-                    } else {
-                        $allowed->groups = groups_get_all_groups($course->id, $USER->id);
-                    }
-                }
+                $allowed->groups = $getgroupsfunc($course, $coursecontext, $USER);
             }
         }
     }
 }
 
+/**
+ * Get all of the allowed types for all of the courses and groups
+ * the logged in user belongs to.
+ *
+ * The returned array will optionally have 5 keys:
+ *      'user' : true if the logged in user can create user events
+ *      'site' : true if the logged in user can create site events
+ *      'course' : array of courses that the user can create events for
+ *      'group': array of groups that the user can create events for
+ *      'groupcourses' : array of courses that the groups belong to (can
+ *                       be different from the list in 'course'.
+ *
+ * @return array The array of allowed types.
+ */
+function calendar_get_all_allowed_types() {
+    global $CFG, $USER;
+
+    require_once($CFG->libdir . '/enrollib.php');
+
+    $types = [];
+
+    calendar_get_allowed_types($allowed);
+
+    if ($allowed->user) {
+        $types['user'] = true;
+    }
+
+    if ($allowed->site) {
+        $types['site'] = true;
+    }
+
+    // This function warms the context cache for the course so the calls
+    // to load the course context in calendar_get_allowed_types don't result
+    // in additional DB queries.
+    $courses = enrol_get_users_courses($USER->id, true);
+    // We want to pre-fetch all of the groups for each course in a single
+    // query to avoid calendar_get_allowed_types from hitting the DB for
+    // each separate course.
+    $groups = groups_get_all_groups_for_courses($courses);
+
+    foreach ($courses as $course) {
+        $coursegroups = isset($groups[$course->id]) ? $groups[$course->id] : null;
+        calendar_get_allowed_types($allowed, $course, $coursegroups);
+
+        if (!empty($allowed->courses)) {
+            if (!isset($types['course'])) {
+                $types['course'] = [$course];
+            } else {
+                $types['course'][] = $course;
+            }
+        }
+
+        if (!empty($allowed->groups)) {
+            if (!isset($types['groupcourses'])) {
+                $types['groupcourses'] = [$course];
+            } else {
+                $types['groupcourses'][] = $course;
+            }
+
+            if (!isset($types['group'])) {
+                $types['group'] = array_values($allowed->groups);
+            } else {
+                $types['group'] = array_merge($types['group'], array_values($allowed->groups));
+            }
+        }
+    }
+
+    return $types;
+}
+
 /**
  * See if user can add calendar entries at all used to print the "New Event" button.
  *
@@ -3340,3 +3415,76 @@ function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses, $
         return $carry + [$event->get_id() => $mapper->from_event_to_stdclass($event)];
     }, []);
 }
+
+/**
+ * Request and render event form fragment.
+ *
+ * @param array $args The fragment arguments.
+ * @return string The rendered mform fragment.
+ */
+function calendar_output_fragment_event_form($args) {
+    global $CFG, $OUTPUT;
+    require_once($CFG->dirroot.'/calendar/event_form.php');
+
+    $html = '';
+    $data = null;
+    $eventid = isset($args['eventid']) ? clean_param($args['eventid'], PARAM_INT) : null;
+    $event = null;
+    $hasformdata = isset($args['formdata']) && !empty($args['formdata']);
+    $formoptions = [];
+
+    if ($hasformdata) {
+        parse_str(clean_param($args['formdata'], PARAM_TEXT), $data);
+    }
+
+    if (isset($args['haserror'])) {
+        $formoptions['haserror'] = clean_param($args['haserror'], PARAM_BOOL);
+    }
+
+    if (is_null($eventid)) {
+        $mform = new \core_calendar\local\event\forms\create(
+            null,
+            $formoptions,
+            'post',
+            '',
+            null,
+            true,
+            $data
+        );
+    } else {
+        $event = calendar_event::load($eventid);
+        $event->count_repeats();
+        $formoptions['event'] = $event;
+        $mform = new \core_calendar\local\event\forms\update(
+            null,
+            $formoptions,
+            'post',
+            '',
+            null,
+            true,
+            $data
+        );
+    }
+
+    if ($hasformdata) {
+        $mform->is_validated();
+    } else if (!is_null($event)) {
+        $mapper = new \core_calendar\local\event\mappers\create_update_form_mapper();
+        $data = $mapper->from_legacy_event_to_data($event);
+        $mform->set_data($data);
+
+        // Check to see if this event is part of a subscription or import.
+        // If so display a warning on edit.
+        if (isset($event->subscriptionid) && ($event->subscriptionid != null)) {
+            $renderable = new \core\output\notification(
+                get_string('eventsubscriptioneditwarning', 'calendar'),
+                \core\output\notification::NOTIFY_INFO
+            );
+
+            $html .= $OUTPUT->render($renderable);
+        }
+    }
+
+    $html .= $mform->render();
+    return $html;
+}
index ee450e1..940be31 100644 (file)
@@ -142,18 +142,13 @@ class core_calendar_renderer extends plugin_renderer_base {
             $time = time();
         }
 
-        $output = html_writer::start_tag('div', array('class'=>'buttons'));
-        $output .= html_writer::start_tag('form', array('action' => CALENDAR_URL . 'event.php', 'method' => 'get'));
-        $output .= html_writer::start_tag('div');
-        $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name' => 'action', 'value' => 'new'));
-        $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name' => 'course', 'value' => $courseid));
-        $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name' => 'time', 'value' => $time));
-        $attributes = array('type' => 'submit', 'value' => get_string('newevent', 'calendar'), 'class' => 'btn btn-secondary');
-        $output .= html_writer::empty_tag('input', $attributes);
-        $output .= html_writer::end_tag('div');
-        $output .= html_writer::end_tag('form');
-        $output .= html_writer::end_tag('div');
-        return $output;
+        $coursecontext = \context_course::instance($courseid);
+        $attributes = [
+            'class' => 'btn btn-secondary pull-xs-right pull-right',
+            'data-context-id' => $coursecontext->id,
+            'data-action' => 'new-event-button'
+        ];
+        return html_writer::tag('button', get_string('newevent', 'calendar'), $attributes);
     }
 
     /**
diff --git a/calendar/templates/modal_event_form.mustache b/calendar/templates/modal_event_form.mustache
new file mode 100644 (file)
index 0000000..e99c491
--- /dev/null
@@ -0,0 +1,61 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template calendar/modal_event_form
+
+    Moodle modal template with save and cancel buttons.
+
+    The purpose of this template is to render a modal.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * title A cleaned string (use clean_text()) to display.
+    * body HTML content for the boday
+
+    Example context (json):
+    {
+        "title": "Example save cancel modal",
+        "body": "Some example content for the body"
+    }
+}}
+
+{{< core/modal }}
+    {{$footer}}
+        <button type="button"
+                class="btn btn-secondary"
+                data-collapsed="true"
+                data-action="more-less-toggle">
+
+            {{#str}} more, calendar {{/str}}
+        </button>
+        <button type="button"
+                class="btn btn-primary"
+                data-context-id="{{contextid}}"
+                data-action="save">
+
+            {{#str}} save {{/str}}
+            <span class="hidden" data-region="loading-icon-container">
+                {{> core/loading }}
+            </span>
+        </button>
+    {{/footer}}
+{{/ core/modal }}
index 2d4ecab..c77b547 100644 (file)
@@ -63,6 +63,9 @@ class behat_calendar extends behat_base {
         $eventname = $data->getRow(1);
         $eventname = $eventname[1];
 
+        // Click to create new event.
+        $this->execute("behat_general::i_wait_seconds", 1);
+
         // Click to create new event.
         $this->execute("behat_general::i_click_on", array(get_string('newevent', 'calendar'), "button"));
 
@@ -70,11 +73,7 @@ class behat_calendar extends behat_base {
         $this->execute("behat_forms::i_set_the_following_fields_to_these_values", $data);
 
         // Save event.
-        $this->execute("behat_forms::press_button", get_string('savechanges'));
-
-        // Check if event is created. Being last step, don't need to wait or check for exceptions.
-        $this->execute("behat_general::assert_page_contains_text", $eventname);
-
+        $this->execute("behat_forms::press_button", get_string('save'));
     }
 
     /**
index 81689db..291c83e 100644 (file)
@@ -1,4 +1,4 @@
-@core @core_calendar
+@core @core_calendar @javascript
 Feature: Perform basic calendar functionality
   In order to ensure the calendar works as expected
   As an admin
@@ -10,6 +10,7 @@ Feature: Perform basic calendar functionality
       | student1 | Student | 1 | student1@example.com |
       | student2 | Student | 2 | student2@example.com |
       | student3 | Student | 3 | student3@example.com |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
     And the following "courses" exist:
       | fullname | shortname | format |
       | Course 1 | C1 | topics |
@@ -17,17 +18,19 @@ Feature: Perform basic calendar functionality
       | user | course | role |
       | student1 | C1 | student |
       | student3 | C1 | student |
+      | teacher1 | C1 | teacher |
     And the following "groups" exist:
       | name | course | idnumber |
       | Group 1 | C1 | G1 |
     And the following "group members" exist:
       | user | group |
       | student1 | G1 |
-    When I log in as "admin"
-    And I am on "Course 1" course homepage with editing mode on
-    And I add the "Calendar" block
+      | teacher1 | G1 |
 
   Scenario: Create a site event
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add the "Calendar" block
     And I create a calendar event with form data:
       | Type of event | site |
       | Event title | Really awesome event! |
@@ -41,8 +44,14 @@ Feature: Perform basic calendar functionality
     And I log in as "student2"
     And I follow "This month"
     And I should see "Really awesome event!"
+    And I log out
 
   Scenario: Create a course event
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add the "Calendar" block
+    And I log out
+    And I log in as "teacher1"
     And I create a calendar event with form data:
       | Type of event | course |
       | Event title | Really awesome event! |
@@ -56,11 +65,16 @@ Feature: Perform basic calendar functionality
     And I log in as "student2"
     And I follow "This month"
     And I should not see "Really awesome event!"
+    And I log out
 
   Scenario: Create a group event
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add the "Calendar" block
+    And I log out
+    And I log in as "teacher1"
     And I create a calendar event with form data:
       | Type of event | group |
-      | Group | Group 1 |
       | Event title | Really awesome event! |
       | Description | Come join this awesome event |
     And I log out
@@ -68,13 +82,11 @@ Feature: Perform basic calendar functionality
     And I am on "Course 1" course homepage
     And I follow "This month"
     And I follow "Really awesome event!"
-    And "Group 1" "text" should exist in the ".eventlist" "css_element"
-    And I log out
-    And I log in as "student3"
-    And I follow "This month"
-    And I should not see "Really awesome event!"
 
   Scenario: Create a user event
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add the "Calendar" block
     And I create a calendar event with form data:
       | Type of event | user |
       | Event title | Really awesome event! |
@@ -86,22 +98,37 @@ Feature: Perform basic calendar functionality
     And I should not see "Really awesome event!"
 
   Scenario: Delete an event
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add the "Calendar" block
+    And I log out
+    And I log in as "teacher1"
     And I create a calendar event with form data:
       | Type of event | user |
       | Event title | Really awesome event! |
       | Description | Come join this awesome event, sucka! |
-    And I click on "Delete event" "link" in the ".event div.commands" "css_element"
+    And I am on "Course 1" course homepage
+    And I follow "This month"
+    And I click on "Really awesome event!" "link"
     And I click on "Delete" "button"
+    And I click on "Yes" "button"
+    And I wait to be redirected
     And I should not see "Really awesome event!"
 
   Scenario: Edit an event
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add the "Calendar" block
     And I create a calendar event with form data:
       | Type of event | user |
       | Event title | Really awesome event! |
       | Description | Come join this awesome event, sucka! |
-    And I click on "Edit event" "link" in the ".event div.commands" "css_element"
+    And I am on "Course 1" course homepage
+    And I follow "This month"
+    And I click on "Really awesome event!" "link"
+    And I click on "Edit" "button"
     And I set the following fields to these values:
       | Event title | Mediocre event :( |
       | Description | Wait, this event isn't that great. |
-    And I press "Save changes"
+    And I press "Save"
     And I should see "Mediocre event"
index a9cedb1..c9a3829 100644 (file)
@@ -30,5 +30,19 @@ Feature: Import and edit calendar events
     And I should see "February 2017"
     And I should see "Event on 2-15-2017"
     And I should see "Event on 2-25-2017"
-    And I follow "Event on 2-15-2017"
-    And I should see "Event source: Test Import"
+    And I click on "Event on 2-15-2017" "link"
+    And I click on "Edit" "button"
+    And I set the following fields to these values:
+      | Event title    | Event on 2-20-2017 |
+      | Description    | Event on 2-20-2017 |
+      | timestart[day] | 20 |
+    And I press "Save"
+    When I view the calendar for "2" "2017"
+    Then I should see "Event on 2-20-2017"
+    And I should see "Event on 2-25-2017"
+    And I should not see "Event on 2-15-2017"
+    And I press "Manage subscriptions"
+    And I press "Remove"
+    And I view the calendar for "2" "2017"
+    And I should not see "Event on 2-25-2017"
+    And I should not see "Event on 2-20-2017"
index 819d03e..339fd65 100644 (file)
@@ -1,4 +1,4 @@
-@core @core_calendar
+@core @core_calendar @javascript
 Feature: Limit displayed upcoming events
   In order to filter what is displayed on the calendar
   As a user
@@ -23,10 +23,9 @@ Feature: Limit displayed upcoming events
     And I follow "This month"
     And I click on "a.next" "css_element"
     And I click on "a.next" "css_element"
-    And I create a calendar event:
+    When I create a calendar event:
       | Type of event     | course |
       | Event title       | Two months away event |
-    When I follow "C1"
     Then I should not see "Two months away event"
     And I am on site homepage
     And I follow "Preferences" in the user menu
index 2f627ed..23efe02 100644 (file)
@@ -409,4 +409,267 @@ class core_calendar_lib_testcase extends advanced_testcase {
         $events = calendar_get_legacy_events($timestart, $timeend, true, true, true);
         $this->assertCount(3, $events);
     }
-}
\ No newline at end of file
+
+    public function test_calendar_get_all_allowed_types_no_types() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $systemcontext = context_system::instance();
+        $sitecontext = context_course::instance(SITEID);
+        $roleid = $generator->create_role();
+
+        $generator->role_assign($roleid, $user->id, $systemcontext->id);
+        $generator->role_assign($roleid, $user->id, $sitecontext->id);
+        $this->setUser($user);
+
+        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $sitecontext, true);
+        assign_capability('moodle/calendar:manageownentries', CAP_PROHIBIT, $roleid, $systemcontext, true);
+
+        $types = calendar_get_all_allowed_types();
+        $this->assertEmpty($types);
+    }
+
+    public function test_calendar_get_all_allowed_types_user() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $context = context_system::instance();
+        $roleid = $generator->create_role();
+
+        $generator->role_assign($roleid, $user->id, $context->id);
+        $this->setUser($user);
+
+        assign_capability('moodle/calendar:manageownentries', CAP_ALLOW, $roleid, $context, true);
+
+        $types = calendar_get_all_allowed_types();
+        $this->assertTrue($types['user']);
+
+        assign_capability('moodle/calendar:manageownentries', CAP_PROHIBIT, $roleid, $context, true);
+
+        $types = calendar_get_all_allowed_types();
+        $this->assertArrayNotHasKey('user', $types);
+    }
+
+    public function test_calendar_get_all_allowed_types_site() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $context = context_course::instance(SITEID);
+        $roleid = $generator->create_role();
+
+        $generator->role_assign($roleid, $user->id, $context->id);
+        $this->setUser($user);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+
+        $types = calendar_get_all_allowed_types();
+        $this->assertTrue($types['site']);
+
+        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
+
+        $types = calendar_get_all_allowed_types();
+        $this->assertArrayNotHasKey('site', $types);
+    }
+
+    public function test_calendar_get_all_allowed_types_course() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course1 = $generator->create_course(); // Has capability.
+        $course2 = $generator->create_course(); // Doesn't have capability.
+        $course3 = $generator->create_course(); // Not enrolled.
+        $context1 = context_course::instance($course1->id);
+        $context2 = context_course::instance($course2->id);
+        $context3 = context_course::instance($course3->id);
+        $roleid = $generator->create_role();
+        $contexts = [$context1, $context2, $context3];
+        $enrolledcourses = [$course1, $course2];
+
+        foreach ($enrolledcourses as $course) {
+            $generator->enrol_user($user->id, $course->id, 'student');
+        }
+
+        foreach ($contexts as $context) {
+            $generator->role_assign($roleid, $user->id, $context->id);
+        }
+
+        $this->setUser($user);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context1, true);
+        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context2, true);
+
+        // The user only has the correct capability in course 1 so that is the only
+        // one that should be in the results.
+        $types = calendar_get_all_allowed_types();
+        $typecourses = $types['course'];
+        $this->assertCount(1, $typecourses);
+        $this->assertEquals($course1->id, $typecourses[0]->id);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context2, true);
+
+        // The user only now has the correct capability in both course 1 and 2 so we
+        // expect both to be in the results.
+        $types = calendar_get_all_allowed_types();
+        $typecourses = $types['course'];
+        // Sort the results by id ascending to ensure the test is consistent
+        // and repeatable.
+        usort($typecourses, function($a, $b) {
+            $aid = $a->id;
+            $bid = $b->id;
+
+            if ($aid == $bid) {
+                return 0;
+            }
+            return ($aid < $bid) ? -1 : 1;
+        });
+
+        $this->assertCount(2, $typecourses);
+        $this->assertEquals($course1->id, $typecourses[0]->id);
+        $this->assertEquals($course2->id, $typecourses[1]->id);
+    }
+
+    public function test_calendar_get_all_allowed_types_group_no_groups() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course = $generator->create_course();
+        $context = context_course::instance($course->id);
+        $roleid = $generator->create_role();
+
+        $generator->enrol_user($user->id, $course->id, 'student');
+        $generator->role_assign($roleid, $user->id, $context->id);
+
+        $this->setUser($user);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+
+        // The user has the correct capability in the course but there are
+        // no groups so we shouldn't see a group type.
+        $types = calendar_get_all_allowed_types();
+        $typecourses = $types['course'];
+        $this->assertCount(1, $typecourses);
+        $this->assertEquals($course->id, $typecourses[0]->id);
+        $this->assertArrayNotHasKey('group', $types);
+        $this->assertArrayNotHasKey('groupcourses', $types);
+    }
+
+    public function test_calendar_get_all_allowed_types_group_no_acces_to_diff_groups() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course = $generator->create_course();
+        $context = context_course::instance($course->id);
+        $group1 = $generator->create_group(array('courseid' => $course->id));
+        $group2 = $generator->create_group(array('courseid' => $course->id));
+        $roleid = $generator->create_role();
+
+        $generator->enrol_user($user->id, $course->id, 'student');
+        $generator->role_assign($roleid, $user->id, $context->id);
+
+        $this->setUser($user);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+        assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $roleid, $context, true);
+
+        // The user has the correct capability in the course but they aren't a member
+        // of any of the groups and don't have the accessallgroups capability.
+        $types = calendar_get_all_allowed_types();
+        $typecourses = $types['course'];
+        $this->assertCount(1, $typecourses);
+        $this->assertEquals($course->id, $typecourses[0]->id);
+        $this->assertArrayNotHasKey('group', $types);
+        $this->assertArrayNotHasKey('groupcourses', $types);
+    }
+
+    public function test_calendar_get_all_allowed_types_group_access_all_groups() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course1 = $generator->create_course();
+        $course2 = $generator->create_course();
+        $context1 = context_course::instance($course1->id);
+        $context2 = context_course::instance($course2->id);
+        $group1 = $generator->create_group(array('courseid' => $course1->id));
+        $group2 = $generator->create_group(array('courseid' => $course1->id));
+        $roleid = $generator->create_role();
+
+        $generator->enrol_user($user->id, $course1->id, 'student');
+        $generator->enrol_user($user->id, $course2->id, 'student');
+        $generator->role_assign($roleid, $user->id, $context1->id);
+        $generator->role_assign($roleid, $user->id, $context2->id);
+
+        $this->setUser($user);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context1, true);
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context2, true);
+        assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, $context1, true);
+        assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, $context2, true);
+
+        // The user has the correct capability in the course and has
+        // the accessallgroups capability.
+        $types = calendar_get_all_allowed_types();
+        $typecourses = $types['course'];
+        $typegroups = $types['group'];
+        $typegroupcourses = $types['groupcourses'];
+        $idascfunc = function($a, $b) {
+            $aid = $a->id;
+            $bid = $b->id;
+
+            if ($aid == $bid) {
+                return 0;
+            }
+            return ($aid < $bid) ? -1 : 1;
+        };
+        // Sort the results by id ascending to ensure the test is consistent
+        // and repeatable.
+        usort($typecourses, $idascfunc);
+        usort($typegroups, $idascfunc);
+
+        $this->assertCount(2, $typecourses);
+        $this->assertEquals($course1->id, $typecourses[0]->id);
+        $this->assertEquals($course2->id, $typecourses[1]->id);
+        $this->assertCount(1, $typegroupcourses);
+        $this->assertEquals($course1->id, $typegroupcourses[0]->id);
+        $this->assertCount(2, $typegroups);
+        $this->assertEquals($group1->id, $typegroups[0]->id);
+        $this->assertEquals($group2->id, $typegroups[1]->id);
+    }
+
+    public function test_calendar_get_all_allowed_types_group_no_access_all_groups() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course = $generator->create_course();
+        $context = context_course::instance($course->id);
+        $group1 = $generator->create_group(array('courseid' => $course->id));
+        $group2 = $generator->create_group(array('courseid' => $course->id));
+        $group3 = $generator->create_group(array('courseid' => $course->id));
+        $roleid = $generator->create_role();
+
+        $generator->enrol_user($user->id, $course->id, 'student');
+        $generator->role_assign($roleid, $user->id, $context->id);
+        $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $user->id));
+        $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $user->id));
+
+        $this->setUser($user);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+        assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $roleid, $context, true);
+
+        // The user has the correct capability in the course but can't access
+        // groups that they are not a member of.
+        $types = calendar_get_all_allowed_types();
+        $typegroups = $types['group'];
+        $typegroupcourses = $types['groupcourses'];
+        $idascfunc = function($a, $b) {
+            $aid = $a->id;
+            $bid = $b->id;
+
+            if ($aid == $bid) {
+                return 0;
+            }
+            return ($aid < $bid) ? -1 : 1;
+        };
+        // Sort the results by id ascending to ensure the test is consistent
+        // and repeatable.
+        usort($typegroups, $idascfunc);
+
+        $this->assertCount(1, $typegroupcourses);
+        $this->assertEquals($course->id, $typegroupcourses[0]->id);
+        $this->assertCount(2, $typegroups);
+        $this->assertEquals($group1->id, $typegroups[0]->id);
+        $this->assertEquals($group2->id, $typegroups[1]->id);
+    }
+}
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 c16ead4..8c8a5e8 100644 (file)
@@ -30,9 +30,10 @@ Feature: Rename roles within a course
     Then "Tutor" "button" should exist
     And "Learner" "button" should exist
     And I navigate to course participants
-    And the "roleid" select box should contain "Tutor"
-    And the "roleid" select box should contain "Learner"
-    And the "roleid" select box should not contain "Student"
+    And I open the autocomplete suggestions list
+    And I should see "Role: Tutor" in the ".form-autocomplete-suggestions" "css_element"
+    And I should see "Role: Learner" in the ".form-autocomplete-suggestions" "css_element"
+    And I should not see "Role: Student" in the ".form-autocomplete-suggestions" "css_element"
     And I am on "Course 1" course homepage
     And I navigate to "Edit settings" in current page administration
     And I set the following fields to these values:
@@ -44,5 +45,6 @@ Feature: Rename roles within a course
     And "Student" "button" should exist
     And "Learner" "button" should not exist
     And I navigate to course participants
-    And the "roleid" select box should contain "Non-editing teacher"
-    And the "roleid" select box should contain "Student"
+    And I open the autocomplete suggestions list
+    And I should see "Role: Non-editing teacher" in the ".form-autocomplete-suggestions" "css_element"
+    And I should see "Role: Student" in the ".form-autocomplete-suggestions" "css_element"
index 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 9e39244..282f452 100644 (file)
@@ -28,9 +28,9 @@ Feature: Users can be added to multiple groups at once
     And I am on "Course 1" course homepage
     And I follow "Participants"
     And I click on "Edit groups for \"Student 1\"" "link" in the "student1" "table_row"
-    And I open the autocomplete suggestions list
+    And I click on ".form-autocomplete-downarrow" "css_element" in the "student1" "table_row"
     And I click on "Group 1" item in the autocomplete list
-    And I open the autocomplete suggestions list
+    And I click on ".form-autocomplete-downarrow" "css_element" in the "student1" "table_row"
     And I click on "Group 2" item in the autocomplete list
     And I press key "27" in the field "Edit groups for \"Student 1\""
     And I click on "Save changes" "link" in the "student1" "table_row"
diff --git