Merge branch 'MDL-59527-master' of git://github.com/damyon/moodle
authorDavid Monllao <david.monllao@gmail.com>
Mon, 17 Jul 2017 12:07:59 +0000 (14:07 +0200)
committerDavid Monllao <david.monllao@gmail.com>
Mon, 17 Jul 2017 12:07:59 +0000 (14:07 +0200)
341 files changed:
.travis.yml
admin/cli/install.php
admin/cli/install_database.php
admin/environment.xml
admin/index.php
admin/searchareas.php
admin/settings/plugins.php
admin/tool/customlang/db/upgrade.php
admin/tool/log/db/upgrade.php
admin/tool/log/store/database/db/upgrade.php
admin/tool/log/store/standard/db/upgrade.php
admin/tool/lp/classes/external.php
admin/tool/lp/db/services.php
admin/tool/lp/tests/behat/behat_tool_lp.php
admin/tool/lp/tests/behat/plan_crud.feature
admin/tool/lp/tests/externallib_test.php
admin/tool/lp/version.php
admin/tool/monitor/db/upgrade.php
admin/tool/oauth2/classes/form/issuer.php
admin/tool/oauth2/lang/en/tool_oauth2.php
admin/tool/uploadcourse/classes/course.php
admin/tool/uploadcourse/classes/step2_form.php
admin/tool/uploadcourse/cli/uploadcourse.php
admin/tool/uploadcourse/tests/course_test.php
auth/cas/db/upgrade.php
auth/ldap/db/upgrade.php
auth/manual/db/upgrade.php
auth/mnet/db/upgrade.php
auth/oauth2/classes/api.php
auth/oauth2/classes/auth.php
auth/oauth2/tests/api_test.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
backup/moodle2/tests/moodle2_test.php
blocks/badges/db/upgrade.php
blocks/calendar_month/db/upgrade.php
blocks/calendar_upcoming/db/upgrade.php
blocks/comments/tests/behat/behat_block_comments.php
blocks/community/db/upgrade.php
blocks/completionstatus/db/upgrade.php
blocks/course_summary/db/upgrade.php
blocks/html/classes/search/content.php [new file with mode: 0644]
blocks/html/db/upgrade.php
blocks/html/lang/en/block_html.php
blocks/html/lib.php
blocks/html/tests/search_content_test.php [new file with mode: 0644]
blocks/moodleblock.class.php
blocks/navigation/db/upgrade.php
blocks/quiz_results/db/upgrade.php
blocks/recent_activity/db/upgrade.php
blocks/rss_client/db/upgrade.php
blocks/section_links/db/upgrade.php
blocks/selfcompletion/db/upgrade.php
blocks/settings/db/upgrade.php
blocks/upgrade.txt
calendar/amd/build/calendar.min.js [new file with mode: 0644]
calendar/amd/build/calendar_events.min.js [new file with mode: 0644]
calendar/amd/build/calendar_repository.min.js [new file with mode: 0644]
calendar/amd/build/summary_modal.min.js [new file with mode: 0644]
calendar/amd/src/calendar.js [new file with mode: 0644]
calendar/amd/src/calendar_events.js [new file with mode: 0644]
calendar/amd/src/calendar_repository.js [new file with mode: 0644]
calendar/amd/src/summary_modal.js [new file with mode: 0644]
calendar/export_execute.php
calendar/externallib.php
calendar/lib.php
calendar/renderer.php
calendar/templates/event_summary_body.mustache [new file with mode: 0644]
calendar/templates/event_summary_modal.mustache [new file with mode: 0644]
calendar/tests/behat/calendar_import.feature
calendar/tests/container_test.php
calendar/view.php
cohort/externallib.php
cohort/lib.php
cohort/tests/externallib_test.php
completion/classes/api.php
completion/classes/manager.php
completion/tests/api_test.php
course/externallib.php
course/lib.php
course/templates/defaultactivitycompletion.mustache
course/tests/behat/behat_course.php
course/tests/courselib_test.php
course/tests/externallib_test.php
course/yui/build/moodle-course-management/moodle-course-management-debug.js
course/yui/build/moodle-course-management/moodle-course-management-min.js
course/yui/build/moodle-course-management/moodle-course-management.js
course/yui/src/management/js/console.js
dataformat/html/classes/writer.php
enrol/database/db/upgrade.php
enrol/externallib.php
enrol/flatfile/db/upgrade.php
enrol/guest/db/upgrade.php
enrol/imsenterprise/db/upgrade.php
enrol/manual/ajax.php
enrol/manual/amd/build/form-potential-user-selector.min.js [new file with mode: 0644]
enrol/manual/amd/build/quickenrolment.min.js [new file with mode: 0644]
enrol/manual/amd/src/form-potential-user-selector.js [new file with mode: 0644]
enrol/manual/amd/src/quickenrolment.js [new file with mode: 0644]
enrol/manual/classes/enrol_users_form.php [new file with mode: 0644]
enrol/manual/db/upgrade.php
enrol/manual/lang/en/enrol_manual.php
enrol/manual/lib.php
enrol/manual/templates/form-user-selector-suggestion.mustache [new file with mode: 0644]
enrol/manual/yui/quickenrolment/assets/skins/sam/quickenrolment.css [deleted file]
enrol/manual/yui/quickenrolment/assets/skins/sam/sprite.png [deleted file]
enrol/manual/yui/quickenrolment/quickenrolment.js [deleted file]
enrol/mnet/db/upgrade.php
enrol/paypal/db/upgrade.php
enrol/self/db/upgrade.php
enrol/tests/behat/add_to_group.feature
enrol/tests/behat/behat_enrol.php
enrol/tests/behat/manage_enrolments_from_participants.feature
enrol/yui/rolemanager/assets/skins/sam/rolemanager.css [deleted file]
enrol/yui/rolemanager/assets/skins/sam/sprite.png [deleted file]
filter/mathjaxloader/db/upgrade.php
filter/mediaplugin/db/upgrade.php
filter/tex/db/upgrade.php
grade/grading/form/rubric/db/upgrade.php
grade/report/overview/db/access.php
grade/report/overview/db/upgrade.php [new file with mode: 0644]
grade/report/overview/lib.php
grade/report/overview/version.php
grade/report/user/db/upgrade.php
group/classes/output/user_groups_editable.php [new file with mode: 0644]
group/lib.php
group/tests/behat/behat_groups.php
install.php
install/lang/de/install.php
lang/en/admin.php
lang/en/calendar.php
lang/en/form.php
lang/en/group.php
lib/amd/build/form-autocomplete.min.js
lib/amd/build/form-cohort-selector.min.js [new file with mode: 0644]
lib/amd/build/inplace_editable.min.js
lib/amd/build/modal.min.js
lib/amd/build/modal_events.min.js
lib/amd/src/form-autocomplete.js
lib/amd/src/form-cohort-selector.js [new file with mode: 0644]
lib/amd/src/inplace_editable.js
lib/amd/src/modal.js
lib/amd/src/modal_events.js
lib/behat/behat_base.php
lib/behat/behat_field_manager.php
lib/behat/form_field/behat_form_autocomplete.php
lib/behat/form_field/behat_form_checkbox.php
lib/behat/form_field/behat_form_field.php
lib/behat/form_field/behat_form_select.php
lib/bennu/iCalendar_components.php
lib/bennu/readme_moodle.txt
lib/blocklib.php
lib/classes/oauth2/api.php
lib/classes/oauth2/issuer.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/output/inplace_editable.php
lib/classes/task/search_index_task.php
lib/classes/task/search_optimize_task.php
lib/db/install.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/db/upgradelib.php
lib/editor/atto/db/upgrade.php
lib/editor/atto/plugins/equation/db/upgrade.php
lib/editor/atto/plugins/equation/db/upgradelib.php [deleted file]
lib/editor/atto/plugins/equation/tests/upgradelib_testcase.php [deleted file]
lib/editor/atto/upgrade.txt
lib/editor/tinymce/db/upgrade.php
lib/editor/tinymce/plugins/spellchecker/db/upgrade.php
lib/external/externallib.php
lib/externallib.php
lib/filelib.php
lib/filestorage/file_storage.php
lib/filestorage/stored_file.php
lib/form/classes/filetypes_util.php
lib/form/cohort.php [new file with mode: 0644]
lib/form/filetypes.php
lib/form/tests/behat/filetypes.feature [new file with mode: 0644]
lib/form/tests/filetypes_util_test.php
lib/form/tests/fixtures/filetypes.php [new file with mode: 0644]
lib/formslib.php
lib/grouplib.php
lib/moodlelib.php
lib/outputlib.php
lib/phpminimumversionlib.php [new file with mode: 0644]
lib/requirejs.php
lib/templates/form_autocomplete_selection.mustache
lib/templates/inplace_editable.mustache
lib/testing/generator/block_generator.php
lib/testing/tests/generator_test.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_general.php
lib/tests/blocklib_test.php
lib/tests/grouplib_test.php
lib/tests/upgradelib_test.php
lib/upgrade.txt
lib/upgradelib.php
media/player/videojs/settings.php
message/classes/api.php
message/output/email/db/upgrade.php
message/output/jabber/db/upgrade.php
message/output/popup/amd/build/message_popover_controller.min.js
message/output/popup/amd/build/notification_area_control_area.min.js
message/output/popup/amd/build/notification_popover_controller.min.js
message/output/popup/amd/src/message_popover_controller.js
message/output/popup/amd/src/notification_area_control_area.js
message/output/popup/amd/src/notification_popover_controller.js
message/output/popup/classes/api.php
message/output/popup/db/upgrade.php
mod/assign/backup/moodle2/restore_assign_stepslib.php
mod/assign/db/upgrade.php
mod/assign/feedback/comments/db/upgrade.php
mod/assign/feedback/editpdf/db/upgrade.php
mod/assign/feedback/file/db/upgrade.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/submission/comments/db/upgrade.php
mod/assign/submission/file/db/upgrade.php
mod/assign/submission/onlinetext/db/upgrade.php
mod/assign/upgrade.txt
mod/assignment/db/upgrade.php
mod/book/db/upgrade.php
mod/book/tests/behat/edit_tags.feature
mod/chat/db/upgrade.php
mod/chat/lib.php
mod/chat/upgrade.txt
mod/choice/db/upgrade.php
mod/choice/lib.php
mod/choice/upgrade.txt
mod/data/db/upgrade.php
mod/data/field/file/field.class.php
mod/data/field/picture/field.class.php
mod/data/import.php
mod/data/lib.php
mod/data/upgrade.txt
mod/feedback/db/upgrade.php
mod/feedback/lib.php
mod/feedback/tests/behat/coursemapping.feature
mod/feedback/upgrade.txt
mod/folder/db/upgrade.php
mod/forum/db/upgrade.php
mod/forum/tests/behat/edit_tags.feature
mod/glossary/db/upgrade.php
mod/glossary/tests/behat/edit_tags.feature
mod/imscp/db/upgrade.php
mod/label/db/upgrade.php
mod/label/mod_form.php
mod/label/tests/generator/lib.php
mod/label/version.php
mod/lesson/continue.php
mod/lesson/db/upgrade.php
mod/lesson/lib.php
mod/lesson/locallib.php
mod/lesson/tests/behat/lesson_question_attempts.feature
mod/lesson/upgrade.txt
mod/lesson/version.php
mod/lti/db/access.php
mod/lti/db/upgrade.php
mod/lti/db/upgradelib.php [deleted file]
mod/lti/lang/en/lti.php
mod/lti/locallib.php
mod/lti/tests/upgradelib_test.php [deleted file]
mod/lti/upgrade.txt
mod/lti/version.php
mod/page/db/upgrade.php
mod/quiz/classes/structure.php
mod/quiz/db/upgrade.php
mod/quiz/lib.php
mod/quiz/report/overview/db/upgrade.php
mod/quiz/report/statistics/db/upgrade.php
mod/quiz/tests/structure_test.php
mod/quiz/upgrade.txt
mod/resource/db/upgrade.php
mod/scorm/db/upgrade.php
mod/scorm/lib.php
mod/scorm/upgrade.txt
mod/survey/db/upgrade.php
mod/url/db/upgrade.php
mod/wiki/db/upgrade.php
mod/wiki/tests/behat/edit_tags.feature
mod/workshop/db/upgrade.php
mod/workshop/form/accumulative/db/upgrade.php
mod/workshop/form/assessment_form.php
mod/workshop/form/comments/db/upgrade.php
mod/workshop/form/numerrors/db/upgrade.php
mod/workshop/form/rubric/db/upgrade.php
mod/workshop/lang/en/deprecated.txt
mod/workshop/lang/en/workshop.php
mod/workshop/lib.php
mod/workshop/locallib.php
mod/workshop/mod_form.php
mod/workshop/submission_form.php
mod/workshop/tests/behat/file_type_restriction.feature [new file with mode: 0644]
mod/workshop/tests/locallib_test.php
mod/workshop/upgrade.txt
my/lib.php
pix/e/cancel.png [new file with mode: 0644]
pix/e/cancel.svg [new file with mode: 0644]
portfolio/boxnet/db/upgrade.php
portfolio/googledocs/db/upgrade.php
portfolio/picasa/db/upgrade.php
question/behaviour/manualgraded/db/upgrade.php
question/type/calculated/db/upgrade.php
question/type/ddmarker/db/upgrade.php
question/type/essay/db/upgrade.php
question/type/match/db/upgrade.php
question/type/multianswer/db/upgrade.php
question/type/multichoice/db/upgrade.php
question/type/numerical/db/upgrade.php
question/type/random/db/upgrade.php
question/type/randomsamatch/db/upgrade.php
question/type/shortanswer/db/upgrade.php
repository/boxnet/db/upgrade.php
repository/dropbox/db/upgrade.php
repository/googledocs/db/upgrade.php
repository/picasa/db/upgrade.php
search/classes/base.php
search/classes/base_block.php [new file with mode: 0644]
search/classes/engine.php
search/classes/manager.php
search/cli/indexer.php
search/tests/base_block_test.php [new file with mode: 0644]
search/tests/fixtures/mock_block_area.php [new file with mode: 0644]
search/tests/fixtures/mock_search_area.php
search/tests/fixtures/mock_search_engine.php
search/tests/manager_test.php
search/upgrade.txt
tag/tests/behat/edit_tag.feature
tag/tests/behat/flag_tags.feature
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/forms.scss
theme/boost/scss/moodle/user.scss
theme/boost/templates/core/form_autocomplete_selection.mustache
theme/bootstrapbase/less/moodle/user.less
theme/bootstrapbase/style/moodle.css
theme/more/db/upgrade.php
user/classes/participants_table.php
user/index.php
user/tests/behat/view_participants.feature
version.php

index d58effa..9c21e74 100644 (file)
@@ -14,7 +14,7 @@ language: php
 php:
     # We only run the highest and lowest supported versions to reduce the load on travis-ci.org.
     - 7.1
-    - 5.6
+    - 7.0
 
 addons:
   postgresql: "9.3"
@@ -65,11 +65,7 @@ matrix:
         # Exclude it on all versions except for 7.1
 
         - env: DB=mysqli   TASK=PHPUNIT
-          php: 5.6
-
-        # Moodle 2.7 is not compatible with PHP 7 for the upgrade test.
-        - env: DB=pgsql    TASK=UPGRADE
-          php: 7.1
+          php: 7.0
 
 cache:
     directories:
@@ -233,9 +229,9 @@ before_script:
         # We need the official upstream.
         git remote add upstream https://github.com/moodle/moodle.git;
 
-        # Checkout 27 STABLE branch.
-        git fetch upstream MOODLE_27_STABLE;
-        git checkout MOODLE_27_STABLE;
+        # Checkout 30 STABLE branch (the first version compatible with PHP 7.x)
+        git fetch upstream MOODLE_30_STABLE;
+        git checkout MOODLE_30_STABLE;
 
         # Perform the upgrade
         php admin/cli/install_database.php --agree-license --adminpass=Password --adminemail=admin@example.com --fullname="Upgrade test" --shortname=Upgrade;
index 0b6c5ca..28e9cb8 100644 (file)
@@ -146,14 +146,9 @@ define('PHPUNIT_TEST', false);
 
 define('IGNORE_COMPONENT_CACHE', true);
 
-// Check that PHP is of a sufficient version
-if (version_compare(phpversion(), "5.6.5") < 0) {
-    $phpversion = phpversion();
-    // do NOT localise - lang strings would not work here and we CAN NOT move it after installib
-    fwrite(STDERR, "Moodle 3.2 or later requires at least PHP 5.6.5 (currently using version $phpversion).\n");
-    fwrite(STDERR, "Please upgrade your server software or install older Moodle version.\n");
-    exit(1);
-}
+// Check that PHP is of a sufficient version as soon as possible.
+require_once(__DIR__.'/../../lib/phpminimumversionlib.php');
+moodle_require_minimum_php_version();
 
 // set up configuration
 global $CFG;
index f41263e..cede6cb 100644 (file)
@@ -62,14 +62,9 @@ Example:
 \$sudo -u www-data /usr/bin/php admin/cli/install_database.php --lang=cs --adminpass=soMePass123 --agree-license
 ";
 
-// Check that PHP is of a sufficient version
-if (version_compare(phpversion(), "5.6.5") < 0) {
-    $phpversion = phpversion();
-    // do NOT localise - lang strings would not work here and we CAN NOT move it after installib
-    fwrite(STDERR, "Moodle 3.2 or later requires at least PHP 5.6.5 (currently using version $phpversion).\n");
-    fwrite(STDERR, "Please upgrade your server software or install older Moodle version.\n");
-    exit(1);
-}
+// Check that PHP is of a sufficient version as soon as possible.
+require_once(__DIR__.'/../../lib/phpminimumversionlib.php');
+moodle_require_minimum_php_version();
 
 // Nothing to do if config.php does not exist
 $configfile = __DIR__.'/../../config.php';
index 916c8a7..b8c7f16 100644 (file)
       </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
+  <MOODLE version="3.4" requires="3.0">
+    <UNICODE level="required">
+      <FEEDBACK>
+        <ON_ERROR message="unicoderequired" />
+      </FEEDBACK>
+    </UNICODE>
+    <DATABASE level="required">
+      <VENDOR name="mariadb" version="5.5.31" />
+      <VENDOR name="mysql" version="5.5.31" />
+      <VENDOR name="postgres" version="9.3" />
+      <VENDOR name="mssql" version="10.0" />
+      <VENDOR name="oracle" version="10.2" />
+    </DATABASE>
+    <PHP version="7.0.0" level="required">
+    </PHP>
+    <PCREUNICODE level="optional">
+      <FEEDBACK>
+        <ON_CHECK message="pcreunicodewarning" />
+      </FEEDBACK>
+    </PCREUNICODE>
+    <PHP_EXTENSIONS>
+      <PHP_EXTENSION name="iconv" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="iconvrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="mbstring" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="mbstringrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="curl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="curlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="openssl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="opensslrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="tokenizer" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="tokenizerrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlrpc" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="xmlrpcrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="soap" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="soaprecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="ctype" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ctyperequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zip" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ziprequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zlib" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="gd" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="gdrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="simplexml" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="simplexmlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="spl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="splrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="pcre" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="dom" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xml" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlreader" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="intl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="intlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="json" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="hash" level="required"/>
+      <PHP_EXTENSION name="fileinfo" level="required"/>
+    </PHP_EXTENSIONS>
+    <PHP_SETTINGS>
+      <PHP_SETTING name="memory_limit" value="96M" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="settingmemorylimit" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="file_uploads" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="settingfileuploads" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="opcache.enable" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="opcacherecommended" />
+        </FEEDBACK>
+      </PHP_SETTING>
+    </PHP_SETTINGS>
+    <CUSTOM_CHECKS>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbstorageengine" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="quizattemptsupgradedmessage" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="slashargumentswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unsupporteddbtablerowformat" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unoconvwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="libcurlwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbfileformat" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbfilepertable" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddblargeprefix" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="ishttpswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="incompleteunicodesupport" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+    </CUSTOM_CHECKS>
+  </MOODLE>
 </COMPATIBILITY_MATRIX>
index 6cf7f45..d0da26b 100644 (file)
@@ -29,14 +29,9 @@ if (!file_exists('../config.php')) {
     die();
 }
 
-// Check that PHP is of a sufficient version as soon as possible
-if (version_compare(phpversion(), '5.6.5') < 0) {
-    $phpversion = phpversion();
-    // do NOT localise - lang strings would not work here and we CAN NOT move it to later place
-    echo "Moodle 3.2 or later requires at least PHP 5.6.5 (currently using version $phpversion).<br />";
-    echo "Please upgrade your server software or install older Moodle version.";
-    die();
-}
+// Check that PHP is of a sufficient version as soon as possible.
+require_once(__DIR__.'/../lib/phpminimumversionlib.php');
+moodle_require_minimum_php_version();
 
 // make sure iconv is available and actually works
 if (!function_exists('iconv')) {
index dcbc7cb..9ce7af6 100644 (file)
@@ -125,6 +125,9 @@ foreach ($searchareas as $area) {
                     $areasconfig[$areaid]->docsprocessed . ' , ' .
                     $areasconfig[$areaid]->recordsprocessed . ' , ' .
                     $areasconfig[$areaid]->docsignored;
+                if ($areasconfig[$areaid]->partial) {
+                    $laststatus .= ' ' . get_string('searchpartial', 'admin');
+                }
             } else {
                 $laststatus = '';
             }
index e63f190..3bb3c4a 100644 (file)
@@ -557,6 +557,13 @@ if ($hassiteconfig) {
     $temp->add(new admin_setting_heading('searchengineheading', new lang_string('searchengine', 'admin'), ''));
     $temp->add(new admin_setting_configselect('searchengine',
                                 new lang_string('selectsearchengine', 'admin'), '', 'solr', $engines));
+    $temp->add(new admin_setting_heading('searchindexingheading', new lang_string('searchoptions', 'admin'), ''));
+    $temp->add(new admin_setting_configcheckbox('searchindexwhendisabled',
+            new lang_string('searchindexwhendisabled', 'admin'), new lang_string('searchindexwhendisabled_desc', 'admin'),
+            0));
+    $temp->add(new admin_setting_configduration('searchindextime',
+            new lang_string('searchindextime', 'admin'), new lang_string('searchindextime_desc', 'admin'),
+            600));
 
     $ADMIN->add('searchplugins', $temp);
     $ADMIN->add('searchplugins', new admin_externalpage('searchareas', new lang_string('searchareas', 'admin'),
index d6fa182..afda140 100644 (file)
@@ -29,15 +29,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_tool_customlang_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
 
index 3d7f4f9..13befec 100644 (file)
@@ -33,15 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_tool_log_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
 
index 789415e..0dbe11a 100644 (file)
@@ -27,15 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_logstore_database_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
 
index 2614e0a..bcd8d1d 100644 (file)
 defined('MOODLE_INTERNAL') || die();
 
 function xmldb_logstore_standard_upgrade($oldversion) {
-    global $CFG, $DB;
-
-    $dbman = $DB->get_manager();
-
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    if ($oldversion < 2016041200) {
-        // This could take a long time. Unfortunately, no way to know how long, and no way to do progress, so setting for 1 hour.
-        upgrade_set_timeout(3600);
-
-        // Define key contextid (foreign) to be added to logstore_standard_log.
-        $table = new xmldb_table('logstore_standard_log');
-        $key = new xmldb_key('contextid', XMLDB_KEY_FOREIGN, array('contextid'), 'context', array('id'));
-
-        // Launch add key contextid.
-        $dbman->add_key($table, $key);
-
-        // Standard savepoint reached.
-        upgrade_plugin_savepoint(true, 2016041200, 'logstore', 'standard');
-    }
+    global $CFG;
 
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
index 6af0d41..77cf893 100644 (file)
@@ -920,119 +920,6 @@ class external extends external_api {
         ));
     }
 
-    /**
-     * Returns the description of external function parameters.
-     *
-     * @return external_function_parameters
-     */
-    public static function search_cohorts_parameters() {
-        $query = new external_value(
-            PARAM_RAW,
-            'Query string'
-        );
-        $includes = new external_value(
-            PARAM_ALPHA,
-            'What other contexts to fetch the frameworks from. (all, parents, self)',
-            VALUE_DEFAULT,
-            'parents'
-        );
-        $limitfrom = new external_value(
-            PARAM_INT,
-            'limitfrom we are fetching the records from',
-            VALUE_DEFAULT,
-            0
-        );
-        $limitnum = new external_value(
-            PARAM_INT,
-            'Number of records to fetch',
-            VALUE_DEFAULT,
-            25
-        );
-        return new external_function_parameters(array(
-            'query' => $query,
-            'context' => self::get_context_parameters(),
-            'includes' => $includes,
-            'limitfrom' => $limitfrom,
-            'limitnum' => $limitnum
-        ));
-    }
-
-    /**
-     * Search cohorts.
-     * TODO: MDL-52243 Move this function to cohorts/externallib.php
-     *
-     * @param string $query
-     * @param array $context
-     * @param string $includes
-     * @param int $limitfrom
-     * @param int $limitnum
-     * @return array
-     */
-    public static function search_cohorts($query, $context, $includes = 'parents', $limitfrom = 0, $limitnum = 25) {
-        global $DB, $CFG, $PAGE;
-        require_once($CFG->dirroot . '/cohort/lib.php');
-
-        $params = self::validate_parameters(self::search_cohorts_parameters(), array(
-            'query' => $query,
-            'context' => $context,
-            'includes' => $includes,
-            'limitfrom' => $limitfrom,
-            'limitnum' => $limitnum,
-        ));
-        $query = $params['query'];
-        $includes = $params['includes'];
-        $context = self::get_context_from_params($params['context']);
-        $limitfrom = $params['limitfrom'];
-        $limitnum = $params['limitnum'];
-
-        self::validate_context($context);
-        $output = $PAGE->get_renderer('tool_lp');
-
-        $manager = has_capability('moodle/cohort:manage', $context);
-        if (!$manager) {
-            require_capability('moodle/cohort:view', $context);
-        }
-
-        // TODO Make this more efficient.
-        if ($includes == 'self') {
-            $results = cohort_get_cohorts($context->id, $limitfrom, $limitnum, $query);
-            $results = $results['cohorts'];
-        } else if ($includes == 'parents') {
-            $results = cohort_get_cohorts($context->id, $limitfrom, $limitnum, $query);
-            $results = $results['cohorts'];
-            if (!$context instanceof context_system) {
-                $results = array_merge($results, cohort_get_available_cohorts($context, COHORT_ALL, $limitfrom, $limitnum, $query));
-            }
-        } else if ($includes == 'all') {
-            $results = cohort_get_all_cohorts($limitfrom, $limitnum, $query);
-            $results = $results['cohorts'];
-        } else {
-            throw new coding_exception('Invalid parameter value for \'includes\'.');
-        }
-
-        $cohorts = array();
-        foreach ($results as $key => $cohort) {
-            $cohortcontext = context::instance_by_id($cohort->contextid);
-            $exporter = new cohort_summary_exporter($cohort, array('context' => $cohortcontext));
-            $newcohort = $exporter->export($output);
-
-            $cohorts[$key] = $newcohort;
-        }
-
-        return array('cohorts' => $cohorts);
-    }
-
-    /**
-     * Returns description of external function result value.
-     *
-     * @return external_description
-     */
-    public static function search_cohorts_returns() {
-        return new external_single_structure(array(
-            'cohorts' => new external_multiple_structure(cohort_summary_exporter::get_read_structure())
-        ));
-    }
-
     /**
      * Returns description of external function.
      *
index 258f382..b7f9e9e 100644 (file)
@@ -131,10 +131,11 @@ $functions = array(
         'capabilities' => '',
         'ajax'         => true,
     ),
+    // This function was originally in this plugin but has been moved to core.
     'tool_lp_search_cohorts' => array(
-        'classname'    => 'tool_lp\external',
+        'classname'    => 'core_cohort_external',
         'methodname'   => 'search_cohorts',
-        'classpath'    => '',
+        'classpath'    => 'cohort/externallib.php',
         'description'  => 'Search for cohorts.',
         'type'         => 'read',
         'capabilities' => 'moodle/cohort:view',
index a43d857..acd0b19 100644 (file)
@@ -63,16 +63,4 @@ class behat_tool_lp extends behat_base {
         $this->execute('behat_general::i_click_on', [$xpathtarget, 'xpath_element']);
     }
 
-    /**
-     * Select item from autocomplete list.
-     *
-     * @Given /^I click on "([^"]*)" item in the autocomplete list$/
-     *
-     * @param string $item
-     */
-    public function i_click_on_item_in_the_autocomplete_list($item) {
-        $xpathtarget = "//ul[@class='form-autocomplete-suggestions']//li//span//span[contains(.,'" . $item . "')]";
-
-        $this->execute('behat_general::i_click_on', [$xpathtarget, 'xpath_element']);
-    }
 }
index 456e32b..7e9fecc 100644 (file)
@@ -28,8 +28,9 @@ Feature: Manage plearning plan
     And I follow "Home"
     And I navigate to "Competencies > Learning plan templates" in site administration
     And I click on ".template-userplans" "css_element" in the "Science template" "table_row"
-    And I click on ".form-autocomplete-downarrow" "css_element"
+    And I open the autocomplete suggestions list
     And I click on "Admin" item in the autocomplete list
+    And I press key "27" in the field "Select users to create learning plans for"
     When I click on "Create learning plans" "button"
     Then I should see "A learning plan was created"
     And I should see "Admin User" in the "Science template" "table_row"
@@ -54,6 +55,7 @@ Feature: Manage plearning plan
     And I click on ".template-cohorts" "css_element" in the "Science template cohort" "table_row"
     And I click on ".form-autocomplete-downarrow" "css_element"
     And I click on "cohort plan" item in the autocomplete list
+    And I press key "27" in the field "Select cohorts to sync"
     When I click on "Add cohorts" "button"
     Then I should see "2 learning plans were created."
     And I follow "Learning plan templates"
index d523735..138a420 100644 (file)
@@ -461,76 +461,4 @@ class tool_lp_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('A', $summary->evidence[1]->gradename);
     }
 
-    /**
-     * Search cohorts.
-     */
-    public function test_search_cohorts() {
-        $this->resetAfterTest(true);
-
-        $syscontext = array('contextid' => context_system::instance()->id);
-        $catcontext = array('contextid' => context_coursecat::instance($this->category->id)->id);
-        $othercatcontext = array('contextid' => context_coursecat::instance($this->othercategory->id)->id);
-
-        $cohort1 = $this->getDataGenerator()->create_cohort(array_merge($syscontext, array('name' => 'Cohortsearch 1')));
-        $cohort2 = $this->getDataGenerator()->create_cohort(array_merge($catcontext, array('name' => 'Cohortsearch 2')));
-        $cohort3 = $this->getDataGenerator()->create_cohort(array_merge($othercatcontext, array('name' => 'Cohortsearch 3')));
-
-        // Check for parameter $includes = 'parents'.
-
-        // A user without permission in the system.
-        $this->setUser($this->user);
-        try {
-            $result = external::search_cohorts("Cohortsearch", $syscontext, 'parents');
-            $this->fail('Invalid permissions in system');
-        } catch (required_capability_exception $e) {
-            // All good.
-        }
-
-        // A user without permission in a category.
-        $this->setUser($this->catuser);
-        try {
-            $result = external::search_cohorts("Cohortsearch", $catcontext, 'parents');
-            $this->fail('Invalid permissions in category');
-        } catch (required_capability_exception $e) {
-            // All good.
-        }
-
-        // A user with permissions in the system.
-        $this->setUser($this->creator);
-        $result = external::search_cohorts("Cohortsearch", $syscontext, 'parents');
-        $this->assertEquals(1, count($result['cohorts']));
-        $this->assertEquals('Cohortsearch 1', $result['cohorts'][$cohort1->id]->name);
-
-        // A user with permissions in the category.
-        $this->setUser($this->catcreator);
-        $result = external::search_cohorts("Cohortsearch", $catcontext, 'parents');
-        $this->assertEquals(2, count($result['cohorts']));
-        $cohorts = array();
-        foreach ($result['cohorts'] as $cohort) {
-            $cohorts[] = $cohort->name;
-        }
-        $this->assertTrue(in_array('Cohortsearch 1', $cohorts));
-        $this->assertTrue(in_array('Cohortsearch 2', $cohorts));
-
-        // Check for parameter $includes = 'self'.
-        $this->setUser($this->creator);
-        $result = external::search_cohorts("Cohortsearch", $othercatcontext, 'self');
-        $this->assertEquals(1, count($result['cohorts']));
-        $this->assertEquals('Cohortsearch 3', $result['cohorts'][$cohort3->id]->name);
-
-        // Check for parameter $includes = 'all'.
-        $this->setUser($this->creator);
-        $result = external::search_cohorts("Cohortsearch", $syscontext, 'all');
-        $this->assertEquals(3, count($result['cohorts']));
-
-        // Detect invalid parameter $includes.
-        $this->setUser($this->creator);
-        try {
-            $result = external::search_cohorts("Cohortsearch", $syscontext, 'invalid');
-            $this->fail('Invalid parameter includes');
-        } catch (coding_exception $e) {
-            // All good.
-        }
-    }
-
 }
index 3dadfb7..4c12650 100644 (file)
@@ -25,6 +25,6 @@
 defined('MOODLE_INTERNAL') || die();
 
 
-$plugin->version   = 2017051500; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2017062700; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2017050500; // Requires this Moodle version.
 $plugin->component = 'tool_lp'; // Full name of the plugin (used for diagnostics).
index 1a47f3a..4d7c475 100644 (file)
@@ -31,34 +31,10 @@ defined('MOODLE_INTERNAL') || die();
  * @return bool always true
  */
 function xmldb_tool_monitor_upgrade($oldversion) {
-    global $DB;
+    global $CFG, $DB;
 
     $dbman = $DB->get_manager();
 
-    if ($oldversion < 2014102000) {
-
-        // Define field lastnotificationsent to be added to tool_monitor_subscriptions.
-        $table = new xmldb_table('tool_monitor_subscriptions');
-        $field = new xmldb_field('lastnotificationsent', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'timecreated');
-
-        // Conditionally launch add field lastnotificationsent.
-        if (!$dbman->field_exists($table, $field)) {
-            $dbman->add_field($table, $field);
-        }
-
-        // Monitor savepoint reached.
-        upgrade_plugin_savepoint(true, 2014102000, 'tool', 'monitor');
-    }
-
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
 
index 3a0b64d..a57b4c2 100644 (file)
@@ -119,6 +119,10 @@ class issuer extends persistent {
         $mform->addElement('checkbox', 'showonloginpage', get_string('issuershowonloginpage', 'tool_oauth2'));
         $mform->addHelpButton('showonloginpage', 'issuershowonloginpage', 'tool_oauth2');
 
+        // Require confirmation email for new accounts.
+        $mform->addElement('advcheckbox', 'requireconfirmation', get_string('issuerrequireconfirmation', 'tool_oauth2'));
+        $mform->addHelpButton('requireconfirmation', 'issuerrequireconfirmation', 'tool_oauth2');
+
         $mform->addElement('hidden', 'sortorder');
         $mform->setType('sortorder', PARAM_INT);
 
index 5cc423c..8c20c6a 100644 (file)
@@ -81,6 +81,8 @@ $string['issuername_help'] = 'Name of the identity issuer. May be displayed on l
 $string['issuername'] = 'Name';
 $string['issuershowonloginpage_help'] = 'If the OAuth 2 authentication plugin is enabled, this login issuer will be listed on the login page to allow users to log in with accounts from this issuer.';
 $string['issuershowonloginpage'] = 'Show on login page';
+$string['issuerrequireconfirmation_help'] = 'Require that all users verify their email address before they can log in with OAuth. This applies to newly created accounts as part of the login process, or when an existing Moodle account is connected to an OAuth login via matching email addresses.';
+$string['issuerrequireconfirmation'] = 'Require email verification';
 $string['issuers'] = 'Issuers';
 $string['loginissuer'] = 'Allow login';
 $string['notconfigured'] = 'Not configured';
index 75500b2..da1592e 100644 (file)
@@ -95,7 +95,7 @@ class tool_uploadcourse_course {
     /** @var array fields allowed as course data. */
     static protected $validfields = array('fullname', 'shortname', 'idnumber', 'category', 'visible', 'startdate', 'enddate',
         'summary', 'format', 'theme', 'lang', 'newsitems', 'showgrades', 'showreports', 'legacyfiles', 'maxbytes',
-        'groupmode', 'groupmodeforce', 'groupmodeforce', 'enablecompletion');
+        'groupmode', 'groupmodeforce', 'enablecompletion');
 
     /** @var array fields required on course creation. */
     static protected $mandatoryfields = array('fullname', 'category');
@@ -676,6 +676,17 @@ class tool_uploadcourse_course {
             return false;
         }
 
+        // TODO MDL-59259 allow to set course format options for the current course format.
+
+        // Special case, 'numsections' is not a course format option any more but still should apply from defaults.
+        if (!$exists || !array_key_exists('numsections', $coursedata)) {
+            if (isset($this->rawdata['numsections']) && is_numeric($this->rawdata['numsections'])) {
+                $coursedata['numsections'] = (int)$this->rawdata['numsections'];
+            } else {
+                $coursedata['numsections'] = get_config('moodlecourse', 'numsections');
+            }
+        }
+
         // Saving data.
         $this->data = $coursedata;
         $this->enrolmentdata = tool_uploadcourse_helper::get_enrolment_data($this->rawdata);
index 4601500..d3c5024 100644 (file)
@@ -166,6 +166,13 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
         $mform->addHelpButton('defaults[groupmodeforce]', 'groupmodeforce', 'group');
         $mform->setDefault('defaults[groupmodeforce]', $courseconfig->groupmodeforce);
 
+        // Completion tracking.
+        if (!empty($CFG->enablecompletion)) {
+            $mform->addElement('selectyesno', 'defaults[enablecompletion]', get_string('enablecompletion', 'completion'));
+            $mform->setDefault('defaults[enablecompletion]', $courseconfig->enablecompletion);
+            $mform->addHelpButton('defaults[enablecompletion]', 'enablecompletion', 'completion');
+        }
+
         // Hidden fields.
         $mform->addElement('hidden', 'importid');
         $mform->setType('importid', PARAM_INT);
index 7d9970f..75c39d8 100644 (file)
@@ -166,6 +166,7 @@ $defaults['groupmode'] = $courseconfig->groupmode;
 $defaults['groupmodeforce'] = $courseconfig->groupmodeforce;
 $defaults['visible'] = $courseconfig->visible;
 $defaults['lang'] =  $courseconfig->lang;
+$defaults['enablecompletion'] = $courseconfig->enablecompletion;
 
 // Course template.
 if (isset($options['templatecourse'])) {
index 3ac9fe0..3356163 100644 (file)
@@ -120,6 +120,36 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
         $this->assertTrue($DB->record_exists('course', array('shortname' => 'c2')));
     }
 
+    public function test_create_with_sections() {
+        global $DB;
+        $this->resetAfterTest(true);
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $defaultnumsections = get_config('moodlecourse', 'numsections');
+
+        // Add new course, make sure default number of sections is created.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $data = array('shortname' => 'newcourse1', 'fullname' => 'New course1', 'format' => 'topics', 'category' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $courseid = $DB->get_field('course', 'id', array('shortname' => 'newcourse1'));
+        $this->assertNotEmpty($courseid);
+        $this->assertEquals($defaultnumsections + 1,
+            $DB->count_records('course_sections', ['course' => $courseid]));
+
+        // Add new course specifying number of sections.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $data = array('shortname' => 'newcourse2', 'fullname' => 'New course2', 'format' => 'topics', 'category' => 1,
+            'numsections' => 15);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $courseid = $DB->get_field('course', 'id', array('shortname' => 'newcourse2'));
+        $this->assertNotEmpty($courseid);
+        $this->assertEquals(15 + 1,
+            $DB->count_records('course_sections', ['course' => $courseid]));
+    }
+
     public function test_delete() {
         global $DB;
         $this->resetAfterTest(true);
index a17ef91..ce506d6 100644 (file)
@@ -30,28 +30,7 @@ defined('MOODLE_INTERNAL') || die();
  * @return bool result
  */
 function xmldb_auth_cas_upgrade($oldversion) {
-    global $CFG, $DB;
-
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    if ($oldversion < 2014111001) {
-        // From now on the default LDAP objectClass setting for AD has been changed, from 'user' to '(samaccounttype=805306368)'.
-        if (is_enabled_auth('cas')
-                && ($DB->get_field('config_plugins', 'value', array('name' => 'user_type', 'plugin' => 'auth/cas')) === 'ad')
-                && ($DB->get_field('config_plugins', 'value', array('name' => 'objectclass', 'plugin' => 'auth/cas')) === '')) {
-            // Save the backwards-compatible default setting.
-            set_config('objectclass', 'user', 'auth/cas');
-        }
-
-        upgrade_plugin_savepoint(true, 2014111001, 'auth', 'cas');
-    }
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
+    global $CFG;
 
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
index a2fe48e..3e1c58f 100644 (file)
@@ -30,28 +30,7 @@ defined('MOODLE_INTERNAL') || die();
  * @return bool result
  */
 function xmldb_auth_ldap_upgrade($oldversion) {
-    global $CFG, $DB;
-
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    if ($oldversion < 2014111001) {
-        // From now on the default LDAP objectClass setting for AD has been changed, from 'user' to '(samaccounttype=805306368)'.
-        if (is_enabled_auth('ldap')
-                && ($DB->get_field('config_plugins', 'value', array('name' => 'user_type', 'plugin' => 'auth/ldap')) === 'ad')
-                && ($DB->get_field('config_plugins', 'value', array('name' => 'objectclass', 'plugin' => 'auth/ldap')) === '')) {
-            // Save the backwards-compatible default setting.
-            set_config('objectclass', 'user', 'auth/ldap');
-        }
-
-        upgrade_plugin_savepoint(true, 2014111001, 'auth', 'ldap');
-    }
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
+    global $CFG;
 
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
index 6a947d8..a73861d 100644 (file)
@@ -30,16 +30,7 @@ defined('MOODLE_INTERNAL') || die();
  * @return bool result
  */
 function xmldb_auth_manual_upgrade($oldversion) {
-    global $CFG, $DB;
-
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
+    global $CFG;
 
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
index c66ead0..99e326c 100644 (file)
@@ -30,16 +30,7 @@ defined('MOODLE_INTERNAL') || die();
  * @return bool result
  */
 function xmldb_auth_mnet_upgrade($oldversion) {
-    global $CFG, $DB;
-
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
+    global $CFG;
 
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
index 9d678a8..1b152b5 100644 (file)
@@ -239,6 +239,50 @@ class api {
         return true;
     }
 
+    /**
+     * Create an account with a linked login that is already confirmed.
+     *
+     * @param array $userinfo as returned from an oauth client.
+     * @param \core\oauth2\issuer $issuer
+     * @return bool
+     */
+    public static function create_new_confirmed_account($userinfo, $issuer) {
+        global $CFG, $DB;
+        require_once($CFG->dirroot.'/user/profile/lib.php');
+        require_once($CFG->dirroot.'/user/lib.php');
+
+        $user = new stdClass();
+        $user->username = $userinfo['username'];
+        $user->email = $userinfo['email'];
+        $user->auth = 'oauth2';
+        $user->mnethostid = $CFG->mnet_localhost_id;
+        $user->lastname = isset($userinfo['lastname']) ? $userinfo['lastname'] : '';
+        $user->firstname = isset($userinfo['firstname']) ? $userinfo['firstname'] : '';
+        $user->url = isset($userinfo['url']) ? $userinfo['url'] : '';
+        $user->alternatename = isset($userinfo['alternatename']) ? $userinfo['alternatename'] : '';
+        $user->secret = random_string(15);
+
+        $user->password = '';
+        // This user is confirmed.
+        $user->confirmed = 1;
+
+        $user->id = user_create_user($user, false, true);
+
+        // The linked account is pre-confirmed.
+        $record = new stdClass();
+        $record->issuerid = $issuer->get('id');
+        $record->username = $userinfo['username'];
+        $record->userid = $user->id;
+        $record->email = $userinfo['email'];
+        $record->confirmtoken = '';
+        $record->confirmtokenexpires = 0;
+
+        $linkedlogin = new linked_login(0, $record);
+        $linkedlogin->create();
+
+        return $user;
+    }
+
     /**
      * Send an email with a link to confirm creating this account.
      *
index aa5f70c..be99012 100644 (file)
@@ -450,15 +450,21 @@ class auth extends \auth_plugin_base {
 
             $moodleuser = \core_user::get_user_by_email($userinfo['email']);
             if (!empty($moodleuser)) {
-                $PAGE->set_url('/auth/oauth2/confirm-link-login.php');
-                $PAGE->set_context(context_system::instance());
-
-                \auth_oauth2\api::send_confirm_link_login_email($userinfo, $issuer, $moodleuser->id);
-                // Request to link to existing account.
-                $emailconfirm = get_string('emailconfirmlink', 'auth_oauth2');
-                $message = get_string('emailconfirmlinksent', 'auth_oauth2', $moodleuser->email);
-                $this->print_confirm_required($emailconfirm, $message);
-                exit();
+                if ($issuer->get('requireconfirmation')) {
+                    $PAGE->set_url('/auth/oauth2/confirm-link-login.php');
+                    $PAGE->set_context(context_system::instance());
+
+                    \auth_oauth2\api::send_confirm_link_login_email($userinfo, $issuer, $moodleuser->id);
+                    // Request to link to existing account.
+                    $emailconfirm = get_string('emailconfirmlink', 'auth_oauth2');
+                    $message = get_string('emailconfirmlinksent', 'auth_oauth2', $moodleuser->email);
+                    $this->print_confirm_required($emailconfirm, $message);
+                    exit();
+                } else {
+                    \auth_oauth2\api::link_login($userinfo, $issuer, $moodleuser->id, true);
+                    $userinfo = get_complete_user_data('id', $moodleuser->id);
+                    // No redirect, we will complete this login.
+                }
 
             } else {
                 // This is a new account.
@@ -506,17 +512,25 @@ class auth extends \auth_plugin_base {
                     redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php'));
                 }
 
-                $PAGE->set_url('/auth/oauth2/confirm-account.php');
-                $PAGE->set_context(context_system::instance());
+                if ($issuer->get('requireconfirmation')) {
+                    $PAGE->set_url('/auth/oauth2/confirm-account.php');
+                    $PAGE->set_context(context_system::instance());
 
-                // Create a new (unconfirmed account) and send an email to confirm it.
-                $user = \auth_oauth2\api::send_confirm_account_email($userinfo, $issuer);
+                    // Create a new (unconfirmed account) and send an email to confirm it.
+                    $user = \auth_oauth2\api::send_confirm_account_email($userinfo, $issuer);
 
-                $this->update_picture($user);
-                $emailconfirm = get_string('emailconfirm');
-                $message = get_string('emailconfirmsent', '', $userinfo['email']);
-                $this->print_confirm_required($emailconfirm, $message);
-                exit();
+                    $this->update_picture($user);
+                    $emailconfirm = get_string('emailconfirm');
+                    $message = get_string('emailconfirmsent', '', $userinfo['email']);
+                    $this->print_confirm_required($emailconfirm, $message);
+                    exit();
+                } else {
+                    // Create a new confirmed account.
+                    $newuser = \auth_oauth2\api::create_new_confirmed_account($userinfo, $issuer);
+                    $userinfo = get_complete_user_data('id', $newuser->id);
+
+                    // No redirect, we will complete this login.
+                }
             }
         }
 
index 817430a..83bf1a6 100644 (file)
@@ -98,4 +98,46 @@ class auth_oauth2_external_testcase extends advanced_testcase {
         $this->assertCount(1, $linkedlogins);
     }
 
+    /**
+     * Test auto-confirming linked logins.
+     */
+    public function test_linked_logins() {
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+        $issuer = \core\oauth2\api::create_standard_issuer('google');
+
+        $user = $this->getDataGenerator()->create_user();
+
+        $info = [];
+        $info['username'] = 'banana';
+        $info['email'] = 'banana@example.com';
+
+        \auth_oauth2\api::link_login($info, $issuer, $user->id, false);
+
+        // Try and match a user with a linked login.
+        $match = \auth_oauth2\api::match_username_to_user('banana', $issuer);
+
+        $this->assertEquals($user->id, $match->get('userid'));
+        $linkedlogins = \auth_oauth2\api::get_linked_logins($user->id, $issuer);
+        \auth_oauth2\api::delete_linked_login($linkedlogins[0]->get('id'));
+
+        $match = \auth_oauth2\api::match_username_to_user('banana', $issuer);
+        $this->assertFalse($match);
+
+        $info = [];
+        $info['username'] = 'apple';
+        $info['email'] = 'apple@example.com';
+        $info['firstname'] = 'Apple';
+        $info['lastname'] = 'Fruit';
+        $info['url'] = 'http://apple.com/';
+        $info['alternamename'] = 'Beatles';
+
+        $newuser = \auth_oauth2\api::create_new_confirmed_account($info, $issuer);
+
+        $match = \auth_oauth2\api::match_username_to_user('apple', $issuer);
+
+        $this->assertEquals($newuser->id, $match->get('userid'));
+    }
+
 }
index 2ea5248..b0c6e89 100644 (file)
@@ -1410,8 +1410,9 @@ class backup_block_instance_structure_step extends backup_structure_step {
         // Define each element separated
 
         $block = new backup_nested_element('block', array('id', 'contextid', 'version'), array(
-            'blockname', 'parentcontextid', 'showinsubcontexts', 'pagetypepattern',
-            'subpagepattern', 'defaultregion', 'defaultweight', 'configdata'));
+                'blockname', 'parentcontextid', 'showinsubcontexts', 'pagetypepattern',
+                'subpagepattern', 'defaultregion', 'defaultweight', 'configdata',
+                'timecreated', 'timemodified'));
 
         $positions = new backup_nested_element('block_positions');
 
index f886496..00132f6 100644 (file)
@@ -3937,6 +3937,14 @@ class restore_block_instance_structure_step extends restore_structure_step {
             $data->configdata = base64_encode(serialize((object)$configdata));
         }
 
+        // Set timecreated, timemodified if not included (older backup).
+        if (empty($data->timecreated)) {
+            $data->timecreated = time();
+        }
+        if (empty($data->timemodified)) {
+            $data->timemodified = $data->timecreated;
+        }
+
         // Create the block instance
         $newitemid = $DB->insert_record('block_instances', $data);
         // Save the mapping (with restorefiles support)
index a5f531c..21773b9 100644 (file)
@@ -464,9 +464,10 @@ class core_backup_moodle2_testcase extends advanced_testcase {
      *
      * @param stdClass $course Course object to backup
      * @param int $newdate If non-zero, specifies custom date for new course
+     * @param callable|null $inbetween If specified, function that is called before restore
      * @return int ID of newly restored course
      */
-    protected function backup_and_restore($course, $newdate = 0) {
+    protected function backup_and_restore($course, $newdate = 0, $inbetween = null) {
         global $USER, $CFG;
 
         // Turn off file logging, otherwise it can't delete the file (Windows).
@@ -481,6 +482,10 @@ class core_backup_moodle2_testcase extends advanced_testcase {
         $bc->execute_plan();
         $bc->destroy();
 
+        if ($inbetween) {
+            $inbetween($backupid);
+        }
+
         // Do restore to new course with default settings.
         $newcourseid = restore_dbops::create_new_course(
                 $course->fullname, $course->shortname . '_2', $course->category);
@@ -802,4 +807,64 @@ class core_backup_moodle2_testcase extends advanced_testcase {
         $enrolment = reset($enrolments);
         $this->assertEquals('self', $enrolment->enrol);
     }
+
+    /**
+     * Test the block instance time fields (timecreated, timemodified) through a backup and restore.
+     */
+    public function test_block_instance_times_backup() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+
+        // Create course and add HTML block.
+        $course = $generator->create_course();
+        $context = context_course::instance($course->id);
+        $page = new moodle_page();
+        $page->set_context($context);
+        $page->set_course($course);
+        $page->set_pagelayout('standard');
+        $page->set_pagetype('course-view');
+        $page->blocks->load_blocks();
+        $page->blocks->add_block_at_end_of_default_region('html');
+
+        // Update (hack in database) timemodified and timecreated to specific values for testing.
+        $blockdata = $DB->get_record('block_instances',
+                ['blockname' => 'html', 'parentcontextid' => $context->id]);
+        $originalblockid = $blockdata->id;
+        $blockdata->timecreated = 12345;
+        $blockdata->timemodified = 67890;
+        $DB->update_record('block_instances', $blockdata);
+
+        // Do backup and restore.
+        $newcourseid = $this->backup_and_restore($course);
+
+        // Confirm that values were transferred correctly into HTML block on new course.
+        $newcontext = context_course::instance($newcourseid);
+        $blockdata = $DB->get_record('block_instances',
+                ['blockname' => 'html', 'parentcontextid' => $newcontext->id]);
+        $this->assertEquals(12345, $blockdata->timecreated);
+        $this->assertEquals(67890, $blockdata->timemodified);
+
+        // Simulate what happens with an older backup that doesn't have those fields, by removing
+        // them from the backup before doing a restore.
+        $before = time();
+        $newcourseid = $this->backup_and_restore($course, 0, function($backupid) use($originalblockid) {
+            global $CFG;
+            $path = $CFG->dataroot . '/temp/backup/' . $backupid . '/course/blocks/html_' .
+                    $originalblockid . '/block.xml';
+            $xml = file_get_contents($path);
+            $xml = preg_replace('~<timecreated>.*?</timemodified>~s', '', $xml);
+            file_put_contents($path, $xml);
+        });
+        $after = time();
+
+        // The fields not specified should default to current time.
+        $newcontext = context_course::instance($newcourseid);
+        $blockdata = $DB->get_record('block_instances',
+                ['blockname' => 'html', 'parentcontextid' => $newcontext->id]);
+        $this->assertTrue($before <= $blockdata->timecreated && $after >= $blockdata->timecreated);
+        $this->assertTrue($before <= $blockdata->timemodified && $after >= $blockdata->timemodified);
+    }
 }
index 7d529ad..7c73f83 100644 (file)
  * @param object $block
  */
 function xmldb_block_badges_upgrade($oldversion, $block) {
-    global $DB;
-
-    if ($oldversion < 2014062600) {
-        // Add this block the default blocks on /my.
-        $blockname = 'badges';
-
-        // Do not try to add the block if we cannot find the default my_pages entry.
-        // Private => 1 refers to MY_PAGE_PRIVATE.
-        if ($systempage = $DB->get_record('my_pages', array('userid' => null, 'private' => 1))) {
-            $page = new moodle_page();
-            $page->set_context(context_system::instance());
-
-            // Check to see if this block is already on the default /my page.
-            $criteria = array(
-                'blockname' => $blockname,
-                'parentcontextid' => $page->context->id,
-                'pagetypepattern' => 'my-index',
-                'subpagepattern' => $systempage->id,
-            );
-
-            if (!$DB->record_exists('block_instances', $criteria)) {
-                // Add the block to the default /my.
-                $page->blocks->add_region(BLOCK_POS_RIGHT);
-                $page->blocks->add_block($blockname, BLOCK_POS_RIGHT, 0, false, 'my-index', $systempage->id);
-            }
-        }
-
-        upgrade_block_savepoint(true, 2014062600, $blockname);
-    }
-
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
+    global $CFG;
 
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
index 09f73a6..1d287b5 100644 (file)
  * @param object $block
  */
 function xmldb_block_calendar_month_upgrade($oldversion, $block) {
-    global $DB;
-
-    if ($oldversion < 2014062600) {
-        // Add this block the default blocks on /my.
-        $blockname = 'calendar_month';
-
-        // Do not try to add the block if we cannot find the default my_pages entry.
-        // Private => 1 refers to MY_PAGE_PRIVATE.
-        if ($systempage = $DB->get_record('my_pages', array('userid' => null, 'private' => 1))) {
-            $page = new moodle_page();
-            $page->set_context(context_system::instance());
-
-            // Check to see if this block is already on the default /my page.
-            $criteria = array(
-                'blockname' => $blockname,
-                'parentcontextid' => $page->context->id,
-                'pagetypepattern' => 'my-index',
-                'subpagepattern' => $systempage->id,
-            );
-
-            if (!$DB->record_exists('block_instances', $criteria)) {
-                // Add the block to the default /my.
-                $page->blocks->add_region(BLOCK_POS_RIGHT);
-                $page->blocks->add_block($blockname, BLOCK_POS_RIGHT, 0, false, 'my-index', $systempage->id);
-            }
-        }
-
-        upgrade_block_savepoint(true, 2014062600, $blockname);
-    }
-
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
+    global $CFG;
 
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
index d867d08..df77631 100644 (file)
  * @param object $block
  */
 function xmldb_block_calendar_upcoming_upgrade($oldversion, $block) {
-    global $DB;
-
-    if ($oldversion < 2014062600) {
-        // Add this block the default blocks on /my.
-        $blockname = 'calendar_upcoming';
-
-        // Do not try to add the block if we cannot find the default my_pages entry.
-        // Private => 1 refers to MY_PAGE_PRIVATE.
-        if ($systempage = $DB->get_record('my_pages', array('userid' => null, 'private' => 1))) {
-            $page = new moodle_page();
-            $page->set_context(context_system::instance());
-
-            // Check to see if this block is already on the default /my page.
-            $criteria = array(
-                'blockname' => $blockname,
-                'parentcontextid' => $page->context->id,
-                'pagetypepattern' => 'my-index',
-                'subpagepattern' => $systempage->id,
-            );
-
-            if (!$DB->record_exists('block_instances', $criteria)) {
-                // Add the block to the default /my.
-                $page->blocks->add_region(BLOCK_POS_RIGHT);
-                $page->blocks->add_block($blockname, BLOCK_POS_RIGHT, 0, false, 'my-index', $systempage->id);
-            }
-        }
-
-        upgrade_block_savepoint(true, 2014062600, $blockname);
-    }
-
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
+    global $CFG;
 
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
index 587695b..26f7495 100644 (file)
@@ -66,7 +66,7 @@ class behat_block_comments extends behat_base {
             $this->find_link(get_string('savecomment'))->click();
             // Delay after clicking so that additional comments will have unique time stamps.
             // We delay 1 second which is all we need.
-            $this->getSession()->wait(1000, false);
+            $this->getSession()->wait(1000);
 
         } else {
 
@@ -104,7 +104,7 @@ class behat_block_comments extends behat_base {
         );
 
         // Wait for the animation to finish, in theory is just 1 sec, adding 4 just in case.
-        $this->getSession()->wait(4 * 1000, false);
+        $this->getSession()->wait(4 * 1000);
     }
 
 }
index 12ae568..efb36bd 100644 (file)
@@ -46,15 +46,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_community_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
 
index a04db38..e345754 100644 (file)
@@ -48,15 +48,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_completionstatus_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
 
index 3d972ce..bf3ccdd 100644 (file)
@@ -48,15 +48,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_course_summary_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
 
diff --git a/blocks/html/classes/search/content.php b/blocks/html/classes/search/content.php
new file mode 100644 (file)
index 0000000..32b20b9
--- /dev/null
@@ -0,0 +1,91 @@
+<?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/>.
+
+/**
+ * Search area for block_html blocks
+ *
+ * @package block_html
+ * @copyright 2017 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_html\search;
+
+use core_search\moodle_recordset;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Search area for block_html blocks
+ *
+ * @package block_html
+ * @copyright 2017 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class content extends \core_search\base_block {
+
+    public function get_document($record, $options = array()) {
+        // Create empty document.
+        $doc = \core_search\document_factory::instance($record->id,
+                $this->componentname, $this->areaname);
+
+        // Get stdclass object with data from DB.
+        $data = unserialize(base64_decode($record->configdata));
+
+        // Get content.
+        $content = content_to_text($data->text, $data->format);
+        $doc->set('content', $content);
+
+        if (isset($data->title)) {
+            // If there is a title, use it as title.
+            $doc->set('title', content_to_text($data->title, false));
+        } else {
+            // If there is no title, use the content text again.
+            $doc->set('title', shorten_text($content));
+        }
+
+        // Set standard fields.
+        $doc->set('contextid', $record->contextid);
+        $doc->set('type', \core_search\manager::TYPE_TEXT);
+        $doc->set('courseid', $record->courseid);
+        $doc->set('modified', $record->timemodified);
+        $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID);
+
+        // Mark document new if appropriate.
+        if (isset($options['lastindexedtime']) &&
+                ($options['lastindexedtime'] < $record->timecreated)) {
+            // If the document was created after the last index time, it must be new.
+            $doc->set_is_new(true);
+        }
+
+        return $doc;
+    }
+
+    public function uses_file_indexing() {
+        return true;
+    }
+
+    public function attach_files($document) {
+        $fs = get_file_storage();
+
+        $context = \context::instance_by_id($document->get('contextid'));
+
+        $files = $fs->get_area_files($context->id, 'block_html', 'content');
+        foreach ($files as $file) {
+            $document->add_stored_file($file);
+        }
+    }
+}
index 2899efe..36ee50b 100644 (file)
@@ -33,15 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_html_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
 
index bf6df6f..7eb2ad4 100644 (file)
@@ -33,3 +33,4 @@ $string['html:myaddinstance'] = 'Add a new HTML block to Dashboard';
 $string['leaveblanktohide'] = 'leave blank to hide the title';
 $string['newhtmlblock'] = '(new HTML block)';
 $string['pluginname'] = 'HTML';
+$string['search:content'] = 'HTML block content';
index 531a32a..a86b827 100644 (file)
@@ -104,7 +104,8 @@ function block_html_global_db_replace($search, $replace) {
         $config = unserialize(base64_decode($instance->configdata));
         if (isset($config->text) and is_string($config->text)) {
             $config->text = str_replace($search, $replace, $config->text);
-            $DB->set_field('block_instances', 'configdata', base64_encode(serialize($config)), array('id' => $instance->id));
+            $DB->update_record('block_instances', ['id' => $instance->id,
+                    'configdata' => base64_encode(serialize($config)), 'timemodified' => time()]);
         }
     }
     $instances->close();
diff --git a/blocks/html/tests/search_content_test.php b/blocks/html/tests/search_content_test.php
new file mode 100644 (file)
index 0000000..5afb8cd
--- /dev/null
@@ -0,0 +1,191 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit test for search indexing.
+ *
+ * @package block_html
+ * @copyright 2017 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_html;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Unit test for search indexing.
+ *
+ * @package block_html
+ * @copyright 2017 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class search_content_testcase extends \advanced_testcase {
+
+    /**
+     * Creates an HTML block on a course.
+     *
+     * @param \stdClass $course Course object
+     * @return \block_html Block instance object
+     */
+    protected function create_block($course) {
+        $page = self::construct_page($course);
+        $page->blocks->add_block_at_end_of_default_region('html');
+
+        // Load the block.
+        $page = self::construct_page($course);
+        $page->blocks->load_blocks();
+        $blocks = $page->blocks->get_blocks_for_region($page->blocks->get_default_region());
+        $block = end($blocks);
+        return $block;
+    }
+
+    /**
+     * Constructs a page object for the test course.
+     *
+     * @param \stdClass $course Moodle course object
+     * @return \moodle_page Page object representing course view
+     */
+    protected static function construct_page($course) {
+        $context = \context_course::instance($course->id);
+        $page = new \moodle_page();
+        $page->set_context($context);
+        $page->set_course($course);
+        $page->set_pagelayout('standard');
+        $page->set_pagetype('course-view');
+        $page->blocks->load_blocks();
+        return $page;
+    }
+
+    /**
+     * Tests all functionality in the search area.
+     */
+    public function test_search_area() {
+        global $CFG, $USER, $DB;
+        require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Create course and add HTML block.
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $before = time();
+        $block = $this->create_block($course);
+
+        // Change block settings to add some text and a file.
+        $itemid = file_get_unused_draft_itemid();
+        $fs = get_file_storage();
+        $usercontext = \context_user::instance($USER->id);
+        $fs->create_file_from_string(['component' => 'user', 'filearea' => 'draft',
+                'contextid' => $usercontext->id, 'itemid' => $itemid, 'filepath' => '/',
+                'filename' => 'file.txt'], 'File content');
+        $data = (object)['title' => 'Block title', 'text' => ['text' => 'Block text',
+                'itemid' => $itemid, 'format' => FORMAT_HTML]];
+        $block->instance_config_save($data);
+        $after = time();
+
+        // Set up fake search engine so we can create documents.
+        \testable_core_search::instance();
+
+        // Do indexing query.
+        $area = new \block_html\search\content();
+        $this->assertEquals('html', $area->get_block_name());
+        $rs = $area->get_recordset_by_timestamp();
+        $count = 0;
+        foreach ($rs as $record) {
+            $count++;
+
+            $this->assertEquals($course->id, $record->courseid);
+
+            // Check context is correct.
+            $blockcontext = \context::instance_by_id($record->contextid);
+            $this->assertInstanceOf('\context_block', $blockcontext);
+            $coursecontext = $blockcontext->get_parent_context();
+            $this->assertEquals($course->id, $coursecontext->instanceid);
+
+            // Check created and modified times are correct.
+            $this->assertTrue($record->timecreated >= $before && $record->timecreated <= $after);
+            $this->assertTrue($record->timemodified >= $before && $record->timemodified <= $after);
+
+            // Get config data.
+            $data = unserialize(base64_decode($record->configdata));
+            $this->assertEquals('Block title', $data->title);
+            $this->assertEquals('Block text', $data->text);
+            $this->assertEquals(FORMAT_HTML, $data->format);
+
+            // Check the get_document function 'new' flag.
+            $doc = $area->get_document($record, ['lastindexedtime' => 1]);
+            $this->assertTrue($doc->get_is_new());
+            $doc = $area->get_document($record, ['lastindexedtime' => time() + 1]);
+            $this->assertFalse($doc->get_is_new());
+
+            // Check the attach_files function results in correct list of associated files.
+            $this->assertCount(0, $doc->get_files());
+            $area->attach_files($doc);
+            $files = $doc->get_files();
+            $this->assertCount(2, $files);
+            foreach ($files as $file) {
+                if ($file->is_directory()) {
+                    continue;
+                }
+                $this->assertEquals('file.txt', $file->get_filename());
+                $this->assertEquals('File content', $file->get_content());
+            }
+
+            // Check the document fields are all as expected.
+            $this->assertEquals('Block title', $doc->get('title'));
+            $this->assertEquals('Block text', $doc->get('content'));
+            $this->assertEquals($blockcontext->id, $doc->get('contextid'));
+            $this->assertEquals(\core_search\manager::TYPE_TEXT, $doc->get('type'));
+            $this->assertEquals($course->id, $doc->get('courseid'));
+            $this->assertEquals($record->timemodified, $doc->get('modified'));
+            $this->assertEquals(\core_search\manager::NO_OWNER_ID, $doc->get('owneruserid'));
+
+            // Also check getting the doc url and context url.
+            $url = new \moodle_url('/course/view.php', ['id' => $course->id], 'inst' . $record->id);
+            $this->assertTrue($url->compare($area->get_doc_url($doc)));
+            $this->assertTrue($url->compare($area->get_context_url($doc)));
+        }
+        $rs->close();
+
+        // Should only be one HTML block systemwide.
+        $this->assertEquals(1, $count);
+
+        // If we run the query starting from 1 second after now, there should be no results.
+        $rs = $area->get_recordset_by_timestamp($after + 1);
+        $count = 0;
+        foreach ($rs as $record) {
+            $count++;
+        }
+        $rs->close();
+        $this->assertEquals(0, $count);
+
+        // Create another block, but this time leave it empty (no data set). Hack the time though.
+        $block = $this->create_block($course);
+        $DB->set_field('block_instances', 'timemodified',
+                $after + 10, ['id' => $block->instance->id]);
+        $rs = $area->get_recordset_by_timestamp($after + 10);
+        $count = 0;
+        foreach ($rs as $record) {
+            // Because there is no configdata we don't index it.
+            $count++;
+        }
+        $rs->close();
+        $this->assertEquals(0, $count);
+    }
+}
+
index 57ae2b3..a81f7ac 100644 (file)
@@ -474,8 +474,8 @@ class block_base {
      */
     function instance_config_save($data, $nolongerused = false) {
         global $DB;
-        $DB->set_field('block_instances', 'configdata', base64_encode(serialize($data)),
-                array('id' => $this->instance->id));
+        $DB->update_record('block_instances', ['id' => $this->instance->id,
+                'configdata' => base64_encode(serialize($data)), 'timemodified' => time()]);
     }
 
     /**
index 538c829..1f9ff6d 100644 (file)
@@ -55,15 +55,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_navigation_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
 
index 68dfa30..20f93f7 100644 (file)
  * @param object $block
  */
 function xmldb_block_quiz_results_upgrade($oldversion, $block) {
-    global $DB, $CFG;
-
-    if ($oldversion < 2015022200) {
-        // Only migrate if the block_activity_results is installed.
-        if (is_dir($CFG->dirroot . '/blocks/activity_results')) {
-
-            // Migrate all instances of block_quiz_results to block_activity_results.
-            $records = $DB->get_records('block_instances', array('blockname' => 'quiz_results'));
-            foreach ($records as $record) {
-                $configdata = '';
-
-                // The block was configured.
-                if (!empty($record->configdata)) {
-
-                    $config = unserialize(base64_decode($record->configdata));
-                    $config->activityparent = 'quiz';
-                    $config->activityparentid = isset($config->quizid) ? $config->quizid : 0;
-                    $config->gradeformat = isset($config->gradeformat) ? $config->gradeformat : 1;
-
-                    // Set the decimal valuue as appropriate.
-                    if ($config->gradeformat == 1) {
-                        // This block is using percentages, do not display any decimal places.
-                        $config->decimalpoints = 0;
-                    } else {
-                        // Get the decimal value from the corresponding quiz.
-                        $config->decimalpoints = $DB->get_field('quiz', 'decimalpoints', array('id' => $config->activityparentid));
-                    }
-
-                    // Get the grade_items record to set the activitygradeitemid.
-                    $info = $DB->get_record('grade_items',
-                            array('iteminstance' => $config->activityparentid, 'itemmodule' => $config->activityparent));
-                    $config->activitygradeitemid = 0;
-                    if ($info) {
-                        $config->activitygradeitemid = $info->id;
-                    }
-
-                    unset($config->quizid);
-                    $configdata = base64_encode(serialize($config));
-                }
-
-                // Save the new configuration and update the record.
-                $record->configdata = $configdata;
-                $record->blockname = 'activity_results';
-                $DB->update_record('block_instances', $record);
-            }
-
-            // Disable the Quiz_results block.
-            if ($block = $DB->get_record("block", array("name" => "quiz_results"))) {
-                $DB->set_field("block", "visible", "0", array("id" => $block->id));
-            }
-
-        }
-        upgrade_block_savepoint(true, 2015022200, 'quiz_results');
-    }
-
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
+    global $CFG;
 
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
@@ -117,4 +55,4 @@ function xmldb_block_quiz_results_upgrade($oldversion, $block) {
     // Put any upgrade step following this.
 
     return true;
-}
\ No newline at end of file
+}
index 186c752..dfc2c41 100644 (file)
@@ -47,15 +47,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_recent_activity_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
 
index 78e0c0a..f0b8caf 100644 (file)
@@ -31,27 +31,7 @@ defined('MOODLE_INTERNAL') || die();
  * @return boolean
  */
 function xmldb_block_rss_client_upgrade($oldversion) {
-    global $DB;
-    $dbman = $DB->get_manager();
-
-    if ($oldversion < 2015071700) {
-        // Support for skipping RSS feeds for a while when they fail.
-        $table = new xmldb_table('block_rss_client');
-        // How many seconds we are currently ignoring this RSS feed for (due to an error).
-        $field = new xmldb_field('skiptime', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'url');
-        if (!$dbman->field_exists($table, $field)) {
-            $dbman->add_field($table, $field);
-        }
-        // When to next update this RSS feed.
-        $field = new xmldb_field('skipuntil', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'skiptime');
-        if (!$dbman->field_exists($table, $field)) {
-            $dbman->add_field($table, $field);
-        }
-        upgrade_block_savepoint(true, 2015071700, 'rss_client');
-    }
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
+    global $CFG;
 
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
index 397768a..6eca3b6 100644 (file)
@@ -49,15 +49,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_section_links_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
 
index cc7971d..0d50fed 100644 (file)
@@ -48,15 +48,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_selfcompletion_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
 
index b48a648..0bb3bfb 100644 (file)
@@ -55,15 +55,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_settings_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
 
index 0fa83c7..6b8fc56 100644 (file)
@@ -1,6 +1,15 @@
 This files describes API changes in /blocks/* - activity modules,
 information provided here is intended especially for developers.
 
+=== 3.4 ===
+
+* The block_instances table now contains fields timecreated and timemodified. If third-party code
+  creates or updates these rows (without using the standard API), it should be modified to set
+  these fields as appropriate.
+* Blocks can now be included in Moodle global search, with some limitations (at present, the search
+  works only for blocks located directly on course pages or site home page). See the HTML block for
+  an example.
+
 === 3.3 ===
 
 * block_manager::get_required_by_theme_block_types() is no longer static.
diff --git a/calendar/amd/build/calendar.min.js b/calendar/amd/build/calendar.min.js
new file mode 100644 (file)
index 0000000..67a1cb2
Binary files /dev/null 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
new file mode 100644 (file)
index 0000000..5496507
Binary files /dev/null and b/calendar/amd/build/calendar_events.min.js differ
diff --git a/calendar/amd/build/calendar_repository.min.js b/calendar/amd/build/calendar_repository.min.js
new file mode 100644 (file)
index 0000000..bfb5d0d
Binary files /dev/null and b/calendar/amd/build/calendar_repository.min.js differ
diff --git a/calendar/amd/build/summary_modal.min.js b/calendar/amd/build/summary_modal.min.js
new file mode 100644 (file)
index 0000000..3c917ad
Binary files /dev/null and b/calendar/amd/build/summary_modal.min.js differ
diff --git a/calendar/amd/src/calendar.js b/calendar/amd/src/calendar.js
new file mode 100644 (file)
index 0000000..59dc033
--- /dev/null
@@ -0,0 +1,116 @@
+// 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 calendar events.
+ *
+ * @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_calendar/calendar_repository'],
+    function($, Ajax, Str, Templates, Notification, CustomEvents, ModalFactory, SummaryModal, CalendarRepository) {
+
+        var SELECTORS = {
+            ROOT: "[data-region='calendar']",
+            EVENT_LINK: "[data-action='view-event']",
+        };
+
+        var modalPromise = null;
+
+        /**
+         * Get the event type lang string.
+         *
+         * @param {String} eventType The event type.
+         * @return {String} The lang string of the event type.
+         */
+        var getEventType = function(eventType) {
+            var lang = 'type' + eventType;
+            return Str.get_string(lang, 'core_calendar').then(function(langStr) {
+                return langStr;
+            }).fail(Notification.exception);
+        };
+
+        /**
+         * Render the event summary modal.
+         *
+         * @param {Number} eventId The calendar event id.
+         * @return {promise} The summary modal promise.
+         */
+        var renderEventSummaryModal = function(eventId) {
+
+            var promise = CalendarRepository.getEventById(eventId);
+
+            return promise.then(function(result) {
+                if (!result.event) {
+                    promise.fail(Notification.exception);
+                } else {
+                    return result.event;
+                }
+            }).then(function(eventdata) {
+                return getEventType(eventdata.eventtype).then(function(langStr) {
+                    eventdata.eventtype = langStr;
+                    return eventdata;
+                });
+            }).then(function(eventdata) {
+                return modalPromise.done(function(modal) {
+                    modal.setTitle(eventdata.name);
+                    modal.setBody(Templates.render('core_calendar/event_summary_body', eventdata));
+                    // Hide edit and delete buttons if I don't have permission.
+                    if (eventdata.caneditevent == false) {
+                        modal.setFooter('');
+                    }
+
+                    modal.show();
+                });
+            });
+        };
+
+        /**
+         * Register event listeners for the module.
+         *
+         * @param {object} root The root element.
+         */
+        var registerEventListeners = function(root) {
+            root = $(root);
+
+            var loading = false;
+            root.on('click', SELECTORS.EVENT_LINK, function(e) {
+                if (!loading) {
+                    loading = true;
+                    e.preventDefault();
+
+                    var eventElement = $(e.target).closest(SELECTORS.EVENT_LINK);
+                    var eventId = eventElement.attr('data-event-id');
+
+                    renderEventSummaryModal(eventId).done(function() {
+                        loading = false;
+                    });
+                }
+            });
+        };
+
+        return {
+            init: function() {
+                modalPromise = ModalFactory.create({
+                    type: SummaryModal.TYPE
+                });
+
+                registerEventListeners(SELECTORS.ROOT);
+            }
+        };
+    });
diff --git a/calendar/amd/src/calendar_events.js b/calendar/amd/src/calendar_events.js
new file mode 100644 (file)
index 0000000..bb88d81
--- /dev/null
@@ -0,0 +1,29 @@
+// 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 events a modal can fire.
+ *
+ * @module     core_calendar/calendar_events
+ * @class      calendar_events
+ * @package    core_calendar
+ * @copyright  2017 Simey Lameze <simey@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([], function() {
+    return {
+        deleted: 'calendar-events:deleted'
+    };
+});
diff --git a/calendar/amd/src/calendar_repository.js b/calendar/amd/src/calendar_repository.js
new file mode 100644 (file)
index 0000000..2de4c41
--- /dev/null
@@ -0,0 +1,72 @@
+// 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 handle calendar ajax actions.
+ *
+ * @module     core_calendar/calendar_repository
+ * @class      repository
+ * @package    core_calendar
+ * @copyright  2017 Simey Lameze <lameze@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/ajax'], function($, Ajax) {
+
+    /**
+     * Delete a calendar event.
+     *
+     * @method deleteEvent
+     * @param {int} eventId The event id.
+     * @return {promise} Resolved with requested calendar event
+     */
+    var deleteEvent = function(eventId) {
+
+        var request = {
+            methodname: 'core_calendar_delete_calendar_events',
+            args: {
+                events: [{
+                    eventid: eventId,
+                    repeat: 1
+                }]
+            }
+        };
+
+        return Ajax.call([request])[0];
+    };
+
+    /**
+     * Get a calendar event by id.
+     *
+     * @method getEventById
+     * @param {int} eventId The event id.
+     * @return {promise} Resolved with requested calendar event
+     */
+    var getEventById = function(eventId) {
+
+        var request = {
+            methodname: 'core_calendar_get_calendar_event_by_id',
+            args: {
+                eventid: eventId
+            }
+        };
+
+        return Ajax.call([request])[0];
+    };
+
+    return {
+        getEventById: getEventById,
+        deleteEvent: deleteEvent
+    };
+});
diff --git a/calendar/amd/src/summary_modal.js b/calendar/amd/src/summary_modal.js
new file mode 100644 (file)
index 0000000..e344d62
--- /dev/null
@@ -0,0 +1,99 @@
+// 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 handle summary modal.
+ *
+ * @module     core_calendar/summary_modal
+ * @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/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'],
+    function($, Str, Notification, CustomEvents, Modal, ModalRegistry, ModalFactory, ModalEvents, CalendarRepository,
+             CalendarEvents) {
+
+    var registered = false;
+    var SELECTORS = {
+        ROOT: "[data-region='summary-modal-container']",
+        EDIT_BUTTON: '[data-action="edit"]',
+        DELETE_BUTTON: '[data-action="delete"]',
+        EVENT_LINK: '[data-action="event-link"]'
+    };
+
+    /**
+     * Constructor for the Modal.
+     *
+     * @param {object} root The root jQuery element for the modal
+     */
+    var ModalEventSummary = function(root) {
+        Modal.call(this, root);
+
+        if (!this.getFooter().find(SELECTORS.EDIT_BUTTON).length) {
+            Notification.exception({message: 'No edit button found'});
+        }
+
+        if (!this.getFooter().find(SELECTORS.DELETE_BUTTON).length) {
+            Notification.exception({message: 'No delete button found'});
+        }
+    };
+
+    ModalEventSummary.TYPE = 'core_calendar-event_summary';
+    ModalEventSummary.prototype = Object.create(Modal.prototype);
+    ModalEventSummary.prototype.constructor = ModalEventSummary;
+
+    /**
+     * Set up all of the event handling for the modal.
+     *
+     * @method registerEventListeners
+     */
+    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);
+            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);
+            }.bind(this));
+            return modal;
+        }.bind(this));
+
+        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));
+    };
+
+    // 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(ModalEventSummary.TYPE, ModalEventSummary, 'core_calendar/event_summary_modal');
+        registered = true;
+    }
+
+    return ModalEventSummary;
+});
index 9952dee..ca162a2 100644 (file)
@@ -216,10 +216,14 @@ foreach($events as $event) {
         //dtend is better than duration, because it works in Microsoft Outlook and works better in Korganizer
         $ev->add_property('dtstart', Bennu::timestamp_to_datetime($event->timestart)); // when event starts.
         $ev->add_property('dtend', Bennu::timestamp_to_datetime($event->timestart + $event->timeduration));
+    } else if ($event->timeduration == 0) {
+        // When no duration is present, the event is instantaneous event, ex - Due date of a module.
+        // Moodle doesn't support all day events yet. See MDL-56227.
+        $ev->add_property('dtstart', Bennu::timestamp_to_datetime($event->timestart));
+        $ev->add_property('dtend', Bennu::timestamp_to_datetime($event->timestart));
     } else {
-        // When no duration is present, ie an all day event, VALUE should be date instead of time and dtend = dtstart + 1 day.
-        $ev->add_property('dtstart', Bennu::timestamp_to_date($event->timestart), array('value' => 'DATE')); // All day event.
-        $ev->add_property('dtend', Bennu::timestamp_to_date($event->timestart + DAYSECS), array('value' => 'DATE')); // All day event.
+        // This can be used to represent all day events in future.
+        throw new coding_exception("Negative duration is not supported yet.");
     }
     if ($event->courseid != 0) {
         $coursecontext = context_course::instance($event->courseid);
index bf0855b..f684ae0 100644 (file)
@@ -687,4 +687,78 @@ class core_calendar_external extends external_api {
                     )
             );
     }
+
+    /**
+     * Returns description of method parameters.
+     *
+     * @return external_function_parameters
+     */
+    public static function get_calendar_event_by_id_parameters() {
+        return new external_function_parameters(
+            array(
+                'eventid' => new external_value(PARAM_INT, 'The event id to be retrieved'),
+            )
+        );
+    }
+    /**
+     * Get calendar event by id.
+     *
+     * @param int $eventid The calendar event id to be retrieved.
+     * @return array Array of event details
+     */
+    public static function get_calendar_event_by_id($eventid) {
+        global $CFG;
+        require_once($CFG->dirroot."/calendar/lib.php");
+
+        // Parameter validation.
+        $params = ['eventid' => $eventid];
+        $params = self::validate_parameters(self::get_calendar_event_by_id_parameters(), $params);
+
+        $warnings = array();
+
+        // We need to get events asked for eventids.
+        $event = calendar_get_events_by_id([$eventid]);
+        $eventobj = calendar_event::load($eventid);
+        list($event[$eventid]->description, $event[$eventid]->format) = $eventobj->format_external_text();
+        $event[$eventid]->caneditevent = calendar_edit_event_allowed($eventobj);
+
+        return array('event' => $event[$eventid], 'warnings' => $warnings);
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     */
+    public static function  get_calendar_event_by_id_returns() {
+
+        return new external_single_structure(array(
+            'event' => new external_single_structure(
+                array(
+                    'id' => new external_value(PARAM_INT, 'event id'),
+                    'name' => new external_value(PARAM_TEXT, 'event name'),
+                    'description' => new external_value(PARAM_RAW, 'Description', VALUE_OPTIONAL, null, NULL_ALLOWED),
+                    'format' => new external_format_value('description'),
+                    'courseid' => new external_value(PARAM_INT, 'course id'),
+                    'groupid' => new external_value(PARAM_INT, 'group id'),
+                    'userid' => new external_value(PARAM_INT, 'user id'),
+                    'repeatid' => new external_value(PARAM_INT, 'repeat id'),
+                    'modulename' => new external_value(PARAM_TEXT, 'module name', VALUE_OPTIONAL, null, NULL_ALLOWED),
+                    'instance' => new external_value(PARAM_INT, 'instance id'),
+                    'eventtype' => new external_value(PARAM_TEXT, 'Event type'),
+                    'timestart' => new external_value(PARAM_INT, 'timestart'),
+                    'timeduration' => new external_value(PARAM_INT, 'time duration'),
+                    'visible' => new external_value(PARAM_INT, 'visible'),
+                    'uuid' => new external_value(PARAM_TEXT, 'unique id of ical events', VALUE_OPTIONAL, null, NULL_NOT_ALLOWED),
+                    'sequence' => new external_value(PARAM_INT, 'sequence'),
+                    'timemodified' => new external_value(PARAM_INT, 'time modified'),
+                    'subscriptionid' => new external_value(PARAM_INT, 'Subscription id', VALUE_OPTIONAL, null, NULL_ALLOWED),
+                    'caneditevent' => new external_value(PARAM_BOOL, 'Whether the user can edit the event'),
+                ),
+                'event'
+            ),
+            'warnings' => new external_warnings()
+            )
+        );
+    }
 }
index f6f1e4a..94e617b 100644 (file)
@@ -2944,7 +2944,8 @@ function calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timez
         // Check to see if the event started at Midnight on the imported calendar.
         date_default_timezone_set($timezone);
         if (date('H:i:s', $eventrecord->timestart) === "00:00:00") {
-            // This event should be an all day event.
+            // This event should be an all day event. This is not correct, we don't do anything differently for all day events.
+            // See MDL-56227.
             $eventrecord->timeduration = 0;
         }
         \core_date::set_default_server_timezone();
index 6711693..ee450e1 100644 (file)
@@ -38,7 +38,7 @@ class core_calendar_renderer extends plugin_renderer_base {
      * @return string
      */
     public function start_layout() {
-        return html_writer::start_tag('div', array('class'=>'maincalendar'));
+        return html_writer::start_tag('div', ['data-region' => 'calendar', 'class' => 'maincalendar']);
     }
 
     /**
@@ -517,7 +517,8 @@ class core_calendar_renderer extends plugin_renderer_base {
                         ];
                         $eventname = get_string('eventnameandcourse', 'calendar', $eventnameparams);
                     }
-                    $link = html_writer::link($dayhref, $eventname);
+                    $link = html_writer::link($dayhref, $eventname, ['data-action' => 'view-event',
+                            'data-event-id' => $events[$eventindex]->id]);
                     $cell->text .= html_writer::tag('li', $link, $attributes);
                 }
                 $cell->text .= html_writer::end_tag('ul');
diff --git a/calendar/templates/event_summary_body.mustache b/calendar/templates/event_summary_body.mustache
new file mode 100644 (file)
index 0000000..e4a155f
--- /dev/null
@@ -0,0 +1,37 @@
+{{!
+    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_calendar/event_summary_body
+
+    This template renders the body of calendar events summary modal.
+
+    Example context (json):
+    {
+        "timestart": 1490320388,
+        "description": "An random event description",
+        "eventtype": "open",
+    }
+}}
+<div data-region="summary-modal-container" data-event-id="{{id}}" data-event-title="{{name}}">
+    <h4>{{#str}} when, core_calendar {{/str}}</h4>
+    {{#userdate}} {{timestart}}, {{#str}} strftimerecentfull {{/str}} {{/userdate}}
+    <br>
+    <h4>{{#str}} description {{/str}}</h4>
+    {{{description}}}
+    <h4>{{#str}} eventtype, core_calendar {{/str}}</h4>
+    {{eventtype}}
+</div>
diff --git a/calendar/templates/event_summary_modal.mustache b/calendar/templates/event_summary_modal.mustache
new file mode 100644 (file)
index 0000000..2ae8dd2
--- /dev/null
@@ -0,0 +1,32 @@
+{{!
+    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_calendar/event_summary_modal
+
+    This template renders the calendar events summary modal.
+
+    Example context (json):
+    {
+        "title": "Assignment due 1",
+    }
+}}
+{{< core/modal }}
+{{$footer}}
+<button type="button" class="btn btn-secondary" data-action="delete">{{#str}} delete {{/str}}</button>
+<button type="button" class="btn btn-primary" data-action="edit">{{#str}} edit {{/str}}</button>
+{{/footer}}
+{{/ core/modal }}
index 43ac339..68ab388 100644 (file)
@@ -31,19 +31,3 @@ Feature: Import and edit calendar events
     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 follow "Edit event"
-    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 changes"
-    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 ba1eb6c..e21692e 100644 (file)
@@ -353,7 +353,13 @@ class core_calendar_container_testcase extends advanced_testcase {
         $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
 
         foreach (core_component::get_plugin_list('mod') as $modname => $unused) {
-            $module = $this->getDataGenerator()->create_module($modname, ['course' => $course->id]);
+            try {
+                $generator = $this->getDataGenerator()->get_plugin_generator('mod_'.$modname);
+            } catch (coding_exception $e) {
+                // Module generator is not implemented.
+                continue;
+            }
+            $module = $generator->create_instance(['course' => $course->id]);
 
             // Create bunch of events of different type (user override, group override, module event).
             $this->create_event(['userid' => $user->id, 'modulename' => $modname, 'instance' => $module->id]);
index 18245a5..f974ba5 100644 (file)
@@ -176,4 +176,5 @@ if (!empty($CFG->enablecalendarexport)) {
 echo $OUTPUT->container_end();
 echo html_writer::end_tag('div');
 echo $renderer->complete_layout();
+$PAGE->requires->js_call_amd('core_calendar/calendar', 'init');
 echo $OUTPUT->footer();
index 233be52..dcd708f 100644 (file)
@@ -274,6 +274,137 @@ class core_cohort_external extends external_api {
         );
     }
 
+    /**
+     * Returns the description of external function parameters.
+     *
+     * @return external_function_parameters
+     */
+    public static function search_cohorts_parameters() {
+        $query = new external_value(
+            PARAM_RAW,
+            'Query string'
+        );
+        $includes = new external_value(
+            PARAM_ALPHA,
+            'What other contexts to fetch the frameworks from. (all, parents, self)',
+            VALUE_DEFAULT,
+            'parents'
+        );
+        $limitfrom = new external_value(
+            PARAM_INT,
+            'limitfrom we are fetching the records from',
+            VALUE_DEFAULT,
+            0
+        );
+        $limitnum = new external_value(
+            PARAM_INT,
+            'Number of records to fetch',
+            VALUE_DEFAULT,
+            25
+        );
+        return new external_function_parameters(array(
+            'query' => $query,
+            'context' => self::get_context_parameters(),
+            'includes' => $includes,
+            'limitfrom' => $limitfrom,
+            'limitnum' => $limitnum
+        ));
+    }
+
+    /**
+     * Search cohorts.
+     *
+     * @param string $query
+     * @param array $context
+     * @param string $includes
+     * @param int $limitfrom
+     * @param int $limitnum
+     * @return array
+     */
+    public static function search_cohorts($query, $context, $includes = 'parents', $limitfrom = 0, $limitnum = 25) {
+        global $DB, $CFG, $PAGE;
+        require_once($CFG->dirroot . '/cohort/lib.php');
+
+        $params = self::validate_parameters(self::search_cohorts_parameters(), array(
+            'query' => $query,
+            'context' => $context,
+            'includes' => $includes,
+            'limitfrom' => $limitfrom,
+            'limitnum' => $limitnum,
+        ));
+        $query = $params['query'];
+        $includes = $params['includes'];
+        $context = self::get_context_from_params($params['context']);
+        $limitfrom = $params['limitfrom'];
+        $limitnum = $params['limitnum'];
+
+        self::validate_context($context);
+        $output = $PAGE->get_renderer('tool_lp');
+
+        $manager = has_capability('moodle/cohort:manage', $context);
+        if (!$manager) {
+            require_capability('moodle/cohort:view', $context);
+        }
+
+        // TODO Make this more efficient.
+        if ($includes == 'self') {
+            $results = cohort_get_cohorts($context->id, $limitfrom, $limitnum, $query);
+            $results = $results['cohorts'];
+        } else if ($includes == 'parents') {
+            $results = cohort_get_cohorts($context->id, $limitfrom, $limitnum, $query);
+            $results = $results['cohorts'];
+            if (!$context instanceof context_system) {
+                $results = array_merge($results, cohort_get_available_cohorts($context, COHORT_ALL, $limitfrom, $limitnum, $query));
+            }
+        } else if ($includes == 'all') {
+            $results = cohort_get_all_cohorts($limitfrom, $limitnum, $query);
+            $results = $results['cohorts'];
+        } else {
+            throw new coding_exception('Invalid parameter value for \'includes\'.');
+        }
+
+        $cohorts = array();
+        foreach ($results as $key => $cohort) {
+            $cohortcontext = context::instance_by_id($cohort->contextid);
+            if (!isset($cohort->description)) {
+                $cohort->description = '';
+            }
+            if (!isset($cohort->descriptionformat)) {
+                $cohort->descriptionformat = FORMAT_PLAIN;
+            }
+
+            list($cohort->description, $cohort->descriptionformat) =
+                external_format_text($cohort->description, $cohort->descriptionformat,
+                        $cohortcontext->id, 'cohort', 'description', $cohort->id);
+
+            $cohorts[$key] = $cohort;
+        }
+
+        return array('cohorts' => $cohorts);
+    }
+
+    /**
+     * Returns description of external function result value.
+     *
+     * @return external_description
+     */
+    public static function search_cohorts_returns() {
+        return new external_single_structure(array(
+            'cohorts' => new external_multiple_structure(
+                new external_single_structure(array(
+                    'id' => new external_value(PARAM_INT, 'ID of the cohort'),
+                    'name' => new external_value(PARAM_RAW, 'cohort name'),
+                    'idnumber' => new external_value(PARAM_RAW, 'cohort idnumber'),
+                    'description' => new external_value(PARAM_RAW, 'cohort description'),
+                    'descriptionformat' => new external_format_value('description'),
+                    'visible' => new external_value(PARAM_BOOL, 'cohort visible'),
+                ))
+            )
+        ));
+    }
+
+
+
     /**
      * Returns description of method parameters
      *
index 467c592..a13eb8b 100644 (file)
@@ -330,6 +330,33 @@ function cohort_can_view_cohort($cohortorid, $currentcontext) {
     return false;
 }
 
+/**
+ * Get a cohort by id. Also does a visibility check and returns false if the user cannot see this cohort.
+ *
+ * @param stdClass|int $cohortorid cohort object or id
+ * @param context $currentcontext current context (course) where visibility is checked
+ * @return stdClass|boolean
+ */
+function cohort_get_cohort($cohortorid, $currentcontext) {
+    global $DB;
+    if (is_numeric($cohortorid)) {
+        $cohort = $DB->get_record('cohort', array('id' => $cohortorid), 'id, contextid, visible');
+    } else {
+        $cohort = $cohortorid;
+    }
+
+    if ($cohort && in_array($cohort->contextid, $currentcontext->get_parent_context_ids())) {
+        if ($cohort->visible) {
+            return $cohort;
+        }
+        $cohortcontext = context::instance_by_id($cohort->contextid);
+        if (has_capability('moodle/cohort:view', $cohortcontext)) {
+            return $cohort;
+        }
+    }
+    return false;
+}
+
 /**
  * Produces a part of SQL query to filter cohorts by the search string
  *
index f55eb1f..690d31a 100644 (file)
@@ -454,4 +454,102 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
         $this->unassignUserCapability('moodle/cohort:assign', $context->id, $roleid);
         core_cohort_external::delete_cohort_members(array($cohortdel1, $cohortdel2));
     }
+
+    /**
+     * Search cohorts.
+     */
+    public function test_search_cohorts() {
+        $this->resetAfterTest(true);
+
+        $creator = $this->getDataGenerator()->create_user();
+        $user = $this->getDataGenerator()->create_user();
+        $catuser = $this->getDataGenerator()->create_user();
+        $catcreator = $this->getDataGenerator()->create_user();
+        $category = $this->getDataGenerator()->create_category();
+        $othercategory = $this->getDataGenerator()->create_category();
+        $syscontext = context_system::instance();
+        $catcontext = context_coursecat::instance($category->id);
+
+        // Fetching default authenticated user role.
+        $userroles = get_archetype_roles('user');
+        $this->assertCount(1, $userroles);
+        $authrole = array_pop($userroles);
+
+        // Reset all default authenticated users permissions.
+        unassign_capability('moodle/cohort:manage', $authrole->id);
+
+        // Creating specific roles.
+        $creatorrole = create_role('Creator role', 'creatorrole', 'creator role description');
+        $userrole = create_role('User role', 'userrole', 'user role description');
+
+        assign_capability('moodle/cohort:manage', CAP_ALLOW, $creatorrole, $syscontext->id);
+
+        // Check for parameter $includes = 'parents'.
+        role_assign($creatorrole, $creator->id, $syscontext->id);
+        role_assign($creatorrole, $catcreator->id, $catcontext->id);
+        role_assign($userrole, $user->id, $syscontext->id);
+        role_assign($userrole, $catuser->id, $catcontext->id);
+
+        $syscontext = array('contextid' => context_system::instance()->id);
+        $catcontext = array('contextid' => context_coursecat::instance($category->id)->id);
+        $othercatcontext = array('contextid' => context_coursecat::instance($othercategory->id)->id);
+
+        $cohort1 = $this->getDataGenerator()->create_cohort(array_merge($syscontext, array('name' => 'Cohortsearch 1')));
+        $cohort2 = $this->getDataGenerator()->create_cohort(array_merge($catcontext, array('name' => 'Cohortsearch 2')));
+        $cohort3 = $this->getDataGenerator()->create_cohort(array_merge($othercatcontext, array('name' => 'Cohortsearch 3')));
+
+        // A user without permission in the system.
+        $this->setUser($user);
+        try {
+            $result = core_cohort_external::search_cohorts("Cohortsearch", $syscontext, 'parents');
+            $this->fail('Invalid permissions in system');
+        } catch (required_capability_exception $e) {
+            // All good.
+        }
+
+        // A user without permission in a category.
+        $this->setUser($catuser);
+        try {
+            $result = core_cohort_external::search_cohorts("Cohortsearch", $catcontext, 'parents');
+            $this->fail('Invalid permissions in category');
+        } catch (required_capability_exception $e) {
+            // All good.
+        }
+
+        // A user with permissions in the system.
+        $this->setUser($creator);
+        $result = core_cohort_external::search_cohorts("Cohortsearch", $syscontext, 'parents');
+        $this->assertEquals(1, count($result['cohorts']));
+        $this->assertEquals('Cohortsearch 1', $result['cohorts'][$cohort1->id]->name);
+
+        // A user with permissions in the category.
+        $this->setUser($catcreator);
+        $result = core_cohort_external::search_cohorts("Cohortsearch", $catcontext, 'parents');
+        $this->assertEquals(2, count($result['cohorts']));
+        $cohorts = array();
+        foreach ($result['cohorts'] as $cohort) {
+            $cohorts[] = $cohort->name;
+        }
+        $this->assertTrue(in_array('Cohortsearch 1', $cohorts));
+
+        // Check for parameter $includes = 'self'.
+        $this->setUser($creator);
+        $result = core_cohort_external::search_cohorts("Cohortsearch", $othercatcontext, 'self');
+        $this->assertEquals(1, count($result['cohorts']));
+        $this->assertEquals('Cohortsearch 3', $result['cohorts'][$cohort3->id]->name);
+
+        // Check for parameter $includes = 'all'.
+        $this->setUser($creator);
+        $result = core_cohort_external::search_cohorts("Cohortsearch", $syscontext, 'all');
+        $this->assertEquals(3, count($result['cohorts']));
+
+        // Detect invalid parameter $includes.
+        $this->setUser($creator);
+        try {
+            $result = core_cohort_external::search_cohorts("Cohortsearch", $syscontext, 'invalid');
+            $this->fail('Invalid parameter includes');
+        } catch (coding_exception $e) {
+            // All good.
+        }
+    }
 }
index 418d849..12d6559 100644 (file)
@@ -45,18 +45,23 @@ class api {
      *
      * @param int $cmid The course module id
      * @param string $modulename The name of the module (eg. assign, quiz)
-     * @param int $instanceid The instance idLOL
+     * @param stdClass|int $instanceorid The instance object or ID.
      * @param int|null $completionexpectedtime The time completion is expected, null if not set
      * @return bool
      */
-    public static function update_completion_date_event($cmid, $modulename, $instanceid, $completionexpectedtime) {
+    public static function update_completion_date_event($cmid, $modulename, $instanceorid, $completionexpectedtime) {
         global $CFG, $DB;
 
         // Required for calendar constant CALENDAR_EVENT_TYPE_ACTION.
         require_once($CFG->dirroot . '/calendar/lib.php');
 
-        $instance = $DB->get_record($modulename, array('id' => $instanceid), '*', MUST_EXIST);
-        $course = $DB->get_record('course', array('id' => $instance->course), '*', MUST_EXIST);
+        $instance = null;
+        if (is_object($instanceorid)) {
+            $instance = $instanceorid;
+        } else {
+            $instance = $DB->get_record($modulename, array('id' => $instanceorid), '*', MUST_EXIST);
+        }
+        $course = get_course($instance->course);
 
         $completion = new \completion_info($course);
 
index 17f7696..2f66775 100644 (file)
@@ -320,6 +320,9 @@ class manager {
                     $needreset[] = $cm->id;
                 }
             }
+            // Update completion calendar events.
+            $completionexpected = ($data['completionexpected']) ? $data['completionexpected'] : null;
+            \core_completion\api::update_completion_date_event($cm->id, $cm->modname, $cm->instance, $completionexpected);
         }
         if ($updated) {
             // Now that modules are fully updated, also update completion data if required.
index 6762484..489d0d0 100644 (file)
@@ -56,7 +56,7 @@ class core_completion_api_testcase extends advanced_testcase {
 
         // Create the completion event.
         $CFG->enablecompletion = true;
-        \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign->id, $time);
+        \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign, $time);
 
         // Check that there is now an event in the database.
         $events = $DB->get_records('event');
@@ -88,10 +88,10 @@ class core_completion_api_testcase extends advanced_testcase {
 
         // Create the event.
         $CFG->enablecompletion = true;
-        \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign->id, $time);
+        \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign, $time);
 
         // Call it again, but this time with a different time.
-        \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign->id, $time + DAYSECS);
+        \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign, $time + DAYSECS);
 
         // Check that there is still only one event in the database.
         $events = $DB->get_records('event');
@@ -123,10 +123,10 @@ class core_completion_api_testcase extends advanced_testcase {
 
         // Create the event.
         $CFG->enablecompletion = true;
-        \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign->id, $time);
+        \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign, $time);
 
         // Call it again, but the time specified as null.
-        \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign->id, null);
+        \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign, null);
 
         // Check that there is no event in the database.
         $this->assertEquals(0, $DB->count_records('event'));
@@ -146,7 +146,7 @@ class core_completion_api_testcase extends advanced_testcase {
 
         // Try and create the completion event with completion disabled.
         $CFG->enablecompletion = false;
-        \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign->id, $time);
+        \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign, $time);
 
         // Check that there is no event in the database.
         $this->assertEquals(0, $DB->count_records('event'));
@@ -166,13 +166,13 @@ class core_completion_api_testcase extends advanced_testcase {
 
         // Create the completion event.
         $CFG->enablecompletion = true;
-        \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign->id, $time);
+        \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign, $time);
 
         // Disable completion.
         $CFG->enablecompletion = false;
 
         // Try and update the completion date.
-        \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign->id, $time + DAYSECS);
+        \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign, $time + DAYSECS);
 
         // Check that there is an event in the database.
         $events = $DB->get_records('event');
@@ -204,13 +204,13 @@ class core_completion_api_testcase extends advanced_testcase {
 
         // Create the completion event.
         $CFG->enablecompletion = true;
-        \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign->id, $time);
+        \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign, $time);
 
         // Disable completion.
         $CFG->enablecompletion = false;
 
         // Should still be able to delete completion events even when completion is disabled.
-        \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign->id, null);
+        \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign, null);
 
         // Check that there is now no event in the database.
         $this->assertEquals(0, $DB->count_records('event'));
index 70349fd..3919365 100644 (file)
@@ -2606,7 +2606,7 @@ class core_course_external extends external_api {
                         'completionexpected' => new external_value(PARAM_INT, 'Completion time expected', VALUE_OPTIONAL),
                         'showdescription' => new external_value(PARAM_INT, 'If the description is showed', VALUE_OPTIONAL),
                         'availability' => new external_value(PARAM_RAW, 'Availability settings', VALUE_OPTIONAL),
-                        'grade' => new external_value(PARAM_INT, 'Grade (max value or scale id)', VALUE_OPTIONAL),
+                        'grade' => new external_value(PARAM_FLOAT, 'Grade (max value or scale id)', VALUE_OPTIONAL),
                         'scale' => new external_value(PARAM_TEXT, 'Scale items (if used)', VALUE_OPTIONAL),
                         'gradepass' => new external_value(PARAM_RAW, 'Grade to pass (float)', VALUE_OPTIONAL),
                         'gradecat' => new external_value(PARAM_INT, 'Grade category', VALUE_OPTIONAL),
index bc444f8..9a68ee3 100644 (file)
@@ -1110,10 +1110,7 @@ function set_coursemodule_name($id, $name) {
     grade_update_mod_grades($grademodule);
 
     // Update calendar events with the new name.
-    $refresheventsfunction = $cm->modname . '_refresh_events';
-    if (function_exists($refresheventsfunction)) {
-        call_user_func($refresheventsfunction, $cm->course);
-    }
+    course_module_update_calendar_events($cm->modname, $grademodule, $cm);
 
     return true;
 }
@@ -1386,6 +1383,80 @@ function delete_mod_from_section($modid, $sectionid) {
     return false;
 }
 
+/**
+ * This function updates the calendar events from the information stored in the module table and the course
+ * module table.
+ *
+ * @param  string $modulename Module name
+ * @param  stdClass $instance Module object. Either the $instance or the $cm must be supplied.
+ * @param  stdClass $cm Course module object. Either the $instance or the $cm must be supplied.
+ * @return bool Returns true if calendar events are updated.
+ * @since  Moodle 3.3.4
+ */
+function course_module_update_calendar_events($modulename, $instance = null, $cm = null) {
+    global $DB;
+
+    if (isset($instance) || isset($cm)) {
+
+        if (!isset($instance)) {
+            $instance = $DB->get_record($modulename, array('id' => $cm->instance), '*', MUST_EXIST);
+        }
+        if (!isset($cm)) {
+            $cm = get_coursemodule_from_instance($modulename, $instance->id, $instance->course);
+        }
+        course_module_calendar_event_update_process($instance, $cm);
+        return true;
+    }
+    return false;
+}
+
+/**
+ * Update all instances through out the site or in a course.
+ *
+ * @param  string  $modulename Module type to update.
+ * @param  integer $courseid   Course id to update events. 0 for the whole site.
+ * @return bool Returns True if the update was successful.
+ * @since  Moodle 3.3.4
+ */
+function course_module_bulk_update_calendar_events($modulename, $courseid = 0) {
+    global $DB;
+
+    $instances = null;
+    if ($courseid) {
+        if (!$instances = $DB->get_records($modulename, array('course' => $courseid))) {
+            return false;
+        }
+    } else {
+        if (!$instances = $DB->get_records($modulename)) {
+            return false;
+        }
+    }
+
+    foreach ($instances as $instance) {
+        $cm = get_coursemodule_from_instance($modulename, $instance->id, $instance->course);
+        course_module_calendar_event_update_process($instance, $cm);
+    }
+    return true;
+}
+
+/**
+ * Calendar events for a module instance are updated.
+ *
+ * @param  stdClass $instance Module instance object.
+ * @param  stdClass $cm Course Module object.
+ * @since  Moodle 3.3.4
+ */
+function course_module_calendar_event_update_process($instance, $cm) {
+    // We need to call *_refresh_events() first because some modules delete 'old' events at the end of the code which
+    // will remove the completion events.
+    $refresheventsfunction = $cm->modname . '_refresh_events';
+    if (function_exists($refresheventsfunction)) {
+        call_user_func($refresheventsfunction, $cm->course, $instance, $cm);
+    }
+    $completionexpected = (!empty($cm->completionexpected)) ? $cm->completionexpected : null;
+    \core_completion\api::update_completion_date_event($cm->id, $cm->modname, $instance, $completionexpected);
+}
+
 /**
  * Moves a section within a course, from a position to another.
  * Be very careful: $section and $destination refer to section number,
@@ -2942,6 +3013,7 @@ class course_request {
         $data->visibleold         = $data->visible;
         $data->lang               = $courseconfig->lang;
         $data->enablecompletion   = $courseconfig->enablecompletion;
+        $data->numsections        = $courseconfig->numsections;
 
         $course = create_course($data);
         $context = context_course::instance($course->id, MUST_EXIST);
@@ -3418,10 +3490,8 @@ function duplicate_module($course, $cm) {
         moveto_module($cm, $section, $newcm);
 
         // Update calendar events with the duplicated module.
-        $refresheventsfunction = $newcm->modname . '_refresh_events';
-        if (function_exists($refresheventsfunction)) {
-            call_user_func($refresheventsfunction, $newcm->course);
-        }
+        // 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.
         $event = \core\event\course_module_created::create_from_cm($newcm);
index cfd0676..db10908 100644 (file)
@@ -64,7 +64,7 @@
                     <div class="col-xs-6 span6">
                         <label class="accesshide" for="modtype_{{id}}">{{#str}}select, core_completion{{/str}} {{formattedname}}</label>
                         <input id="modtype_{{id}}" type="checkbox" class="m-r-1" name="modids[]" value="{{id}}" aria-label="{{#str}}checkactivity, completion, {{formattedname}}{{/str}}">
-                        <img src="{{icon}}" alt=" " role="presentation" />
+                        <img class="iconlarge activityicon" src="{{icon}}" alt=" " role="presentation" />
                         <span>{{formattedname}}</span>
                     </div>
                     <div class="activity-completionstatus col-xs-6 span6">
index 8cdeef6..4b20f93 100644 (file)
@@ -1830,7 +1830,7 @@ class behat_course extends behat_base {
         $node->click();
 
         // Smooth expansion.
-        $this->getSession()->wait(1000, false);
+        $this->getSession()->wait(1000);
     }
 
     /**
index 5ce1ffa..c51f30c 100644 (file)
@@ -3237,6 +3237,8 @@ class core_course_courselib_testcase extends advanced_testcase {
 
         $this->resetAfterTest(true);
 
+        $this->setAdminUser();
+
         $CFG->enablecompletion = true;
 
         $this->setTimezone('UTC');
@@ -3755,4 +3757,122 @@ class core_course_courselib_testcase extends advanced_testcase {
         $this->assertEquals(COURSE_TIMELINE_PAST, course_classify_for_timeline($completedcourse));
         $this->assertEquals(COURSE_TIMELINE_INPROGRESS, course_classify_for_timeline($inprogresscourse));
     }
+
+    /**
+     * Test the main function for updating all calendar events for a module.
+     */
+    public function test_course_module_calendar_event_update_process() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $completionexpected = time();
+        $duedate = time();
+
+        $course = $this->getDataGenerator()->create_course(['enablecompletion' => COMPLETION_ENABLED]);
+        $assign = $this->getDataGenerator()->create_module('assign', [
+                    'course' => $course,
+                    'completionexpected' => $completionexpected,
+                    'duedate' => $duedate
+                ]);
+
+        $cm = get_coursemodule_from_instance('assign', $assign->id, $course->id);
+        $events = $DB->get_records('event', ['courseid' => $course->id, 'instance' => $assign->id]);
+        // Check that both events are using the expected dates.
+        foreach ($events as $event) {
+            if ($event->eventtype == \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED) {
+                $this->assertEquals($completionexpected, $event->timestart);
+            }
+            if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) {
+                $this->assertEquals($duedate, $event->timestart);
+            }
+        }
+
+        // We have to manually update the module and the course module.
+        $newcompletionexpected = time() + DAYSECS * 60;
+        $newduedate = time() + DAYSECS * 45;
+        $newmodulename = 'Assign - new name';
+
+        $moduleobject = (object)array('id' => $assign->id, 'duedate' => $newduedate, 'name' => $newmodulename);
+        $DB->update_record('assign', $moduleobject);
+        $cmobject = (object)array('id' => $cm->id, 'completionexpected' => $newcompletionexpected);
+        $DB->update_record('course_modules', $cmobject);
+
+        $assign = $DB->get_record('assign', ['id' => $assign->id]);
+        $cm = get_coursemodule_from_instance('assign', $assign->id, $course->id);
+
+        course_module_calendar_event_update_process($assign, $cm);
+
+        $events = $DB->get_records('event', ['courseid' => $course->id, 'instance' => $assign->id]);
+        // Now check that the details have been updated properly from the function.
+        foreach ($events as $event) {
+            if ($event->eventtype == \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED) {
+                $this->assertEquals($newcompletionexpected, $event->timestart);
+                $this->assertEquals(get_string('completionexpectedfor', 'completion', (object)['instancename' => $newmodulename]),
+                        $event->name);
+            }
+            if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) {
+                $this->assertEquals($newduedate, $event->timestart);
+                $this->assertEquals($newmodulename, $event->name);
+            }
+        }
+    }
+
+    /**
+     * Test the higher level checks for updating calendar events for an instance.
+     */
+    public function test_course_module_update_calendar_events() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $completionexpected = time();
+        $duedate = time();
+
+        $course = $this->getDataGenerator()->create_course(['enablecompletion' => COMPLETION_ENABLED]);
+        $assign = $this->getDataGenerator()->create_module('assign', [
+                    'course' => $course,
+                    'completionexpected' => $completionexpected,
+                    'duedate' => $duedate
+                ]);
+
+        $cm = get_coursemodule_from_instance('assign', $assign->id, $course->id);
+
+        // Both the instance and cm objects are missing.
+        $this->assertFalse(course_module_update_calendar_events('assign'));
+        // Just using the assign instance.
+        $this->assertTrue(course_module_update_calendar_events('assign', $assign));
+        // Just using the course module object.
+        $this->assertTrue(course_module_update_calendar_events('assign', null, $cm));
+        // Using both the assign instance and the course module object.
+        $this->assertTrue(course_module_update_calendar_events('assign', $assign, $cm));
+    }
+
+    /**
+     * Test the higher level checks for updating calendar events for a module.
+     */
+    public function test_course_module_bulk_update_calendar_events() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $completionexpected = time();
+        $duedate = time();
+
+        $course = $this->getDataGenerator()->create_course(['enablecompletion' => COMPLETION_ENABLED]);
+        $course2 = $this->getDataGenerator()->create_course(['enablecompletion' => COMPLETION_ENABLED]);
+        $assign = $this->getDataGenerator()->create_module('assign', [
+                    'course' => $course,
+                    'completionexpected' => $completionexpected,
+                    'duedate' => $duedate
+                ]);
+
+        // No assign instances in this course.
+        $this->assertFalse(course_module_bulk_update_calendar_events('assign', $course2->id));
+        // No book instances for the site.
+        $this->assertFalse(course_module_bulk_update_calendar_events('book'));
+        // Update all assign instances.
+        $this->assertTrue(course_module_bulk_update_calendar_events('assign'));
+        // Update the assign instances for this course.
+        $this->assertTrue(course_module_bulk_update_calendar_events('assign', $course->id));
+    }
 }
index d932dd8..63aa87f 100644 (file)
@@ -1809,23 +1809,25 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $course = self::getDataGenerator()->create_course();
         $record = array(
             'course' => $course->id,
-            'name' => 'First Chat'
+            'name' => 'First quiz',
+            'grade' => 90.00
         );
         $options = array(
             'idnumber' => 'ABC',
             'visible' => 0
         );
         // Hidden activity.
-        $chat = self::getDataGenerator()->create_module('chat', $record, $options);
+        $quiz = self::getDataGenerator()->create_module('quiz', $record, $options);
 
         // Test admin user can see the complete hidden activity.
-        $result = core_course_external::get_course_module_by_instance('chat', $chat->id);
+        $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id);
         $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result);
 
         $this->assertCount(0, $result['warnings']);
         // Test we retrieve all the fields.
-        $this->assertCount(23, $result['cm']);
+        $this->assertCount(26, $result['cm']);
         $this->assertEquals($record['name'], $result['cm']['name']);
+        $this->assertEquals($record['grade'], $result['cm']['grade']);
         $this->assertEquals($options['idnumber'], $result['cm']['idnumber']);
 
         $student = $this->getDataGenerator()->create_user();
@@ -1836,30 +1838,30 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
 
         // The user shouldn't be able to see the activity.
         try {
-            core_course_external::get_course_module_by_instance('chat', $chat->id);
+            core_course_external::get_course_module_by_instance('quiz', $quiz->id);
             $this->fail('Exception expected due to invalid permissions.');
         } catch (moodle_exception $e) {
             $this->assertEquals('requireloginerror', $e->errorcode);
         }
 
         // Make module visible.
-        set_coursemodule_visible($chat->cmid, 1);
+        set_coursemodule_visible($quiz->cmid, 1);
 
         // Test student user.
-        $result = core_course_external::get_course_module_by_instance('chat', $chat->id);
+        $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id);
         $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result);
 
         $this->assertCount(0, $result['warnings']);
         // Test we retrieve only the few files we can see.
         $this->assertCount(11, $result['cm']);
-        $this->assertEquals($chat->cmid, $result['cm']['id']);
+        $this->assertEquals($quiz->cmid, $result['cm']['id']);
         $this->assertEquals($course->id, $result['cm']['course']);
-        $this->assertEquals('chat', $result['cm']['modname']);
-        $this->assertEquals($chat->id, $result['cm']['instance']);
+        $this->assertEquals('quiz', $result['cm']['modname']);
+        $this->assertEquals($quiz->id, $result['cm']['instance']);
 
         // Try with an invalid module name.
         try {
-            core_course_external::get_course_module_by_instance('abc', $chat->id);
+            core_course_external::get_course_module_by_instance('abc', $quiz->id);
             $this->fail('Exception expected due to invalid module name.');
         } catch (dml_read_exception $e) {
             $this->assertEquals('dmlreadexception', $e->errorcode);
index abda207..0bef68f 100644 (file)
Binary files a/course/yui/build/moodle-course-management/moodle-course-management-debug.js and b/course/yui/build/moodle-course-management/moodle-course-management-debug.js differ
index 1c994ed..2e524a0 100644 (file)
Binary files a/course/yui/build/moodle-course-management/moodle-course-management-min.js and b/course/yui/build/moodle-course-management/moodle-course-management-min.js differ
index 0c71169..10220c9 100644 (file)
Binary files a/course/yui/build/moodle-course-management/moodle-course-management.js and b/course/yui/build/moodle-course-management/moodle-course-management.js differ
index 633fd47..4c7c034 100644 (file)
@@ -277,7 +277,7 @@ Console.prototype = {
         if (!this.categoriesinit) {
             this.get('categorylisting').delegate('click', this.handleCategoryDelegation, 'a[data-action]', this);
             this.get('categorylisting').delegate('click', this.handleCategoryDelegation, 'input[name="bcat[]"]', this);
-            this.get('categorylisting').delegate('click', this.handleBulkSortByaction, '#menuselectsortby', this);
+            this.get('categorylisting').delegate('change', this.handleBulkSortByaction, '#menuselectsortby', this);
             this.categoriesinit = true;
             Y.log(count + ' categories being managed', 'info', 'moodle-course-management');
         } else {
index 0eaa555..a7b60d4 100644 (file)
@@ -45,7 +45,8 @@ class writer extends \core\dataformat\base {
      * Write the start of the output
      */
     public function start_output() {
-        echo "<!DOCTYPE html><html>";
+        echo "<!DOCTYPE html><html><head>";
+        echo \html_writer::empty_tag('meta', ['charset' => 'UTF-8']);
         echo \html_writer::tag('title', $this->filename);
         echo "<style>
 html, body {
@@ -73,6 +74,7 @@ table {
     margin: auto;
 }
 </style>
+</head>
 <body>";
     }
 
index 78f3a3d..eaf2e1e 100644 (file)
@@ -27,15 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_enrol_database_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
 
index 83cb31e..f824422 100644 (file)
@@ -385,6 +385,97 @@ class core_enrol_external extends external_api {
         );
     }
 
+    /**
+     * Returns description of method parameters value
+     *
+     * @return external_description
+     */
+    public static function get_potential_users_parameters() {
+        return new external_function_parameters(
+            array(
+                'courseid' => new external_value(PARAM_INT, 'course id'),
+                'enrolid' => new external_value(PARAM_INT, 'enrolment id'),
+                'search' => new external_value(PARAM_RAW, 'query'),
+                'searchanywhere' => new external_value(PARAM_BOOL, 'find a match anywhere, or only at the beginning'),
+                'page' => new external_value(PARAM_INT, 'Page number'),
+                'perpage' => new external_value(PARAM_INT, 'Number per page'),
+            )
+        );
+    }
+
+    /**
+     * Get potential users.
+     *
+     * @param int $courseid Course id
+     * @param int $enrolid Enrolment id
+     * @param string $search The query
+     * @param boolean $searchanywhere Match anywhere in the string
+     * @param int $page Page number
+     * @param int $perpage Max per page
+     * @return array An array of users
+     */
+    public static function get_potential_users($courseid, $enrolid, $search, $searchanywhere, $page, $perpage) {
+        global $PAGE, $DB, $CFG;
+
+        require_once($CFG->dirroot.'/enrol/locallib.php');
+        require_once($CFG->dirroot.'/user/lib.php');
+
+        $params = self::validate_parameters(
+            self::get_potential_users_parameters(),
+            array(
+                'courseid' => $courseid,
+                'enrolid' => $enrolid,
+                'search' => $search,
+                'searchanywhere' => $searchanywhere,
+                'page' => $page,
+                'perpage' => $perpage
+            )
+        );
+        $context = context_course::instance($params['courseid']);
+        try {
+            self::validate_context($context);
+        } catch (Exception $e) {
+            $exceptionparam = new stdClass();
+            $exceptionparam->message = $e->getMessage();
+            $exceptionparam->courseid = $params['courseid'];
+            throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam);
+        }
+        require_capability('moodle/course:enrolreview', $context);
+
+        $course = $DB->get_record('course', array('id' => $params['courseid']));
+        $manager = new course_enrolment_manager($PAGE, $course);
+
+        $users = $manager->get_potential_users($params['enrolid'],
+                                               $params['search'],
+                                               $params['searchanywhere'],
+                                               $params['page'],
+                                               $params['perpage']);
+
+        $results = array();
+        $requiredfields = ['id', 'fullname', 'profileimageurl', 'profileimageurlsmall'];
+        foreach ($users['users'] as $id => $user) {
+            // Note: We pass the course here to validate that the current user can at least view user details in this course.
+            // The user we are looking at is not in this course yet though - but we only fetch the minimal set of
+            // user records, and the user has been validated to have course:enrolreview in this course. Otherwise
+            // there is no way to find users who aren't in the course in order to enrol them.
+            if ($userdetails = user_get_user_details($user, $course, $requiredfields)) {
+                $results[] = $userdetails;
+            }
+        }
+        return $results;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     */
+    public static function get_potential_users_returns() {
+        global $CFG;
+        require_once($CFG->dirroot . '/user/externallib.php');
+        return new external_multiple_structure(core_user_external::user_description());
+    }
+
     /**
      * Returns description of method parameters
      *
index 4626f79..32f4164 100644 (file)
@@ -27,15 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_enrol_flatfile_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
 
index 75a721e..8f66610 100644 (file)
@@ -27,15 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_enrol_guest_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
 
index a2f1f59..e3383d5 100644 (file)
@@ -33,15 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_enrol_imsenterprise_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
 
index 8cb081a..a78f19c 100644 (file)
@@ -61,66 +61,36 @@ $outcome->error = '';
 $searchanywhere = get_user_preferences('userselector_searchanywhere', false);
 
 switch ($action) {
-    case 'getassignable':
-        $otheruserroles = optional_param('otherusers', false, PARAM_BOOL);
-        $outcome->response = array_reverse($manager->get_assignable_roles($otheruserroles), true);
-        break;
-    case 'searchusers':
+    case 'enrol':
         $enrolid = required_param('enrolid', PARAM_INT);
-        $search = optional_param('search', '', PARAM_RAW);
-        $page = optional_param('page', 0, PARAM_INT);
-        $addedenrollment = optional_param('enrolcount', 0, PARAM_INT);
-        $perpage = optional_param('perpage', 25, PARAM_INT);  //  This value is hard-coded to 25 in quickenrolment.js
-        $outcome->response = $manager->get_potential_users($enrolid, $search, $searchanywhere, $page, $perpage, $addedenrollment);
-        $extrafields = get_extra_user_fields($context);
-        $useroptions = array();
-        // User is not enrolled yet, either link to site profile or do not link at all.
-        if (has_capability('moodle/user:viewdetails', context_system::instance())) {
-            $useroptions['courseid'] = SITEID;
-        } else {
-            $useroptions['link'] = false;
+        $cohorts = $users = [];
+
+        $userids = optional_param('userlist', [], PARAM_SEQUENCE);
+        $userid = optional_param('userid', 0, PARAM_INT);
+        if ($userid) {
+            $userids[] = $userid;
         }
-        $viewfullnames = has_capability('moodle/site:viewfullnames', $context);
-        foreach ($outcome->response['users'] as &$user) {
-            $user->picture = $OUTPUT->user_picture($user, $useroptions);
-            $user->fullname = fullname($user, $viewfullnames);
-            $fieldvalues = array();
-            foreach ($extrafields as $field) {
-                $fieldvalues[] = s($user->{$field});
-                unset($user->{$field});
+        if ($userids) {
+            foreach ($userids as $userid) {
+                $users[] = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
             }
-            $user->extrafields = implode(', ', $fieldvalues);
         }
-        // Chrome will display users in the order of the array keys, so we need
-        // to ensure that the results ordered array keys. Fortunately, the JavaScript
-        // does not care what the array keys are. It uses user.id where necessary.
-        $outcome->response['users'] = array_values($outcome->response['users']);
-        $outcome->success = true;
-        break;
-    case 'searchcohorts':
-        $enrolid = required_param('enrolid', PARAM_INT);
-        $search = optional_param('search', '', PARAM_RAW);
-        $page = optional_param('page', 0, PARAM_INT);
-        $addedenrollment = optional_param('enrolcount', 0, PARAM_INT);
-        $perpage = optional_param('perpage', 25, PARAM_INT);  //  This value is hard-coded to 25 in quickenrolment.js
-        $outcome->response = enrol_manual_get_potential_cohorts($context, $enrolid, $search, $page, $perpage, $addedenrollment);
-        $outcome->success = true;
-        break;
-    case 'enrol':
-        $enrolid = required_param('enrolid', PARAM_INT);
-        $cohort = $user = null;
+        $cohortids = optional_param('cohortlist', [], PARAM_SEQUENCE);
         $cohortid = optional_param('cohortid', 0, PARAM_INT);
-        if (!$cohortid) {
-            $userid = required_param('userid', PARAM_INT);
-            $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
-        } else {
-            $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST);
-            if (!cohort_can_view_cohort($cohort, $context)) {
-                throw new enrol_ajax_exception('invalidenrolinstance'); // TODO error text!
+        if ($cohortid) {
+            $cohortids[] = $cohortid;
+        }
+        if ($cohortids) {
+            foreach ($cohortids as $cohortid) {
+                $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST);
+                if (!cohort_can_view_cohort($cohort, $context)) {
+                    throw new enrol_ajax_exception('invalidenrolinstance'); // TODO error text!
+                }
+                $cohorts[] = $cohort;
             }
         }
 
-        $roleid = optional_param('role', null, PARAM_INT);
+        $roleid = optional_param('roletoassign', null, PARAM_INT);
         $duration = optional_param('duration', 0, PARAM_FLOAT);
         $startdate = optional_param('startdate', 0, PARAM_INT);
         $recovergrades = optional_param('recovergrades', 0, PARAM_INT);
@@ -169,9 +139,10 @@ switch ($action) {
         }
         $plugin = $plugins[$instance->enrol];
         if ($plugin->allow_enrol($instance) && has_capability('enrol/'.$plugin->get_name().':enrol', $context)) {
-            if ($user) {
+            foreach ($users as $user) {
                 $plugin->enrol_user($instance, $user->id, $roleid, $timestart, $timeend, null, $recovergrades);
-            } else {
+            }
+            foreach ($cohorts as $cohort) {
                 $plugin->enrol_cohort($instance, $cohort->id, $roleid, $timestart, $timeend, null, $recovergrades);
             }
         } else {
diff --git a/enrol/manual/amd/build/form-potential-user-selector.min.js b/enrol/manual/amd/build/form-potential-user-selector.min.js
new file mode 100644 (file)
index 0000000..d697a8b
Binary files /dev/null and b/enrol/manual/amd/build/form-potential-user-selector.min.js differ
diff --git a/enrol/manual/amd/build/quickenrolment.min.js b/enrol/manual/amd/build/quickenrolment.min.js
new file mode 100644 (file)
index 0000000..583e136
Binary files /dev/null and b/enrol/manual/amd/build/quickenrolment.min.js differ
diff --git a/enrol/manual/amd/src/form-potential-user-selector.js b/enrol/manual/amd/src/form-potential-user-selector.js
new file mode 100644 (file)
index 0000000..49bdcbf
--- /dev/null
@@ -0,0 +1,98 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Potential user selector module.
+ *
+ * @module     enrol_manual/form-potential-user-selector
+ * @class      form-potential-user-selector
+ * @package    enrol_manual
+ * @copyright  2016 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery', 'core/ajax', 'core/templates'], function($, Ajax, Templates) {
+
+    return /** @alias module:enrol_manual/form-potential-user-selector */ {
+
+        processResults: function(selector, results) {
+            var users = [];
+            $.each(results, function(index, user) {
+                users.push({
+                    value: user.id,
+                    label: user._label
+                });
+            });
+            return users;
+        },
+
+        transport: function(selector, query, success, failure) {
+            var promise;
+            var courseid = $(selector).attr('courseid');
+            if (typeof courseid === "undefined") {
+                courseid = '1';
+            }
+            var enrolid = $(selector).attr('enrolid');
+            if (typeof enrolid === "undefined") {
+                enrolid = '';
+            }
+
+            promise = Ajax.call([{
+                methodname: 'core_enrol_get_potential_users',
+                args: {
+                    courseid: courseid,
+                    enrolid: enrolid,
+                    search: query,
+                    searchanywhere: true,
+                    page: 0,
+                    perpage: 30
+                }
+            }]);
+
+            promise[0].then(function(results) {
+                var promises = [],
+                    i = 0;
+
+                // Render the label.
+                $.each(results, function(index, user) {
+                    var ctx = user,
+                        identity = [];
+                    $.each(['idnumber', 'email', 'phone1', 'phone2', 'department', 'institution'], function(i, k) {
+                        if (typeof user[k] !== 'undefined' && user[k] !== '') {
+                            ctx.hasidentity = true;
+                            identity.push(user[k]);
+                        }
+                    });
+                    ctx.identity = identity.join(', ');
+                    promises.push(Templates.render('enrol_manual/form-user-selector-suggestion', ctx));
+                });
+
+                // Apply the label to the results.
+                return $.when.apply($.when, promises).then(function() {
+                    var args = arguments;
+                    $.each(results, function(index, user) {
+                        user._label = args[i];
+                        i++;
+                    });
+                    success(results);
+                    return;
+                });
+
+            }).fail(failure);
+        }
+
+    };
+
+});
diff --git a/enrol/manual/amd/src/quickenrolment.js b/enrol/manual/amd/src/quickenrolment.js
new file mode 100644 (file)
index 0000000..eedda35
--- /dev/null
@@ -0,0 +1,180 @@
+// 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/>.
+
+/**
+ * Quick enrolment AMD module.
+ *
+ * @module     enrol_manual/quickenrolment
+ * @copyright  2016 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['core/templates',
+         'jquery',
+         'core/str',
+         'core/config',
+         'core/notification',
+         'core/modal_factory',
+         'core/modal_events',
+         'core/fragment',
+       ],
+       function(Template, $, Str, Config, Notification, ModalFactory, ModalEvents, Fragment) {
+
+    /**
+     * Constructor
+     *
+     * @param {Object} options Object containing options. The only valid option at this time is contextid.
+     * Each call to templates.render gets it's own instance of this class.
+     */
+    var QuickEnrolment = function(options) {
+        this.contextid = options.contextid;
+
+        this.initModal();
+    };
+    // Class variables and functions.
+
+    /** @var {number} courseid - */
+    QuickEnrolment.prototype.courseid = 0;
+
+    /** @var {Modal} modal */
+    QuickEnrolment.prototype.modal = null;
+
+    /**
+     * Private method
+     *
+     * @method initModal
+     * @private
+     */
+    QuickEnrolment.prototype.initModal = function() {
+        var triggerButtons = $('.enrolusersbutton.enrol_manual_plugin [type="submit"]');
+
+        var strparams = [
+            {key: 'enroluserscohorts', component: 'enrol_manual'},
+            {key: 'enrolusers', component: 'enrol_manual'}
+        ];
+
+        $.when(Str.get_strings(strparams)).then(function(strlist) {
+            var modalSaveChanges = strlist[0],
+                modalTitle = strlist[1];
+
+            return ModalFactory.create({
+                type: ModalFactory.types.SAVE_CANCEL,
+                title: modalTitle,
+                body: this.getBody()
+            }, triggerButtons).then(function(modal) {
+                this.modal = modal;
+                this.modal.setLarge();
+                this.modal.setSaveButtonText(modalSaveChanges);
+
+                // We want the reset the form every time it is opened.
+                this.modal.getRoot().on(ModalEvents.hidden, function() {
+                    this.modal.setBody(this.getBody());
+                }.bind(this));
+
+                this.modal.getRoot().on(ModalEvents.save, this.submitForm.bind(this));
+                this.modal.getRoot().on('submit', 'form', this.submitFormAjax.bind(this));
+            }.bind(this));
+        }.bind(this)).fail(Notification.exception);
+    };
+
+    /**
+     * This triggers a form submission, so that any mform elements can do final tricks before the form submission is processed.
+     *
+     * @method submitForm
+     * @param {Event} e Form submission event.
+     * @private
+     */
+    QuickEnrolment.prototype.submitForm = function(e) {
+        e.preventDefault();
+        this.modal.getRoot().find('form').submit();
+    };
+
+    /**
+     * Private method
+     *
+     * @method submitForm
+     * @private
+     * @param {Event} e Form submission event.
+     */
+    QuickEnrolment.prototype.submitFormAjax = function(e) {
+        // We don't want to do a real form submission.
+        e.preventDefault();
+
+        var formData = this.modal.getRoot().find('form').serialize();
+
+        this.modal.hide();
+
+        var settings = {
+            type: 'GET',
+            processData: false,
+            contentType: "application/json"
+        };
+
+        var script = Config.wwwroot + '/enrol/manual/ajax.php?' + formData;
+        $.ajax(script, settings)
+            .then(function(response) {
+
+                if (response.error) {
+                    Notification.addNotification({
+                        message: response.error,
+                        type: "error"
+                    });
+                } else {
+                    // Reload the page, don't show changed data warnings.
+                    if (typeof window.M.core_formchangechecker !== "undefined") {
+                        window.M.core_formchangechecker.reset_form_dirty_state();
+                    }
+                    window.location.reload();
+                }
+                return;
+            })
+            .fail(Notification.exception);
+    };
+
+    /**
+     * Private method
+     *
+     * @method getBody
+     * @private
+     * @return {Promise}
+     */
+    QuickEnrolment.prototype.getBody = function() {
+        return Fragment.loadFragment('enrol_manual', 'enrol_users_form', this.contextid, {}).fail(Notification.exception);
+    };
+
+    /**
+     * Private method
+     *
+     * @method getFooter
+     * @private
+     * @return {Promise}
+     */
+    QuickEnrolment.prototype.getFooter = function() {
+        return Template.render('enrol_manual/enrol_modal_footer', {});
+    };
+
+    return /** @alias module:enrol_manual/quickenrolment */ {
+        // Public variables and functions.
+        /**
+         * Every call to init creates a new instance of the class with it's own event listeners etc.
+         *
+         * @method init
+         * @public
+         * @param {object} config - config variables for the module.
+         */
+        init: function(config) {
+            (new QuickEnrolment(config));
+        }
+    };
+});
diff --git a/enrol/manual/classes/enrol_users_form.php b/enrol/manual/classes/enrol_users_form.php
new file mode 100644 (file)
index 0000000..089810e
--- /dev/null
@@ -0,0 +1,132 @@
+<?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/>.
+
+/**
+ * Enrol users form.
+ *
+ * Simple form to search for users and add them using a manual enrolment to this course.
+ *
+ * @package enrol_manual
+ * @copyright 2016 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+
+class enrol_manual_enrol_users_form extends moodleform {
+
+    /**
+     * Form definition.
+     * @return void
+     */
+    public function definition() {
+        global $PAGE, $DB, $CFG;
+
+
+        require_once($CFG->dirroot . '/enrol/locallib.php');
+
+        $context = $this->_customdata->context;
+
+        // Get the course and enrolment instance.
+        $coursecontext = $context->get_course_context();
+        $course = $DB->get_record('course', ['id' => $coursecontext->instanceid]);
+        $manager = new course_enrolment_manager($PAGE, $course);
+
+        $instance = null;
+        foreach ($manager->get_enrolment_instances() as $tempinstance) {
+            if ($tempinstance->enrol == 'manual') {
+                if ($instance === null) {
+                    $instance = $tempinstance;
+                    break;
+                }
+            }
+        }
+
+        $mform = $this->_form;
+        $mform->setDisableShortforms();
+        $mform->disable_form_change_checker();
+        // Build the list of options for the enrolment period dropdown.
+        $unlimitedperiod = get_string('unlimited');
+        $periodmenu = array();
+        $periodmenu[''] = $unlimitedperiod;
+        for ($i=1; $i<=365; $i++) {
+            $seconds = $i * 86400;
+            $periodmenu[$seconds] = get_string('numdays', '', $i);
+        }
+        // Work out the apropriate default settings.
+        $defaultperiod = $instance->enrolperiod;
+        if ($instance->enrolperiod > 0 && !isset($periodmenu[$instance->enrolperiod])) {
+            $periodmenu[$instance->enrolperiod] = format_time($instance->enrolperiod);
+        }
+        if (empty($extendbase)) {
+            if (!$extendbase = get_config('enrol_manual', 'enrolstart')) {
+                // Default to now if there is no system setting.
+                $extendbase = 4;
+            }
+        }
+
+        // Build the list of options for the starting from dropdown.
+        $now = time();
+        $today = make_timestamp(date('Y', $now), date('m', $now), date('d', $now), 0, 0, 0);
+        $dateformat = get_string('strftimedatefullshort');
+
+        // Enrolment start.
+        $basemenu = array();
+        if ($course->startdate > 0) {
+            $basemenu[2] = get_string('coursestart') . ' (' . userdate($course->startdate, $dateformat) . ')';
+        }
+        $basemenu[3] = get_string('today') . ' (' . userdate($today, $dateformat) . ')';
+        $basemenu[4] = get_string('now', 'enrol_manual') . ' (' . userdate($now, get_string('strftimedatetimeshort')) . ')';
+
+        $mform->addElement('header', 'main', get_string('enrolmentoptions', 'enrol'));
+        $options = array(
+            'ajax' => 'enrol_manual/form-potential-user-selector',
+            'multiple' => true,
+            'courseid' => $course->id,
+            'enrolid' => $instance->id
+        );
+        $mform->addElement('autocomplete', 'userlist', get_string('selectusers', 'enrol_manual'), array(), $options);
+
+        $options = ['contextid' => $context->id, 'multiple' => true];
+        $mform->addElement('cohort', 'cohortlist', get_string('selectcohorts', 'enrol_manual'), $options);
+
+        $roles = get_assignable_roles($context);
+        $mform->addElement('select', 'roletoassign', get_string('assignrole', 'enrol_manual'), $roles);
+        $keys = array_keys($roles);
+        $defaultrole = end($keys);
+        $mform->setDefault('roletoassign', $defaultrole);
+
+        $mform->addAdvancedStatusElement('main');
+
+        $mform->addElement('checkbox', 'recovergrades', get_string('recovergrades', 'enrol'));
+        $mform->setAdvanced('recovergrades');
+        $mform->addElement('select', 'duration', get_string('defaultperiod', 'enrol_manual'), $periodmenu);
+        $mform->setDefault('duration', $defaultperiod);
+        $mform->setAdvanced('duration');
+        $mform->addElement('select', 'startdate', get_string('startingfrom'), $basemenu);
+        $mform->setDefault('startdate', $extendbase);
+        $mform->setAdvanced('startdate');
+
+        $mform->addElement('hidden', 'id', $course->id);
+        $mform->setType('id', PARAM_INT);
+        $mform->addElement('hidden', 'action', 'enrol');
+        $mform->setType('action', PARAM_ALPHA);
+        $mform->addElement('hidden', 'enrolid', $instance->id);
+        $mform->setType('enrolid', PARAM_INT);
+    }
+}
index 9c9fad4..2a0608c 100644 (file)
@@ -27,21 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_enrol_manual_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v2.8.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    // Moodle v2.9.0 release upgrade line.
-    // Put any upgrade step following this.
-
-    if ($oldversion < 2015091500) {
-        // We keep today as default enrolment start time on upgrades.
-        set_config('enrolstart', 3, 'enrol_manual');
-        upgrade_plugin_savepoint(true, 2015091500, 'enrol', 'manual');
-    }
-
-    // Moodle v3.0.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
 
index 2b3c572..29edcc5 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['advanced'] = 'Advanced';
 $string['alterstatus'] = 'Alter status';
 $string['altertimeend'] = 'Alter end time';
 $string['altertimestart'] = 'Alter start time';
 $string['assignrole'] = 'Assign role';
+$string['assignroles'] = 'Assign roles';
 $string['browseusers'] = 'Browse users';
 $string['browsecohorts'] = 'Browse cohorts';
 $string['confirmbulkdeleteenrolment'] = 'Are you sure you want to delete these users enrolments?';
@@ -37,6 +39,7 @@ $string['deleteselectedusers'] = 'Delete selected user enrolments';
 $string['editselectedusers'] = 'Edit selected user enrolments';
 $string['enrolledincourserole'] = 'Enrolled in "{$a->course}" as "{$a->role}"';
 $string['enrolusers'] = 'Enrol users';
+$string['enroluserscohorts'] = 'Enrol selected users and cohorts';
 $string['expiredaction'] = 'Enrolment expiry action';
 $string['expiredaction_help'] = 'Select action to carry out when user enrolment expires. Please note that some user data and settings are purged from course during course unenrolment.';
 $string['expirymessageenrollersubject'] = 'Enrolment expiry notification';
@@ -60,6 +63,9 @@ $string['messageprovider:expiry_notification'] = 'Manual enrolment expiry notifi
 $string['now'] = 'Now';
 $string['pluginname'] = 'Manual enrolments';
 $string['pluginname_desc'] = 'The manual enrolments plugin allows users to be enrolled manually via a link in the course administration settings, by a user with appropriate permissions such as a teacher. The plugin should normally be enabled, since certain other enrolment plugins, such as self enrolment, require it.';
+$string['selection'] = 'Selection';
+$string['selectusers'] = 'Select users';
+$string['selectcohorts'] = 'Select cohorts';
 $string['status'] = 'Enable manual enrolments';
 $string['status_desc'] = 'Allow course access of internally enrolled users. This should be kept enabled in most cases.';
 $string['status_help'] = 'This setting determines whether users can be enrolled manually, via a link in the course administration settings, by a user with appropriate permissions such as a teacher.';
index f41dae3..1eef9b0 100644 (file)
@@ -194,7 +194,7 @@ class enrol_manual_plugin extends enrol_plugin {
      * @return enrol_user_button
      */
     public function get_manual_enrol_button(course_enrolment_manager $manager) {
-        global $CFG;
+        global $CFG, $PAGE;
         require_once($CFG->dirroot.'/cohort/lib.php');
 
         $instance = null;
@@ -211,72 +211,18 @@ class enrol_manual_plugin extends enrol_plugin {
             return false;
         }
 
-        if (!$manuallink = $this->get_manual_enrol_link($instance)) {
+        $link = $this->get_manual_enrol_link($instance);
+        if (!$link) {
             return false;
         }
 
-        $button = new enrol_user_button($manuallink, get_string('enrolusers', 'enrol_manual'), 'get');
+        $button = new enrol_user_button($link, get_string('enrolusers', 'enrol_manual'), 'get');
         $button->class .= ' enrol_manual_plugin';
 
-        $startdate = $manager->get_course()->startdate;
-        if (!$defaultstart = get_config('enrol_manual', 'enrolstart')) {
-            // Default to now if there is no system setting.
-            $defaultstart = 4;
-        }
-        $startdateoptions = array();
-        $dateformat = get_string('strftimedatefullshort');
-        if ($startdate > 0) {
-            $startdateoptions[2] = get_string('coursestart') . ' (' . userdate($startdate, $dateformat) . ')';
-        }
-        $now = time();
-        $today = make_timestamp(date('Y', $now), date('m', $now), date('d', $now), 0, 0, 0);
-        $startdateoptions[3] = get_string('today') . ' (' . userdate($today, $dateformat) . ')';
-        $startdateoptions[4] = get_string('now', 'enrol_manual') . ' (' . userdate($now, get_string('strftimedatetimeshort')) . ')';
-        $defaultduration = $instance->enrolperiod > 0 ? $instance->enrolperiod / DAYSECS : '';
-
-        $modules = array('moodle-enrol_manual-quickenrolment', 'moodle-enrol_manual-quickenrolment-skin');
-        $arguments = array(
-            'instances'           => $instances,
-            'courseid'            => $instance->courseid,
-            'ajaxurl'             => '/enrol/manual/ajax.php',
-            'url'                 => $manager->get_moodlepage()->url->out(false),
-            'optionsStartDate'    => $startdateoptions,
-            'defaultRole'         => $instance->roleid,
-            'defaultDuration'     => $defaultduration,
-            'defaultStartDate'    => (int)$defaultstart,
-            'disableGradeHistory' => $CFG->disablegradehistory,
-            'recoverGradesDefault'=> '',
-            'cohortsAvailable'    => cohort_get_available_cohorts($manager->get_context(), COHORT_WITH_NOTENROLLED_MEMBERS_ONLY, 0, 1) ? true : false
-        );
-
-        if ($CFG->recovergradesdefault) {
-            $arguments['recoverGradesDefault'] = ' checked="checked"';
-        }
+        $context = context_course::instance($instance->courseid);
+        $arguments = array('contextid' => $context->id);
 
-        $function = 'M.enrol_manual.quickenrolment.init';
-        $button->require_yui_module($modules, $function, array($arguments));
-        $button->strings_for_js(array(
-            'ajaxoneuserfound',
-            'ajaxxusersfound',
-            'ajaxnext25',
-            'enrol',
-            'enrolmentoptions',
-            'enrolusers',
-            'enrolxusers',
-            'errajaxfailedenrol',
-            'errajaxsearch',
-            'foundxcohorts',
-            'none',
-            'usersearch',
-            'unlimitedduration',
-            'startdatetoday',
-            'durationdays',
-            'enrolperiod',
-            'finishenrollingusers',
-            'recovergrades'), 'enrol');
-        $button->strings_for_js(array('browseusers', 'browsecohorts'), 'enrol_manual');
-        $button->strings_for_js('assignroles', 'role');
-        $button->strings_for_js('startingfrom', 'moodle');
+        $PAGE->requires->js_call_amd('enrol_manual/quickenrolment', 'init', array($arguments));
 
         return $button;
     }
@@ -730,3 +676,25 @@ class enrol_manual_plugin extends enrol_plugin {
     }
 
 }
+
+/**
+ * Serve the manual enrol users form as a fragment.
+ *
+ * @param array $args List of named arguments for the fragment loader.
+ * @return string
+ */
+function enrol_manual_output_fragment_enrol_users_form($args) {
+    $args = (object) $args;
+    $context = $args->context;
+    $o = '';
+
+    require_capability('enrol/manual:enrol', $context);
+    $mform = new enrol_manual_enrol_users_form(null, $args);
+
+    ob_start();
+    $mform->display();
+    $o .= ob_get_contents();
+    ob_end_clean();
+
+    return $o;
+}
diff --git a/enrol/manual/templates/form-user-selector-suggestion.mustache b/enrol/manual/templates/form-user-selector-suggestion.mustache
new file mode 100644 (file)
index 0000000..a55b867
--- /dev/null
@@ -0,0 +1,57 @@
+{{!
+    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 enrol_manual/form-user-selector-suggestion
+
+    Moodle template for the list of valid options in an autocomplate form element.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * profileimageurlsmall Url to a small profile image.
+    * profileimageurl Url to a profile image.
+    * fullname Users full name
+    * hasidentity boolean to say if there are identity fields to show
+    * identity concatenated list of identity fields.
+    * id user id field
+    * email user email field
+    * idnumber user idnumber field
+    * phone1 user phone1 field
+    * phone2 user phone2 field
+    * department user department field
+    * institution user institution field
+
+    Example context (json):
+    { "id": "1",
+      "fullname": "Admin",
+      "hasidentity": true,
+      "identity": "Admin User, 0144114141",
+      "profileimageurl": "invalid url",
+      "profileimageurlsmall": "invalid url"
+    }
+}}
+<span>
+    <img height="18" src="{{profileimageurlsmall}}" alt="" role="presentation">
+    <span>{{fullname}}</span>
+    {{#hasidentity}}
+    <span><small>{{identity}}</small></span>
+    {{/hasidentity}}
+</span>
diff --git a/enrol/manual/yui/quickenrolment/assets/skins/sam/quickenrolment.css b/enrol/manual/yui/quickenrolment/assets/skins/sam/quickenrolment.css
deleted file mode 100644 (file)
index 53cb56c..0000000
+++ /dev/null
@@ -1,197 +0,0 @@
-/* stylelint-disable unit-blacklist */
-/**************************************
-
-Structure of the user enroller panel
-
-.user-enroller-panel(.visible)
-    .uep-wrap
-        .uep-header
-        .uep-content
-            .uep-ajax-content
-                .uep-search-results
-                    .totalusers
-                    .users
-                        .user.clearfix(.odd|.even)(.enrolled)
-                            .count
-                            .picture
-                            .details
-                                .fullname
-                                .extrafields
-                            .options
-                                .enrol
-                    .uep-more-results
-            .uep-loading-lightbox(.hidden)
-                .loading-icon
-        .uep-footer
-            .uep-search
-                input
-            .uep-searchoptions
-                .collapsibleheading
-                .collapsiblearea(.hidden)
-                    .uep-enrolment-option
-                        .role
-                        .startdate
-                        .duration
-
-**************************************/
-
-.user-enroller-panel {
-    width: 400px;
-    position: absolute;
-}
-
-.user-enroller-panel.hidden {
-    display: none;
-}
-
-.user-enroller-panel .uep-wrap {
-    height: inherit;
-    position: relative;
-}
-
-.user-enroller-panel .uep-search-results .user .count {
-    display: none;
-}
-
-.user-enroller-panel .uep-search-results .cohort .count {
-    display: none;
-}
-
-.user-enroller-panel .uep-content {
-    position: relative;
-    width: 100%;
-    box-sizing: border-box;
-    max-height: 1000px;
-}
-
-.user-enroller-panel .uep-ajax-content {
-    height: 375px;
-    overflow: auto;
-}
-
-.user-enroller-panel .uep-search-results .user .picture {
-    width: 45px;
-    float: left;
-    margin: 3px;
-}
-/* Note this file isn't auto flipped, so we need dir-rtl rules*/
-.dir-rtl .user-enroller-panel .uep-search-results .user .picture {
-    float: right;
-}
-.user-enroller-panel .uep-search-results .user .details {
-    width: 180px;
-    float: left;
-    margin: 3px;
-}
-/* Note this file isn't auto flipped, so we need dir-rtl rules*/
-.dir-rtl .user-enroller-panel .uep-search-results .user .details {
-    float: right;
-}
-.user-enroller-panel .uep-search-results .user .options {
-    padding-right: 7px;
-    margin: 3px;
-}
-
-.user-enroller-panel .uep-search-results .user .options .enrol {
-    margin: 3px;
-    float: right;
-    cursor: pointer;
-}
-/* Note this file isn't auto flipped, so we need dir-rtl rules*/
-.dir-rtl .user-enroller-panel .uep-search-results .user .options .enrol {
-    float: left;
-}
-
-.user-enroller-panel .uep-search-results .cohort {
-    width: 100%;
-    text-align: left;
-}
-
-.user-enroller-panel .uep-search-results .cohort .count {
-    display: none;
-}
-
-.user-enroller-panel .uep-search-results .cohort .details {
-    width: 180px;
-    float: left;
-    margin: 5px;
-}
-
-.user-enroller-panel .uep-search-results .cohort .options .enrol {
-    margin: 3px;
-    float: right;
-    cursor: pointer;
-}
-
-.user-enroller-panel .uep-search-results .cohort.enrolled .count {
-    width: 40px;
-}
-
-.user-enroller-panel .uep-loading-lightbox {
-    position: absolute;
-    width: 100%;
-    height: 100%;
-    background-color: #ddd;
-    top: 0;
-    left: 0;
-    min-width: 50px;
-    min-height: 50px;
-}
-
-.user-enroller-panel .uep-loading-lightbox.hidden {
-    display: none;
-}
-
-.user-enroller-panel .uep-loading-lightbox .loading-icon {
-    margin: auto;
-    vertical-align: middle;
-    margin-top: 125px;
-    display: block;
-}
-
-.user-enroller-panel .uep-footer {
-    text-align: right;
-}
-
-.user-enroller-panel .uep-search {
-    margin: 3px;
-}
-
-.user-enroller-panel .uep-search label {
-    padding-right: 8px;
-}
-
-.user-enroller-panel .uep-search input {
-    width: 50%;
-}
-
-.user-enroller-panel .uep-search input.uep-search-btn {
-    width: 20%;
-}
-
-.user-enroller-panel .uep-searchoptions {
-    margin: 3px;
-    cursor: pointer;
-}
-
-.user-enroller-panel .uep-controls select {
-    margin-left: 1em;
-    margin-bottom: 0;
-}
-
-.user-enroller-panel .collapsibleheading img {
-    margin-right: 8px;
-}
-
-.user-enroller-panel .collapsiblearea {
-    border: 1px solid #ddd;
-    padding: 0.5rem;
-}
-
-.user-enroller-panel .collapsiblearea.hidden {
-    display: none;
-}
-
-.user-enroller-panel .collapsiblearea .uep-enrolment-option {
-    margin: 5px 1em;
-}
diff --git a/enrol/manual/yui/quickenrolment/assets/skins/sam/sprite.png b/enrol/manual/yui/quickenrolment/assets/skins/sam/sprite.png
deleted file mode 100644 (file)
index b2a31cd..0000000
Binary files a/enrol/manual/yui/quickenrolment/assets/skins/sam/sprite.png and /dev/null differ
diff --git a/enrol/manual/yui/quickenrolment/quickenrolment.js b/enrol/manual/yui/quickenrolment/quickenrolment.js
deleted file mode 100644 (file)
index 8f235cc..0000000
+++ /dev/null
@@ -1,668 +0,0 @@
-YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
-
-    var UEP = {
-        NAME : 'Enrolment Manager',
-        /** Properties **/
-        BASE : 'base',
-        SEARCH : 'search',
-        SEARCHBTN : 'searchbtn',
-        PARAMS : 'params',
-        URL : 'url',
-        AJAXURL : 'ajaxurl',
-        MULTIPLE : 'multiple',
-        PAGE : 'page',
-        COURSEID : 'courseid',
-        USERS : 'users',
-        USERCOUNT : 'userCount',
-        REQUIREREFRESH : 'requiresRefresh',
-        LASTSEARCH : 'lastPreSearchValue',
-        INSTANCES : 'instances',
-        OPTIONSTARTDATE : 'optionsStartDate',
-        DEFAULTROLE : 'defaultRole',
-        DEFAULTSTARTDATE : 'defaultStartDate',
-        DEFAULTDURATION : 'defaultDuration',
-        ASSIGNABLEROLES : 'assignableRoles',
-        DISABLEGRADEHISTORY : 'disableGradeHistory',
-        RECOVERGRADESDEFAULT : 'recoverGradesDefault',
-        ENROLCOUNT : 'enrolCount',
-        PERPAGE : 'perPage',
-        COHORTSAVAILABLE : 'cohortsAvailable',
-        COHORTCOUNT : 'cohortCount'
-    };
-    /** CSS classes for nodes in structure **/
-    var CSS = {
-        PANEL : 'user-enroller-panel',
-        WRAP : 'uep-wrap modal-dialog',
-        HEADER : 'uep-header',
-        CONTENT : 'uep-content',
-        AJAXCONTENT : 'uep-ajax-content',
-        SEARCHRESULTS : 'uep-search-results',
-        TOTALUSERS : 'totalusers',
-        USERS : 'users',
-        USER : 'user',
-        MORERESULTS : 'uep-more-results',
-        LIGHTBOX : 'uep-loading-lightbox',
-        LOADINGICON : 'loading-icon',
-        FOOTER : 'uep-footer',
-        ENROL : 'enrol',
-        ENROLLED : 'enrolled',
-        COUNT : 'count',
-        PICTURE : 'picture',
-        DETAILS : 'details',
-        FULLNAME : 'fullname',
-        EXTRAFIELDS : 'extrafields',
-        OPTIONS : 'options',
-        ODD  : 'odd',
-        EVEN : 'even',
-        HIDDEN : 'hidden',
-        RECOVERGRADES : 'recovergrades',
-        RECOVERGRADESTITLE : 'recovergradestitle',
-        SEARCHOPTIONS : 'uep-searchoptions',
-        COLLAPSIBLEHEADING : 'collapsibleheading',
-        COLLAPSIBLEAREA : 'collapsiblearea',
-        ENROLMENTOPTION : 'uep-enrolment-option',
-        SEARCHCONTROLS : 'uep-controls',
-        ROLE : 'role',
-        STARTDATE : 'startdate',
-        DURATION : 'duration',
-        ACTIVE : 'active',
-        SEARCH : 'uep-search',
-        SEARCHBTN : 'uep-search-btn',
-        CLOSE : 'close',
-        CLOSEBTN : 'close-button',
-        ENTITYSELECTOR : 'uep-entity-selector',
-        COHORTS : 'cohorts',
-        COHORT : 'cohort',
-        COHORTNAME : 'cohortname',
-        TOTALCOHORTS : 'totalcohorts'
-    };
-    var create = Y.Node.create;
-
-    var USERENROLLER = function(config) {
-        USERENROLLER.superclass.constructor.apply(this, arguments);
-    };
-    Y.extend(USERENROLLER, Y.Base, {
-        _searchTimeout : null,
-        _loadingNode : null,
-        _escCloseEvent : null,
-        initializer : function(config) {
-            var recovergrades = null;
-            if (this.get(UEP.DISABLEGRADEHISTORY) != true) {
-                recovergrades = create('<div class="'+CSS.ENROLMENTOPTION+' '+CSS.RECOVERGRADES+'"></div>')
-                    .append(create('<label class="'+CSS.RECOVERGRADESTITLE+'" for="'+CSS.RECOVERGRADES+'">'+M.util.get_string('recovergrades', 'enrol')+'</label>'))
-                    .append(create('<input type="checkbox" class="m-x-1" id="'+CSS.RECOVERGRADES+'" name="'+CSS.RECOVERGRADES+'"'+ this.get(UEP.RECOVERGRADESDEFAULT) +' />'))
-            }
-
-            this.set(UEP.BASE, create('<div class="'+CSS.PANEL+' '+CSS.HIDDEN+'"></div>')
-                .append(create('<div class="'+CSS.WRAP+' modal show modal-dialog modal-content"></div>')
-                    .append(create('<div class="'+CSS.HEADER+' header modal-header"></div>')
-                        .append(create('<div class="'+CSS.CLOSE+'">&times;</div>'))
-                        .append(create('<h2 class="modal-title">'+M.util.get_string('enrolusers', 'enrol')+'</h2>')))
-                    .append(create('<div class="'+CSS.CONTENT+' modal-body"></div>')
-                        .append(create('<div class="'+CSS.SEARCHCONTROLS+' form-inline"></div>')
-                            .append(create('<div class="'+CSS.ENROLMENTOPTION+' '+CSS.ROLE+'"><label for="id_enrol_manual_assignable_roles">'+M.util.get_string('assignroles', 'role')+'</label></div>')
-                                    .append(create('<select id="id_enrol_manual_assignable_roles" class="custom-select"><option value="">'+M.util.get_string('none', 'enrol')+'</option></select>'))
-                            )
-                            .append(create('<div class="'+CSS.ENTITYSELECTOR+' m-y-1"></div>'))
-                            .append(create('<div class="'+CSS.SEARCHOPTIONS+'"></div>')
-                                .append(create('<div class="'+CSS.COLLAPSIBLEHEADING+'"><img alt="" />'+M.util.get_string('enrolmentoptions', 'enrol')+'</div>'))
-                                .append(create('<div class="'+CSS.COLLAPSIBLEAREA+' '+CSS.HIDDEN+'"></div>')
-                                    .append(recovergrades)
-                                    .append(create('<div class="'+CSS.ENROLMENTOPTION+' '+CSS.STARTDATE+'">'+M.util.get_string('startingfrom', 'moodle')+'</div>')
-                                        .append(create('<select class="custom-select"></select>')))
-                                    .append(create('<div class="'+CSS.ENROLMENTOPTION+' '+CSS.DURATION+'">'+M.util.get_string('enrolperiod', 'enrol')+'</div>')
-                                        .append(create('<select class="custom-select"><option value="0" selected="selected">'+M.util.get_string('unlimitedduration', 'enrol')+'</option></select>')))
-                                )
-                            )
-                            .append(create('<div class="'+CSS.SEARCH+'"><label for="enrolusersearch" class="accesshide">'+M.util.get_string('usersearch', 'enrol')+'</label></div>')
-                                .append(create('<input class="form-control" type="text" id="enrolusersearch" value="" />'))
-                                .append(create('<input type="button" id="searchbtn" class="'+CSS.SEARCHBTN+' btn btn-secondary m-l-1" value="'+M.util.get_string('usersearch', 'enrol')+'" />'))
-                            )
-                        )
-                        .append(create('<div class="'+CSS.AJAXCONTENT+'"></div>'))
-                        .append(create('<div class="'+CSS.LIGHTBOX+' '+CSS.HIDDEN+'"></div>')
-                            .append(create('<img alt="loading" class="'+CSS.LOADINGICON+'" />')
-                                .setAttribute('src', M.util.image_url('i/loading', 'moodle')))
-                            .setStyle('opacity', 0.5)))
-                    .append(create('<div class="'+CSS.FOOTER+' modal-footer"></div>')
-                        .append(create('<div class="'+CSS.CLOSEBTN+'"></div>')
-                            .append(create('<input type="button" class="btn btn-primary" value="'+M.util.get_string('finishenrollingusers', 'enrol')+'" />'))
-                        )
-                    )
-                )
-            );
-
-            this.set(UEP.SEARCH, this.get(UEP.BASE).one('#enrolusersearch'));
-            this.set(UEP.SEARCHBTN, this.get(UEP.BASE).one('#searchbtn'));
-            Y.all('.enrol_manual_plugin input').each(function(node){
-                if (node.getAttribute('type', 'submit')) {
-                    node.on('click', this.show, this);
-                }
-            }, this);
-            this.get(UEP.BASE).one('.'+CSS.HEADER+' .'+CSS.CLOSE).on('click', this.hide, this);
-            this.get(UEP.BASE).one('.'+CSS.FOOTER+' .'+CSS.CLOSEBTN+' input').on('click', this.hide, this);
-            this._loadingNode = this.get(UEP.BASE).one('.'+CSS.CONTENT+' .'+CSS.LIGHTBOX);
-            var params = this.get(UEP.PARAMS);
-            params['id'] = this.get(UEP.COURSEID);
-            this.set(UEP.PARAMS, params);
-
-            Y.on('key', this.preSearch, this.get(UEP.SEARCH), 'down:13', this);
-            this.get(UEP.SEARCHBTN).on('click', this.preSearch, this);
-
-            if (this.get(UEP.COHORTSAVAILABLE)) {
-                this.get(UEP.BASE).one('.'+CSS.ENTITYSELECTOR)
-                    .append(create('<input type="radio" id="id_enrol_manual_entity_users" name="enrol_manual_entity" value="users" class="m-r-1" checked="checked"/>'))
-                    .append(create('<label for="id_enrol_manual_entity_users">'+ M.util.get_string('browseusers', 'enrol_manual')+'</label>'))
-                    .append(create('<input type="radio" id="id_enrol_manual_entity_cohorts" name="enrol_manual_entity" class="m-x-1" value="cohorts"/>'))
-                    .append(create('<label for="id_enrol_manual_entity_cohorts">'+M.util.get_string('browsecohorts', 'enrol_manual')+'</label>'));
-                this.get(UEP.BASE).one('#id_enrol_manual_entity_cohorts').on('change', this.search, this);
-                this.get(UEP.BASE).one('#id_enrol_manual_entity_users').on('change', this.search, this);
-            } else {
-                this.get(UEP.BASE).one('.'+CSS.ENTITYSELECTOR)
-                    .append(create('<input type="hidden" name="enrol_manual_entity" value="users"/>'));
-            }
-
-            Y.one(document.body).append(this.get(UEP.BASE));
-
-            var base = this.get(UEP.BASE);
-            base.plug(Y.Plugin.Drag);
-            base.dd.addHandle('.'+CSS.HEADER+' h2');
-            base.one('.'+CSS.HEADER+' h2').setStyle('cursor', 'move');
-
-            var collapsedimage = 't/collapsed'; // ltr mode
-            if ( Y.one(document.body).hasClass('dir-rtl') ) {
-                collapsedimage = 't/collapsed_rtl';
-            } else {
-                collapsedimage = 't/collapsed';
-            }
-
-            this.get(UEP.BASE).one('.'+CSS.SEARCHOPTIONS+' .'+CSS.COLLAPSIBLEHEADING).one('img').setAttribute('src', M.util.image_url(collapsedimage, 'moodle'));
-            this.populateStartDates();
-            this.populateDuration();
-            this.get(UEP.BASE).one('.'+CSS.SEARCHOPTIONS+' .'+CSS.COLLAPSIBLEHEADING).on('click', function(){
-                this.get(UEP.BASE).one('.'+CSS.SEARCHOPTIONS+' .'+CSS.COLLAPSIBLEHEADING).toggleClass(CSS.ACTIVE);
-                this.get(UEP.BASE).one('.'+CSS.SEARCHOPTIONS+' .'+CSS.COLLAPSIBLEAREA).toggleClass(CSS.HIDDEN);
-                if (this.get(UEP.BASE).one('.'+CSS.SEARCHOPTIONS+' .'+CSS.COLLAPSIBLEAREA).hasClass(CSS.HIDDEN)) {
-                    this.get(UEP.BASE).one('.'+CSS.SEARCHOPTIONS+' .'+CSS.COLLAPSIBLEHEADING).one('img').setAttribute('src', M.util.image_url(collapsedimage, 'moodle'));
-                } else {
-                    this.get(UEP.BASE).one('.'+CSS.SEARCHOPTIONS+' .'+CSS.COLLAPSIBLEHEADING).one('img').setAttribute('src', M.util.image_url('t/expanded', 'moodle'));
-                }
-            }, this);
-            this.populateAssignableRoles();
-        },
-        populateAssignableRoles : function() {
-            this.on('assignablerolesloaded', function(){
-                var roles = this.get(UEP.ASSIGNABLEROLES);
-                var s = this.get(UEP.BASE).one('.'+CSS.ENROLMENTOPTION+'.'+CSS.ROLE+' select');
-                var v = this.get(UEP.DEFAULTROLE);
-                var index = 0, count = 0;
-                for (var i in roles) {
-                    count++;
-                    var option = create('<option value="' + roles[i].id + '">' + roles[i].name + '</option>');
-                    if (roles[i].id == v) {
-                        index = count;
-                    }
-                    s.append(option);
-                }
-                s.set('selectedIndex', index);
-                Y.one('#id_enrol_manual_assignable_roles').focus();
-            }, this);
-            this.getAssignableRoles();
-        },
-        populateStartDates : function() {
-            var select = this.get(UEP.BASE).one('.'+CSS.ENROLMENTOPTION+'.'+CSS.STARTDATE+' select');
-            var defaultvalue = this.get(UEP.DEFAULTSTARTDATE);
-            var options = this.get(UEP.OPTIONSTARTDATE);
-            var index = 0, count = 0;
-            for (var i in options) {
-                var option = create('<option value="'+i+'">'+options[i]+'</option>');
-                if (i == defaultvalue) {
-                    index = count;
-                }
-                select.append(option);
-                count++;
-            }
-            select.set('selectedIndex', index);
-        },
-        populateDuration : function() {
-            var select = this.get(UEP.BASE).one('.'+CSS.ENROLMENTOPTION+'.'+CSS.DURATION+' select');
-            var defaultvalue = this.get(UEP.DEFAULTDURATION);
-            var prefix = Math.round(defaultvalue) != defaultvalue ? '≈' : '';
-            var index = 0, count = 0;
-            var durationdays = M.util.get_string('durationdays', 'enrol', '{a}');
-            for (var i = 1; i <= 365; i++) {
-                count++;
-                var option = create('<option value="'+i+'">'+durationdays.replace('{a}', i)+'</option>');
-                if (i == defaultvalue) {
-                    index = count;
-                }
-                select.append(option);
-            }
-            if (!index && defaultvalue > 0) {
-                select.append(create('<option value="'+defaultvalue+'">'+durationdays.replace('{a}',
-                    prefix + (Math.round(defaultvalue * 100) / 100))+'</option>'));
-                index = ++count;
-            }
-            select.set('selectedIndex', index);
-        },
-        getAssignableRoles : function(){
-            Y.io(M.cfg.wwwroot+'/enrol/ajax.php', {
-                method:'POST',
-                data:'id='+this.get(UEP.COURSEID)+'&action=getassignable&sesskey='+M.cfg.sesskey,
-                on: {
-                    complete: function(tid, outcome, args) {
-                        try {
-                            var roles = Y.JSON.parse(outcome.responseText);
-                            this.set(UEP.ASSIGNABLEROLES, roles.response);
-                        } catch (e) {
-                            new M.core.exception(e);
-                        }
-                        this.getAssignableRoles = function() {
-                            this.fire('assignablerolesloaded');
-                        };
-                        this.getAssignableRoles();
-                    }
-                },
-                context:this
-            });
-        },
-        preSearch : function(e) {
-            this.search(e, false);
-            /*
-            var value = this.get(UEP.SEARCH).get('value');
-            if (value.length < 3 || value == this.get(UEP.LASTSEARCH)) {
-                return;
-            }
-            this.set(UEP.LASTSEARCH, value);
-            if (this._searchTimeout) {
-                clearTimeout(this._searchTimeout);
-                this._searchTimeout = null;
-            }
-            var self = this;
-            this._searchTimeout = setTimeout(function(){
-                self._searchTimeout = null;
-                self.search(null, false);
-            }, 300);
-            */
-        },
-        show : function(e) {
-            e.preventDefault();
-            e.halt();
-
-            var base = this.get(UEP.BASE);
-            base.removeClass(CSS.HIDDEN);
-            var x = (base.get('winWidth') - 400)/2;
-            var y = (parseInt(base.get('winHeight'))-base.get('offsetHeight'))/2 + parseInt(base.get('docScrollY'));
-            if (y < parseInt(base.get('winHeight'))*0.1) {
-                y = parseInt(base.get('winHeight'))*0.1;
-            }
-            base.setXY([x,y]);
-            var zindex = 0;
-            Y.all('.moodle-has-zindex').each(function() {
-                if (parseInt(this.getComputedStyle('zIndex'), 10) > zindex) {
-                    zindex = parseInt(this.getComputedStyle('zIndex'), 10);
-                }
-            });
-            base.setStyle('zIndex', zindex + 1);
-
-            if (this.get(UEP.USERS)===null) {
-                this.search(e, false);
-            }
-
-            this._escCloseEvent = Y.on('key', this.hide, document.body, 'down:27', this);
-            var rolesselect = Y.one('#id_enrol_manual_assignable_roles');
-            if (rolesselect) {
-                rolesselect.focus();
-            }
-        },
-        hide : function(e) {
-            if (this._escCloseEvent) {
-                this._escCloseEvent.detach();
-                this._escCloseEvent = null;
-            }
-            this.get(UEP.BASE).addClass(CSS.HIDDEN);
-            if (this.get(UEP.REQUIREREFRESH)) {
-                window.location = this.get(UEP.URL);
-            }
-        },
-        currentEntity : function() {
-            var entity = CSS.USER;
-            var cohortsinput = Y.one('#id_enrol_manual_entity_cohorts');
-            if (cohortsinput && cohortsinput.get('checked')) {
-                entity = CSS.COHORT;
-            }
-            return entity;
-        },
-        search : function(e, append) {
-            var entity = this.currentEntity();
-
-            if (e) {
-                e.halt();
-                e.preventDefault();
-            }