Merge branch 'MDL-63136-master' of git://github.com/rezaies/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 11 Sep 2018 21:52:02 +0000 (23:52 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 11 Sep 2018 21:52:02 +0000 (23:52 +0200)
150 files changed:
admin/environment.xml
admin/tool/cohortroles/lang/en/tool_cohortroles.php
admin/tool/customlang/lang/en/tool_customlang.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/log/store/database/lang/en/logstore_database.php
admin/tool/log/store/legacy/lang/en/logstore_legacy.php
admin/tool/log/store/standard/lang/en/logstore_standard.php
admin/tool/messageinbound/lang/en/tool_messageinbound.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/policy/classes/form/accept_policy.php
admin/tool/policy/lang/en/tool_policy.php
admin/tool/policy/tests/behat/acceptances.feature
admin/tool/usertours/lang/en/tool_usertours.php
auth/ldap/lang/en/auth_ldap.php
blog/lib.php
calendar/classes/external/event_exporter_base.php
calendar/lib.php
calendar/templates/event_item.mustache
completion/classes/api.php
completion/tests/behat/completion_no_calendar_capabilities.feature [new file with mode: 0644]
completion/tests/capabilities_test.php [new file with mode: 0644]
course/amd/build/actions.min.js
course/amd/src/actions.js
course/changenumsections.php
course/classes/management_renderer.php
course/format/lib.php
course/format/renderer.php
course/lib.php
course/tests/behat/course_category_management_listing.feature
course/tests/courselib_test.php
enrol/flatfile/lang/en/enrol_flatfile.php
enrol/lti/lang/en/enrol_lti.php
filter/algebra/algebradebug.php
filter/tex/lib.php
filter/tex/texdebug.php
install/lang/ca/admin.php
install/lang/el/admin.php
install/lang/gl/error.php
lang/en/admin.php
lang/en/auth.php
lang/en/backup.php
lang/en/badges.php
lang/en/blog.php
lang/en/cohort.php
lang/en/competency.php
lang/en/enrol.php
lang/en/error.php
lang/en/grades.php
lang/en/message.php
lang/en/moodle.php
lang/en/portfolio.php
lang/en/webservice.php
lib/amd/build/form-autocomplete.min.js
lib/amd/src/form-autocomplete.js
lib/db/upgrade.php
lib/editor/atto/lib.php
lib/messagelib.php
lib/outputrenderers.php
lib/pear/HTML/QuickForm.php
lib/pear/HTML/QuickForm/element.php
lib/pear/HTML/QuickForm/hierselect.php
lib/pear/HTML/QuickForm/utils.php [new file with mode: 0644]
lib/phpunit/tests/advanced_test.php
lib/templates/form_autocomplete_input.mustache
lib/templates/paging_bar.mustache [moved from theme/boost/templates/core/paging_bar.mustache with 85% similarity]
lib/tests/messagelib_test.php
lib/tests/tablelib_test.php
lib/upgradelib.php
message/output/airnotifier/lang/en/message_airnotifier.php
message/tests/externallib_test.php
mnet/service/enrol/lang/en/mnetservice_enrol.php
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/mod_form.php
mod/assign/tests/behat/assign_no_calendar_capabilities.feature [new file with mode: 0644]
mod/assign/tests/lib_test.php
mod/chat/lib.php
mod/chat/tests/behat/chat_no_calendar_capabilities.feature [new file with mode: 0644]
mod/chat/tests/lib_test.php
mod/choice/locallib.php
mod/choice/tests/behat/choice_no_calendar_capabilities.feature [new file with mode: 0644]
mod/choice/tests/lib_test.php
mod/data/edit.php
mod/data/export.php
mod/data/field.php
mod/data/locallib.php
mod/data/preset.php
mod/data/templates.php
mod/data/tests/behat/data_no_calendar_capabilities.feature [new file with mode: 0644]
mod/data/tests/lib_test.php
mod/feedback/classes/complete_form.php
mod/feedback/classes/external.php
mod/feedback/item/multichoice/lib.php
mod/feedback/lib.php
mod/feedback/tests/behat/feedback_no_calendar_capabilities.feature [new file with mode: 0644]
mod/feedback/tests/external_test.php
mod/feedback/tests/lib_test.php
mod/feedback/upgrade.txt
mod/lesson/lang/en/lesson.php
mod/lesson/lib.php
mod/lesson/tests/behat/lesson_no_calendar_capabilities.feature [new file with mode: 0644]
mod/lesson/tests/lib_test.php
mod/lti/lang/en/lti.php
mod/quiz/lang/en/quiz.php
mod/quiz/lib.php
mod/quiz/tests/behat/quiz_no_calendar_capabilities.feature [new file with mode: 0644]
mod/quiz/tests/lib_test.php
mod/scorm/locallib.php
mod/scorm/tests/behat/scorm_no_calendar_capabilities.feature [new file with mode: 0644]
mod/scorm/tests/lib_test.php
mod/workshop/amd/build/modform.min.js [new file with mode: 0644]
mod/workshop/amd/src/modform.js [new file with mode: 0644]
mod/workshop/backup/moodle1/lib.php
mod/workshop/backup/moodle2/backup_workshop_stepslib.php
mod/workshop/backup/moodle2/restore_workshop_stepslib.php
mod/workshop/classes/external/workshop_summary_exporter.php
mod/workshop/db/install.xml
mod/workshop/db/upgrade.php
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/delete_submission.feature
mod/workshop/tests/behat/export_submission.feature
mod/workshop/tests/behat/file_type_restriction.feature
mod/workshop/tests/behat/submission_types.feature [new file with mode: 0644]
mod/workshop/tests/behat/workshop_assessment.feature
mod/workshop/tests/external_test.php
mod/workshop/upgrade.txt
mod/workshop/version.php
question/type/ddwtos/questiontype.php
question/type/ddwtos/tests/questiontype_test.php
repository/url/lang/en/repository_url.php
theme/boost/classes/output/core_renderer.php
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/drawer.scss
theme/boost/style/moodle.css
theme/boost/templates/core/form_autocomplete_input.mustache
theme/boost/templates/core/navbar.mustache
user/lib.php
user/profile/field/checkbox/lang/en/profilefield_checkbox.php
user/profile/field/datetime/lang/en/profilefield_datetime.php
user/profile/field/menu/lang/en/profilefield_menu.php
user/profile/field/text/lang/en/profilefield_text.php
user/profile/field/textarea/lang/en/profilefield_textarea.php
user/tests/behat/filter_participants.feature
version.php

index 2b8d4ef..fbda3f3 100644 (file)
       </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
+  <MOODLE version="3.6" requires="3.1">
+    <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.6" />
+      <VENDOR name="postgres" version="9.4" />
+      <VENDOR name="mssql" version="10.0" />
+      <VENDOR name="oracle" version="11.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_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="sixtyfourbitswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+    </CUSTOM_CHECKS>
+  </MOODLE>
 </COMPATIBILITY_MATRIX>
index a8afe83..2f6eb21 100644 (file)
@@ -43,10 +43,10 @@ $string['selectusers'] = 'Select users to assign role';
 $string['taskname'] = 'Sync cohort role assignments';
 $string['thisuserroles'] = 'Roles assigned relative to this user';
 $string['privacy:metadata:tool_cohortroles'] = 'The Cohort roles management plugin stores user cohort role mappings.';
-$string['privacy:metadata:tool_cohortroles:id'] = 'The ID of the cohort role mapping record.';
-$string['privacy:metadata:tool_cohortroles:cohortid'] = 'The ID of the cohort.';
-$string['privacy:metadata:tool_cohortroles:roleid'] = 'The ID of the role.';
-$string['privacy:metadata:tool_cohortroles:userid'] = 'The ID of the user.';
-$string['privacy:metadata:tool_cohortroles:timecreated'] = 'The date/time of when the cohort  role mapping was created.';
-$string['privacy:metadata:tool_cohortroles:timemodified'] = 'The date/time of when the cohort role mapping was modified.';
-$string['privacy:metadata:tool_cohortroles:usermodified'] = 'The ID of the user who last modified the cohort role mapping.';
+$string['privacy:metadata:tool_cohortroles:id'] = 'The ID of the cohort role mapping record';
+$string['privacy:metadata:tool_cohortroles:cohortid'] = 'The ID of the cohort';
+$string['privacy:metadata:tool_cohortroles:roleid'] = 'The ID of the role';
+$string['privacy:metadata:tool_cohortroles:userid'] = 'The ID of the user';
+$string['privacy:metadata:tool_cohortroles:timecreated'] = 'The time when the cohort role mapping was created';
+$string['privacy:metadata:tool_cohortroles:timemodified'] = 'The time when the cohort role mapping was modified';
+$string['privacy:metadata:tool_cohortroles:usermodified'] = 'The ID of the user who last modified the cohort role mapping';
index 7192622..9e7a2d6 100644 (file)
@@ -30,7 +30,7 @@ $string['checkin'] = 'Save strings to language pack';
 $string['checkout'] = 'Open language pack for editing';
 $string['checkoutdone'] = 'Language pack loaded';
 $string['checkoutinprogress'] = 'Loading language pack';
-$string['confirmcheckin'] = 'You are about to save modifications to your local language pack. This will export the customised strings from the translator into you Moodle data directory and Moodle will start using the modified strings. Press \'Continue\' to proceed with saving.';
+$string['confirmcheckin'] = 'You are about to save modifications to your local language pack. This will export the customised strings from the translator into your site data directory and your site will start using the modified strings. Press \'Continue\' to proceed with saving.';
 $string['customlang:edit'] = 'Edit local translation';
 $string['customlang:view'] = 'View local translation';
 $string['filter'] = 'Filter strings';
index 4fa38fa..eb2c97f 100644 (file)
@@ -197,7 +197,7 @@ $string['privacy:metadata:request:requestedby'] = 'The ID of the user making the
 $string['privacy:metadata:request:dpocomment'] = 'Any comments made by the site\'s privacy officer regarding the request.';
 $string['privacy:metadata:request:timecreated'] = 'The timestamp indicating when the request was made by the user.';
 $string['privacyrequestexpiry'] = 'Data request expiry';
-$string['privacyrequestexpiry_desc'] = 'The amount of time that approved data requests will be available for download before expiring. 0 means no time limit.';
+$string['privacyrequestexpiry_desc'] = 'The time that approved data requests will be available for download before expiring. If set to zero, then there is no time limit.';
 $string['protected'] = 'Protected';
 $string['protectedlabel'] = 'The retention of this data has a higher legal precedent over a user\'s request to be forgotten. This data will only be deleted after the retention period has expired.';
 $string['purpose'] = 'Purpose';
@@ -223,7 +223,7 @@ $string['requeststatus'] = 'Status';
 $string['requestsubmitted'] = 'Your request has been submitted to the privacy officer';
 $string['requesttype'] = 'Type';
 $string['requesttypeuser'] = '{$a->typename} ({$a->user})';
-$string['requesttype_help'] = 'Select the reason why you would like to contact the privacy officer';
+$string['requesttype_help'] = 'Select the reason for contacting the privacy officer. Be aware that deletion of all personal  data will result in you no longer being able to log in to the site.';
 $string['requesttypedelete'] = 'Delete all of my personal data';
 $string['requesttypedeleteshort'] = 'Delete';
 $string['requesttypeexport'] = 'Export all of my personal data';
index 8f49696..31edf3a 100644 (file)
@@ -52,7 +52,7 @@ $string['privacy:metadata:log:origin'] = 'The origin of the event';
 $string['privacy:metadata:log:other'] = 'Additional information about the event';
 $string['privacy:metadata:log:realuserid'] = 'The ID of the real user behind the event, when masquerading a user.';
 $string['privacy:metadata:log:relateduserid'] = 'The ID of a user related to this event';
-$string['privacy:metadata:log:timecreated'] = 'The time at which the event occurred';
+$string['privacy:metadata:log:timecreated'] = 'The time when the event occurred';
 $string['privacy:metadata:log:userid'] = 'The ID of the user who triggered this event';
 $string['read'] = 'Read';
 $string['tablenotfound'] = 'Specified table was not found';
index 5f35555..d40f4cf 100644 (file)
@@ -31,7 +31,7 @@ $string['privacy:metadata:log'] = 'A collection of past events';
 $string['privacy:metadata:log:action'] = 'A description of the action';
 $string['privacy:metadata:log:info'] = 'Additional information';
 $string['privacy:metadata:log:ip'] = 'The IP address used at the time of the event';
-$string['privacy:metadata:log:time'] = 'The date at wich the action took place';
+$string['privacy:metadata:log:time'] = 'The time when the action took place';
 $string['privacy:metadata:log:url'] = 'The URL related to the event';
 $string['privacy:metadata:log:userid'] = 'The ID of the user who performed the action';
 $string['taskcleanup'] = 'Legacy log table cleanup';
index 33c229f..14a1d2b 100644 (file)
@@ -33,6 +33,6 @@ $string['privacy:metadata:log:origin'] = 'The origin of the event';
 $string['privacy:metadata:log:other'] = 'Additional information about the event';
 $string['privacy:metadata:log:realuserid'] = 'The ID of the real user behind the event, when masquerading a user.';
 $string['privacy:metadata:log:relateduserid'] = 'The ID of a user related to this event';
-$string['privacy:metadata:log:timecreated'] = 'The time at which the event occurred';
+$string['privacy:metadata:log:timecreated'] = 'The time when the event occurred';
 $string['privacy:metadata:log:userid'] = 'The ID of the user who triggered this event';
 $string['taskcleanup'] = 'Log table cleanup';
index c847b03..a9c378d 100644 (file)
@@ -96,9 +96,9 @@ $string['oneyear'] = 'One year';
 $string['pluginname'] = 'Inbound message configuration';
 $string['privacy:metadata:coreuserkey'] = 'User\'s keys to validate the email received';
 $string['privacy:metadata:messagelist'] = 'A list of message identifiers which failed validation and requires further authorisation';
-$string['privacy:metadata:messagelist:address'] = 'The address at which the email was sent';
+$string['privacy:metadata:messagelist:address'] = 'The address where the email was sent';
 $string['privacy:metadata:messagelist:messageid'] = 'The message ID';
-$string['privacy:metadata:messagelist:timecreated'] = 'The time at which the record was made';
+$string['privacy:metadata:messagelist:timecreated'] = 'The time when the record was made';
 $string['privacy:metadata:messagelist:userid'] = 'The ID of user who need to approve the message';
 $string['replysubjectprefix'] = 'Re:';
 $string['requirevalidation'] = 'Validate sender address';
index 195af12..f2d08ac 100644 (file)
@@ -84,7 +84,7 @@ $string['mobilefeatures'] = 'Mobile features';
 $string['mobilenotificationsdisabledwarning'] = 'Mobile notifications are not enabled. They should be enabled in Manage message outputs.';
 $string['mobilesettings'] = 'Mobile settings';
 $string['offlineuse'] = 'Offline use';
-$string['pluginname'] = 'Moodle Mobile tools';
+$string['pluginname'] = 'Moodle app tools';
 $string['pluginnotenabledorconfigured'] = 'Plugin not enabled or configured.';
 $string['remoteaddons'] = 'Remote add-ons';
 $string['selfsignedoruntrustedcertificatewarning'] = 'It seems that the HTTPS certificate is self-signed or not trusted. The mobile app will only work with trusted sites.';
index f53af7a..3ff0daf 100644 (file)
@@ -71,10 +71,12 @@ class accept_policy extends \moodleform {
 
         $mform->addElement('hidden', 'returnurl');
         $mform->setType('returnurl', PARAM_LOCALURL);
-
-        $mform->addElement('static', 'user', get_string('acceptanceusers', 'tool_policy'), join(', ', $usernames));
-        $mform->addElement('static', 'policy', get_string('acceptancepolicies', 'tool_policy'),
-            join(', ', $versionnames));
+        $useracceptancelabel = (count($usernames) > 1) ? get_string('acceptanceusers', 'tool_policy') :
+                get_string('user');
+        $mform->addElement('static', 'user', $useracceptancelabel, join(', ', $usernames));
+        $policyacceptancelabel = (count($versionnames) > 1) ? get_string('acceptancepolicies', 'tool_policy') :
+                get_string('policydochdrpolicy', 'tool_policy');
+        $mform->addElement('static', 'policy', $policyacceptancelabel, join(', ', $versionnames));
 
         if ($revoke) {
             $mform->addElement('static', 'ack', '', get_string('revokeacknowledgement', 'tool_policy'));
index 8f2a1d5..3ede09b 100644 (file)
@@ -25,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['acceptanceacknowledgement'] = 'I acknowledge that I have received a request to give consent on behalf of user(s).';
+$string['acceptanceacknowledgement'] = 'I acknowledge that I have received a request to give consent on behalf of the above user(s).';
 $string['acceptancecount'] = '{$a->agreedcount} of {$a->policiescount}';
 $string['acceptancenote'] = 'Remarks';
 $string['acceptancepolicies'] = 'Policies';
@@ -53,7 +53,7 @@ $string['agreepolicies'] = 'Please agree to the following policies';
 $string['backtoprevious'] = 'Go back to previous page';
 $string['backtotop'] = 'Back to top';
 $string['consentbulk'] = 'Consent';
-$string['consentdetails'] = 'Give consent on behalf of user';
+$string['consentdetails'] = 'Give consent on behalf of user(s)';
 $string['consentpagetitle'] = 'Consent';
 $string['contactdpo'] = 'For any questions about the policies please contact the privacy officer.';
 $string['dataproc'] = 'Personal data processing';
@@ -78,7 +78,7 @@ $string['filterpolicy'] = 'Policy: {$a}';
 $string['guestconsent:continue'] = 'Continue';
 $string['guestconsentmessage'] = 'If you continue browsing this website, you agree to our policies:';
 $string['iagree'] = 'I agree to the {$a}';
-$string['iagreetothepolicy'] = 'Give consent on behalf of user';
+$string['iagreetothepolicy'] = 'Give consent';
 $string['inactivate'] = 'Set status to "Inactive"';
 $string['inactivating'] = 'Inactivating a policy';
 $string['inactivatingconfirm'] = '<p>You are about to inactivate policy <em>\'{$a->name}\'</em> version <em>\'{$a->revision}\'</em>.</p>';
@@ -159,7 +159,7 @@ $string['privacy:metadata:versions:contentformat'] = 'The format of the content
 $string['privacysettings'] = 'Privacy settings';
 $string['readpolicy'] = 'Please read our {$a}';
 $string['refertofullpolicytext'] = 'Please refer to the full {$a} if you would like to review the text.';
-$string['revokeacknowledgement'] = 'I acknowledge that I have received a request to withdraw consent on behalf of user(s).';
+$string['revokeacknowledgement'] = 'I acknowledge that I have received a request to withdraw consent on behalf of the above user(s).';
 $string['revokedetails'] = 'Withdraw user consent';
 $string['save'] = 'Save';
 $string['saveasdraft'] = 'Save as draft';
index b8c6d6e..3d436d4 100644 (file)
@@ -58,12 +58,12 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
     And I navigate to "Users > Privacy and policies > Manage policies" in site administration
     And I click on "1 of 4 (25%)" "link" in the "This site policy" "table_row"
     And I click on "Consent not given" "link" in the "User One" "table_row"
-    Then I should see "Give consent on behalf of user"
+    Then I should see "Give consent"
     And I should see "User One"
     And I should see "This site policy"
-    And I should see "I acknowledge that I have received a request to give consent on behalf of user(s)."
+    And I should see "I acknowledge that I have received a request to give consent on behalf of the above user(s)."
     And I set the field "Remarks" to "Consent received from a parent"
-    And I press "Give consent on behalf of user"
+    And I press "Give consent"
     And "Consent given on behalf of user" "icon" should exist in the "User One" "table_row"
     And "Max Manager" "link" should exist in the "User One" "table_row"
     And "Consent received from a parent" "text" should exist in the "User One" "table_row"
@@ -84,12 +84,12 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
     And I navigate to "Users > Privacy and policies > Manage policies" in site administration
     And I click on "1 of 4 (25%)" "link" in the "This site policy" "table_row"
     And I click on "Consent not given" "link" in the "User One" "table_row"
-    Then I should see "Give consent on behalf of user"
+    Then I should see "Give consent"
     And I should see "User One"
     And I should see "This site policy"
-    And I should see "I acknowledge that I have received a request to give consent on behalf of user(s)."
+    And I should see "I acknowledge that I have received a request to give consent on behalf of the above user(s)."
     And I set the field "Remarks" to "Consent received from a parent"
-    And I press "Give consent on behalf of user"
+    And I press "Give consent"
     And "Consent given on behalf of user" "icon" should exist in the "User One" "table_row"
     And "Max Manager" "link" should exist in the "User One" "table_row"
     And "Consent received from a parent" "text" should exist in the "User One" "table_row"
@@ -151,12 +151,12 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
     And I press "Next"
     And I navigate to "Users > Privacy and policies > User agreements" in site administration
     And I click on "Consent not given; click to give consent on behalf of user for This site policy" "link" in the "User One" "table_row"
-    Then I should see "Give consent on behalf of user"
+    Then I should see "Give consent"
     And I should see "User One"
     And I should see "This site policy"
-    And I should see "I acknowledge that I have received a request to give consent on behalf of user(s)."
+    And I should see "I acknowledge that I have received a request to give consent on behalf of the above user(s)."
     And I set the field "Remarks" to "Consent received from a parent"
-    And I press "Give consent on behalf of user"
+    And I press "Give consent"
     And "Consent given on behalf of user" "icon" should exist in the "User One" "table_row"
     And "Consent not given; click to give consent on behalf of user for This privacy policy" "icon" should exist in the "User One" "table_row"
     And I click on "1 of 2" "link" in the "User One" "table_row"
@@ -184,12 +184,12 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
     And I press "Next"
     And I navigate to "Users > Privacy and policies > User agreements" in site administration
     And I click on "Consent not given; click to give consent on behalf of user for This site policy" "link" in the "User One" "table_row"
-    Then I should see "Give consent on behalf of user"
+    Then I should see "Give consent"
     And I should see "User One"
     And I should see "This site policy"
-    And I should see "I acknowledge that I have received a request to give consent on behalf of user(s)."
+    And I should see "I acknowledge that I have received a request to give consent on behalf of the above user(s)."
     And I set the field "Remarks" to "Consent received from a parent"
-    And I press "Give consent on behalf of user"
+    And I press "Give consent"
     And "Consent given on behalf of user" "icon" should exist in the "User One" "table_row"
     And "Consent not given; click to give consent on behalf of user for This privacy policy" "icon" should exist in the "User One" "table_row"
     And I click on "1 of 2" "link" in the "User One" "table_row"
@@ -249,12 +249,12 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
     And I navigate to "Users > Privacy and policies > Manage policies" in site administration
     And I click on "1 of 4 (25%)" "link" in the "This site policy" "table_row"
     And I click on "Consent not given" "link" in the "User One" "table_row"
-    Then I should see "Give consent on behalf of user"
+    Then I should see "Give consent"
     And I should see "User One"
     And I should see "This site policy"
-    And I should see "I acknowledge that I have received a request to give consent on behalf of user(s)."
+    And I should see "I acknowledge that I have received a request to give consent on behalf of the above user(s)."
     And I set the field "Remarks" to "Consent received from a parent"
-    And I press "Give consent on behalf of user"
+    And I press "Give consent"
     And "Consent given on behalf of user" "icon" should exist in the "User One" "table_row"
     And "Max Manager" "link" should not exist in the "User One" "table_row"
     And "Admin User" "link" should exist in the "User One" "table_row"
index 4c4333c..078e046 100644 (file)
@@ -154,7 +154,7 @@ $string['tour1_content_customisation'] = 'To customise the look of your site and
 $string['tour1_title_blockregion'] = 'Block region';
 $string['tour1_content_blockregion'] = 'There is still a block region over here. We recommend removing the Navigation and Administration blocks completely, as all the functionality is elsewhere in the Boost theme.';
 $string['tour1_title_addingblocks'] = 'Adding blocks';
-$string['tour1_content_addingblocks'] = 'In fact, think carefully about including any blocks on your pages. Blocks are not shown on the Moodle Mobile app, so as a general rule it\'s much better to make sure your site works well without any blocks.';
+$string['tour1_content_addingblocks'] = 'In fact, think carefully about including any blocks on your pages. Blocks are not shown in the Moodle app, so as a general rule it\'s much better to make sure your site works well without any blocks.';
 $string['tour1_title_end'] = 'End of tour';
 $string['tour1_content_end'] = 'This is the end of your user tour. It won\'t show again unless you reset it using the link in the footer. As an admin you can also create your own tours like this!';
 
@@ -170,9 +170,9 @@ $string['tour2_content_opendrawer'] = 'Try opening the nav drawer now.';
 $string['tour2_title_participants'] = 'Course participants';
 $string['tour2_content_participants'] = 'View participants here. This is also where you go to add or remove students.';
 $string['tour2_title_addblock'] = 'Add a block';
-$string['tour2_content_addblock'] = 'If you turn editing on you can add blocks from the nav drawer. However, think carefully about including any blocks on your pages. Blocks are not shown on the Moodle Mobile app, so for the best user experience it is better to make sure your course works well without any blocks.';
+$string['tour2_content_addblock'] = 'If you turn editing on you can add blocks from the nav drawer. However, think carefully about including any blocks on your pages. Blocks are not shown in the Moodle app, so for the best user experience it is better to make sure your course works well without any blocks.';
 $string['tour2_title_addingblocks'] = 'Adding blocks';
-$string['tour2_content_addingblocks'] = 'You can add blocks to this page using this button. However, think carefully about including any blocks on your pages. Blocks are not shown on the Moodle Mobile app, so for the best user experience it is better to make sure your course works well without any blocks.';
+$string['tour2_content_addingblocks'] = 'You can add blocks to this page using this button. However, think carefully about including any blocks on your pages. Blocks are not shown in the Moodle app, so for the best user experience it is better to make sure your course works well without any blocks.';
 $string['tour2_title_end'] = 'End of tour';
 $string['tour2_content_end'] = 'This is the end of your user tour. It won\'t show again unless you reset it using the link in the footer. The site admin can also create further tours for this site if required.';
 $string['privacy:metadata:preference:requested'] = 'The time that a user last manually requested a user tour.';
index 5040996..6d92020 100644 (file)
@@ -149,7 +149,7 @@ $string['start_tls'] = 'Use regular LDAP service (port 389) with TLS encryption'
 $string['start_tls_key'] = 'Use TLS';
 $string['updateremfail'] = 'Error updating LDAP record. Error code: {$a->errno}; Error string: {$a->errstring}<br/>Key ({$a->key}) - old moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
 $string['updateremfailamb'] = 'Failed to update LDAP with ambiguous field {$a->key}; old moodle value: \'{$a->ouvalue}\', new value: \'{$a->nuvalue}\'';
-$string['updateremfailfield'] = 'Failed to update LDAP with non-existent field (\'{$a->ldapkey}\'). Key ({$a->key}) - old moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
+$string['updateremfailfield'] = 'Failed to update LDAP with non-existent field (\'{$a->ldapkey}\'). Key ({$a->key}) - old Moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
 $string['updatepasserror'] = 'Error in user_update_password(). Error code: {$a->errno}; Error string: {$a->errstring}';
 $string['updatepasserrorexpire'] = 'Error in user_update_password() when reading password expiry time. Error code: {$a->errno}; Error string: {$a->errstring}';
 $string['updatepasserrorexpiregrace'] = 'Error in user_update_password() when modifying expirationtime and/or gracelogins. Error code: {$a->errno}; Error string: {$a->errstring}';
index 2e53d97..b81c645 100644 (file)
@@ -135,6 +135,25 @@ function blog_remove_associations_for_course($courseid) {
     $DB->delete_records('blog_association', array('contextid' => $context->id));
 }
 
+/**
+ * Remove module associated blogs and blog tag instances.
+ *
+ * @param  int $modcontextid Module context ID.
+ */
+function blog_remove_associations_for_module($modcontextid) {
+    global $DB;
+
+    if (!empty($assocblogids = $DB->get_fieldset_select('blog_association', 'blogid',
+        'contextid = :contextid', ['contextid' => $modcontextid]))) {
+        list($sql, $params) = $DB->get_in_or_equal($assocblogids, SQL_PARAMS_NAMED);
+
+        $DB->delete_records_select('tag_instance', "itemid $sql", $params);
+        $DB->delete_records_select('post', "id $sql AND module = :module",
+            array_merge($params, ['module' => 'blog']));
+        $DB->delete_records('blog_association', ['contextid' => $modcontextid]);
+    }
+}
+
 /**
  * Given a record in the {blog_external} table, checks the blog's URL
  * for new entries not yet copied into Moodle.
index 770b5c7..d20cfb3 100644 (file)
@@ -255,6 +255,9 @@ class event_exporter_base extends exporter {
         $values['iscourseevent'] = false;
         $values['iscategoryevent'] = false;
         if ($moduleproxy = $event->get_course_module()) {
+            // We need a separate property to flag if an event is action event.
+            // That's required because canedit return true but action action events cannot be edited on the calendar UI.
+            // But they are considered editable because you can drag and drop the event on the month view.
             $values['isactionevent'] = true;
         } else if ($event->get_type() == 'course') {
             $values['iscourseevent'] = true;
index 3d8be58..2a0bc58 100644 (file)
@@ -426,11 +426,20 @@ class calendar_event {
      * Pass in a object containing the event properties and this function will
      * insert it into the database and deal with any associated files
      *
+     * Capability checking should be performed if the user is directly manipulating the event
+     * and no other capability has been tested. However if the event is not being manipulated
+     * directly by the user and another capability has been checked for them to do this then
+     * capabilites should not be checked.
+     *
+     * For example if a user is editing an event in the calendar the check should be true,
+     * but if you are updating an event in an activities settings are changed then the calendar
+     * capabilites should not be checked.
+     *
      * @see self::create()
      * @see self::update()
      *
      * @param \stdClass $data object of event
-     * @param bool $checkcapability if moodle should check calendar managing capability or not
+     * @param bool $checkcapability If Moodle should check the user can manage the calendar events for this call or not.
      * @return bool event updated
      */
     public function update($data, $checkcapability=true) {
@@ -914,10 +923,19 @@ class calendar_event {
     }
 
     /**
-     * Creates a new event and returns an event object
+     * Creates a new event and returns an event object.
+     *
+     * Capability checking should be performed if the user is directly creating the event
+     * and no other capability has been tested. However if the event is not being created
+     * directly by the user and another capability has been checked for them to do this then
+     * capabilites should not be checked.
+     *
+     * For example if a user is creating an event in the calendar the check should be true,
+     * but if you are creating an event in an activity when it is created then the calendar
+     * capabilites should not be checked.
      *
      * @param \stdClass|array $properties An object containing event properties
-     * @param bool $checkcapability Check caps or not
+     * @param bool $checkcapability If Moodle should check the user can manage the calendar events for this call or not.
      * @throws \coding_exception
      *
      * @return calendar_event|bool The event object or false if it failed
index 157a652..109d86c 100644 (file)
                             {{#pix}}t/delete, core, {{#str}}delete{{/str}}{{/pix}}
                         </a>
                     {{/candelete}}
-                    <a href="{{editurl}}" data-action="edit">
-                        {{#pix}}t/edit, core, {{#str}}edit{{/str}}{{/pix}}
-                    </a>
+                    {{^isactionevent}}
+                        <a href="{{editurl}}" data-action="edit">
+                            {{#pix}}t/edit, core, {{#str}}edit{{/str}}{{/pix}}
+                        </a>
+                    {{/isactionevent}}
                 {{/canedit}}
             </div>
             {{#icon}}<div class="d-inline-block mt-1 align-top">{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}</div>{{/icon}}
index f8040c5..b569e37 100644 (file)
@@ -94,7 +94,7 @@ class api {
                 $event->timeduration = 0;
 
                 $calendarevent = \calendar_event::load($event->id);
-                $calendarevent->update($event);
+                $calendarevent->update($event, false);
             } else {
                 // Calendar event is no longer needed.
                 $calendarevent = \calendar_event::load($event->id);
@@ -115,7 +115,7 @@ class api {
                 $event->visible = instance_is_visible($modulename, $instance);
                 $event->timeduration = 0;
 
-                \calendar_event::create($event);
+                \calendar_event::create($event, false);
             }
         }
 
diff --git a/completion/tests/behat/completion_no_calendar_capabilities.feature b/completion/tests/behat/completion_no_calendar_capabilities.feature
new file mode 100644 (file)
index 0000000..f690f76
--- /dev/null
@@ -0,0 +1,44 @@
+@core @core_completion
+Feature: Completion with no calendar capabilites
+  In order to allow work effectively
+  As a teacher
+  I need to be able to create activities with completion enabled without calendar capabilities
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode | enablecompletion |
+      | Course 1 | C1 | 0 | 1 | 1 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "admin"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Permissions" in current page administration
+    And I override the system permissions of "Teacher" role with:
+      | capability | permission |
+      | moodle/calendar:manageentries | Prohibit |
+    And I log out
+
+  Scenario: Editing completion date
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    When I add a "Forum" to section "1" and I fill the form with:
+      | Forum name | Test forum name |
+      | Description | Test forum description |
+      | Completion tracking | Show activity as complete when conditions are met |
+      | id_completionexpected_enabled | 1 |
+      | id_completionexpected_day | 1 |
+      | id_completionexpected_month | 1 |
+      | id_completionexpected_year | 2017 |
+    And I log out
+    When I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I follow "Test forum name"
+    And I navigate to "Edit settings" in current page administration
+    And I set the following fields to these values:
+      | id_completionexpected_year | 2018 |
+    And I press "Save and return to course"
+    Then I should see "Test forum name"
diff --git a/completion/tests/capabilities_test.php b/completion/tests/capabilities_test.php
new file mode 100644 (file)
index 0000000..5dd4239
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tests that completion works without requiring unnecessary capabilities.
+ *
+ * @package    core_completion
+ * @copyright  2018 University of Nottingham
+ * @author     Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tests that completion works without requiring unnecessary capabilities.
+ *
+ * @package    core_completion
+ * @copyright  2018 University of Nottingham
+ * @author     Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_completion_capabilities_testcase extends advanced_testcase {
+    /**
+     * A user who does not have capabilities to add events to the calendar should be able to create activities.
+     */
+    public function test_creation_with_no_calendar_capabilities() {
+        $this->resetAfterTest();
+        $course = self::getDataGenerator()->create_course(['enablecompletion' => 1]);
+        $context = context_course::instance($course->id);
+        $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $roleid = self::getDataGenerator()->create_role();
+        self::getDataGenerator()->role_assign($roleid, $user->id, $context->id);
+        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
+        $generator = self::getDataGenerator()->get_plugin_generator('mod_forum');
+        // Create an instance as a user without the calendar capabilities.
+        $this->setUser($user);
+        $params = array(
+            'course' => $course->id,
+            'completionexpected' => time() + 2000,
+        );
+        $generator->create_instance($params);
+    }
+}
index 904a587..2875e39 100644 (file)
Binary files a/course/amd/build/actions.min.js and b/course/amd/build/actions.min.js differ
index 9a2a3c7..76d4232 100644 (file)
@@ -588,9 +588,10 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
                 // Add a handler for "Add sections" link to ask for a number of sections to add.
                 str.get_string('numberweeks').done(function(strNumberSections) {
                     var trigger = $(SELECTOR.ADDSECTIONS),
-                        modalTitle = trigger.attr('data-add-sections');
+                        modalTitle = trigger.attr('data-add-sections'),
+                        newSections = trigger.attr('new-sections');
                     var modalBody = $('<div><label for="add_section_numsections"></label> ' +
-                        '<input id="add_section_numsections" type="number" min="1" value="1"></div>');
+                        '<input id="add_section_numsections" type="number" min="1" max="' + newSections + '" value="1"></div>');
                     modalBody.find('label').html(strNumberSections);
                     ModalFactory.create({
                         title: modalTitle,
index c3d2a55..8f6ea28 100644 (file)
@@ -45,6 +45,30 @@ require_login($course);
 require_capability('moodle/course:update', context_course::instance($course->id));
 require_sesskey();
 
+$desirednumsections = 0;
+$courseformat = course_get_format($course);
+$lastsectionnumber = $courseformat->get_last_section_number();
+$maxsections = $courseformat->get_max_sections();
+
+if (isset($courseformatoptions['numsections']) && $increase !== null) {
+    $desirednumsections = $courseformatoptions['numsections'] + 1;
+} else if (course_get_format($course)->uses_sections() && $insertsection !== null) {
+    // Count the sections in the course.
+    $desirednumsections = $lastsectionnumber + $numsections;
+}
+
+if ($desirednumsections > $maxsections) {
+    // Increase in number of sections is not allowed.
+    \core\notification::warning(get_string('maxsectionslimit', 'moodle', $maxsections));
+    $increase = null;
+    $insertsection = null;
+    $numsections = 0;
+
+    if (!$returnurl) {
+        $returnurl = course_get_url($course);
+    }
+}
+
 if (isset($courseformatoptions['numsections']) && $increase !== null) {
     if ($increase) {
         // Add an additional section.
index 300af2c..40bef75 100644 (file)
@@ -571,47 +571,14 @@ class core_course_management_renderer extends plugin_renderer_base {
             $html .= html_writer::div($str, 'listing-pagination-totals dimmed');
         }
 
-        if ($totalcourses <= $perpage) {
-            return $html;
-        }
-        $aside = 2;
-        $span = $aside * 2 + 1;
-        $start = max($page - $aside, 0);
-        $end = min($page + $aside, $totalpages - 1);
-        if (($end - $start) < $span) {
-            if ($start == 0) {
-                $end = min($totalpages - 1, $span - 1);
-            } else if ($end == ($totalpages - 1)) {
-                $start = max(0, $end - $span + 1);
-            }
-        }
-        $items = array();
         if ($viewmode !== 'default') {
             $baseurl = new moodle_url('/course/management.php', array('categoryid' => $category->id,
                 'view' => $viewmode));
         } else {
             $baseurl = new moodle_url('/course/management.php', array('categoryid' => $category->id));
         }
-        if ($page > 0) {
-            $items[] = $this->action_button(new moodle_url($baseurl, array('page' => 0)), get_string('first'));
-            $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $page - 1)), get_string('prev'));
-            $items[] = '...';
-        }
-        for ($i = $start; $i <= $end; $i++) {
-            $class = '';
-            if ($page == $i) {
-                $class = 'active-page';
-            }
-            $pageurl = new moodle_url($baseurl, array('page' => $i));
-            $items[] = $this->action_button($pageurl, $i + 1, null, $class, get_string('pagea', 'moodle', $i+1));
-        }
-        if ($page < ($totalpages - 1)) {
-            $items[] = '...';
-            $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $page + 1)), get_string('next'));
-            $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $totalpages - 1)), get_string('last'));
-        }
 
-        $html .= html_writer::div(join('', $items), 'listing-pagination');
+        $html .= $this->output->paging_bar($totalcourses, $page, $perpage, $baseurl);
         return $html;
     }
 
@@ -1055,7 +1022,7 @@ class core_course_management_renderer extends plugin_renderer_base {
         }
 
         $menu = new action_menu;
-        $menu->attributes['class'] .= ' view-mode-selector vms';
+        $menu->attributes['class'] .= ' view-mode-selector vms ml-1';
 
         $selected = null;
         foreach ($modes as $mode => $modestr) {
@@ -1079,7 +1046,7 @@ class core_course_management_renderer extends plugin_renderer_base {
 
         $menu->set_menu_trigger($selected);
 
-        $html = html_writer::start_div('view-mode-selector vms');
+        $html = html_writer::start_div('view-mode-selector vms d-flex');
         $html .= get_string('viewing').' '.$this->render($menu);
         $html .= html_writer::end_div();
 
index f70371c..0ba58c6 100644 (file)
@@ -287,6 +287,18 @@ abstract class format_base {
         return (int)max(array_keys($sections));
     }
 
+    /**
+     * Method used to get the maximum number of sections for this course format.
+     * @return int
+     */
+    public function get_max_sections() {
+        $maxsections = get_config('moodlecourse', 'maxsections');
+        if (!isset($maxsections) || !is_numeric($maxsections)) {
+            $maxsections = 52;
+        }
+        return $maxsections;
+    }
+
     /**
      * Returns true if the course has a front page.
      *
index 357c0a3..45ff162 100644 (file)
@@ -952,7 +952,10 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
             return '';
         }
 
-        $options = course_get_format($course)->get_format_options();
+        $format = course_get_format($course);
+        $options = $format->get_format_options();
+        $maxsections = $format->get_max_sections();
+        $lastsection = $format->get_last_section_number();
         $supportsnumsections = array_key_exists('numsections', $options);
 
         if ($supportsnumsections) {
@@ -963,13 +966,15 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
             echo html_writer::start_tag('div', array('id' => 'changenumsections', 'class' => 'mdl-right'));
 
             // Increase number of sections.
-            $straddsection = get_string('increasesections', 'moodle');
-            $url = new moodle_url('/course/changenumsections.php',
-                array('courseid' => $course->id,
-                      'increase' => true,
-                      'sesskey' => sesskey()));
-            $icon = $this->output->pix_icon('t/switch_plus', $straddsection);
-            echo html_writer::link($url, $icon.get_accesshide($straddsection), array('class' => 'increase-sections'));
+            if ($lastsection < $maxsections) {
+                $straddsection = get_string('increasesections', 'moodle');
+                $url = new moodle_url('/course/changenumsections.php',
+                    array('courseid' => $course->id,
+                          'increase' => true,
+                          'sesskey' => sesskey()));
+                $icon = $this->output->pix_icon('t/switch_plus', $straddsection);
+                echo html_writer::link($url, $icon.get_accesshide($straddsection), array('class' => 'increase-sections'));
+            }
 
             if ($course->numsections > 0) {
                 // Reduce number of sections sections.
@@ -985,11 +990,14 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
             echo html_writer::end_tag('div');
 
         } else if (course_get_format($course)->uses_sections()) {
+            if ($lastsection >= $maxsections) {
+                // Don't allow more sections if we already hit the limit.
+                return;
+            }
             // Current course format does not have 'numsections' option but it has multiple sections suppport.
             // Display the "Add section" link that will insert a section in the end.
             // Note to course format developers: inserting sections in the other positions should check both
             // capabilities 'moodle/course:update' and 'moodle/course:movesections'.
-
             echo html_writer::start_tag('div', array('id' => 'changenumsections', 'class' => 'mdl-right'));
             if (get_string_manager()->string_exists('addsections', 'format_'.$course->format)) {
                 $straddsections = get_string('addsections', 'format_'.$course->format);
@@ -1002,8 +1010,9 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                 $url->param('sectionreturn', $sectionreturn);
             }
             $icon = $this->output->pix_icon('t/add', $straddsections);
+            $newsections = $maxsections - $lastsection;
             echo html_writer::link($url, $icon . $straddsections,
-                array('class' => 'add-sections', 'data-add-sections' => $straddsections));
+                array('class' => 'add-sections', 'data-add-sections' => $straddsections, 'new-sections' => $newsections));
             echo html_writer::end_tag('div');
         }
     }
index bf54e66..aba5bdc 100644 (file)
@@ -1169,6 +1169,9 @@ function course_delete_module($cmid, $async = false) {
         }
     }
 
+    // Delete associated blogs and blog tag instances.
+    blog_remove_associations_for_module($modcontext->id);
+
     // Delete completion and availability data; it is better to do this even if the
     // features are not turned on, in case they were turned on previously (these will be
     // very quick on an empty table).
index f09cae2..5bef53f 100644 (file)
@@ -385,7 +385,7 @@ Feature: Course category management interface performs as expected
     And I should see course listing "Course 9" before "Course 10"
     And I should see course listing "Course 10" before "Course 11"
     And I should see course listing "Course 11" before "Course 12"
-    And "#course-listing .listing-pagination" "css_element" should not exist
+    And "#course-listing .pagination" "css_element" should not exist
     And I click on "Per page: 20" "link" in the ".course-listing-actions" "css_element"
     And I should see "5" in the ".courses-per-page" "css_element"
     And I should see "10" in the ".courses-per-page" "css_element"
@@ -407,16 +407,15 @@ Feature: Course category management interface performs as expected
     And I should not see "Course 10"
     And I should not see "Course 11"
     And I should not see "Course 12"
-    And "#course-listing .listing-pagination" "css_element" should exist
+    And "#course-listing .pagination" "css_element" should exist
     And I should see "Showing courses 1 to 5 of 12 courses"
-    And I should not see "First" in the "#course-listing .listing-pagination" "css_element"
-    And I should not see "Prev" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "1" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "2" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "3" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "Next" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "Last" in the "#course-listing .listing-pagination" "css_element"
-    And I click on "2" "link" in the "#course-listing .listing-pagination" "css_element"
+    And I should not see "First" in the "#course-listing .pagination" "css_element"
+    And I should not see "Prev" in the "#course-listing .pagination" "css_element"
+    And I should see "1" in the "#course-listing .pagination" "css_element"
+    And I should see "2" in the "#course-listing .pagination" "css_element"
+    And I should see "3" in the "#course-listing .pagination" "css_element"
+    And I should see "Next" in the "#course-listing .pagination" "css_element"
+    And I click on "2" "link" in the "#course-listing .pagination" "css_element"
     And a new page should have loaded since I started watching
     And I start watching to see if a new page loads
     And I should see the "Course categories and courses" management page
@@ -432,16 +431,14 @@ Feature: Course category management interface performs as expected
     And I should see course listing "Course 9" before "Course 10"
     And I should not see "Course 11"
     And I should not see "Course 12"
-    And "#course-listing .listing-pagination" "css_element" should exist
+    And "#course-listing .pagination" "css_element" should exist
     And I should see "Showing courses 6 to 10 of 12 courses"
-    And I should see "First" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "Prev" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "1" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "2" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "3" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "Next" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "Last" in the "#course-listing .listing-pagination" "css_element"
-    And I click on "Next" "link" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "Prev" in the "#course-listing .pagination" "css_element"
+    And I should see "1" in the "#course-listing .pagination" "css_element"
+    And I should see "2" in the "#course-listing .pagination" "css_element"
+    And I should see "3" in the "#course-listing .pagination" "css_element"
+    And I should see "Next" in the "#course-listing .pagination" "css_element"
+    And I click on "Next" "link" in the "#course-listing .pagination" "css_element"
     And a new page should have loaded since I started watching
     And I start watching to see if a new page loads
     And I should see the "Course categories and courses" management page
@@ -457,66 +454,14 @@ Feature: Course category management interface performs as expected
     And I should not see "Course 9" in the "#course-listing" "css_element"
     And I should not see "Course 10" in the "#course-listing" "css_element"
     And I should see course listing "Course 11" before "Course 12"
-    And "#course-listing .listing-pagination" "css_element" should exist
+    And "#course-listing .pagination" "css_element" should exist
     And I should see "Showing courses 11 to 12 of 12 courses"
-    And I should see "First" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "Prev" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "1" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "2" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "3" in the "#course-listing .listing-pagination" "css_element"
-    And I should not see "Next" in the "#course-listing .listing-pagination" "css_element"
-    And I should not see "Last" in the "#course-listing .listing-pagination" "css_element"
-    And I click on "First" "link" in the "#course-listing .listing-pagination" "css_element"
-    And a new page should have loaded since I started watching
-    And I start watching to see if a new page loads
-    And I should see the "Course categories and courses" management page
-    And I should see "Per page: 5" in the ".course-listing-actions" "css_element"
-    And I should see course listing "Course 1" before "Course 2"
-    And I should see course listing "Course 2" before "Course 3"
-    And I should see course listing "Course 3" before "Course 4"
-    And I should see course listing "Course 4" before "Course 5"
-    And I should not see "Course 6" in the "#course-listing" "css_element"
-    And I should not see "Course 7" in the "#course-listing" "css_element"
-    And I should not see "Course 8" in the "#course-listing" "css_element"
-    And I should not see "Course 9" in the "#course-listing" "css_element"
-    And I should not see "Course 10" in the "#course-listing" "css_element"
-    And I should not see "Course 11" in the "#course-listing" "css_element"
-    And I should not see "Course 12" in the "#course-listing" "css_element"
-    And "#course-listing .listing-pagination" "css_element" should exist
-    And I should see "Showing courses 1 to 5 of 12 courses"
-    And I should not see "First" in the "#course-listing .listing-pagination" "css_element"
-    And I should not see "Prev" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "1" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "2" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "3" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "Next" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "Last" in the "#course-listing .listing-pagination" "css_element"
-    And I click on "Last" "link" in the "#course-listing .listing-pagination" "css_element"
-    And a new page should have loaded since I started watching
-    And I start watching to see if a new page loads
-    And I should see the "Course categories and courses" management page
-    And I should see "Per page: 5" in the ".course-listing-actions" "css_element"
-    And I should see "Course 11" in the "#course-listing" "css_element"
-    And I should not see "Course 2" in the "#course-listing" "css_element"
-    And I should not see "Course 3" in the "#course-listing" "css_element"
-    And I should not see "Course 4" in the "#course-listing" "css_element"
-    And I should not see "Course 5" in the "#course-listing" "css_element"
-    And I should not see "Course 6" in the "#course-listing" "css_element"
-    And I should not see "Course 7" in the "#course-listing" "css_element"
-    And I should not see "Course 8" in the "#course-listing" "css_element"
-    And I should not see "Course 9" in the "#course-listing" "css_element"
-    And I should not see "Course 10" in the "#course-listing" "css_element"
-    And I should see course listing "Course 11" before "Course 12"
-    And "#course-listing .listing-pagination" "css_element" should exist
-    And I should see "Showing courses 11 to 12 of 12 courses"
-    And I should see "First" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "Prev" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "1" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "2" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "3" in the "#course-listing .listing-pagination" "css_element"
-    And I should not see "Next" in the "#course-listing .listing-pagination" "css_element"
-    And I should not see "Last" in the "#course-listing .listing-pagination" "css_element"
-    And I click on "Prev" "link" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "Prev" in the "#course-listing .pagination" "css_element"
+    And I should see "1" in the "#course-listing .pagination" "css_element"
+    And I should see "2" in the "#course-listing .pagination" "css_element"
+    And I should see "3" in the "#course-listing .pagination" "css_element"
+    And I should not see "Next" in the "#course-listing .pagination" "css_element"
+    And I click on "Prev" "link" in the "#course-listing .pagination" "css_element"
     And a new page should have loaded since I started watching
     And I should see the "Course categories and courses" management page
     And I should see "Per page: 5" in the ".course-listing-actions" "css_element"
@@ -531,15 +476,13 @@ Feature: Course category management interface performs as expected
     And I should see course listing "Course 9" before "Course 10"
     And I should not see "Course 11"
     And I should not see "Course 12"
-    And "#course-listing .listing-pagination" "css_element" should exist
+    And "#course-listing .pagination" "css_element" should exist
     And I should see "Showing courses 6 to 10 of 12 courses"
-    And I should see "First" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "Prev" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "1" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "2" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "3" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "Next" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "Last" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "Prev" in the "#course-listing .pagination" "css_element"
+    And I should see "1" in the "#course-listing .pagination" "css_element"
+    And I should see "2" in the "#course-listing .pagination" "css_element"
+    And I should see "3" in the "#course-listing .pagination" "css_element"
+    And I should see "Next" in the "#course-listing .pagination" "css_element"
 
   Scenario: Test pagination is only shown when required
     Given the following "categories" exist:
@@ -566,7 +509,7 @@ Feature: Course category management interface performs as expected
     And I should see course listing "Course 2" before "Course 3"
     And I should see course listing "Course 3" before "Course 4"
     And I should see course listing "Course 4" before "Course 5"
-    And "#course-listing .listing-pagination" "css_element" should not exist
+    And "#course-listing .pagination" "css_element" should not exist
     And I click on "5" "link" in the ".course-listing-actions" "css_element"
     # Redirect
     And I should see "Per page: 5" in the ".course-listing-actions" "css_element"
@@ -574,7 +517,7 @@ Feature: Course category management interface performs as expected
     And I should see course listing "Course 2" before "Course 3"
     And I should see course listing "Course 3" before "Course 4"
     And I should see course listing "Course 4" before "Course 5"
-    And "#course-listing .listing-pagination" "css_element" should not exist
+    And "#course-listing .pagination" "css_element" should not exist
 
   # We need at least 30 courses for this next test.
   @javascript
@@ -645,7 +588,7 @@ Feature: Course category management interface performs as expected
     And I should see course listing "Course 19" before "Course 20"
     And I should see course listing "Course 21" before "Course 22"
     And I should see course listing "Course 31" before "Course 32"
-    And "#course-listing .listing-pagination" "css_element" should not exist
+    And "#course-listing .pagination" "css_element" should not exist
     And I click on "Per page: 100" "link" in the ".course-listing-actions" "css_element"
     And I click on "5" "link" in the ".courses-per-page" "css_element"
     And a new page should have loaded since I started watching
@@ -656,37 +599,9 @@ Feature: Course category management interface performs as expected
     And I should see course listing "Course 4" before "Course 5"
     And I should not see "Course 6"
     And I should see "Showing courses 1 to 5 of 32 courses"
-    And I should not see "First" in the "#course-listing .listing-pagination" "css_element"
-    And I should not see "Prev" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "1" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "2" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "3" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "4" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "5" in the "#course-listing .listing-pagination" "css_element"
-    And I should not see "6" in the "#course-listing .listing-pagination" "css_element"
-    And I should not see "7" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "Next" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "Last" in the "#course-listing .listing-pagination" "css_element"
-    And I click on "Last" "link" in the "#course-listing .listing-pagination" "css_element"
-    And a new page should have loaded since I started watching
-    And I start watching to see if a new page loads
-    And I should see the "Course categories and courses" management page
-    And I should see "Per page: 5" in the ".course-listing-actions" "css_element"
-    And I should not see "Course 30"
-    And I should see course listing "Course 31" before "Course 32"
-    And I should see "Showing courses 31 to 32 of 32 courses"
-    And I should see "First" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "Prev" in the "#course-listing .listing-pagination" "css_element"
-    And I should not see "1" in the "#course-listing .listing-pagination" "css_element"
-    And I should not see "2" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "3" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "4" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "5" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "6" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "7" in the "#course-listing .listing-pagination" "css_element"
-    And I should not see "Next" in the "#course-listing .listing-pagination" "css_element"
-    And I should not see "Last" in the "#course-listing .listing-pagination" "css_element"
-    And I click on "4" "link" in the "#course-listing .listing-pagination" "css_element"
+    And I should not see "Prev" in the "#course-listing .pagination" "css_element"
+    And I should see "Next" in the "#course-listing .pagination" "css_element"
+    And I click on "4" "link" in the "#course-listing .pagination" "css_element"
     And a new page should have loaded since I started watching
     And I should see the "Course categories and courses" management page
     And I should see "Per page: 5" in the ".course-listing-actions" "css_element"
@@ -697,17 +612,6 @@ Feature: Course category management interface performs as expected
     And I should see course listing "Course 19" before "Course 20"
     And I should not see "Course 21"
     And I should see "Showing courses 16 to 20 of 32 courses"
-    And I should see "First" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "Prev" in the "#course-listing .listing-pagination" "css_element"
-    And I should not see "1" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "2" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "3" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "4" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "5" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "6" in the "#course-listing .listing-pagination" "css_element"
-    And I should not see "7" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "Next" in the "#course-listing .listing-pagination" "css_element"
-    And I should see "Last" in the "#course-listing .listing-pagination" "css_element"
 
   Scenario: Test clicking to edit a course.
     Given the following "categories" exist:
index 249948d..ce18504 100644 (file)
@@ -303,6 +303,50 @@ class core_course_courselib_testcase extends advanced_testcase {
         return $moduleinfo;
     }
 
+    /**
+     * Create module associated blog and tags.
+     *
+     * @param object $course Course.
+     * @param object $modulecontext The context of the module.
+     */
+    private function create_module_asscociated_blog($course, $modulecontext) {
+        global $DB, $CFG;
+
+        // Create default group.
+        $group = new stdClass();
+        $group->courseid = $course->id;
+        $group->name = 'Group';
+        $group->id = $DB->insert_record('groups', $group);
+
+        // Create default user.
+        $user = $this->getDataGenerator()->create_user(array(
+            'username' => 'testuser',
+            'firstname' => 'Firsname',
+            'lastname' => 'Lastname'
+        ));
+
+        // Create default post.
+        $post = new stdClass();
+        $post->userid = $user->id;
+        $post->groupid = $group->id;
+        $post->content = 'test post content text';
+        $post->module = 'blog';
+        $post->id = $DB->insert_record('post', $post);
+
+        // Create default tag.
+        $tag = $this->getDataGenerator()->create_tag(array('userid' => $user->id,
+            'rawname' => 'Testtagname', 'isstandard' => 1));
+        // Apply the tag to the blog.
+        $DB->insert_record('tag_instance', array('tagid' => $tag->id, 'itemtype' => 'user',
+            'component' => 'core', 'itemid' => $post->id, 'ordering' => 0));
+
+        require_once($CFG->dirroot . '/blog/locallib.php');
+        $blog = new blog_entry($post->id);
+        $blog->add_association($modulecontext->id);
+
+        return $blog;
+    }
+
     /**
      * Test create_module() for multiple modules defined in the $modules array (first declaration of the function).
      */
@@ -1521,6 +1565,8 @@ class core_course_courselib_testcase extends advanced_testcase {
         // Get the module context.
         $modcontext = context_module::instance($module->cmid);
 
+        $assocblog = $this->create_module_asscociated_blog($course, $modcontext);
+
         // Verify context exists.
         $this->assertInstanceOf('context_module', $modcontext);
 
@@ -1565,6 +1611,18 @@ class core_course_courselib_testcase extends advanced_testcase {
         $cmcount = $DB->count_records('course_modules', array('id' => $module->cmid));
         $this->assertEmpty($cmcount);
 
+        // Verify the blog_association record has been deleted.
+        $this->assertCount(0, $DB->get_records('blog_association',
+                array('contextid' => $modcontext->id)));
+
+        // Verify the blog post record has been deleted.
+        $this->assertCount(0, $DB->get_records('post',
+                array('id' => $assocblog->id)));
+
+        // Verify the tag instance record has been deleted.
+        $this->assertCount(0, $DB->get_records('tag_instance',
+                array('itemid' => $assocblog->id)));
+
         // Test clean up of module specific messes.
         switch ($type) {
             case 'assign':
index 508fa9a..9e031fc 100644 (file)
@@ -63,10 +63,10 @@ It could look something like this:
    add, student, 21, CF101, 1091115000, 1091215000
 </pre>';
 $string['privacy:metadata:enrol_flatfile'] = 'The Flat file (CSV) enrolment plugin may store personal data relating to future enrolments in the enrol_flatfile table.';
-$string['privacy:metadata:enrol_flatfile:action'] = 'The enrolment action expected at the given date.';
+$string['privacy:metadata:enrol_flatfile:action'] = 'The enrolment action expected on the given date';
 $string['privacy:metadata:enrol_flatfile:courseid'] = 'The course ID to which the enrolment relates';
 $string['privacy:metadata:enrol_flatfile:roleid'] = 'The ID of the role to be assigned or unassigned';
-$string['privacy:metadata:enrol_flatfile:timestart'] = 'The time at which the enrolment change starts.';
-$string['privacy:metadata:enrol_flatfile:timeend'] = 'The time at which the enrolment change ends.';
-$string['privacy:metadata:enrol_flatfile:timemodified'] = 'The modification time of this enrolment change.';
+$string['privacy:metadata:enrol_flatfile:timestart'] = 'The time when the enrolment starts';
+$string['privacy:metadata:enrol_flatfile:timeend'] = 'The time when the enrolment ends';
+$string['privacy:metadata:enrol_flatfile:timemodified'] = 'The time when the enrolment is modified';
 $string['privacy:metadata:enrol_flatfile:userid'] = 'The ID of the user to which the role assignment relates';
index 82938b4..7f62bb6 100644 (file)
@@ -66,8 +66,8 @@ $string['pluginname_desc'] = 'The \'Publish as LTI tool\' plugin, together with
 $string['privacy:metadata:enrol_lti_users'] = 'The list of users enrolled via an LTI provider';
 $string['privacy:metadata:enrol_lti_users:userid'] = 'The ID of the user';
 $string['privacy:metadata:enrol_lti_users:lastgrade'] = 'The last grade the user was recorded of having';
-$string['privacy:metadata:enrol_lti_users:lastaccess'] = 'The date at which the user was enrolled';
-$string['privacy:metadata:enrol_lti_users:timecreated'] = 'The date at which the user was enrolled';
+$string['privacy:metadata:enrol_lti_users:lastaccess'] = 'The time when the user last accessed the course';
+$string['privacy:metadata:enrol_lti_users:timecreated'] = 'The time when the user was enrolled';
 $string['registration'] = 'Published tool registration';
 $string['registrationurl'] = 'Registration URL';
 $string['registrationurl_help'] = 'If a registration URL (also called proxy URL) is used, then the tool is automatically configured.';
index 52cf068..cf5bf96 100644 (file)
@@ -338,17 +338,8 @@ Mathematics Tools</a> forum in the Using Moodle course on moodle.org.</li>
 running Unix, a likely cause is that the mimetex binary you are using is
 incompatible with your operating system. You can try compiling it from the
 C sources downloaded from <a href="http://www.forkosh.com/mimetex.zip">
-http://www.forkosh.com/mimetex.zip</a>, or looking for an appropriate
-binary at <a href="http://moodle.org/download/mimetex/">
-http://moodle.org/download/mimetex/</a>. You may then also need to
-edit your moodle/filter/algebra/pix.php file to add
-<br /><?php echo "case &quot;" . PHP_OS . "&quot;:" ;?><br ?> to the list of operating systems
-in the switch (PHP_OS) statement. Windows users may have a problem properly
-unzipping mimetex.exe. Make sure that mimetex.exe is is <b>PRECISELY</b>
-433152 bytes in size. If not, download fresh copy from
-<a href="http://moodle.org/download/mimetex/windows/mimetex.exe">
-http://moodle.org/download/mimetex/windows/mimetex.exe</a>. Lastly check
-the execute permissions on your mimetex binary, as outlined in item 2 above.</li>
+http://www.forkosh.com/mimetex.zip</a>. Lastly check the execute permissions
+on your mimetex binary, as outlined in item 2 above.</li>
 </ol>
 </body>
 </html>
index 4aad3ec..f579fa5 100644 (file)
@@ -29,15 +29,6 @@ defined('MOODLE_INTERNAL') || die();
 function filter_tex_get_executable($debug=false) {
     global $CFG;
 
-    $error_message1 = "Your system is not configured to run mimeTeX. You need to download the appropriate<br />"
-                     ."executable for you ".PHP_OS." platform from <a href=\"http://moodle.org/download/mimetex/\">"
-                     ."http://moodle.org/download/mimetex/</a>, or obtain the C source<br /> "
-                     ."from <a href=\"http://www.forkosh.com/mimetex.zip\">"
-                     ."http://www.forkosh.com/mimetex.zip</a>, compile it and "
-                     ."put the executable into your<br /> moodle/filter/tex/ directory.";
-
-    $error_message2 = "Custom mimetex is not executable!<br /><br />";
-
     if ((PHP_OS == "WINNT") || (PHP_OS == "WIN32") || (PHP_OS == "Windows")) {
         return "$CFG->dirroot/filter/tex/mimetex.exe";
     }
index e713d57..dadbe18 100644 (file)
@@ -377,16 +377,7 @@ If this fails or is not available, the Mimetex executable is tried. If this
 fails a likely cause is that the mimetex binary you are using is
 incompatible with your operating system. You can try compiling it from the
 C sources downloaded from <a href="http://www.forkosh.com/mimetex.zip">
-http://www.forkosh.com/mimetex.zip</a>, or looking for an appropriate
-binary at <a href="http://moodle.org/download/mimetex/">
-http://moodle.org/download/mimetex/</a>. You may then also need to
-edit your moodle/filter/tex/pix.php file to add
-<br /><?php echo "case &quot;" . PHP_OS . "&quot;:" ;?><br ?> to the list of operating systems
-in the switch (PHP_OS) statement. Windows users may have a problem properly
-unzipping mimetex.exe. Make sure that mimetex.exe is is <b>PRECISELY</b>
-433152 bytes in size. If not, download a fresh copy from
-<a href="http://moodle.org/download/mimetex/windows/mimetex.exe">
-http://moodle.org/download/mimetex/windows/mimetex.exe</a>.
+http://www.forkosh.com/mimetex.zip</a>.
 Another possible problem which may affect
 both Unix and Windows servers is that the web server doesn't have execute permission
 on the mimetex binary. In that case change permissions accordingly</li>
index ce05e41..a510167 100644 (file)
@@ -36,7 +36,7 @@ $string['cliincorrectvalueerror'] = 'Error, valor incorrecte "{$a->value}" per a
 $string['cliincorrectvalueretry'] = 'Valor incorrecte; torneu-ho a provar.';
 $string['clitypevalue'] = 'Valor de tipus';
 $string['clitypevaluedefault'] = 'valor de tipus, premeu la tecla de retorn (<em>Enter</em>) per fer servir un valor per defecte ({$a})';
-$string['cliunknowoption'] = 'Opcions invàlides:
+$string['cliunknowoption'] = 'Opcions no reconegudes:
  {$a}
 L\'opció --help us orientarà.';
 $string['cliyesnoprompt'] = 'Escriu y (significa Sí) o n (significa No)';
index 5bb4259..6a9b92b 100644 (file)
@@ -41,3 +41,4 @@ $string['cliunknowoption'] = 'Μη αναγνωρίσιμες επιλογές:
 $string['cliyesnoprompt'] = 'πατώντας ν (σημαίνει ναι) αλλιώς πατώντας ο (σημαίνει όχι)';
 $string['environmentrequireinstall'] = 'απαιτείται να εγκατασταθεί/ ενεργοποιηθεί';
 $string['environmentrequireversion'] = 'απαιτείται η έκδοση {$a->needed} ενώ εσείς έχετε την {$a->current}';
+$string['upgradekeyset'] = 'Κλειδί αναβάθμισης (αφήστε κενό για να μην το ορίσετε)';
index 06f0cb7..2c16045 100644 (file)
@@ -39,7 +39,7 @@ $string['cannotdownloadcomponents'] = 'Non foi posíbel descargar compoñentes';
 $string['cannotdownloadzipfile'] = 'Non foi posíbel descargar o ficheiro ZIP';
 $string['cannotfindcomponent'] = 'Non foi posíbel atopar o compoñente';
 $string['cannotsavemd5file'] = 'Non é posíbel gardar o ficheiro md5';
-$string['cannotsavezipfile'] = 'Non é posíbel gardar o ficheiro ZIP';
+$string['cannotsavezipfile'] = 'Non é posíbel gardar o arquivo ZIP';
 $string['cannotunzipfile'] = 'Non é posíbel descomprimir o ficheiro';
 $string['componentisuptodate'] = 'O compoñente está actualizado';
 $string['dmlexceptiononinstall'] = '<p>Produciuse un erro na base de datos [{$a->errorcode}].<br />{$a->debuginfo}</p>';
@@ -50,4 +50,4 @@ $string['remotedownloaderror'] = '<p>Fallo a descarga do compoñente cara o seu
 <p>Debe descargar o ficheiro <a href="{$a->url}">{$a->url}</a> manualmente, copialo en «{$a->dest}» no seu servidor e descomprimilo alí.</p>';
 $string['wrongdestpath'] = 'Camiño de destino errado.';
 $string['wrongsourcebase'] = 'URL da fonte errado.';
-$string['wrongzipfilename'] = 'Nome de ficheiro ZIP errado.';
+$string['wrongzipfilename'] = 'Nome de arquivo ZIP errado';
index 6c38f65..740b253 100644 (file)
@@ -760,7 +760,7 @@ $string['mnetrestore_extusers_admin'] = '<strong>Note:</strong> This backup file
 $string['mnetrestore_extusers_mismatch'] = '<strong>Note:</strong> This backup file apparently originates from a different Moodle installation and contains remote Moodle Network user accounts that may fail to restore. This operation is unsupported. If you are certain that it was created on this Moodle installation, or you can ensure that all the needed Moodle Network Hosts are configured, you may want to still try the restore.';
 $string['mnetrestore_extusers_noadmin'] = '<strong>Note:</strong> This backup file seems to come from a different Moodle installation and contains remote Moodle Network user accounts. You are not allowed to execute this type of restore. Contact the administrator of the site or, alternatively, restore this course without any user information (modules, files...)';
 $string['mnetrestore_extusers_switchuserauth'] = 'Remote Moodle Network user {$a->username} (coming from {$a->mnethosturl}) switched to local {$a->auth} authenticated user.';
-$string['mobilenotconfiguredwarning'] = 'Moodle Mobile is not enabled.';
+$string['mobilenotconfiguredwarning'] = 'The Moodle app is not enabled.';
 $string['modchooserdefault'] = 'Activity chooser default';
 $string['modeditdefaults'] = 'Default values for activity settings';
 $string['modsettings'] = 'Manage activities';
@@ -958,7 +958,7 @@ $string['quizattemptsupgradedmessage'] = 'In Moodle 2.1 there was a major upgrad
 $string['recaptchaprivatekey'] = 'ReCAPTCHA secret key';
 $string['recaptchapublickey'] = 'ReCAPTCHA site key';
 $string['register'] = 'Register your site';
-$string['registermoodlenet'] = '<p>We\'d love to stay in touch and provide you with important things for your Moodle site!</p><p>By registering:</p><ul><li>You\'ll be one of the first to find out about important notifications such as security alerts and new Moodle releases.</li><li>You can access and activate mobile push notifications from your Moodle site through our free <a href="https://download.moodle.org/mobile/">Moodle Mobile app</a>.</li><li>You are contributing to our <a href="https://moodle.net/stats/">Moodle statistics</a> of the worldwide community, which help us improve Moodle and our community sites.</li><li>If you wish, your site can be included in the <a href="https://moodle.net/sites/">list of registered Moodle sites</a> in your country.</li></ul>';
+$string['registermoodlenet'] = '<p>We\'d love to stay in touch and provide you with important things for your Moodle site!</p><p>By registering:</p><ul><li>You\'ll be one of the first to find out about important notifications such as security alerts and new Moodle releases.</li><li>You can access and activate mobile push notifications from your Moodle site through our free <a href="https://download.moodle.org/mobile/">Moodle app</a>.</li><li>You are contributing to our <a href="https://moodle.net/stats/">Moodle statistics</a> of the worldwide community, which help us improve Moodle and our community sites.</li><li>If you wish, your site can be included in the <a href="https://moodle.net/sites/">list of registered Moodle sites</a> in your country.</li></ul>';
 $string['registermoodleorg'] = 'When you register your site';
 $string['registermoodleorgli1'] = 'You are added to a low-volume mailing list for important notifications such as security alerts and new releases of Moodle.';
 $string['registermoodleorgli2'] = 'Statistics about your site will be added to the {$a} of the worldwide Moodle community.';
@@ -1080,6 +1080,7 @@ $string['sitepolicyhandlerplugin'] = '{$a->name} ({$a->component})';
 $string['sitepolicyguest'] = 'Site policy URL for guests';
 $string['sitepolicyguest_help'] = 'The URL of the site policy that all guests must see and agree to before accessing the site. Note that this setting will only have an effect if the site policy handler is set to default (core).';
 $string['sitesectionhelp'] = 'If selected, a topic section will be displayed on the site\'s front page.';
+$string['sixtyfourbitswarning'] = 'It has been detected that your site is not using a 64-bit PHP version. It is recommended that you upgrade your site to ensure future compatibility.';
 $string['slasharguments'] = 'Use slash arguments';
 $string['slashargumentswarning'] = 'It is recommended that the use of slash arguments is enabled. In future it will be required. For more details, see the documentation <a href="https://docs.moodle.org/en/admin/environment/slasharguments">Using slash arguments</a>.';
 $string['smartpix'] = 'Smart pix search';
index b358466..1233c20 100644 (file)
@@ -142,8 +142,8 @@ $string['privacy:metadata:userpref:createpassword'] = 'Indicates that a password
 $string['privacy:metadata:userpref:forcepasswordchange'] = 'Indicates whether the user should change their password upon logging in';
 $string['privacy:metadata:userpref:loginfailedcount'] = 'The number of times the user failed to log in';
 $string['privacy:metadata:userpref:loginfailedcountsincesuccess'] = 'The number of times the user failed to login since their last successful login';
-$string['privacy:metadata:userpref:loginfailedlast'] = 'The date at which the last failed login attempt was recorded';
-$string['privacy:metadata:userpref:loginlockout'] = 'Indicates whether the user\'s account is locked due to failed login attempts, and the date at which the account entered the lockout state';
+$string['privacy:metadata:userpref:loginfailedlast'] = 'The date when the last failed login attempt was recorded';
+$string['privacy:metadata:userpref:loginlockout'] = 'Whether the user\'s account is locked due to failed login attempts, and the date when the account was locked';
 $string['privacy:metadata:userpref:loginlockoutignored'] = 'Indicates that a user\'s account should never be subject to lockouts';
 $string['privacy:metadata:userpref:loginlockoutsecret'] = 'When locked, the secret the user must use for unlocking their account';
 $string['potentialidps'] = 'Log in using your account on:';
index 39feee5..422296d 100644 (file)
@@ -231,8 +231,8 @@ $string['privacy:metadata:backup:externalpurpose'] = 'The purpose of this archiv
 $string['privacy:metadata:backup_controllers'] = 'The list of backup operations';
 $string['privacy:metadata:backup_controllers:itemid'] = 'The ID of the course';
 $string['privacy:metadata:backup_controllers:operation'] = 'The operation that was performed, eg. restore.';
-$string['privacy:metadata:backup_controllers:timecreated'] = 'The date at which the action was created';
-$string['privacy:metadata:backup_controllers:timemodified'] = 'The date at which the action was modified';
+$string['privacy:metadata:backup_controllers:timecreated'] = 'The time when the action was created';
+$string['privacy:metadata:backup_controllers:timemodified'] = 'The time when the action was modified';
 $string['privacy:metadata:backup_controllers:type'] = 'The type of the item being operated on, eg. activity.';
 $string['qcategory2coursefallback'] = 'The questions category "{$a->name}", originally at system/course category context in backup file, will be created at course context by restore';
 $string['qcategorycannotberestored'] = 'The questions category "{$a->name}" cannot be created by restore';
@@ -326,7 +326,7 @@ $string['timetaken'] = 'Time taken';
 $string['title'] = 'Title';
 $string['totalcategorysearchresults'] = 'Total categories: {$a}';
 $string['totalcoursesearchresults'] = 'Total courses: {$a}';
-$string['undefinedrolemapping'] = 'Role mapping undefined for: \'{$a}\' archetype';
+$string['undefinedrolemapping'] = 'Role mapping undefined for \'{$a}\' archetype.';
 $string['unnamedsection'] = 'Unnamed section';
 $string['userinfo'] = 'Userinfo';
 $string['module'] = 'Module';
index a3ff6b0..42e7801 100644 (file)
@@ -408,7 +408,7 @@ $string['privacy:metadata:external:backpacks:image'] = 'The image of the badge';
 $string['privacy:metadata:external:backpacks:issuer'] = 'Some information about the issuer';
 $string['privacy:metadata:external:backpacks:url'] = 'The Moodle URL where the issued badge information can be seen';
 $string['privacy:metadata:issued'] = 'A record of badges awarded';
-$string['privacy:metadata:issued:dateexpire'] = 'The date at which the award expires';
+$string['privacy:metadata:issued:dateexpire'] = 'The date when the badge expires';
 $string['privacy:metadata:issued:dateissued'] = 'The date of the award';
 $string['privacy:metadata:issued:userid'] = 'The ID of the user who was awarded a badge';
 $string['privacy:metadata:manualaward'] = 'A record of manual awards';
index 2be7f58..d3cdd9e 100644 (file)
@@ -132,22 +132,22 @@ $string['privacy:metadata:core_comments'] = 'Comments associated with blog entri
 $string['privacy:metadata:core_files'] = 'Files attached to blog entries';
 $string['privacy:metadata:core_tag'] = 'Tags associated with blog entries';
 $string['privacy:metadata:external'] = 'A link to an external RSS feed';
-$string['privacy:metadata:external:userid'] = 'The ID of the user who added the external blog entry.';
+$string['privacy:metadata:external:userid'] = 'The ID of the user who added the external blog entry';
 $string['privacy:metadata:external:name'] = 'The name of the feed';
 $string['privacy:metadata:external:description'] = 'The description of the feed';
 $string['privacy:metadata:external:url'] = 'The URL of the feed';
 $string['privacy:metadata:external:filtertags'] = 'The list of tags to filter the entries with';
-$string['privacy:metadata:external:timemodified'] = 'Date at which the association was last modified';
-$string['privacy:metadata:external:timefetched'] = 'Date at which the feed was last fetched';
+$string['privacy:metadata:external:timemodified'] = 'The time when the association was last modified';
+$string['privacy:metadata:external:timefetched'] = 'The time when the feed was last fetched';
 $string['privacy:metadata:post'] = 'The information related to blog entries';
-$string['privacy:metadata:post:userid'] = 'The ID of the user who added the blog entry.';
-$string['privacy:metadata:post:subject'] = 'Blog entry title.';
-$string['privacy:metadata:post:summary'] = 'Blog entry.';
-$string['privacy:metadata:post:content'] = 'The content of an external blog entry.';
+$string['privacy:metadata:post:userid'] = 'The ID of the user who added the blog entry';
+$string['privacy:metadata:post:subject'] = 'The blog entry title';
+$string['privacy:metadata:post:summary'] = 'The blog entry text';
+$string['privacy:metadata:post:content'] = 'The content of an external blog entry';
 $string['privacy:metadata:post:uniquehash'] = 'A unique identifier for an external entry, typically a URL';
 $string['privacy:metadata:post:publishstate'] = 'Whether the entry is visible to others or not';
-$string['privacy:metadata:post:created'] = 'Date when the entry was created.';
-$string['privacy:metadata:post:lastmodified'] = 'Date when the entry was last modified.';
+$string['privacy:metadata:post:created'] = 'The date when the blog entry was created';
+$string['privacy:metadata:post:lastmodified'] = 'The date when the blog entry was last modified';
 $string['privacy:metadata:post:usermodified'] = 'The user who last modified the entry';
 $string['privacy:path:blogassociations'] = 'Associated blog posts';
 $string['privacy:unknown'] = 'Unknown';
index 5f0ea26..9badc38 100644 (file)
@@ -62,7 +62,7 @@ $string['invalidtheme'] = 'Cohort theme does not exist';
 $string['idnumber'] = 'Cohort ID';
 $string['memberscount'] = 'Cohort size';
 $string['name'] = 'Name';
-$string['namecolumnmissing'] = 'There is something wrong with the format of the CSV file. Please check that it includes the correct column names. Note that Upload cohorts only allows you to add new users to an existing cohort and does not allow removal from an existing cohort.';
+$string['namecolumnmissing'] = 'There is something wrong with the format of the CSV file. Please check that it includes the correct column names. To add users to a cohort, go to \'Upload users\' in the Site administration.';
 $string['namefieldempty'] = 'Field name can not be empty';
 $string['newnamefor'] = 'New name for cohort {$a}';
 $string['newidnumberfor'] = 'New ID number for cohort {$a}';
index ca114fc..42377c7 100644 (file)
@@ -146,10 +146,10 @@ $string['privacy:metadata:plan:description'] = 'The description of the learning
 $string['privacy:metadata:plan:duedate'] = 'The due date of the learning plan';
 $string['privacy:metadata:plan:name'] = 'The name of the learning plan';
 $string['privacy:metadata:plan:reviewerid'] = 'The ID of the reviewer of the learning plan';
-$string['privacy:metadata:plan:status'] = 'The status of the learning palan';
+$string['privacy:metadata:plan:status'] = 'The status of the learning plan';
 $string['privacy:metadata:plan:userid'] = 'The ID of the user whose learning plan it is';
-$string['privacy:metadata:timecreated'] = 'The date at which the record was created';
-$string['privacy:metadata:timemodified'] = 'The date at which the record was edited';
+$string['privacy:metadata:timecreated'] = 'The time when the record was created';
+$string['privacy:metadata:timemodified'] = 'The time when the record was edited';
 $string['privacy:metadata:usercomp:grade'] = 'The grade given for the competency';
 $string['privacy:metadata:usercomp:proficiency'] = 'Whether proficiency is achieved';
 $string['privacy:metadata:usercomp:reviewerid'] = 'The ID of the reviewer';
index 7315719..8f452db 100644 (file)
@@ -150,12 +150,12 @@ $string['extremovedsuspendnoroles'] = 'Disable course enrolment and remove roles
 $string['extremovedkeep'] = 'Keep user enrolled';
 $string['extremovedunenrol'] = 'Unenrol user from course';
 $string['privacy:metadata:user_enrolments'] = 'Enrolments';
-$string['privacy:metadata:user_enrolments:enrolid'] = 'The instance of the enrol plugin.';
-$string['privacy:metadata:user_enrolments:modifierid'] = 'The ID of the user who last modified the user enrolment.';
-$string['privacy:metadata:user_enrolments:status'] = 'The status of the user enrolment in a course.';
-$string['privacy:metadata:user_enrolments:tableexplanation'] = 'This is where Enrol management stores enrolled users.';
-$string['privacy:metadata:user_enrolments:timecreated'] = 'The date/time of when the user enrolment was created.';
-$string['privacy:metadata:user_enrolments:timeend'] = 'The date/time of when the user enrolment ends.';
-$string['privacy:metadata:user_enrolments:timestart'] = 'The date/time of when the user enrolment starts.';
-$string['privacy:metadata:user_enrolments:timemodified'] = 'The date/time of when the user enrolment was modified.';
-$string['privacy:metadata:user_enrolments:userid'] = 'The ID of the user.';
+$string['privacy:metadata:user_enrolments:enrolid'] = 'The instance of the enrolment plugin';
+$string['privacy:metadata:user_enrolments:modifierid'] = 'The ID of the user who last modified the user enrolment';
+$string['privacy:metadata:user_enrolments:status'] = 'The status of the user enrolment in a course';
+$string['privacy:metadata:user_enrolments:tableexplanation'] = 'The core enrol plugin stores enrolled users.';
+$string['privacy:metadata:user_enrolments:timecreated'] = 'The time when the user enrolment was created';
+$string['privacy:metadata:user_enrolments:timeend'] = 'The time when the user enrolment ends';
+$string['privacy:metadata:user_enrolments:timestart'] = 'The time when the user enrolment starts';
+$string['privacy:metadata:user_enrolments:timemodified'] = 'The time when the user enrolment was modified';
+$string['privacy:metadata:user_enrolments:userid'] = 'The ID of the user';
index f15f401..95b9450 100644 (file)
@@ -383,7 +383,7 @@ $string['loginasonecourse'] = 'You cannot enter this course.<br /> You have to t
 $string['maxbytesfile'] = 'The file {$a->file} is too large. The maximum size you can upload is {$a->size}.';
 $string['maxareabytes'] = 'The file is larger than the space remaining in this area.';
 $string['messagingdisable'] = 'Messaging is disabled on this site';
-$string['mimetexisnotexist'] = 'Your system is not configured to run mimeTeX. You need to download the appropriate executable for you PHP_OS platform from <a href="http://moodle.org/download/mimetex/">http://moodle.org/download/mimetex/</a>, or obtain the C source from <a href="http://www.forkosh.com/mimetex.zip"> http://www.forkosh.com/mimetex.zip</a>, compile it and put the executable into your moodle/filter/tex/ directory.';
+$string['mimetexisnotexist'] = 'Your system is not configured to run mimeTeX. You need to obtain the C source from <a href="http://www.forkosh.com/mimetex.zip">http://www.forkosh.com/mimetex.zip</a>, compile it and put the executable into your moodle/filter/tex/ directory.';
 $string['mimetexnotexecutable'] = 'Custom mimetex is not executable!';
 $string['missingfield'] = 'Field "{$a}" is missing';
 $string['missingkeyinsql'] = 'ERROR: missing param "{$a}" in query';
index b3d26eb..db461c9 100644 (file)
@@ -632,19 +632,19 @@ $string['privacy:metadata:grades:aggregationweight'] = 'The weight in aggregatio
 $string['privacy:metadata:grades:feedback'] = 'The feedback';
 $string['privacy:metadata:grades:finalgrade'] = 'The grade';
 $string['privacy:metadata:grades:information'] = 'Some information additional information';
-$string['privacy:metadata:grades:timemodified'] = 'Time at which the grade was last modified';
+$string['privacy:metadata:grades:timemodified'] = 'The time when the grade was last modified';
 $string['privacy:metadata:grades:userid'] = 'The ID of the user whose grade it is';
 $string['privacy:metadata:grades:usermodified'] = 'The ID of the user who last modified the record';
 $string['privacy:metadata:gradeshistory'] = 'A record of the previous grades';
 $string['privacy:metadata:history:loggeduser'] = 'The ID of the user who was logged in when the versioning occurred';
-$string['privacy:metadata:history:timemodified'] = 'Time at which the versioning occurred';
+$string['privacy:metadata:history:timemodified'] = 'The time when grade versioning occurred';
 $string['privacy:metadata:itemshistory'] = 'A record of previous versions of grade items';
 $string['privacy:metadata:outcomes'] = 'A record of outcomes';
-$string['privacy:metadata:outcomes:timemodified'] = 'Time at which the record was modified';
+$string['privacy:metadata:outcomes:timemodified'] = 'The time when the record was modified';
 $string['privacy:metadata:outcomes:usermodified'] = 'The user who last modified the record';
 $string['privacy:metadata:outcomeshistory'] = 'A record of previous versions of outcomes';
 $string['privacy:metadata:scale'] = 'A record of scales';
-$string['privacy:metadata:scale:timemodified'] = 'Time at which the record was last modified';
+$string['privacy:metadata:scale:timemodified'] = 'The time when the record was last modified';
 $string['privacy:metadata:scale:userid'] = 'The user who last modified the record';
 $string['privacy:metadata:scalehistory'] = 'A record of previous versions of scales';
 $string['privacy:path:relatedtome'] = 'Related to me';
index 5428f68..4517a30 100644 (file)
@@ -105,19 +105,19 @@ $string['privacy:metadata:messages:fullmessagehtml'] = 'The HTML format of the f
 $string['privacy:metadata:messages:useridfrom'] = 'The ID of the user who sent the message';
 $string['privacy:metadata:messages:smallmessage'] = 'A small version of the message';
 $string['privacy:metadata:messages:subject'] = 'The subject of the message';
-$string['privacy:metadata:messages:timecreated'] = 'The date at which the message was created';
+$string['privacy:metadata:messages:timecreated'] = 'The time when the message was created';
 $string['privacy:metadata:message_contacts'] = 'The list of contacts';
 $string['privacy:metadata:message_contacts:blocked'] = 'Flag whether or not the user is blocked';
 $string['privacy:metadata:message_contacts:contactid'] = 'The ID of the user who is a contact';
 $string['privacy:metadata:message_contacts:userid'] = 'The ID of the user whose contact list we are viewing';
 $string['privacy:metadata:message_conversation_members'] = 'The list of users in a conversation';
 $string['privacy:metadata:message_conversation_members:conversationid'] = 'The ID of the conversation';
-$string['privacy:metadata:message_conversation_members:timecreated'] = 'The date at which the member was created';
+$string['privacy:metadata:message_conversation_members:timecreated'] = 'The time when the member was created';
 $string['privacy:metadata:message_conversation_members:userid'] = 'The ID of the user in a conversation';
 $string['privacy:metadata:message_user_actions'] = 'The list of message user actions';
 $string['privacy:metadata:message_user_actions:action'] = 'The action that was performed';
 $string['privacy:metadata:message_user_actions:messageid'] = 'The ID of the message this action belongs to';
-$string['privacy:metadata:message_user_actions:timecreated'] = 'The date at which the action was created';
+$string['privacy:metadata:message_user_actions:timecreated'] = 'The time when the action was created';
 $string['privacy:metadata:message_user_actions:userid'] = 'The ID of the user who performed this action';
 $string['privacy:metadata:notifications'] = 'Notifications';
 $string['privacy:metadata:notifications:component'] = 'The component responsible for sending the notification';
@@ -129,8 +129,8 @@ $string['privacy:metadata:notifications:fullmessageformat'] = 'The notification
 $string['privacy:metadata:notifications:fullmessagehtml'] = 'The HTML of the notification';
 $string['privacy:metadata:notifications:smallmessage'] = 'The small message of the notification';
 $string['privacy:metadata:notifications:subject'] = 'The subject of the notification';
-$string['privacy:metadata:notifications:timeread'] = 'The date at which the notification was read';
-$string['privacy:metadata:notifications:timecreated'] = 'The date at which the notification was created';
+$string['privacy:metadata:notifications:timeread'] = 'The time when the notification was read';
+$string['privacy:metadata:notifications:timecreated'] = 'The time when the notification was created';
 $string['privacy:metadata:notifications:useridfrom'] = 'The ID of the user who sent the notification';
 $string['privacy:metadata:notifications:useridto'] = 'The ID of the user who received the notification';
 $string['privacy:metadata:preference:core_message_settings'] = 'Settings related to messaging';
index df32932..a854d19 100644 (file)
@@ -1162,6 +1162,7 @@ $string['markedthistopic'] = 'This topic is highlighted as the current topic';
 $string['markthistopic'] = 'Highlight this topic as the current topic';
 $string['matchingsearchandrole'] = 'Matching \'{$a->search}\' and {$a->role}';
 $string['maxareabytesreached'] = 'The file (or the total size of several files) is larger than the space remaining in this area.';
+$string['maxsectionslimit'] = 'Cannot create new section as it would exceed the maximum number of sections allowed for this course ({$a}).';
 $string['maxfilesize'] = 'Maximum size for new files: {$a}';
 $string['maxfilesreached'] = 'You are allowed to attach a maximum of {$a} file(s) to this item';
 $string['maximumchars'] = 'Maximum of {$a} characters';
@@ -1567,7 +1568,7 @@ $string['privacy:metadata:log:course'] = 'course';
 $string['privacy:metadata:log:info'] = 'Additional information';
 $string['privacy:metadata:log:ip'] = 'The IP address used at the time of the event';
 $string['privacy:metadata:log:module'] = 'module';
-$string['privacy:metadata:log:time'] = 'The date at wich the action took place';
+$string['privacy:metadata:log:time'] = 'The time when the action took place';
 $string['privacy:metadata:log:url'] = 'The URL related to the event';
 $string['privacy:metadata:log:userid'] = 'The ID of the user who performed the action';
 $string['privacy:metadata:task_adhoc'] = 'The status of adhoc tasks.';
index 32d846a..8cdbc11 100644 (file)
@@ -167,21 +167,21 @@ $string['pluginismisconfigured'] = 'Portfolio plugin is misconfigured, skipping.
 $string['portfolio'] = 'Portfolio';
 $string['portfolios'] = 'Portfolios';
 $string['privacy:metadata'] = 'The portfolio subsystem acts as a channel, passing requests from plugins to the various portfolio plugins.';
-$string['privacy:metadata:name'] = 'Name of the preference.';
-$string['privacy:metadata:instance'] = 'Identifier for the portfolio.';
-$string['privacy:metadata:instancesummary'] = 'This stores portfolio both instances and preferences for the portfolios user is using.';
+$string['privacy:metadata:name'] = 'Preference name';
+$string['privacy:metadata:instance'] = 'Portfolio identifier';
+$string['privacy:metadata:instancesummary'] = 'Stores data about portfolio instances and preferences.';
 $string['privacy:metadata:portfolio_log'] = 'Log of portfolio transfers (used to later check for duplicates)';
 $string['privacy:metadata:portfolio_log:caller_class'] = 'Name of the class used to create the transfer';
 $string['privacy:metadata:portfolio_log:caller_component'] = 'Component name responsible for exporting';
-$string['privacy:metadata:portfolio_log:time'] = 'Time of transfer (in the case of a queued transfer this is the time the actual transfer ran, not when the user started)';
-$string['privacy:metadata:portfolio_log:userid'] = 'User who exported content';
-$string['privacy:metadata:portfolio_tempdata'] = 'Stores temporary data for portfolio exports, cleaned by cron after one day';
+$string['privacy:metadata:portfolio_log:time'] = 'Time of transfer (in the case of a queued transfer this is the time the actual transfer ran, not when the user started it)';
+$string['privacy:metadata:portfolio_log:userid'] = 'ID of user who exported content';
+$string['privacy:metadata:portfolio_tempdata'] = 'Stores temporary data for portfolio exports.';
 $string['privacy:metadata:portfolio_tempdata:data'] = 'Export data';
 $string['privacy:metadata:portfolio_tempdata:expirytime'] = 'Time this record will expire';
 $string['privacy:metadata:portfolio_tempdata:instance'] = 'Portfolio plugin instance being used';
 $string['privacy:metadata:portfolio_tempdata:userid'] = 'User performing export';
 $string['privacy:metadata:value'] = 'Value for the preference';
-$string['privacy:metadata:userid'] = 'The user Identifier.';
+$string['privacy:metadata:userid'] = 'User ID';
 $string['privacy:path'] = 'Portfolio instances';
 $string['queuesummary'] = 'Currently queued transfers';
 $string['returntowhereyouwere'] = 'Return to where you were';
index 1aac870..88f1730 100644 (file)
@@ -82,11 +82,11 @@ $string['errornotemptydefaultparamarray'] = 'The web service description paramet
 $string['erroroptionalparamarray'] = 'The web service description parameter named \'{$a}\' is an single or multiple structure. It can not be set as VALUE_OPTIONAL. Check web service description.';
 $string['eventwebservicefunctioncalled'] = 'Web service function called';
 $string['eventwebserviceloginfailed'] = 'Web service login failed';
-$string['eventwebserviceservicecreated'] = 'Web service service created';
-$string['eventwebserviceservicedeleted'] = 'Web service service deleted';
-$string['eventwebserviceserviceupdated'] = 'Web service service updated';
-$string['eventwebserviceserviceuseradded'] = 'Web service service user added';
-$string['eventwebserviceserviceuserremoved'] = 'Web service service user removed';
+$string['eventwebserviceservicecreated'] = 'Web service created';
+$string['eventwebserviceservicedeleted'] = 'Web service deleted';
+$string['eventwebserviceserviceupdated'] = 'Web service updated';
+$string['eventwebserviceserviceuseradded'] = 'Web service user added';
+$string['eventwebserviceserviceuserremoved'] = 'Web service user removed';
 $string['eventwebservicetokencreated'] = 'Web service token created';
 $string['eventwebservicetokensent'] = 'Web service token sent';
 $string['execute'] = 'Execute';
@@ -143,15 +143,15 @@ $string['potusersmatching'] = 'Not authorised users matching';
 $string['print'] = 'Print all';
 $string['privacy:metadata:serviceusers'] = 'A list of users who can use a certain external services';
 $string['privacy:metadata:serviceusers:iprestriction'] = 'IP restricted to use the service';
-$string['privacy:metadata:serviceusers:timecreated'] = 'The date at which the record was created';
+$string['privacy:metadata:serviceusers:timecreated'] = 'The date when the record was created';
 $string['privacy:metadata:serviceusers:userid'] = 'The ID of the user';
-$string['privacy:metadata:serviceusers:validuntil'] = 'The date at which the authorisation ends';
+$string['privacy:metadata:serviceusers:validuntil'] = 'The date that the authorisation is valid until';
 $string['privacy:metadata:tokens'] = 'A record of tokens for interacting with Moodle through web services or Mobile applications.';
 $string['privacy:metadata:tokens:creatorid'] = 'The ID of the user who created the token';
 $string['privacy:metadata:tokens:iprestriction'] = 'IP restricted to use this token';
-$string['privacy:metadata:tokens:lastaccess'] = 'The date at which the token was last used';
+$string['privacy:metadata:tokens:lastaccess'] = 'The date when the token was last used';
 $string['privacy:metadata:tokens:privatetoken'] = 'A more private token occasionally used to validate certain operations, such as SSO';
-$string['privacy:metadata:tokens:timecreated'] = 'The date at which the token was created';
+$string['privacy:metadata:tokens:timecreated'] = 'The date when the token was created';
 $string['privacy:metadata:tokens:token'] = 'The user\'s token';
 $string['privacy:metadata:tokens:tokentype'] = 'The type of token';
 $string['privacy:metadata:tokens:userid'] = 'The ID of the user whose token it is';
@@ -185,8 +185,8 @@ $string['selectspecificuserdescription'] = 'Add the web services user as an auth
 $string['service'] = 'Service';
 $string['servicehelpexplanation'] = 'A service is a set of functions. A service can be accessed by all users or just specified users.';
 $string['servicename'] = 'Service name';
-$string['servicenotavailable'] = 'Web service is not available (it doesn\'t exist or might be disabled)';
-$string['servicerequireslogin'] = 'Web service is not available (the session has been logged out or has expired)';
+$string['servicenotavailable'] = 'Web service is not available. (It doesn\'t exist or might be disabled.)';
+$string['servicerequireslogin'] = 'Web service is not available. (The session has been logged out or has expired.)';
 $string['servicesbuiltin'] = 'Built-in services';
 $string['servicescustom'] = 'Custom services';
 $string['serviceusers'] = 'Authorised users';
index 7755fdc..0369c31 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
index b4c0053..58a7fc2 100644 (file)
@@ -882,9 +882,23 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
             context.options = suggestions;
             context.items = [];
 
-            var renderInput = templates.render('core/form_autocomplete_input', context);
-            var renderDatalist = templates.render('core/form_autocomplete_suggestions', context);
-            var renderSelection = templates.render('core/form_autocomplete_selection', context);
+            // Collect rendered inline JS to be executed once the HTML is shown.
+            var collectedjs = '';
+
+            var renderInput = templates.render('core/form_autocomplete_input', context).then(function(html, js) {
+                collectedjs += js;
+                return html;
+            });
+
+            var renderDatalist = templates.render('core/form_autocomplete_suggestions', context).then(function(html, js) {
+                collectedjs += js;
+                return html;
+            });
+
+            var renderSelection = templates.render('core/form_autocomplete_selection', context).then(function(html, js) {
+                collectedjs += js;
+                return html;
+            });
 
             return $.when(renderInput, renderDatalist, renderSelection).then(function(input, suggestions, selection) {
                 originalSelect.hide();
@@ -892,6 +906,8 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                 originalSelect.after(input);
                 originalSelect.after(selection);
 
+                templates.runTemplateJS(collectedjs);
+
                 // Update the form label to point to the text input.
                 originalLabel.attr('for', state.inputId);
                 // Add the event handlers.
index 8fbb66a..80f33f8 100644 (file)
@@ -2301,5 +2301,32 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2018073000.00);
     }
 
+    if ($oldversion < 2018083100.01) {
+        // Remove module associated blog posts for non-existent (deleted) modules.
+        $sql = "SELECT ba.contextid as modcontextid
+                  FROM {blog_association} ba
+                  JOIN {post} p
+                       ON p.id = ba.blogid
+             LEFT JOIN {context} c
+                       ON c.id = ba.contextid
+                 WHERE p.module = :module
+                       AND c.contextlevel IS NULL
+              GROUP BY ba.contextid";
+        if ($deletedmodules = $DB->get_records_sql($sql, array('module' => 'blog'))) {
+            foreach ($deletedmodules as $module) {
+                $assocblogids = $DB->get_fieldset_select('blog_association', 'blogid',
+                    'contextid = :contextid', ['contextid' => $module->modcontextid]);
+                list($sql, $params) = $DB->get_in_or_equal($assocblogids, SQL_PARAMS_NAMED);
+
+                $DB->delete_records_select('tag_instance', "itemid $sql", $params);
+                $DB->delete_records_select('post', "id $sql AND module = :module",
+                    array_merge($params, ['module' => 'blog']));
+                $DB->delete_records('blog_association', ['contextid' => $module->modcontextid]);
+            }
+        }
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2018083100.01);
+    }
+
     return true;
 }
index 220d1ef..bcd5c38 100644 (file)
@@ -180,8 +180,8 @@ class atto_texteditor extends texteditor {
         }
         $contentcss     = $PAGE->theme->editor_css_url()->out(false);
 
-        // Autosave disabled for guests.
-        if (isguestuser()) {
+        // Autosave disabled for guests and not logged in users.
+        if (isguestuser() OR !isloggedin()) {
             $autosave = false;
         }
         // Note <> is a safe separator, because it will not appear in the output of s().
index 060e508..16ccefe 100644 (file)
@@ -63,7 +63,8 @@ function message_send(\core\message\message $eventdata) {
     // Fetch default (site) preferences
     $defaultpreferences = get_message_output_default_preferences();
     $preferencebase = $eventdata->component.'_'.$eventdata->name;
-    // If message provider is disabled then don't do any processing.
+
+    // If the message provider is disabled via preferences, then don't send the message.
     if (!empty($defaultpreferences->{$preferencebase.'_disable'})) {
         return $messageid;
     }
@@ -88,6 +89,20 @@ function message_send(\core\message\message $eventdata) {
         return false;
     }
 
+    // If the provider's component is disabled or the user can't receive messages from it, don't send the message.
+    $isproviderallowed = false;
+    foreach (message_get_providers_for_user($eventdata->userto->id) as $provider) {
+        if ($provider->component === $eventdata->component && $provider->name === $eventdata->name) {
+            $isproviderallowed = true;
+            break;
+        }
+    }
+    if (!$isproviderallowed) {
+        debugging('Attempt to send msg from a provider '.$eventdata->component.'/'.$eventdata->name.
+            ' that is inactive or not allowed for the user id='.$eventdata->userto->id, DEBUG_NORMAL);
+        return false;
+    }
+
     // Verify all necessary data fields are present.
     if (!isset($eventdata->userto->auth) or !isset($eventdata->userto->suspended)
             or !isset($eventdata->userto->deleted) or !isset($eventdata->userto->emailstop)) {
index 10b589d..454adf3 100644 (file)
@@ -3007,41 +3007,15 @@ EOD;
     }
 
     /**
-     * Internal implementation of paging bar rendering.
+     * Returns HTML to display the paging bar.
      *
      * @param paging_bar $pagingbar
-     * @return string
+     * @return string the HTML to output.
      */
     protected function render_paging_bar(paging_bar $pagingbar) {
-        $output = '';
-        $pagingbar = clone($pagingbar);
-        $pagingbar->prepare($this, $this->page, $this->target);
-
-        if ($pagingbar->totalcount > $pagingbar->perpage) {
-            $output .= get_string('page') . ':';
-
-            if (!empty($pagingbar->previouslink)) {
-                $output .= ' (' . $pagingbar->previouslink . ') ';
-            }
-
-            if (!empty($pagingbar->firstlink)) {
-                $output .= ' ' . $pagingbar->firstlink . ' ...';
-            }
-
-            foreach ($pagingbar->pagelinks as $link) {
-                $output .= "  $link";
-            }
-
-            if (!empty($pagingbar->lastlink)) {
-                $output .= ' ... ' . $pagingbar->lastlink . ' ';
-            }
-
-            if (!empty($pagingbar->nextlink)) {
-                $output .= '  (' . $pagingbar->nextlink . ')';
-            }
-        }
-
-        return html_writer::tag('div', $output, array('class' => 'paging'));
+        // Any more than 10 is not usable and causes weird wrapping of the pagination.
+        $pagingbar->maxdisplay = 10;
+        return $this->render_from_template('core/paging_bar', $pagingbar->export_for_template($this));
     }
 
     /**
index d74dd55..0207066 100644 (file)
 
 require_once('PEAR.php');
 require_once('HTML/Common.php');
+/**
+ * Static utility methods.
+ */
+require_once('HTML/QuickForm/utils.php');
 
 $GLOBALS['HTML_QUICKFORM_ELEMENT_TYPES'] =
         array(
@@ -834,9 +838,15 @@ class HTML_QuickForm extends HTML_Common {
 
         } elseif (false !== ($pos = strpos($elementName, '['))) {
             $base = substr($elementName, 0, $pos);
-            $idx  = "['" . str_replace(array(']', '['), array('', "']['"), substr($elementName, $pos + 1, -1)) . "']";
+            $keys = str_replace(
+                array('\\', '\'', ']', '['), array('\\\\', '\\\'', '', "']['"),
+                substr($elementName, $pos + 1, -1)
+            );
+            $idx  = "['" . $keys . "']";
+            $keyArray = explode("']['", $keys);
+
             if (isset($this->_submitValues[$base])) {
-                $value = eval("return (isset(\$this->_submitValues['{$base}']{$idx})) ? \$this->_submitValues['{$base}']{$idx} : null;");
+                $value = HTML_QuickForm_utils::recursiveValue($this->_submitValues[$base], $keyArray, NULL);
             }
 
             if ((is_array($value) || null === $value) && isset($this->_submitFiles[$base])) {
index 89597aa..18332ea 100644 (file)
 // $Id$
 
 require_once('HTML/Common.php');
+/**
+ * Static utility methods.
+ */
+require_once('HTML/QuickForm/utils.php');
 
 /**
  * Base class for form elements
@@ -351,8 +355,12 @@ class HTML_QuickForm_element extends HTML_Common
         if (isset($values[$elementName])) {
             return $values[$elementName];
         } elseif (strpos($elementName, '[')) {
-            $myVar = "['" . str_replace(array(']', '['), array('', "']['"), $elementName) . "']";
-            return eval("return (isset(\$values$myVar)) ? \$values$myVar : null;");
+            $keys = str_replace(
+                array('\\', '\'', ']', '['), array('\\\\', '\\\'', '', "']['"),
+                $elementName
+            );
+            $arrayKeys = explode("']['", $keys);
+            return HTML_QuickForm_utils::recursiveValue($values, $arrayKeys);
         } else {
             return null;
         }
@@ -483,10 +491,12 @@ class HTML_QuickForm_element extends HTML_Common
             if (!strpos($name, '[')) {
                 return array($name => $value);
             } else {
-                $valueAry = array();
-                $myIndex  = "['" . str_replace(array(']', '['), array('', "']['"), $name) . "']";
-                eval("\$valueAry$myIndex = \$value;");
-                return $valueAry;
+                $keys = str_replace(
+                    array('\\', '\'', ']', '['), array('\\\\', '\\\'', '', "']['"),
+                    $name
+                );
+                $keysArray = explode("']['", $keys);
+                return HTML_QuickForm_utils::recursiveBuild($keysArray, $value);
             }
         }
     }
index 3762229..1bce43b 100644 (file)
 
 require_once('HTML/QuickForm/group.php');
 require_once('HTML/QuickForm/select.php');
+/**
+ * Static utility methods.
+ */
+require_once 'HTML/QuickForm/utils.php';
 
 /**
  * Class to dynamically create two or more HTML Select elements
@@ -233,16 +237,19 @@ class HTML_QuickForm_hierselect extends HTML_QuickForm_group
      */
     function _setOptions()
     {
-        $toLoad = '';
+        $arrayKeys = [];
         foreach (array_keys($this->_elements) AS $key) {
-            $array = eval("return isset(\$this->_options[{$key}]{$toLoad})? \$this->_options[{$key}]{$toLoad}: null;");
-            if (is_array($array)) {
-                $select =& $this->_elements[$key];
-                $select->_options = array();
-                $select->loadArray($array);
-
-                $value  = is_array($v = $select->getValue()) ? $v[0] : key($array);
-                $toLoad .= '[\'' . str_replace(array('\\', '\''), array('\\\\', '\\\''), $value) . '\']';
+            if (isset($this->_options[$key])) {
+                if ((empty($arrayKeys)) || HTML_QuickForm_utils::recursiveIsset($this->_options[$key], $arrayKeys)) {
+                    $array = empty($arrayKeys) ? $this->_options[$key] : HTML_QuickForm_utils::recursiveValue($this->_options[$key], $arrayKeys);
+                    if (is_array($array)) {
+                        $select =& $this->_elements[$key];
+                        $select->_options = array();
+                        $select->loadArray($array);
+                        $value = is_array($v = $select->getValue()) ? $v[0] : key($array);
+                        $arrayKeys[] = $value;
+                    }
+                }
             }
         }
     } // end func _setOptions
@@ -585,4 +592,4 @@ JAVASCRIPT;
 
     // }}}
 } // end class HTML_QuickForm_hierselect
-?>
\ No newline at end of file
+?>
diff --git a/lib/pear/HTML/QuickForm/utils.php b/lib/pear/HTML/QuickForm/utils.php
new file mode 100644 (file)
index 0000000..00c6ba8
--- /dev/null
@@ -0,0 +1,159 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * utility functions
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.01 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_01.txt If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category    HTML
+ * @package     HTML_QuickForm
+ * @author      Chuck Burgess <ashnazg@php.net>
+ * @copyright   2001-2018 The PHP Group
+ * @license     http://www.php.net/license/3_01.txt PHP License 3.01
+ * @version     CVS: $Id$
+ * @link        http://pear.php.net/package/HTML_QuickForm
+ */
+
+/**
+ * Provides a collection of static methods for array manipulation.
+ *
+ * (courtesy of CiviCRM project (https://civicrm.org/)
+ *
+ * @category    HTML
+ * @package     HTML_QuickForm
+ * @author      Chuck Burgess <ashnazg@php.net>
+ * @version     Release: @package_version@
+ * @since       3.2
+ */
+class HTML_QuickForm_utils
+{
+    /**
+     * Get a single value from an array-tree.
+     *
+     * @param   array     $values   Ex: ['foo' => ['bar' => 123]].
+     * @param   array     $path     Ex: ['foo', 'bar'].
+     * @param   mixed     $default
+     * @return  mixed               Ex 123.
+     *
+     * @access  public
+     * @static
+     */
+    static function pathGet($values, $path, $default = NULL) {
+        foreach ($path as $key) {
+            if (!is_array($values) || !isset($values[$key])) {
+                return $default;
+            }
+            $values = $values[$key];
+        }
+        return $values;
+    }
+
+    /**
+     * Check if a key isset which may be several layers deep.
+     *
+     * This is a helper for when the calling function does not know how many layers deep
+     * the path array is so cannot easily check.
+     *
+     * @param   array $values
+     * @param   array $path
+     * @return  bool
+     *
+     * @access  public
+     * @static
+     */
+    static function pathIsset($values, $path) {
+        foreach ($path as $key) {
+            if (!is_array($values) || !isset($values[$key])) {
+                return FALSE;
+            }
+            $values = $values[$key];
+        }
+        return TRUE;
+    }
+
+    /**
+     * Set a single value in an array tree.
+     *
+     * @param   array   $values     Ex: ['foo' => ['bar' => 123]].
+     * @param   array   $pathParts  Ex: ['foo', 'bar'].
+     * @param   mixed   $value      Ex: 456.
+     * @return  void
+     *
+     * @access  public
+     * @static
+     */
+    static function pathSet(&$values, $pathParts, $value) {
+        $r = &$values;
+        $last = array_pop($pathParts);
+        foreach ($pathParts as $part) {
+            if (!isset($r[$part])) {
+                $r[$part] = array();
+            }
+            $r = &$r[$part];
+        }
+        $r[$last] = $value;
+    }
+
+    /**
+     * Check if a key isset which may be several layers deep.
+     *
+     * This is a helper for when the calling function does not know how many layers deep the
+     * path array is so cannot easily check.
+     *
+     * @param   array $array
+     * @param   array $path
+     * @return  bool
+     *
+     * @access  public
+     * @static
+     */
+    static function recursiveIsset($array, $path) {
+        return self::pathIsset($array, $path);
+    }
+
+    /**
+     * Check if a key isset which may be several layers deep.
+     *
+     * This is a helper for when the calling function does not know how many layers deep the
+     * path array is so cannot easily check.
+     *
+     * @param   array   $array
+     * @param   array   $path       An array of keys,
+     *                              e.g [0, 'bob', 8] where we want to check if $array[0]['bob'][8]
+     * @param   mixed   $default    Value to return if not found.
+     * @return  bool
+     *
+     * @access  public
+     * @static
+     */
+    static function recursiveValue($array, $path, $default = NULL) {
+        return self::pathGet($array, $path, $default);
+    }
+
+    /**
+     * Append the value to the array using the key provided.
+     *
+     * e.g if value is 'llama' & path is [0, 'email', 'location'] result will be
+     * [0 => ['email' => ['location' => 'llama']]
+     *
+     * @param           $path
+     * @param           $value
+     * @param   array   $source
+     * @return  array
+     *
+     * @access  public
+     * @static
+     */
+    static function recursiveBuild($path, $value, $source = array()) {
+        self::pathSet($source, $path, $value);
+        return $source;
+    }
+}
+?>
index 2aaa254..39c7c16 100644 (file)
@@ -516,21 +516,16 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
         $message3->smallmessage      = 'small message';
         $message3->notification      = 0;
 
-        try {
-            message_send($message3);
-            $this->fail('coding expcetion expected if invalid component specified');
-        } catch (moodle_exception $e) {
-            $this->assertInstanceOf('coding_exception', $e);
-        }
+        $this->assertFalse(message_send($message3));
+        $this->assertDebuggingCalled('Attempt to send msg from a provider xxxx_yyyyy/instantmessage '.
+            'that is inactive or not allowed for the user id='.$user1->id);
 
         $message3->component = 'moodle';
         $message3->name      = 'yyyyyy';
-        try {
-            message_send($message3);
-            $this->fail('coding expcetion expected if invalid name specified');
-        } catch (moodle_exception $e) {
-            $this->assertInstanceOf('coding_exception', $e);
-        }
+
+        $this->assertFalse(message_send($message3));
+        $this->assertDebuggingCalled('Attempt to send msg from a provider moodle/yyyyyy '.
+            'that is inactive or not allowed for the user id='.$user1->id);
 
         message_send($message1);
         $this->assertEquals(1, $sink->count());
index ad064f2..7bf5032 100644 (file)
 {{^showSuggestions}}
 <input type="text" id="{{inputId}}" placeholder="{{placeholder}}" role="textbox" aria-owns="{{selectionId}}"/>
 {{/showSuggestions}}
+
+{{#js}}
+require(['jquery'], function($) {
+    // Set the minimum width of the input so that the placeholder is whole displayed.
+    var inputElement = $(document.getElementById('{{inputId}}'));
+    if (inputElement.length) {
+        inputElement.css('min-width', inputElement.attr('placeholder').length + 'ch');
+    }
+});
+{{/js}}
similarity index 85%
rename from theme/boost/templates/core/paging_bar.mustache
rename to lib/templates/paging_bar.mustache
index 5dedc98..93ef0f8 100644 (file)
@@ -1,9 +1,9 @@
 {{#haspages}}
-    <nav aria-label="{{label}}">
-        <ul class="pagination mt-3">
+    <nav aria-label="{{label}}" class="pagination pagination-centered justify-content-center">
+        <ul class="m-t-1 pagination ">
             {{#previous}}
                 <li class="page-item">
-                    <a href="{{url}}" class="page-link">
+                    <a href="{{url}}" class="page-link" aria-label="Previous">
                         <span aria-hidden="true">&laquo;</span>
                         <span class="sr-only">{{#str}}previous{{/str}}</span>
                     </a>
@@ -37,7 +37,7 @@
             {{/last}}
             {{#next}}
                 <li class="page-item">
-                    <a href="{{url}}" class="page-link">
+                    <a href="{{url}}" class="page-link" aria-label="Next">
                         <span aria-hidden="true">&raquo;</span>
                         <span class="sr-only">{{#str}}next{{/str}}</span>
                     </a>
index ed6e8ab..679175c 100644 (file)
@@ -287,6 +287,8 @@ class core_messagelib_testcase extends advanced_testcase {
             $this->assertInstanceOf('coding_exception', $e);
         }
         $this->assertCount(0, $sink->get_messages());
+        $this->assertDebuggingCalled('Attempt to send msg from a provider xxxxx/instantmessage '.
+            'that is inactive or not allowed for the user id='.$user2->id);
 
         $message->component = 'moodle';
         $message->name = 'xxx';
@@ -297,6 +299,8 @@ class core_messagelib_testcase extends advanced_testcase {
             $this->assertInstanceOf('coding_exception', $e);
         }
         $this->assertCount(0, $sink->get_messages());
+        $this->assertDebuggingCalled('Attempt to send msg from a provider moodle/xxx '.
+            'that is inactive or not allowed for the user id='.$user2->id);
         $sink->close();
         $this->assertFalse($DB->record_exists('messages', array()));
 
@@ -430,6 +434,7 @@ class core_messagelib_testcase extends advanced_testcase {
         $this->assertInstanceOf('\core\event\message_sent', $events[0]);
         $eventsink->clear();
 
+        // No messages are sent when the feature is disabled.
         $CFG->messaging = 0;
 
         $message = new \core\message\message();
@@ -446,20 +451,19 @@ class core_messagelib_testcase extends advanced_testcase {
         $message->notification      = '0';
 
         $messageid = message_send($message);
+        $this->assertFalse($messageid);
+        $this->assertDebuggingCalled('Attempt to send msg from a provider moodle/instantmessage '.
+            'that is inactive or not allowed for the user id='.$user2->id);
         $emails = $sink->get_messages();
         $this->assertCount(0, $emails);
-        $savedmessage = $DB->get_record('messages', array('id' => $messageid), '*', MUST_EXIST);
         $sink->clear();
-        $this->assertTrue($DB->record_exists('message_user_actions', array('userid' => $user2->id, 'messageid' => $messageid,
-            'action' => \core_message\api::MESSAGE_ACTION_READ)));
         $DB->delete_records('messages', array());
         $DB->delete_records('message_user_actions', array());
         $events = $eventsink->get_events();
-        $this->assertCount(2, $events);
-        $this->assertInstanceOf('\core\event\message_sent', $events[0]);
-        $this->assertInstanceOf('\core\event\message_viewed', $events[1]);
+        $this->assertCount(0, $events);
         $eventsink->clear();
 
+        // Example of a message that is sent and viewed.
         $CFG->messaging = 1;
 
         $message = new \core\message\message();
index 1e1fe7b..9346e05 100644 (file)
@@ -141,8 +141,8 @@ class core_tablelib_testcase extends basic_testcase {
         $columns = $this->generate_columns(2);
         $headers = $this->generate_headers(2);
 
-        // Search for pagination controls containing '1.*2</a>.*Next</a>'.
-        $this->expectOutputRegex('/1.*2<\/a>.*' . get_string('next') . '<\/a>/');
+        // Search for pagination controls containing 'page-link"\saria-label="Next"'.
+        $this->expectOutputRegex('/page-link"\saria-label="Next"/');
 
         $this->run_table_test(
             $columns,
index 6f35c36..f83ad6c 100644 (file)
@@ -2338,6 +2338,22 @@ function check_is_https(environment_results $result) {
     return null;
 }
 
+/**
+ * Check if the site is using 64 bits PHP.
+ *
+ * @param  environment_results $result
+ * @return environment_results|null updated results object, or null if the site is using 64 bits PHP.
+ */
+function check_sixtyfour_bits(environment_results $result) {
+
+    if (PHP_INT_SIZE === 4) {
+         $result->setInfo('php not 64 bits');
+         $result->setStatus(false);
+         return $result;
+    }
+    return null;
+}
+
 /**
  * Assert the upgrade key is provided, if it is defined.
  *
index f95ee5a..05a7337 100644 (file)
@@ -37,7 +37,7 @@ $string['deletedevice'] = 'Delete the device. Note that an app can register the
 $string['devicetoken'] = 'Device token';
 $string['errorretrievingkey'] = 'An error occurred while retrieving the access key. Your site must be registered to use this service. If your site is already registered, please try updating your registration.';
 $string['keyretrievedsuccessfully'] = 'Key retrieved successfully';
-$string['nodevices'] = 'No registered devices. Devices will automatically appear after you install the Moodle Mobile app and add this site.';
+$string['nodevices'] = 'No registered devices. Devices will automatically appear after you install the Moodle app and add this site.';
 $string['nopermissiontomanagedevices'] = 'You don\'t have permission to manage devices.';
 $string['notconfigured'] = 'The Airnotifier server hasn\'t been configured so Airnotifier messages cannot be sent';
 $string['pluginname'] = 'Mobile';
index 9ae8b7e..f78cc46 100644 (file)
@@ -534,9 +534,10 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         // Now, create some notifications...
         // We are creating fake notifications but based on real ones.
 
-        // This one omits notification = 1.
+        // This one comes from a disabled plugin's provider and therefore is not sent.
         $eventdata = new \core\message\message();
         $eventdata->courseid          = $course->id;
+        $eventdata->notification      = 1;
         $eventdata->modulename        = 'moodle';
         $eventdata->component         = 'enrol_paypal';
         $eventdata->name              = 'paypal_enrolment';
@@ -548,6 +549,24 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $eventdata->fullmessagehtml   = '';
         $eventdata->smallmessage      = '';
         message_send($eventdata);
+        $this->assertDebuggingCalled('Attempt to send msg from a provider enrol_paypal/paypal_enrolment '.
+            'that is inactive or not allowed for the user id='.$user1->id);
+
+        // This one omits notification = 1.
+        $message = new \core\message\message();
+        $message->courseid          = $course->id;
+        $message->component         = 'enrol_manual';
+        $message->name              = 'expiry_notification';
+        $message->userfrom          = $user2;
+        $message->userto            = $user1;
+        $message->subject           = 'Test: This is not a notification but otherwise is valid';
+        $message->fullmessage       = 'Test: Full message';
+        $message->fullmessageformat = FORMAT_MARKDOWN;
+        $message->fullmessagehtml   = markdown_to_html($message->fullmessage);
+        $message->smallmessage      = $message->subject;
+        $message->contexturlname    = $course->fullname;
+        $message->contexturl        = (string)new moodle_url('/course/view.php', array('id' => $course->id));
+        message_send($message);
 
         $message = new \core\message\message();
         $message->courseid          = $course->id;
index ae19f01..a39d069 100644 (file)
@@ -38,10 +38,10 @@ $string['otherenrolledusers'] = 'Other enrolled users';
 $string['pluginname'] = 'Remote enrolment service';
 $string['refetch'] = 'Re-fetch up to date state from remote hosts';
 $string['privacy:metadata:mnetservice_enrol_enrolments'] = 'Remote enrolment service';
-$string['privacy:metadata:mnetservice_enrol_enrolments:enroltime'] = 'The date/time of when the enrolment was modified.';
-$string['privacy:metadata:mnetservice_enrol_enrolments:enroltype'] = 'The name of the enrol plugin at the remote server that was used to enrol our student into their course.';
+$string['privacy:metadata:mnetservice_enrol_enrolments:enroltime'] = 'The time when the enrolment was modified';
+$string['privacy:metadata:mnetservice_enrol_enrolments:enroltype'] = 'The enrolment type on the remote server used to enrol the user in their course';
 $string['privacy:metadata:mnetservice_enrol_enrolments:hostid'] = 'The ID of the remote MNet host';
-$string['privacy:metadata:mnetservice_enrol_enrolments:remotecourseid'] = 'ID of the course at  the remote server.';
-$string['privacy:metadata:mnetservice_enrol_enrolments:rolename'] = 'The name of the role at  the remote server.';
-$string['privacy:metadata:mnetservice_enrol_enrolments:tableexplanation'] = 'This table stores the information about enrolments of our local users in courses on remote hosts.';
-$string['privacy:metadata:mnetservice_enrol_enrolments:userid'] = 'The ID of our local user on this server';
+$string['privacy:metadata:mnetservice_enrol_enrolments:remotecourseid'] = 'The ID of the course on the remote server';
+$string['privacy:metadata:mnetservice_enrol_enrolments:rolename'] = 'The name of role on the remote server';
+$string['privacy:metadata:mnetservice_enrol_enrolments:tableexplanation'] = 'The Remote enrolment service stores information about enrolments of local users in courses on remote hosts.';
+$string['privacy:metadata:mnetservice_enrol_enrolments:userid'] = 'The ID of the local user on this server';
index 06775e5..624e4d9 100644 (file)
@@ -134,8 +134,8 @@ $string['currentattemptof'] = 'This is attempt {$a->attemptnumber} ( {$a->maxatt
 $string['cutoffdate'] = 'Cut-off date';
 $string['cutoffdatecolon'] = 'Cut-off date: {$a}';
 $string['cutoffdate_help'] = 'If set, the assignment will not accept submissions after this date without an extension.';
-$string['cutoffdatevalidation'] = 'The cut-off date cannot be earlier than the due date.';
-$string['cutoffdatefromdatevalidation'] = 'Cut-off date must be after the allow submissions from date.';
+$string['cutoffdatevalidation'] = 'Cut-off date cannot be earlier than the due date.';
+$string['cutoffdatefromdatevalidation'] = 'Cut-off date cannot be earlier than the allow submissions from date.';
 $string['defaultlayout'] = 'Restore default layout';
 $string['defaultsettings'] = 'Default assignment settings';
 $string['defaultsettings_help'] = 'These settings define the defaults for all new assignments.';
@@ -157,7 +157,7 @@ $string['submissionempty'] = 'Nothing was submitted';
 $string['submissionmodified'] = 'You have existing submission data. Please leave this page and try again.';
 $string['submissionmodifiedgroup'] = 'The submission has been modified by somebody else. Please leave this page and try again.';
 $string['duedatereached'] = 'The due date for this assignment has now passed';
-$string['duedatevalidation'] = 'Due date must be after the allow submissions from date.';
+$string['duedatevalidation'] = 'Due date cannot be earlier than the allow submissions from date.';
 $string['editattemptfeedback'] = 'Edit the grade and feedback for attempt number {$a}.';
 $string['editonline'] = 'Edit online';
 $string['editingpreviousfeedbackwarning'] = 'You are editing the feedback for a previous attempt. This is attempt {$a->attemptnumber} out of {$a->totalattempts}.';
index ee78784..edc5c50 100644 (file)
@@ -345,7 +345,7 @@ function assign_update_events($assign, $override = null) {
                 unset($event->id);
             }
             $event->name      = $eventname.' ('.get_string('duedate', 'assign').')';
-            calendar_event::create($event);
+            calendar_event::create($event, false);
         }
     }
 
index a607b2c..a2dd3e4 100644 (file)
@@ -274,7 +274,10 @@ class assign {
     public function get_return_params() {
         global $PAGE;
 
-        $params = $PAGE->url->params();
+        $params = array();
+        if (!WS_SERVER) {
+            $params = $PAGE->url->params();
+        }
         unset($params['id']);
         unset($params['action']);
         return $params;
@@ -1328,9 +1331,9 @@ class assign {
             // Now process the event.
             if ($event->id) {
                 $calendarevent = calendar_event::load($event->id);
-                $calendarevent->update($event);
+                $calendarevent->update($event, false);
             } else {
-                calendar_event::create($event);
+                calendar_event::create($event, false);
             }
         } else {
             $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id,
@@ -1349,9 +1352,9 @@ class assign {
             // Now process the event.
             if ($event->id) {
                 $calendarevent = calendar_event::load($event->id);
-                $calendarevent->update($event);
+                $calendarevent->update($event, false);
             } else {
-                calendar_event::create($event);
+                calendar_event::create($event, false);
             }
         } else {
             $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id,
index ffb9a15..4cfc941 100644 (file)
@@ -223,18 +223,18 @@ class mod_assign_mod_form extends moodleform_mod {
     public function validation($data, $files) {
         $errors = parent::validation($data, $files);
 
-        if ($data['allowsubmissionsfromdate'] && $data['duedate']) {
-            if ($data['allowsubmissionsfromdate'] > $data['duedate']) {
+        if (!empty($data['allowsubmissionsfromdate']) && !empty($data['duedate'])) {
+            if ($data['duedate'] < $data['allowsubmissionsfromdate']) {
                 $errors['duedate'] = get_string('duedatevalidation', 'assign');
             }
         }
-        if ($data['duedate'] && $data['cutoffdate']) {
-            if ($data['duedate'] > $data['cutoffdate']) {
+        if (!empty($data['cutoffdate']) && !empty($data['duedate'])) {
+            if ($data['cutoffdate'] < $data['duedate'] ) {
                 $errors['cutoffdate'] = get_string('cutoffdatevalidation', 'assign');
             }
         }
-        if ($data['allowsubmissionsfromdate'] && $data['cutoffdate']) {
-            if ($data['allowsubmissionsfromdate'] > $data['cutoffdate']) {
+        if (!empty($data['allowsubmissionsfromdate']) && !empty($data['cutoffdate'])) {
+            if ($data['cutoffdate'] < $data['allowsubmissionsfromdate']) {
                 $errors['cutoffdate'] = get_string('cutoffdatefromdatevalidation', 'assign');
             }
         }
diff --git a/mod/assign/tests/behat/assign_no_calendar_capabilities.feature b/mod/assign/tests/behat/assign_no_calendar_capabilities.feature
new file mode 100644 (file)
index 0000000..e97a77d
--- /dev/null
@@ -0,0 +1,58 @@
+@mod @mod_assign
+Feature: Assignment with no calendar capabilites
+  In order to allow work effectively
+  As a teacher
+  I need to be able to create assignments even when I cannot edit calendar events
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "admin"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Permissions" in current page administration
+    And I override the system permissions of "Teacher" role with:
+      | capability | permission |
+      | moodle/calendar:manageentries | Prohibit |
+    And I log out
+
+  Scenario: Editing an assignment
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    When I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test assignment name |
+      | Description | Test assignment description |
+      | id_allowsubmissionsfromdate_enabled | 1 |
+      | id_allowsubmissionsfromdate_day | 1 |
+      | id_allowsubmissionsfromdate_month | 1 |
+      | id_allowsubmissionsfromdate_year | 2017 |
+      | id_duedate_enabled | 1 |
+      | id_duedate_day | 1 |
+      | id_duedate_month | 2 |
+      | id_duedate_year | 2017 |
+      | id_cutoffdate_enabled | 1 |
+      | id_cutoffdate_day | 2 |
+      | id_cutoffdate_month | 2 |
+      | id_cutoffdate_year | 2017 |
+      | id_gradingduedate_enabled | 1 |
+      | id_gradingduedate_day | 1 |
+      | id_gradingduedate_month | 3 |
+      | id_gradingduedate_year | 2017 |
+    And I log out
+    When I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I follow "Test assignment name"
+    And I navigate to "Edit settings" in current page administration
+    And I set the following fields to these values:
+      | id_allowsubmissionsfromdate_year | 2018 |
+      | id_duedate_year | 2018 |
+      | id_cutoffdate_year | 2018 |
+      | id_gradingduedate_year | 2018 |
+    And I press "Save and return to course"
+    Then I should see "Test assignment name"
index 54054b3..759f534 100644 (file)
@@ -1472,4 +1472,29 @@ class mod_assign_lib_testcase extends advanced_testcase {
         // is changed.
         $this->assertNotEmpty($moduleupdatedevents);
     }
+
+    /**
+     * A user who does not have capabilities to add events to the calendar should be able to create an assignment.
+     */
+    public function test_creation_with_no_calendar_capabilities() {
+        $this->resetAfterTest();
+        $course = self::getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+        $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $roleid = self::getDataGenerator()->create_role();
+        self::getDataGenerator()->role_assign($roleid, $user->id, $context->id);
+        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
+        $generator = self::getDataGenerator()->get_plugin_generator('mod_assign');
+        // Create an instance as a user without the calendar capabilities.
+        $this->setUser($user);
+        $time = time();
+        $params = array(
+            'course' => $course->id,
+            'allowsubmissionsfromdate' => $time,
+            'duedate' => $time + 500,
+            'cutoffdate' => $time + 600,
+            'gradingduedate' => $time + 700,
+        );
+        $generator->create_instance($params);
+    }
 }
index 7e2aaad..774ebee 100644 (file)
@@ -134,7 +134,7 @@ function chat_add_instance($chat) {
         $event->timesort    = $chat->chattime;
         $event->timeduration = 0;
 
-        calendar_event::create($event);
+        calendar_event::create($event, false);
     }
 
     if (!empty($chat->completionexpected)) {
@@ -174,7 +174,7 @@ function chat_update_instance($chat) {
             $event->timesort    = $chat->chattime;
 
             $calendarevent = calendar_event::load($event->id);
-            $calendarevent->update($event);
+            $calendarevent->update($event, false);
         } else {
             // Do not publish this event, so delete it.
             $calendarevent = calendar_event::load($event->id);
@@ -197,7 +197,7 @@ function chat_update_instance($chat) {
             $event->timesort    = $chat->chattime;
             $event->timeduration = 0;
 
-            calendar_event::create($event);
+            calendar_event::create($event, false);
         }
     }
 
@@ -501,7 +501,7 @@ function chat_prepare_update_events($chat, $cm = null) {
     if ($event->id = $DB->get_field('event', 'id', array('modulename' => 'chat', 'instance' => $chat->id,
             'eventtype' => CHAT_EVENT_TYPE_CHATTIME))) {
         $calendarevent = calendar_event::load($event->id);
-        $calendarevent->update($event);
+        $calendarevent->update($event, false);
     } else if ($chat->schedule > 0) {
         // The chat is scheduled and the event should be published.
         $event->courseid    = $chat->course;
@@ -512,7 +512,7 @@ function chat_prepare_update_events($chat, $cm = null) {
         $event->eventtype   = CHAT_EVENT_TYPE_CHATTIME;
         $event->timeduration = 0;
         $event->visible = $cm->visible;
-        calendar_event::create($event);
+        calendar_event::create($event, false);
     }
 }
 
diff --git a/mod/chat/tests/behat/chat_no_calendar_capabilities.feature b/mod/chat/tests/behat/chat_no_calendar_capabilities.feature
new file mode 100644 (file)
index 0000000..a990349
--- /dev/null
@@ -0,0 +1,43 @@
+@mod @mod_chat
+Feature: Chat with no calendar capabilites
+  In order to allow work effectively
+  As a teacher
+  I need to be able to create chats even when I cannot edit calendar events
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "admin"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Permissions" in current page administration
+    And I override the system permissions of "Teacher" role with:
+      | capability | permission |
+      | moodle/calendar:manageentries | Prohibit |
+    And I log out
+
+  Scenario: Editing a chat
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    When I add a "Chat" to section "1" and I fill the form with:
+      | Name of this chat room | Test chat name |
+      | Description | Test chat description |
+      | Repeat/publish session times | No repeats - publish the specified time only |
+      | id_chattime_day | 1 |
+      | id_chattime_month | 1 |
+      | id_chattime_year | 2017 |
+    And I log out
+    When I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I follow "Test chat name"
+    And I navigate to "Edit settings" in current page administration
+    And I set the following fields to these values:
+      | id_chattime_year | 2018 |
+    And I press "Save and return to course"
+    Then I should see "Test chat name"
index fda8b4a..9df1d41 100644 (file)
@@ -363,4 +363,25 @@ class mod_chat_lib_testcase extends advanced_testcase {
 
         return calendar_event::create($event);
     }
+
+    /**
+     * A user who does not have capabilities to add events to the calendar should be able to create an chat.
+     */
+    public function test_creation_with_no_calendar_capabilities() {
+        $this->resetAfterTest();
+        $course = self::getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+        $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $roleid = self::getDataGenerator()->create_role();
+        self::getDataGenerator()->role_assign($roleid, $user->id, $context->id);
+        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
+        $generator = self::getDataGenerator()->get_plugin_generator('mod_chat');
+        // Create an instance as a user without the calendar capabilities.
+        $this->setUser($user);
+        $params = array(
+            'course' => $course->id,
+            'chattime' => time() + 500,
+        );
+        $generator->create_instance($params);
+    }
 }
index ab33373..b5cb29b 100644 (file)
@@ -58,7 +58,7 @@ function choice_set_events($choice) {
             $event->visible      = instance_is_visible('choice', $choice);
             $event->timeduration = 0;
             $calendarevent = calendar_event::load($event->id);
-            $calendarevent->update($event);
+            $calendarevent->update($event, false);
         } else {
             // Calendar event is on longer needed.
             $calendarevent = calendar_event::load($event->id);
@@ -78,7 +78,7 @@ function choice_set_events($choice) {
             $event->timesort     = $choice->timeopen;
             $event->visible      = instance_is_visible('choice', $choice);
             $event->timeduration = 0;
-            calendar_event::create($event);
+            calendar_event::create($event, false);
         }
     }
 
@@ -97,7 +97,7 @@ function choice_set_events($choice) {
             $event->visible      = instance_is_visible('choice', $choice);
             $event->timeduration = 0;
             $calendarevent = calendar_event::load($event->id);
-            $calendarevent->update($event);
+            $calendarevent->update($event, false);
         } else {
             // Calendar event is on longer needed.
             $calendarevent = calendar_event::load($event->id);
@@ -117,7 +117,7 @@ function choice_set_events($choice) {
             $event->timesort     = $choice->timeclose;
             $event->visible      = instance_is_visible('choice', $choice);
             $event->timeduration = 0;
-            calendar_event::create($event);
+            calendar_event::create($event, false);
         }
     }
 }
diff --git a/mod/choice/tests/behat/choice_no_calendar_capabilities.feature b/mod/choice/tests/behat/choice_no_calendar_capabilities.feature
new file mode 100644 (file)
index 0000000..0f4554c
--- /dev/null
@@ -0,0 +1,50 @@
+@mod @mod_choice
+Feature: Choice with no calendar capabilites
+  In order to allow work effectively
+  As a teacher
+  I need to be able to create choices even when I cannot edit calendar events
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "admin"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Permissions" in current page administration
+    And I override the system permissions of "Teacher" role with:
+      | capability | permission |
+      | moodle/calendar:manageentries | Prohibit |
+    And I log out
+
+  Scenario: Editing a choice
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    When I add a "Choice" to section "1" and I fill the form with:
+      | Choice name | Test choice name |
+      | Description | Test choice description |
+      | option[0] | Option 1 |
+      | option[1] | Option 2 |
+      | id_timeopen_enabled | 1 |
+      | id_timeopen_day | 1 |
+      | id_timeopen_month | 1 |
+      | id_timeopen_year | 2017 |
+      | id_timeclose_enabled | 1 |
+      | id_timeclose_day | 1 |
+      | id_timeclose_month | 2 |
+      | id_timeclose_year | 2017 |
+    And I log out
+    When I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I follow "Test choice name"
+    And I navigate to "Edit settings" in current page administration
+    And I set the following fields to these values:
+      | id_timeopen_year | 2018 |
+      | id_timeclose_year | 2018 |
+    And I press "Save and return to course"
+    Then I should see "Test choice name"
index eb452e1..72a32fe 100644 (file)
@@ -979,4 +979,27 @@ class mod_choice_lib_testcase extends externallib_advanced_testcase {
         $this->expectException('moodle_exception');
         choice_user_submit_response($optionids[1], $choicewithoptions, $user2->id, $course, $cm);
     }
+
+    /**
+     * A user who does not have capabilities to add events to the calendar should be able to create an choice.
+     */
+    public function test_creation_with_no_calendar_capabilities() {
+        $this->resetAfterTest();
+        $course = self::getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+        $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $roleid = self::getDataGenerator()->create_role();
+        self::getDataGenerator()->role_assign($roleid, $user->id, $context->id);
+        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
+        $generator = self::getDataGenerator()->get_plugin_generator('mod_choice');
+        // Create an instance as a user without the calendar capabilities.
+        $this->setUser($user);
+        $time = time();
+        $params = array(
+            'course' => $course->id,
+            'timeopen' => $time + 200,
+            'timeclose' => $time + 500,
+        );
+        $generator->create_instance($params);
+    }
 }
index 5c6a6bf..d7ec1db 100644 (file)
@@ -156,6 +156,7 @@ if ($rid) {
 
 $PAGE->set_title($data->name);
 $PAGE->set_heading($course->fullname);
+$PAGE->force_settings_menu(true);
 
 // Process incoming data for adding/updating records.
 
index b2444f3..e6d6e50 100644 (file)
@@ -84,6 +84,7 @@ if($mform->is_cancelled()) {
     // build header to match the rest of the UI
     $PAGE->set_title($data->name);
     $PAGE->set_heading($course->fullname);
+    $PAGE->force_settings_menu(true);
     echo $OUTPUT->header();
     echo $OUTPUT->heading(format_string($data->name), 2);
     echo $OUTPUT->box(format_module_intro('data', $data, $cm->id), 'generalbox', 'intro');
index d5040af..f014bb1 100644 (file)
@@ -241,6 +241,7 @@ foreach ($plugins as $plugin=>$fulldir){
 asort($menufield);    //sort in alphabetical order
 $PAGE->set_title(get_string('course') . ': ' . $course->fullname);
 $PAGE->set_heading($course->fullname);
+$PAGE->force_settings_menu(true);
 
 $PAGE->set_pagetype('mod-data-field-' . $newtype);
 if (($mode == 'new') && (!empty($newtype)) && confirm_sesskey()) {          ///  Adding a new field
index e2f6442..db8a779 100644 (file)
@@ -613,7 +613,7 @@ function data_set_events($data) {
             $event->visible      = instance_is_visible('data', $data);
             $event->timeduration = 0;
             $calendarevent = calendar_event::load($event->id);
-            $calendarevent->update($event);
+            $calendarevent->update($event, false);
         } else {
             // Calendar event is on longer needed.
             $calendarevent = calendar_event::load($event->id);
@@ -633,7 +633,7 @@ function data_set_events($data) {
             $event->timesort     = $data->timeavailablefrom;
             $event->visible      = instance_is_visible('data', $data);
             $event->timeduration = 0;
-            calendar_event::create($event);
+            calendar_event::create($event, false);
         }
     }
 
@@ -652,7 +652,7 @@ function data_set_events($data) {
             $event->visible      = instance_is_visible('data', $data);
             $event->timeduration = 0;
             $calendarevent = calendar_event::load($event->id);
-            $calendarevent->update($event);
+            $calendarevent->update($event, false);
         } else {
             // Calendar event is on longer needed.
             $calendarevent = calendar_event::load($event->id);
@@ -672,7 +672,7 @@ function data_set_events($data) {
             $event->timesort     = $data->timeavailableto;
             $event->visible      = instance_is_visible('data', $data);
             $event->timeduration = 0;
-            calendar_event::create($event);
+            calendar_event::create($event, false);
         }
     }
 }
index 5cb45e9..9a166b2 100644 (file)
@@ -51,6 +51,7 @@ require_capability('mod/data:managetemplates', $context);
 $PAGE->set_url(new moodle_url('/mod/data/preset.php', array('d'=>$data->id)));
 $PAGE->set_title(get_string('course') . ': ' . $course->fullname);
 $PAGE->set_heading($course->fullname);
+$PAGE->force_settings_menu(true);
 
 // fill in missing properties needed for updating of instance
 $data->course     = $cm->course;
index 8fa48c5..ce5af6c 100644 (file)
@@ -103,6 +103,7 @@ $PAGE->requires->js('/mod/data/data.js');
 $PAGE->set_title($data->name);
 $PAGE->set_heading($course->fullname);
 $PAGE->set_pagelayout('admin');
+$PAGE->force_settings_menu(true);
 echo $OUTPUT->header();
 echo $OUTPUT->heading(format_string($data->name), 2);
 echo $OUTPUT->box(format_module_intro('data', $data, $cm->id), 'generalbox', 'intro');
diff --git a/mod/data/tests/behat/data_no_calendar_capabilities.feature b/mod/data/tests/behat/data_no_calendar_capabilities.feature
new file mode 100644 (file)
index 0000000..98d7d49
--- /dev/null
@@ -0,0 +1,58 @@
+@mod @mod_data
+Feature: Database with no calendar capabilites
+  In order to allow work effectively
+  As a teacher
+  I need to be able to create databases even when I cannot edit calendar events
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "admin"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Permissions" in current page administration
+    And I override the system permissions of "Teacher" role with:
+      | capability | permission |
+      | moodle/calendar:manageentries | Prohibit |
+    And I log out
+
+  Scenario: Editing a database
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    When I add a "Database" to section "1" and I fill the form with:
+      | Name | Test database name |
+      | Description | Test database description |
+      | id_timeavailablefrom_enabled | 1 |
+      | id_timeavailablefrom_day | 1 |
+      | id_timeavailablefrom_month | 1 |
+      | id_timeavailablefrom_year | 2017 |
+      | id_timeavailableto_enabled | 1 |
+      | id_timeavailableto_day | 1 |
+      | id_timeavailableto_month | 4 |
+      | id_timeavailableto_year | 2017 |
+      | id_timeviewfrom_enabled | 1 |
+      | id_timeviewfrom_day | 1 |
+      | id_timeviewfrom_month | 3 |
+      | id_timeviewfrom_year | 2017 |
+      | id_timeviewto_enabled | 1 |
+      | id_timeviewto_day | 1 |
+      | id_timeviewto_month | 4 |
+      | id_timeviewto_year | 2017 |
+    And I log out
+    When I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I follow "Test database name"
+    And I navigate to "Edit settings" in current page administration
+    And I set the following fields to these values:
+      | id_timeavailablefrom_year | 2018 |
+      | id_timeavailableto_year | 2018 |
+      | id_timeviewfrom_year | 2018 |
+      | id_timeviewto_year | 2018 |
+    And I press "Save and return to course"
+    Then I should see "Test database name"
index 7405187..84e8a7b 100644 (file)
@@ -1984,4 +1984,29 @@ class mod_data_lib_testcase extends advanced_testcase {
         $this->assertNull($min);
         $this->assertNull($max);
     }
+
+    /**
+     * A user who does not have capabilities to add events to the calendar should be able to create an database.
+     */
+    public function test_creation_with_no_calendar_capabilities() {
+        $this->resetAfterTest();
+        $course = self::getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+        $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $roleid = self::getDataGenerator()->create_role();
+        self::getDataGenerator()->role_assign($roleid, $user->id, $context->id);
+        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
+        $generator = self::getDataGenerator()->get_plugin_generator('mod_data');
+        // Create an instance as a user without the calendar capabilities.
+        $this->setUser($user);
+        $time = time();
+        $params = array(
+            'course' => $course->id,
+            'timeavailablefrom' => $time + 200,
+            'timeavailableto' => $time + 2000,
+            'timeviewfrom' => $time + 400,
+            'timeviewto' => $time + 2000,
+        );
+        $generator->create_instance($params);
+    }
 }
index 44480a2..7bf40ab 100644 (file)
@@ -293,15 +293,22 @@ class mod_feedback_complete_form extends moodleform {
      */
     public function add_form_element($item, $element, $addrequiredrule = true, $setdefaultvalue = true) {
         global $OUTPUT;
-        // Add element to the form.
-        if (is_array($element)) {
-            if ($this->is_frozen() && $element[0] === 'text') {
-                // Convert 'text' element to 'static' when freezing for better display.
-                $element = ['static', $element[1], $element[2]];
+
+        if (is_array($element) && $element[0] == 'group') {
+            // For groups, use the mforms addGroup API.
+            // $element looks like: ['group', $groupinputname, $name, $objects, $separator, $appendname],
+            $element = $this->_form->addGroup($element[3], $element[1], $element[2], $element[4], $element[5]);
+        } else {
+            // Add non-group element to the form.
+            if (is_array($element)) {
+                if ($this->is_frozen() && $element[0] === 'text') {
+                    // Convert 'text' element to 'static' when freezing for better display.
+                    $element = ['static', $element[1], $element[2]];
+                }
+                $element = call_user_func_array(array($this->_form, 'createElement'), $element);
             }
-            $element = call_user_func_array(array($this->_form, 'createElement'), $element);
+            $element = $this->_form->addElement($element);
         }
-        $element = $this->_form->addElement($element);
 
         // Prepend standard CSS classes to the element classes.
         $attributes = $element->getAttributes();
index 2d89f59..9642de3 100644 (file)
@@ -147,34 +147,48 @@ class mod_feedback_external extends external_api {
      * Utility function for validating a feedback.
      *
      * @param int $feedbackid feedback instance id
-     * @return array array containing the feedback persistent, course, context and course module objects
+     * @param int $courseid courseid course where user completes the feedback (for site feedbacks only)
+     * @return array containing the feedback, feedback course, context, course module and the course where is being completed.
+     * @throws moodle_exception
      * @since  Moodle 3.3
      */
-    protected static function validate_feedback($feedbackid) {
+    protected static function validate_feedback($feedbackid, $courseid = 0) {
         global $DB, $USER;
 
         // Request and permission validation.
         $feedback = $DB->get_record('feedback', array('id' => $feedbackid), '*', MUST_EXIST);
-        list($course, $cm) = get_course_and_cm_from_instance($feedback, 'feedback');
+        list($feedbackcourse, $cm) = get_course_and_cm_from_instance($feedback, 'feedback');
 
         $context = context_module::instance($cm->id);
         self::validate_context($context);
 
-        return array($feedback, $course, $cm, $context);
+        // Set default completion course.
+        $completioncourse = (object) array('id' => 0);
+        if ($feedbackcourse->id == SITEID && $courseid) {
+            $completioncourse = get_course($courseid);
+            self::validate_context(context_course::instance($courseid));
+
+            $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $courseid);
+            if (!$feedbackcompletion->check_course_is_mapped()) {
+                throw new moodle_exception('cannotaccess', 'mod_feedback');
+            }
+        }
+
+        return array($feedback, $feedbackcourse, $cm, $context, $completioncourse);
     }
 
     /**
      * Utility function for validating access to feedback.
      *
      * @param  stdClass   $feedback feedback object
-     * @param  stdClass   $course   course object
+     * @param  stdClass   $course   course where user completes the feedback (for site feedbacks only)
      * @param  stdClass   $cm       course module
      * @param  stdClass   $context  context object
      * @throws moodle_exception
-     * @return feedback_completion feedback completion instance
+     * @return mod_feedback_completion feedback completion instance
      * @since  Moodle 3.3
      */
-    protected static function validate_feedback_access($feedback,  $course, $cm, $context, $checksubmit = false) {
+    protected static function validate_feedback_access($feedback, $course, $cm, $context, $checksubmit = false) {
         $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $course->id);
 
         if (!$feedbackcompletion->can_complete()) {
@@ -204,7 +218,9 @@ class mod_feedback_external extends external_api {
     public static function get_feedback_access_information_parameters() {
         return new external_function_parameters (
             array(
-                'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id.')
+                'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id.'),
+                'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).',
+                    VALUE_DEFAULT, 0),
             )
         );
     }
@@ -213,20 +229,23 @@ class mod_feedback_external extends external_api {
      * Return access information for a given feedback.
      *
      * @param int $feedbackid feedback instance id
+     * @param int $courseid course where user completes the feedback (for site feedbacks only)
      * @return array of warnings and the access information
      * @since Moodle 3.3
      * @throws  moodle_exception
      */
-    public static function get_feedback_access_information($feedbackid) {
+    public static function get_feedback_access_information($feedbackid, $courseid = 0) {
         global $PAGE;
 
         $params = array(
-            'feedbackid' => $feedbackid
+            'feedbackid' => $feedbackid,
+            'courseid' => $courseid,
         );
         $params = self::validate_parameters(self::get_feedback_access_information_parameters(), $params);
 
-        list($feedback, $course, $cm, $context) = self::validate_feedback($params['feedbackid']);
-        $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $course->id);
+        list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'],
+            $params['courseid']);
+        $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $completioncourse->id);
 
         $result = array();
         // Capabilities first.
@@ -284,6 +303,8 @@ class mod_feedback_external extends external_api {
                 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id'),
                 'moduleviewed' => new external_value(PARAM_BOOL, 'If we need to mark the module as viewed for completion',
                     VALUE_DEFAULT, false),
+                'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).',
+                    VALUE_DEFAULT, 0),
             )
         );
     }
@@ -293,18 +314,20 @@ class mod_feedback_external extends external_api {
      *
      * @param int $feedbackid feedback instance id
      * @param bool $moduleviewed If we need to mark the module as viewed for completion
+     * @param int $courseid course where user completes the feedback (for site feedbacks only)
      * @return array of warnings and status result
      * @since Moodle 3.3
      * @throws moodle_exception
      */
-    public static function view_feedback($feedbackid, $moduleviewed = false) {
+    public static function view_feedback($feedbackid, $moduleviewed = false, $courseid = 0) {
 
-        $params = array('feedbackid' => $feedbackid, 'moduleviewed' => $moduleviewed);
+        $params = array('feedbackid' => $feedbackid, 'moduleviewed' => $moduleviewed, 'courseid' => $courseid);
         $params = self::validate_parameters(self::view_feedback_parameters(), $params);
         $warnings = array();
 
-        list($feedback, $course, $cm, $context) = self::validate_feedback($params['feedbackid']);
-        $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $course->id);
+        list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'],
+            $params['courseid']);
+        $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $completioncourse->id);
 
         // Trigger module viewed event.
         $feedbackcompletion->trigger_module_viewed();
@@ -348,6 +371,8 @@ class mod_feedback_external extends external_api {
         return new external_function_parameters (
             array(
                 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id'),
+                'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).',
+                    VALUE_DEFAULT, 0),
             )
         );
     }
@@ -356,19 +381,21 @@ class mod_feedback_external extends external_api {
      * Returns the temporary completion record for the current user.
      *
      * @param int $feedbackid feedback instance id
+     * @param int $courseid course where user completes the feedback (for site feedbacks only)
      * @return array of warnings and status result
      * @since Moodle 3.3
      * @throws moodle_exception
      */
-    public static function get_current_completed_tmp($feedbackid) {
+    public static function get_current_completed_tmp($feedbackid, $courseid = 0) {
         global $PAGE;
 
-        $params = array('feedbackid' => $feedbackid);
+        $params = array('feedbackid' => $feedbackid, 'courseid' => $courseid);
         $params = self::validate_parameters(self::get_current_completed_tmp_parameters(), $params);
         $warnings = array();
 
-        list($feedback, $course, $cm, $context) = self::validate_feedback($params['feedbackid']);
-        $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $course->id);
+        list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'],
+            $params['courseid']);
+        $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $completioncourse->id);
 
         if ($completed = $feedbackcompletion->get_current_completed_tmp()) {
             $exporter = new feedback_completedtmp_exporter($completed);
@@ -405,6 +432,8 @@ class mod_feedback_external extends external_api {
         return new external_function_parameters (
             array(
                 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id'),
+                'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).',
+                    VALUE_DEFAULT, 0),
             )
         );
     }
@@ -413,19 +442,21 @@ class mod_feedback_external extends external_api {
      * Returns the items (questions) in the given feedback.
      *
      * @param int $feedbackid feedback instance id
+     * @param int $courseid course where user completes the feedback (for site feedbacks only)
      * @return array of warnings and feedbacks
      * @since Moodle 3.3
      */
-    public static function get_items($feedbackid) {
+    public static function get_items($feedbackid, $courseid = 0) {
         global $PAGE;
 
-        $params = array('feedbackid' => $feedbackid);
+        $params = array('feedbackid' => $feedbackid, 'courseid' => $courseid);
         $params = self::validate_parameters(self::get_items_parameters(), $params);
         $warnings = array();
 
-        list($feedback, $course, $cm, $context) = self::validate_feedback($params['feedbackid']);
+        list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'],
+            $params['courseid']);
 
-        $feedbackstructure = new mod_feedback_structure($feedback, $cm, $course->id);
+        $feedbackstructure = new mod_feedback_structure($feedback, $cm, $completioncourse->id);
         $returneditems = array();
         if ($items = $feedbackstructure->get_items()) {
             foreach ($items as $item) {
@@ -470,6 +501,8 @@ class mod_feedback_external extends external_api {
         return new external_function_parameters (
             array(
                 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id'),
+                'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).',
+                    VALUE_DEFAULT, 0),
             )
         );
     }
@@ -478,19 +511,21 @@ class mod_feedback_external extends external_api {
      * Starts or continues a feedback submission
      *
      * @param array $feedbackid feedback instance id
+     * @param int $courseid course where user completes a feedback (for site feedbacks only).
      * @return array of warnings and launch information
      * @since Moodle 3.3
      */
-    public static function launch_feedback($feedbackid) {
+    public static function launch_feedback($feedbackid, $courseid = 0) {
         global $PAGE;
 
-        $params = array('feedbackid' => $feedbackid);
+        $params = array('feedbackid' => $feedbackid, 'courseid' => $courseid);
         $params = self::validate_parameters(self::launch_feedback_parameters(), $params);
         $warnings = array();
 
-        list($feedback, $course, $cm, $context) = self::validate_feedback($params['feedbackid']);
+        list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'],
+            $params['courseid']);
         // Check we can do a new submission (or continue an existing).
-        $feedbackcompletion = self::validate_feedback_access($feedback,  $course, $cm, $context, true);
+        $feedbackcompletion = self::validate_feedback_access($feedback, $completioncourse, $cm, $context, true);
 
         $gopage = $feedbackcompletion->get_resume_page();
         if ($gopage === null) {
@@ -530,6 +565,8 @@ class mod_feedback_external extends external_api {
             array(
                 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id'),
                 'page' => new external_value(PARAM_INT, 'The page to get starting by 0'),
+                'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).',
+                    VALUE_DEFAULT, 0),
             )
         );
     }
@@ -539,19 +576,21 @@ class mod_feedback_external extends external_api {
      *
      * @param int $feedbackid feedback instance id
      * @param int $page the page to get starting by 0
+     * @param int $courseid course where user completes the feedback (for site feedbacks only)
      * @return array of warnings and launch information
      * @since Moodle 3.3
      */
-    public static function get_page_items($feedbackid, $page) {
+    public static function get_page_items($feedbackid, $page, $courseid = 0) {
         global $PAGE;
 
-        $params = array('feedbackid' => $feedbackid, 'page' => $page);
+        $params = array('feedbackid' => $feedbackid, 'page' => $page, 'courseid' => $courseid);
         $params = self::validate_parameters(self::get_page_items_parameters(), $params);
         $warnings = array();
 
-        list($feedback, $course, $cm, $context) = self::validate_feedback($params['feedbackid']);
+        list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'],
+            $params['courseid']);
 
-        $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $course->id);
+        $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $completioncourse->id);
 
         $page = $params['page'];
         $pages = $feedbackcompletion->get_pages();
@@ -615,6 +654,8 @@ class mod_feedback_external extends external_api {
                     ), 'The data to be processed.', VALUE_DEFAULT, array()
                 ),
                 'goprevious' => new external_value(PARAM_BOOL, 'Whether we want to jump to previous page.', VALUE_DEFAULT, false),
+                'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).',
+                    VALUE_DEFAULT, 0),
             )
         );
     }
@@ -626,20 +667,23 @@ class mod_feedback_external extends external_api {
      * @param array $page the page being processed
      * @param array $responses the responses to be processed
      * @param bool $goprevious whether we want to jump to previous page
+     * @param int $courseid course where user completes the feedback (for site feedbacks only)
      * @return array of warnings and launch information
      * @since Moodle 3.3
      */
-    public static function process_page($feedbackid, $page, $responses = [], $goprevious = false) {
+    public static function process_page($feedbackid, $page, $responses = [], $goprevious = false, $courseid = 0) {
         global $USER, $SESSION;
 
-        $params = array('feedbackid' => $feedbackid, 'page' => $page, 'responses' => $responses, 'goprevious' => $goprevious);
+        $params = array('feedbackid' => $feedbackid, 'page' => $page, 'responses' => $responses, 'goprevious' => $goprevious,
+            'courseid' => $courseid);
         $params = self::validate_parameters(self::process_page_parameters(), $params);
         $warnings = array();
         $siteaftersubmit = $completionpagecontents = '';
 
-        list($feedback, $course, $cm, $context) = self::validate_feedback($params['feedbackid']);
+        list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'],
+            $params['courseid']);
         // Check we can do a new submission (or continue an existing).
-        $feedbackcompletion = self::validate_feedback_access($feedback,  $course, $cm, $context, true);
+        $feedbackcompletion = self::validate_feedback_access($feedback, $completioncourse, $cm, $context, true);
 
         // Create the $_POST object required by the feedback question engine.
         $_POST = array();
@@ -653,7 +697,7 @@ class mod_feedback_external extends external_api {
         }
         // Force fields.
         $_POST['id'] = $cm->id;
-        $_POST['courseid'] = $course->id;
+        $_POST['courseid'] = $courseid;
         $_POST['gopage'] = $params['page'];
         $_POST['_qf__mod_feedback_complete_form'] = 1;
 
@@ -725,6 +769,8 @@ class mod_feedback_external extends external_api {
                 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id'),
                 'groupid' => new external_value(PARAM_INT, 'Group id, 0 means that the function will determine the user group',
                                                 VALUE_DEFAULT, 0),
+                'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).',
+                    VALUE_DEFAULT, 0),
             )
         );
     }
@@ -733,20 +779,23 @@ class mod_feedback_external extends external_api {
      * Retrieves the feedback analysis.
      *
      * @param array $feedbackid feedback instance id
+     * @param int $groupid group id, 0 means that the function will determine the user group
+     * @param int $courseid course where user completes the feedback (for site feedbacks only)
      * @return array of warnings and launch information
      * @since Moodle 3.3
      */
-    public static function get_analysis($feedbackid, $groupid = 0) {
+    public static function get_analysis($feedbackid, $groupid = 0, $courseid = 0) {
         global $PAGE;
 
-        $params = array('feedbackid' => $feedbackid, 'groupid' => $groupid);
+        $params = array('feedbackid' => $feedbackid, 'groupid' => $groupid, 'courseid' => $courseid);
         $params = self::validate_parameters(self::get_analysis_parameters(), $params);
         $warnings = $itemsdata = array();
 
-        list($feedback, $course, $cm, $context) = self::validate_feedback($params['feedbackid']);
+        list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'],
+            $params['courseid']);
 
         // Check permissions.
-        $feedbackstructure = new mod_feedback_structure($feedback, $cm);
+        $feedbackstructure = new mod_feedback_structure($feedback, $cm, $completioncourse->id);
         if (!$feedbackstructure->can_view_analysis()) {
             throw new required_capability_exception($context, 'mod/feedback:viewanalysepage', 'nopermission', '');
         }
@@ -850,6 +899,8 @@ class mod_feedback_external extends external_api {
         return new external_function_parameters (
             array(
                 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id.'),
+                'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).',
+                    VALUE_DEFAULT, 0),
             )
         );
     }
@@ -858,18 +909,20 @@ class mod_feedback_external extends external_api {
      * Retrieves responses from the current unfinished attempt.
      *
      * @param array $feedbackid feedback instance id
+     * @param int $courseid course where user completes the feedback (for site feedbacks only)
      * @return array of warnings and launch information
      * @since Moodle 3.3
      */
-    public static function get_unfinished_responses($feedbackid) {
+    public static function get_unfinished_responses($feedbackid, $courseid = 0) {
         global $PAGE;
 
-        $params = array('feedbackid' => $feedbackid);
+        $params = array('feedbackid' => $feedbackid, 'courseid' => $courseid);
         $params = self::validate_parameters(self::get_unfinished_responses_parameters(), $params);
         $warnings = $itemsdata = array();
 
-        list($feedback, $course, $cm, $context) = self::validate_feedback($params['feedbackid']);
-        $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $course->id);
+        list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'],
+            $params['courseid']);
+        $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $completioncourse->id);
 
         $responses = array();
         $unfinished = $feedbackcompletion->get_unfinished_responses();
@@ -912,6 +965,8 @@ class mod_feedback_external extends external_api {
         return new external_function_parameters (
             array(
                 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id.'),
+                'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).',
+                    VALUE_DEFAULT, 0),
             )
         );
     }
@@ -920,18 +975,20 @@ class mod_feedback_external extends external_api {
      * Retrieves responses from the last finished attempt.
      *
      * @param array $feedbackid feedback instance id
+     * @param int $courseid course where user completes the feedback (for site feedbacks only)
      * @return array of warnings and the responses
      * @since Moodle 3.3
      */
-    public static function get_finished_responses($feedbackid) {
+    public static function get_finished_responses($feedbackid, $courseid = 0) {
         global $PAGE;
 
-        $params = array('feedbackid' => $feedbackid);
+        $params = array('feedbackid' => $feedbackid, 'courseid' => $courseid);
         $params = self::validate_parameters(self::get_finished_responses_parameters(), $params);
         $warnings = $itemsdata = array();
 
-        list($feedback, $course, $cm, $context) = self::validate_feedback($params['feedbackid']);
-        $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $course->id);
+        list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'],
+            $params['courseid']);
+        $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $completioncourse->id);
 
         $responses = array();
         // Load and get the responses from the last completed feedback.
@@ -982,6 +1039,8 @@ class mod_feedback_external extends external_api {
                                                 VALUE_DEFAULT, 'lastaccess'),
                 'page' => new external_value(PARAM_INT, 'The page of records to return.', VALUE_DEFAULT, 0),
                 'perpage' => new external_value(PARAM_INT, 'The number of records to return per page.', VALUE_DEFAULT, 0),
+                'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).',
+                    VALUE_DEFAULT, 0),
             )
         );
     }
@@ -994,18 +1053,25 @@ class mod_feedback_external extends external_api {
      * @param str $sort sort param, must be firstname, lastname or lastaccess (default)
      * @param int $page the page of records to return
      * @param int $perpage the number of records to return per page
+     * @param int $courseid course where user completes the feedback (for site feedbacks only)
      * @return array of warnings and users ids
      * @since Moodle 3.3
      */
-    public static function get_non_respondents($feedbackid, $groupid = 0, $sort = 'lastaccess', $page = 0, $perpage = 0) {
+    public static function get_non_respondents($feedbackid, $groupid = 0, $sort = 'lastaccess', $page = 0, $perpage = 0,
+            $courseid = 0) {
+
         global $CFG;
         require_once($CFG->dirroot . '/mod/feedback/lib.php');
 
-        $params = array('feedbackid' => $feedbackid, 'groupid' => $groupid, 'sort' => $sort, 'page' => $page, 'perpage' => $perpage);
+        $params = array('feedbackid' => $feedbackid, 'groupid' => $groupid, 'sort' => $sort, 'page' => $page,
+            'perpage' => $perpage, 'courseid' => $courseid);
         $params = self::validate_parameters(self::get_non_respondents_parameters(), $params);
         $warnings = $nonrespondents = array();
 
-        list($feedback, $course, $cm, $context) = self::validate_feedback($params['feedbackid']);
+        list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'],
+            $params['courseid']);
+        $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $completioncourse->id);
+        $completioncourseid = $feedbackcompletion->get_courseid();
 
         if ($feedback->anonymous != FEEDBACK_ANONYMOUS_NO || $feedback->course == SITEID) {
             throw new moodle_exception('anonymous', 'feedback');
@@ -1048,7 +1114,7 @@ class mod_feedback_external extends external_api {
         $users = feedback_get_incomplete_users($cm, $groupid, $params['sort'], $page, $perpage, true);
         foreach ($users as $user) {
             $nonrespondents[] = [
-                'courseid' => $course->id,
+                'courseid' => $completioncourseid,
                 'userid'   => $user->id,
                 'fullname' => fullname($user),
                 'started'  => $user->feedbackstarted
@@ -1102,6 +1168,8 @@ class mod_feedback_external extends external_api {
                                                 VALUE_DEFAULT, 0),
                 'page' => new external_value(PARAM_INT, 'The page of records to return.', VALUE_DEFAULT, 0),
                 'perpage' => new external_value(PARAM_INT, 'The number of records to return per page', VALUE_DEFAULT, 0),
+                'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).',
+                    VALUE_DEFAULT, 0),
             )
         );
     }
@@ -1113,17 +1181,20 @@ class mod_feedback_external extends external_api {
      * @param int $groupid Group id, 0 means that the function will determine the user group
      * @param int $page the page of records to return
      * @param int $perpage the number of records to return per page
+     * @param int $courseid course where user completes the feedback (for site feedbacks only)
      * @return array of warnings and users attemps and responses
      * @throws moodle_exception
      * @since Moodle 3.3
      */
-    public static function get_responses_analysis($feedbackid, $groupid = 0, $page = 0, $perpage = 0) {
+    public static function get_responses_analysis($feedbackid, $groupid = 0, $page = 0, $perpage = 0, $courseid = 0) {
 
-        $params = array('feedbackid' => $feedbackid, 'groupid' => $groupid, 'page' => $page, 'perpage' => $perpage);
+        $params = array('feedbackid' => $feedbackid, 'groupid' => $groupid, 'page' => $page, 'perpage' => $perpage,
+            'courseid' => $courseid);
         $params = self::validate_parameters(self::get_responses_analysis_parameters(), $params);
         $warnings = $itemsdata = array();
 
-        list($feedback, $course, $cm, $context) = self::validate_feedback($params['feedbackid']);
+        list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'],
+            $params['courseid']);
 
         // Check permissions.
         require_capability('mod/feedback:viewreports', $context);
@@ -1147,7 +1218,7 @@ class mod_feedback_external extends external_api {
             }
         }
 
-        $feedbackstructure = new mod_feedback_structure($feedback, $cm, $course->id);
+        $feedbackstructure = new mod_feedback_structure($feedback, $cm, $completioncourse->id);
         $responsestable = new mod_feedback_responses_table($feedbackstructure, $groupid);
         // Ensure responses number is correct prior returning them.
         $feedbackstructure->shuffle_anonym_responses();
@@ -1222,6 +1293,8 @@ class mod_feedback_external extends external_api {
         return new external_function_parameters (
             array(
                 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id'),
+                'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).',
+                    VALUE_DEFAULT, 0),
             )
         );
     }
@@ -1234,15 +1307,16 @@ class mod_feedback_external extends external_api {
      * @since Moodle 3.3
      * @throws moodle_exception
      */
-    public static function get_last_completed($feedbackid) {
+    public static function get_last_completed($feedbackid, $courseid = 0) {
         global $PAGE;
 
-        $params = array('feedbackid' => $feedbackid);
+        $params = array('feedbackid' => $feedbackid, 'courseid' => $courseid);
         $params = self::validate_parameters(self::get_last_completed_parameters(), $params);
         $warnings = array();
 
-        list($feedback, $course, $cm, $context) = self::validate_feedback($params['feedbackid']);
-        $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $course->id);
+        list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'],
+            $params['courseid']);
+        $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $completioncourse->id);
 
         if ($feedbackcompletion->is_anonymous()) {
              throw new moodle_exception('anonymous', 'feedback');
index 9dea3fe..0c0286b 100644 (file)
@@ -316,14 +316,19 @@ class feedback_item_multichoice extends feedback_item_base {
         $inputname = $item->typ . '_' . $item->id;
         $options = $this->get_options($item);
         $separator = !empty($info->horizontal) ? ' ' : '<br>';
-        $tmpvalue = $form->get_item_value($item);
+        $tmpvalue = $form->get_item_value($item) ?? 0; // Used for element defaults, so must be a valid value (not null).
 
+        // Subtypes:
+        // r = radio
+        // c = checkbox
+        // d = dropdown.
         if ($info->subtype === 'd' || ($info->subtype === 'r' && $form->is_frozen())) {
             // Display as a dropdown in the complete form or a single value in the response view.
             $element = $form->add_form_element($item,
-                    ['select', $inputname.'[0]', $name, array(0 => '') + $options, array('class' => $class)],
+                    ['select', $inputname, $name, array(0 => '') + $options, array('class' => $class)],
                     false, false);
-            $form->set_element_default($inputname.'[0]', $tmpvalue);
+            $form->set_element_default($inputname, $tmpvalue);
+            $form->set_element_type($inputname, PARAM_INT);
         } else if ($info->subtype === 'c' && $form->is_frozen()) {
             // Display list of checkbox values in the response view.
             $objs = [];
@@ -354,26 +359,26 @@ class feedback_item_multichoice extends feedback_item_base {
             } else {
                 // Radio.
                 if (!array_key_exists(0, $options)) {
-                    // Always add '0' as hidden element, otherwise form submit data may not have this element.
-                    $objs[] = ['hidden', $inputname.'[0]'];
+                    // Always add a hidden element to the group to guarantee we get a value in the submit data.
+                    $objs[] = ['hidden', $inputname, 0];
                 }
                 foreach ($options as $idx => $label) {
-                    $objs[] = ['radio', $inputname.'[0]', '', $label, $idx];
+                    $objs[] = ['radio', $inputname, '', $label, $idx];
                 }
                 // Span to hold the element id. The id is used for drag and drop reordering.
                 $objs[] = ['static', '', '', html_writer::span('', '', ['id' => 'feedback_item_' . $item->id])];
                 $element = $form->add_form_group_element($item, 'group_'.$inputname, $name, $objs, $separator, $class);
-                $form->set_element_default($inputname.'[0]', $tmpvalue);
-                $form->set_element_type($inputname.'[0]', PARAM_INT);
+                $form->set_element_default($inputname, $tmpvalue);
+                $form->set_element_type($inputname, PARAM_INT);
             }
         }
 
         // Process 'required' rule.
         if ($item->required) {
             $elementname = $element->getName();
-            $form->add_validation_rule(function($values, $files) use ($elementname, $item) {
+            $form->add_validation_rule(function($values) use ($elementname, $item) {
                 $inputname = $item->typ . '_' . $item->id;
-                return empty($values[$inputname]) || !array_filter($values[$inputname]) ?
+                return empty($values[$inputname]) || (is_array($values[$inputname]) && !array_filter($values[$inputname])) ?
                     array($elementname => get_string('required')) : true;
             });
         }
@@ -385,6 +390,9 @@ class feedback_item_multichoice extends feedback_item_base {
      * @return string
      */
     public function create_value($value) {
+        // Could be an array (multichoice checkbox) or single value (multichoice radio or dropdown).
+        $value = is_array($value) ? $value : [$value];
+
         $value = array_unique(array_filter($value));
         return join(FEEDBACK_MULTICHOICE_LINE_SEP, $value);
     }
index 0b2d3e5..191602a 100644 (file)
@@ -826,7 +826,7 @@ function feedback_set_events($feedback) {
             // Calendar event exists so update it.
             $event->id = $eventid;
             $calendarevent = calendar_event::load($event->id);
-            $calendarevent->update($event);
+            $calendarevent->update($event, false);
         } else {
             // Event doesn't exist so create one.
             $event->courseid     = $feedback->course;
@@ -835,7 +835,7 @@ function feedback_set_events($feedback) {
             $event->modulename   = 'feedback';
             $event->instance     = $feedback->id;
             $event->eventtype    = FEEDBACK_EVENT_TYPE_OPEN;
-            calendar_event::create($event);
+            calendar_event::create($event, false);
         }
     } else if ($eventid) {
         // Calendar event is on longer needed.
@@ -861,7 +861,7 @@ function feedback_set_events($feedback) {
             // Calendar event exists so update it.
             $event->id = $eventid;
             $calendarevent = calendar_event::load($event->id);
-            $calendarevent->update($event);
+            $calendarevent->update($event, false);
         } else {
             // Event doesn't exist so create one.
             $event->courseid     = $feedback->course;
@@ -869,7 +869,7 @@ function feedback_set_events($feedback) {
             $event->userid       = 0;
             $event->modulename   = 'feedback';
             $event->instance     = $feedback->id;
-            calendar_event::create($event);
+            calendar_event::create($event, false);
         }
     } else if ($eventid) {
         // Calendar event is on longer needed.
diff --git a/mod/feedback/tests/behat/feedback_no_calendar_capabilities.feature b/mod/feedback/tests/behat/feedback_no_calendar_capabilities.feature
new file mode 100644 (file)
index 0000000..bf348ad
--- /dev/null
@@ -0,0 +1,48 @@
+@mod @mod_feedback
+Feature: Feedback with no calendar capabilites
+  In order to allow work effectively
+  As a teacher
+  I need to be able to create feedbacks even when I cannot edit calendar events
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "admin"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Permissions" in current page administration
+    And I override the system permissions of "Teacher" role with:
+      | capability | permission |
+      | moodle/calendar:manageentries | Prohibit |
+    And I log out
+
+  Scenario: Editing a feedback
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    When I add a "Feedback" to section "1" and I fill the form with:
+      | Name | Test feedback name |
+      | Description | Test feedback description |
+      | id_timeopen_enabled | 1 |
+      | id_timeopen_day | 1 |
+      | id_timeopen_month | 1 |
+      | id_timeopen_year | 2017 |
+      | id_timeclose_enabled | 1 |
+      | id_timeclose_day | 1 |
+      | id_timeclose_month | 2 |
+      | id_timeclose_year | 2017 |
+    And I log out
+    When I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I follow "Test feedback name"
+    And I navigate to "Edit settings" in current page administration
+    And I set the following fields to these values:
+      | id_timeopen_year | 2018 |
+      | id_timeclose_year | 2018 |
+    And I press "Save and return to course"
+    Then I should see "Test feedback name"
index 8138107..9aff6c5 100644 (file)
@@ -518,7 +518,7 @@ class mod_feedback_external_testcase extends externallib_advanced_testcase {
         $this->assertCount(7, $tmpitems);   // 2 from the first page + 5 from the second page.
 
         // And finally, save everything! We are going to modify one previous recorded value.
-        $data[2]['value'] = 'b';
+        $data[2]['value'] = 2; // 2 is value of the option 'b'.
         $secondpagedata = [$data[2], $data[3], $data[4], $data[5], $data[6]];
         $result = mod_feedback_external::process_page($this->feedback->id, 1, $secondpagedata);
         $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
@@ -530,7 +530,93 @@ class mod_feedback_external_testcase extends externallib_advanced_testcase {
         // Check if the one we modified was correctly saved.
         $itemid = $itemscreated[4]->id;
         $itemsaved = $DB->get_field('feedback_value', 'value', array('item' => $itemid));
-        $this->assertEquals('b', $itemsaved);
+        $mcitem = new feedback_item_multichoice();
+        $itemval = $mcitem->get_printval($itemscreated[4], (object) ['value' => $itemsaved]);
+        $this->assertEquals('b', $itemval);
+
+        // Check that the answers are saved for course 0.
+        foreach ($items as $item) {
+            $this->assertEquals(0, $item->course_id);
+        }
+        $completed = $DB->get_record('feedback_completed', []);
+        $this->assertEquals(0, $completed->courseid);
+    }
+
+    /**
+     * Test process_page for a site feedback.
+     */
+    public function test_process_page_site_feedback() {
+        global $DB;
+        $pagecontents = 'You finished it!';
+        $this->feedback = $this->getDataGenerator()->create_module('feedback',
+            array('course' => SITEID, 'page_after_submit' => $pagecontents));
+
+        // Test user with full capabilities.
+        $this->setUser($this->student);
+
+        // Add questions to the feedback, we are adding 2 pages of questions.
+        $itemscreated = self::populate_feedback($this->feedback, 2);
+
+        $data = [];
+        foreach ($itemscreated as $item) {
+
+            if (empty($item->hasvalue)) {
+                continue;
+            }
+
+            switch ($item->typ) {
+                case 'textarea':
+                case 'textfield':
+                    $value = 'Lorem ipsum';
+                    break;
+                case 'numeric':
+                    $value = 5;
+                    break;
+                case 'multichoice':
+                    $value = '1';
+                    break;
+                case 'multichoicerated':
+                    $value = '1';
+                    break;
+                case 'info':
+                    $value = format_string($this->course->shortname, true, array('context' => $this->context));
+                    break;
+                default:
+                    $value = '';
+            }
+            $data[] = ['name' => $item->typ . '_' . $item->id, 'value' => $value];
+        }
+
+        // Process first page.
+        $firstpagedata = [$data[0], $data[1]];
+        $result = mod_feedback_external::process_page($this->feedback->id, 0, $firstpagedata, false, $this->course->id);
+        $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
+        $this->assertEquals(1, $result['jumpto']);
+        $this->assertFalse($result['completed']);
+
+        // Process second page.
+        $data[2]['value'] = 2; // 2 is value of the option 'b';
+        $secondpagedata = [$data[2], $data[3], $data[4], $data[5], $data[6]];
+        $result = mod_feedback_external::process_page($this->feedback->id, 1, $secondpagedata, false, $this->course->id);
+        $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
+        $this->assertTrue($result['completed']);
+        $this->assertTrue(strpos($result['completionpagecontents'], $pagecontents) !== false);
+        // Check all the items were saved.
+        $items = $DB->get_records('feedback_value');
+        $this->assertCount(7, $items);
+        // Check if the one we modified was correctly saved.
+        $itemid = $itemscreated[4]->id;
+        $itemsaved = $DB->get_field('feedback_value', 'value', array('item' => $itemid));
+        $mcitem = new feedback_item_multichoice();
+        $itemval = $mcitem->get_printval($itemscreated[4], (object) ['value' => $itemsaved]);
+        $this->assertEquals('b', $itemval);
+
+        // Check that the answers are saved for the correct course.
+        foreach ($items as $item) {
+            $this->assertEquals($this->course->id, $item->course_id);
+        }
+        $completed = $DB->get_record('feedback_completed', []);
+        $this->assertEquals($this->course->id, $completed->courseid);
     }
 
     /**
@@ -936,4 +1022,55 @@ class mod_feedback_external_testcase extends externallib_advanced_testcase {
         $this->expectException('moodle_exception');
         mod_feedback_external::get_last_completed($this->feedback->id);
     }
+
+    /**
+     * Test get_feedback_access_information for site feedback.
+     */
+    public function test_get_feedback_access_information_for_site_feedback() {
+
+        $sitefeedback = $this->getDataGenerator()->create_module('feedback', array('course' => SITEID));
+        $this->setUser($this->student);
+        // Access the site feedback via the site activity.
+        $result = mod_feedback_external::get_feedback_access_information($sitefeedback->id);
+        $result = external_api::clean_returnvalue(mod_feedback_external::get_feedback_access_information_returns(), $result);
+        $this->assertTrue($result['cancomplete']);
+        $this->assertTrue($result['cansubmit']);
+
+        // Access the site feedback via course where I'm enrolled.
+        $result = mod_feedback_external::get_feedback_access_information($sitefeedback->id, $this->course->id);
+        $result = external_api::clean_returnvalue(mod_feedback_external::get_feedback_access_information_returns(), $result);
+        $this->assertTrue($result['cancomplete']);
+        $this->assertTrue($result['cansubmit']);
+
+        // Access the site feedback via course where I'm not enrolled.
+        $othercourse = $this->getDataGenerator()->create_course();
+
+        $this->expectException('moodle_exception');
+        mod_feedback_external::get_feedback_access_information($sitefeedback->id, $othercourse->id);
+    }
+
+    /**
+     * Test get_feedback_access_information for site feedback mapped.
+     */
+    public function test_get_feedback_access_information_for_site_feedback_mapped() {
+        global $DB;
+
+        $sitefeedback = $this->getDataGenerator()->create_module('feedback', array('course' => SITEID));
+        $this->setUser($this->student);
+        $DB->insert_record('feedback_sitecourse_map', array('feedbackid' => $sitefeedback->id, 'courseid' => $this->course->id));
+
+        // Access the site feedback via course where I'm enrolled and mapped.
+        $result = mod_feedback_external::get_feedback_access_information($sitefeedback->id, $this->course->id);
+        $result = external_api::clean_returnvalue(mod_feedback_external::get_feedback_access_information_returns(), $result);
+        $this->assertTrue($result['cancomplete']);
+        $this->assertTrue($result['cansubmit']);
+
+        // Access the site feedback via course where I'm enrolled but not mapped.
+        $othercourse = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($this->student->id, $othercourse->id, $this->studentrole->id, 'manual');
+
+        $this->expectException('moodle_exception');
+        $this->expectExceptionMessage(get_string('cannotaccess', 'mod_feedback'));
+        mod_feedback_external::get_feedback_access_information($sitefeedback->id, $othercourse->id);
+    }
 }
index 37cbf30..0990f40 100644 (file)
@@ -852,4 +852,27 @@ class mod_feedback_lib_testcase extends advanced_testcase {
         // was successfully modified.
         $this->assertNotEmpty($moduleupdatedevents);
     }
+
+    /**
+     * A user who does not have capabilities to add events to the calendar should be able to create an feedback.
+     */
+    public function test_creation_with_no_calendar_capabilities() {
+        $this->resetAfterTest();
+        $course = self::getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+        $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $roleid = self::getDataGenerator()->create_role();
+        self::getDataGenerator()->role_assign($roleid, $user->id, $context->id);
+        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
+        $generator = self::getDataGenerator()->get_plugin_generator('mod_feedback');
+        // Create an instance as a user without the calendar capabilities.
+        $this->setUser($user);
+        $time = time();
+        $params = array(
+            'course' => $course->id,
+            'timeopen' => $time + 200,
+            'timeclose' => $time + 2000,
+        );
+        $generator->create_instance($params);
+    }
 }
index 705655e..e084e60 100644 (file)
@@ -19,6 +19,8 @@
   * feedback_is_feedback_in_sitecourse_map()
   * feedback_clean_up_sitecourse_map()
   * feedback_print_numeric_option_list()
+* Web Service functions now support an optional courseid parameter (course where user completes the feedback).
+  This new parameter was necessary to support site feedbacks.
 
 === 3.5 ===
 
index fc7e92e..eeb241b 100644 (file)
@@ -444,14 +444,14 @@ $string['privacy:metadata:attempts:answerid'] = 'The answer ID';
 $string['privacy:metadata:attempts:retry'] = 'The attempt number';
 $string['privacy:metadata:attempts:correct'] = 'Whether the attempt was correct';
 $string['privacy:metadata:attempts:useranswer'] = 'Details about the user\'s answer';
-$string['privacy:metadata:attempts:timeseen'] = 'Time at which the attempt was made';
+$string['privacy:metadata:attempts:timeseen'] = 'The time when the attempt was made';
 $string['privacy:metadata:attempts'] = 'A record of page attempts';
 $string['privacy:metadata:grades:userid'] = 'The user ID';
 $string['privacy:metadata:grades:grade'] = 'The grade given';
-$string['privacy:metadata:grades:completed'] = 'The date at which the grade was given';
+$string['privacy:metadata:grades:completed'] = 'The date when the grade was given';
 $string['privacy:metadata:grades'] = 'A record of the grades for each lesson';
 $string['privacy:metadata:timer:userid'] = 'The user ID';
-$string['privacy:metadata:timer:starttime'] = 'The date at which the attempt started';
+$string['privacy:metadata:timer:starttime'] = 'The date when the attempt started';
 $string['privacy:metadata:timer:lessontime'] = 'The last moment when we recorded activity';
 $string['privacy:metadata:timer:completed'] = 'Whether the attempt is complete';
 $string['privacy:metadata:timer:timemodifiedoffline'] = 'The last moment when we recorded activity from the mobile app';
@@ -460,11 +460,11 @@ $string['privacy:metadata:branch:userid'] = 'The user ID';
 $string['privacy:metadata:branch:pageid'] = 'The page ID';
 $string['privacy:metadata:branch:retry'] = 'The attempt number';
 $string['privacy:metadata:branch:flag'] = 'Whether the next page was calculated randomely';
-$string['privacy:metadata:branch:timeseen'] = 'Time at which the page was viewed ';
+$string['privacy:metadata:branch:timeseen'] = 'The time when the page was viewed';
 $string['privacy:metadata:branch:nextpageid'] = 'The next page ID';
 $string['privacy:metadata:branch'] = 'A record of the pages viewed';
 $string['privacy:metadata:overrides:userid'] = 'The user ID';
-$string['privacy:metadata:overrides:available'] = 'Time at which the students can start attempting the lesson';
+$string['privacy:metadata:overrides:available'] = 'The time when the lesson may be attempted';
 $string['privacy:metadata:overrides:deadline'] = 'The deadline for completing the lesson.';
 $string['privacy:metadata:overrides:timelimit'] = 'Time limit to complete the lesson, in seconds.';
 $string['privacy:metadata:overrides:review'] = 'Whether trying a question again is allowed';
index fb55485..885e8ae 100644 (file)
@@ -217,7 +217,7 @@ function lesson_update_events($lesson, $override = null) {
                 }
                 $event->name = get_string('lessoneventopens', 'lesson', $eventname);
                 // The method calendar_event::create will reuse a db record if the id field is set.
-                calendar_event::create($event);
+                calendar_event::create($event, false);
             }
             if ($deadline && $addclose) {
                 if ($oldevent = array_shift($oldevents)) {
@@ -236,7 +236,7 @@ function lesson_update_events($lesson, $override = null) {
                         $event->priority = $closepriorities[$deadline];
                     }
                 }
-                calendar_event::create($event);
+                calendar_event::create($event, false);
             }
         }
     }
diff --git a/mod/lesson/tests/behat/lesson_no_calendar_capabilities.feature b/mod/lesson/tests/behat/lesson_no_calendar_capabilities.feature
new file mode 100644 (file)
index 0000000..80e5cf8
--- /dev/null
@@ -0,0 +1,48 @@
+@mod @mod_lesson
+Feature: Lesson with no calendar capabilites
+  In order to allow work effectively
+  As a teacher
+  I need to be able to create lessons even when I cannot edit calendar events
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "admin"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Permissions" in current page administration
+    And I override the system permissions of "Teacher" role with:
+      | capability | permission |
+      | moodle/calendar:manageentries | Prohibit |
+    And I log out
+
+  Scenario: Editing a lesson
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    When I add a "Lesson" to section "1" and I fill the form with:
+      | Name | Test lesson name |
+      | Description | Test lesson description |
+      | id_available_enabled | 1 |
+      | id_available_day | 1 |
+      | id_available_month | 1 |
+      | id_available_year | 2017 |
+      | id_deadline_enabled | 1 |
+      | id_deadline_day | 1 |
+      | id_deadline_month | 2 |
+      | id_deadline_year | 2017 |
+    And I log out
+    When I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I follow "Test lesson name"
+    And I navigate to "Edit settings" in current page administration
+    And I set the following fields to these values:
+      | id_available_year | 2018 |
+      | id_deadline_year | 2018 |
+    And I press "Save and return to course"
+    Then I should see "Test lesson name"
index a122c0f..049724f 100644 (file)
@@ -723,4 +723,27 @@ class mod_lesson_lib_testcase extends advanced_testcase {
         $this->assertNull($min);
         $this->assertNull($max);
     }
+
+    /**
+     * A user who does not have capabilities to add events to the calendar should be able to create an lesson.
+     */
+    public function test_creation_with_no_calendar_capabilities() {
+        $this->resetAfterTest();
+        $course = self::getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+        $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $roleid = self::getDataGenerator()->create_role();
+        self::getDataGenerator()->role_assign($roleid, $user->id, $context->id);
+        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
+        $generator = self::getDataGenerator()->get_plugin_generator('mod_lesson');
+        // Create an instance as a user without the calendar capabilities.
+        $this->setUser($user);
+        $time = time();
+        $params = array(
+            'course' => $course->id,
+            'available' => $time + 200,
+            'deadline' => $time + 2000,
+        );
+        $generator->create_instance($params);
+    }
 }
index 792762f..dd9ba65 100644 (file)
@@ -242,7 +242,7 @@ real estate to the tool, and others provide a more integrated feel with the Mood
 $string['launchoptions'] = 'Launch options';
 $string['leaveblank'] = 'Leave blank if you do not need them';
 $string['lti'] = 'LTI';
-$string['lti:addinstance'] = 'Add new external tool activities';
+$string['lti:addinstance'] = 'Add a new external tool';
 $string['lti:addcoursetool'] = 'Add course-specific tool configurations';
 $string['lti:grade'] = 'View grades returned by the external tool';
 $string['lti:manage'] = 'Be an Instructor when the tool is launched';
@@ -377,8 +377,8 @@ $string['privacy:metadata:lti_tool_proxies:name'] = 'LTI proxy name';
 $string['privacy:metadata:lti_types'] = 'LTI types';
 $string['privacy:metadata:lti_types:name'] = 'LTI type name';
 $string['privacy:metadata:role'] = 'The role in the course for the user accessing the LTI Consumer';
-$string['privacy:metadata:timecreated'] = 'The date at which the record was created';
-$string['privacy:metadata:timemodified'] = 'The date at which the record was modified';
+$string['privacy:metadata:timecreated'] = 'The time when the record was created';
+$string['privacy:metadata:timemodified'] = 'The time when the record was modified';
 $string['privacy:metadata:userid'] = 'The ID of the user accessing the LTI Consumer';
 $string['privacy:metadata:useridnumber'] = 'The ID number of the user accessing the LTI Consumer';
 $string['privacy:metadata:username'] = 'The username of the user accessing the LTI Consumer';
index e6c0242..3b73a57 100644 (file)
@@ -174,7 +174,7 @@ $string['comments'] = 'Comments';
 $string['completedon'] = 'Completed on';
 $string['completionpass'] = 'Require passing grade';
 $string['completionpassdesc'] = 'Student must achieve a passing grade to complete this activity';
-$string['completionpass_help'] = 'If enabled, this activity is considered complete when the student receives a passing grade, with the pass grade set in the gradebook.';
+$string['completionpass_help'] = 'If enabled, this activity is considered complete when the student receives a pass grade (as specified in the Grade section of the quiz settings) or higher.';
 $string['completionattemptsexhausted'] = 'Or all available attempts completed';
 $string['completionattemptsexhausteddesc'] = 'Complete if all available attempts are exhausted';
 $string['completionattemptsexhausted_help'] = 'Mark quiz complete when the student has exhausted the maximum number of attempts.';
@@ -394,7 +394,7 @@ $string['grademethod_help'] = 'When multiple attempts are allowed, the following
 * Last attempt (all other attempts are ignored)';
 $string['gradesdeleted'] = 'Quiz grades deleted';
 $string['gradesofar'] = '{$a->method}: {$a->mygrade} / {$a->quizgrade}.';
-$string['gradetopassnotset'] = 'This quiz does not have a grade to pass set so you cannot use this option. Please use the require grade setting instead.';
+$string['gradetopassnotset'] = 'This quiz does not yet have a grade to pass set. It may be set in the Grade section of the quiz settings.';
 $string['gradetopassmustbeset'] = 'Grade to pass cannot be zero as this quiz has its completion method set to require passing grade. Please set a non-zero value.';
 $string['gradingdetails'] = 'Marks for this submission: {$a->raw}/{$a->max}.';
 $string['gradingdetailsadjustment'] = 'With previous penalties this gives <strong>{$a->cur}/{$a->max}</strong>.';
index a8caba4..9fd12e9 100644 (file)
@@ -1340,7 +1340,7 @@ function quiz_update_events($quiz, $override = null) {
                 }
                 $event->name = get_string('quizeventopens', 'quiz', $eventname);
                 // The method calendar_event::create will reuse a db record if the id field is set.
-                calendar_event::create($event);
+                calendar_event::create($event, false);
             }
             if ($timeclose && $addclose) {
                 if ($oldevent = array_shift($oldevents)) {
@@ -1359,7 +1359,7 @@ function quiz_update_events($quiz, $override = null) {
                         $event->priority = $closepriorities[$timeclose];
                     }
                 }
-                calendar_event::create($event);
+                calendar_event::create($event, false);
             }
         }
     }
diff --git a/mod/quiz/tests/behat/quiz_no_calendar_capabilities.feature b/mod/quiz/tests/behat/quiz_no_calendar_capabilities.feature
new file mode 100644 (file)
index 0000000..52b1f21
--- /dev/null
@@ -0,0 +1,48 @@
+@mod @mod_quiz
+Feature: Quiz with no calendar capabilites
+  In order to allow work effectively
+  As a teacher
+  I need to be able to create quiz even when I cannot edit calendar events
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "admin"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Permissions" in current page administration
+    And I override the system permissions of "Teacher" role with:
+      | capability | permission |
+      | moodle/calendar:manageentries | Prohibit |
+    And I log out
+
+  Scenario: Editing a quiz
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    When I add a "Quiz" to section "1" and I fill the form with:
+      | Name | Test quiz name |
+      | Description | Test quiz description |
+      | id_timeopen_enabled | 1 |
+      | id_timeopen_day | 1 |
+      | id_timeopen_month | 1 |
+      | id_timeopen_year | 2017 |
+      | id_timeclose_enabled | 1 |
+      | id_timeclose_day | 1 |
+      | id_timeclose_month | 2 |
+      | id_timeclose_year | 2017 |
+    And I log out
+    When I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I follow "Test quiz name"
+    And I navigate to "Edit settings" in current page administration
+    And I set the following fields to these values:
+      | id_timeopen_year | 2018 |
+      | id_timeclose_year | 2018 |
+    And I press "Save and return to course"
+    Then I should see "Test quiz name"
index 8a54c36..65d8b56 100644 (file)
@@ -736,4 +736,27 @@ class mod_quiz_lib_testcase extends advanced_testcase {
         $this->assertEquals(mod_quiz_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions);
         $this->assertEquals(mod_quiz_get_completion_active_rule_descriptions(new stdClass()), []);
     }
+
+    /**
+     * A user who does not have capabilities to add events to the calendar should be able to create a quiz.
+     */
+    public function test_creation_with_no_calendar_capabilities() {
+        $this->resetAfterTest();
+        $course = self::getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+        $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $roleid = self::getDataGenerator()->create_role();
+        self::getDataGenerator()->role_assign($roleid, $user->id, $context->id);
+        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
+        $generator = self::getDataGenerator()->get_plugin_generator('mod_quiz');
+        // Create an instance as a user without the calendar capabilities.
+        $this->setUser($user);
+        $time = time();
+        $params = array(
+            'course' => $course->id,
+            'timeopen' => $time + 200,
+            'timeclose' => $time + 2000,
+        );
+        $generator->create_instance($params);
+    }
 }
index 27e4b94..cf61bb0 100644 (file)
@@ -2397,7 +2397,7 @@ function scorm_update_calendar(stdClass $scorm, $cmid) {
             $event->timeduration = 0;
 
             $calendarevent = calendar_event::load($event->id);
-            $calendarevent->update($event);
+            $calendarevent->update($event, false);
         } else {
             // Calendar event is on longer needed.
             $calendarevent = calendar_event::load($event->id);
@@ -2418,7 +2418,7 @@ function scorm_update_calendar(stdClass $scorm, $cmid) {
             $event->visible = instance_is_visible('scorm', $scorm);
             $event->timeduration = 0;
 
-            calendar_event::create($event);
+            calendar_event::create($event, false);
         }
     }
 
@@ -2438,7 +2438,7 @@ function scorm_update_calendar(stdClass $scorm, $cmid) {
             $event->timeduration = 0;
 
             $calendarevent = calendar_event::load($event->id);
-            $calendarevent->update($event);
+            $calendarevent->update($event, false);
         } else {
             // Calendar event is on longer needed.
             $calendarevent = calendar_event::load($event->id);
@@ -2459,7 +2459,7 @@ function scorm_update_calendar(stdClass $scorm, $cmid) {
             $event->visible = instance_is_visible('scorm', $scorm);
             $event->timeduration = 0;
 
-            calendar_event::create($event);
+            calendar_event::create($event, false);
         }
     }
 
diff --git a/mod/scorm/tests/behat/scorm_no_calendar_capabilities.feature b/mod/scorm/tests/behat/scorm_no_calendar_capabilities.feature
new file mode 100644 (file)
index 0000000..77cd7a0
--- /dev/null
@@ -0,0 +1,52 @@
+@mod @mod_scorm
+Feature: Scorm with no calendar capabilites
+  In order to allow work effectively
+  As a teacher
+  I need to be able to create SCORM activities even when I cannot edit calendar events
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "admin"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Permissions" in current page administration
+    And I override the system permissions of "Teacher" role with:
+      | capability | permission |
+      | moodle/calendar:manageentries | Prohibit |
+    And I log out
+
+  @javascript @_file_upload @_switch_iframe
+  Scenario: Editing a chat
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    When I add a "SCORM package" to section "1"
+    And I set the following fields to these values:
+      | Name | Test scorm name |
+      | Description | Test scorm description |
+      | id_timeopen_enabled | 1 |
+      | id_timeopen_day | 1 |
+      | id_timeopen_month | 1 |
+      | id_timeopen_year | 2017 |
+      | id_timeclose_enabled | 1 |
+      | id_timeclose_day | 1 |
+      | id_timeclose_month | 2 |
+      | id_timeclose_year | 2017 |
+    And I upload "mod/scorm/tests/packages/singlesco_scorm12.zip" file to "Package file" filemanager
+    And I click on "Save and display" "button"
+    And I log out
+    When I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I follow "Test scorm name"
+    And I navigate to "Edit settings" in current page administration
+    And I set the following fields to these values:
+      | id_timeopen_year | 2018 |
+      | id_timeclose_year | 2018 |
+    And I press "Save and return to course"
+    Then I should see "Test scorm name"
index 0e98e3b..173e879 100644 (file)
@@ -736,4 +736,27 @@ class mod_scorm_lib_testcase extends externallib_advanced_testcase {
         $this->assertNull($min);
         $this->assertNull($max);
     }
+
+    /**
+     * A user who does not have capabilities to add events to the calendar should be able to create a SCORM.
+     */
+    public function test_creation_with_no_calendar_capabilities() {
+        $this->resetAfterTest();
+        $course = self::getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+        $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $roleid = self::getDataGenerator()->create_role();
+        self::getDataGenerator()->role_assign($roleid, $user->id, $context->id);
+        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
+        $generator = self::getDataGenerator()->get_plugin_generator('mod_scorm');
+        // Create an instance as a user without the calendar capabilities.
+        $this->setUser($user);
+        $time = time();
+        $params = array(
+            'course' => $course->id,
+            'timeopen' => $time + 200,
+            'timeclose' => $time + 2000,
+        );
+        $generator->create_instance($params);
+    }
 }
diff --git a/mod/workshop/amd/build/modform.min.js b/mod/workshop/amd/build/modform.min.js
new file mode 100644 (file)
index 0000000..860dc01
Binary files /dev/null and b/mod/workshop/amd/build/modform.min.js differ
diff --git a/mod/workshop/amd/src/modform.js b/mod/workshop/amd/src/modform.js
new file mode 100644 (file)
index 0000000..2352370
--- /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/>.
+
+/**
+ * Additional javascript for the Workshop module form.
+ *
+ * @module      mod_workshop/modform
+ * @copyright   The Open University 2018
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery'], function($) {
+
+    var submissionTypes = {
+        text: {
+            available: null,
+            required: null,
+            requiredHidden: null
+        },
+        file: {
+            available: null,
+            required: null,
+            requiredHidden: null
+        }
+    };
+
+    /**
+     * Determine whether one of the submission types has been marked as not available.
+     *
+     * If it has been marked not available, clear and disable its required checkbox.  Then determine if the other submission
+     * type is available, and if it is, check and disable its required checkbox.
+     *
+     * @param {Object} checkUnavailable
+     * @param {Object} checkAvailable
+     */
+    function checkAvailability(checkUnavailable, checkAvailable) {
+        if (!checkUnavailable.available.prop('checked')) {
+            checkUnavailable.required.prop('disabled', true);
+            checkUnavailable.required.prop('checked', false);
+            if (checkAvailable.available.prop('checked')) {
+                checkAvailable.required.prop('disabled', true);
+                checkAvailable.required.prop('checked', true);
+                // Also set the checkbox's hidden field to 1 so a 'required' value is submitted for the submission type.
+                checkAvailable.requiredHidden.val(1);
+            }
+        }
+    }
+
+    /**
+     * Enable the submission type's required checkbox and uncheck it.
+     *
+     * @param {Object} submissionType
+     */
+    function enableRequired(submissionType) {
+        submissionType.required.prop('disabled', false);
+        submissionType.required.prop('checked', false);
+        submissionType.requiredHidden.val(0);
+    }
+
+    /**
+     * Check which submission types have been marked as available, and disable required checkboxes as necessary.
+     */
+    function submissionTypeChanged() {
+        checkAvailability(submissionTypes.file, submissionTypes.text);
+        checkAvailability(submissionTypes.text, submissionTypes.file);
+        if (submissionTypes.text.available.prop('checked') && submissionTypes.file.available.prop('checked')) {
+            enableRequired(submissionTypes.text);
+            enableRequired(submissionTypes.file);
+        }
+    }
+
+    return /** @alias module:mod_workshop/modform */ {
+        /**
+         * Find all the required fields, set up event listeners, and set the initial state of required checkboxes.
+         */
+        init: function() {
+            submissionTypes.text.available = $('#id_submissiontypetextavailable');
+            submissionTypes.text.required = $('#id_submissiontypetextrequired');
+            submissionTypes.text.requiredHidden = $('input[name="submissiontypetextrequired"][type="hidden"]');
+            submissionTypes.file.available = $('#id_submissiontypefileavailable');
+            submissionTypes.file.required = $('#id_submissiontypefilerequired');
+            submissionTypes.file.requiredHidden = $('input[name="submissiontypefilerequired"][type="hidden"]');
+            submissionTypes.text.available.on('change', submissionTypeChanged);
+            submissionTypes.file.available.on('change', submissionTypeChanged);
+            submissionTypeChanged();
+        }
+    };
+});
index ddc4a01..587d276 100644 (file)
@@ -369,7 +369,14 @@ function workshop_upgrade_transform_instance(stdClass $old) {
     $new->name          = $old->name;
     $new->intro         = $old->description;
     $new->introformat   = $old->format;
-    $new->nattachments  = $old->nattachments;
+    if ($old->nattachments == 0) {
+        // Convert to the new method for disabling file submissions.
+        $new->submissiontypefile = WORKSHOP_SUBMISSION_TYPE_DISABLED;
+        $new->submissiontypetext = WORKSHOP_SUBMISSION_TYPE_REQUIRED;
+        $new->nattachments = 1;
+    } else {
+        $new->nattachments  = $old->nattachments;
+    }
     $new->maxbytes      = $old->maxbytes;
     $new->grade         = $old->grade;
     $new->gradinggrade  = $old->gradinggrade;
@@ -409,4 +416,4 @@ function workshop_upgrade_transform_instance(stdClass $old) {
     }
 
     return $new;
-}
\ No newline at end of file
+}
index 3e9d8d1..d357331 100644 (file)
@@ -53,8 +53,8 @@ class backup_workshop_activity_structure_step extends backup_activity_structure_
             'instructauthorsformat', 'instructreviewers',
             'instructreviewersformat', 'timemodified', 'phase', 'useexamples',
             'usepeerassessment', 'useselfassessment', 'grade', 'gradinggrade',
-            'strategy', 'evaluation', 'gradedecimals', 'nattachments', 'submissionfiletypes',
-            'latesubmissions', 'maxbytes', 'examplesmode', 'submissionstart',
+            'strategy', 'evaluation', 'gradedecimals', 'submissiontypetext', 'submissiontypefile', 'nattachments',
+            'submissionfiletypes', 'latesubmissions', 'maxbytes', 'examplesmode', 'submissionstart',
             'submissionend', 'assessmentstart', 'assessmentend',
             'conclusion', 'conclusionformat', 'overallfeedbackmode',
             'overallfeedbackfiles', 'overallfeedbackfiletypes', 'overallfeedbackmaxbytes'));
index 1812f03..7a2a5ae 100644 (file)
@@ -111,6 +111,13 @@ class restore_workshop_activity_structure_step extends restore_activity_structur
         $data->assessmentstart = $this->apply_date_offset($data->assessmentstart);
         $data->assessmentend = $this->apply_date_offset($data->assessmentend);
 
+        if ($data->nattachments == 0) {
+            // Convert to the new method for disabling file submissions.
+            $data->submissiontypefile = WORKSHOP_SUBMISSION_TYPE_DISABLED;
+            $data->submissiontypetext = WORKSHOP_SUBMISSION_TYPE_REQUIRED;
+            $data->nattachments = 1;
+        }
+
         // insert the workshop record
         $newitemid = $DB->insert_record('workshop', $data);
         // immediately after inserting "activity" record, call this
index bbacd5c..d7a8332 100644 (file)
@@ -145,10 +145,24 @@ class workshop_summary_exporter extends exporter {
                 'description' => 'Number of digits that should be shown after the decimal point when displaying grades.',
                 'optional' => true,
             ),
+            'submissiontypetext' => array (
+                'type' => PARAM_INT,
+                'default' => 1,
+                'description' => 'Indicates whether text is required as part of each submission. ' .
+                        '0 for no, 1 for optional, 2 for required.',
+                'optional' => true
+            ),
+            'submissiontypefile' => array (
+                'type' => PARAM_INT,
+                'default' => 1,
+                'description' => 'Indicates whether a file upload is required as part of each submission. ' .
+                        '0 for no, 1 for optional, 2 for required.',
+                'optional' => true
+            ),
             'nattachments' => array(
                 'type' => PARAM_INT,
-                'default' => 0,
-                'description' => 'Number of required submission attachments.',
+                'default' => 1,
+                'description' => 'Maximum number of submission attachments.',
                 'optional' => true,
             ),
             'submissionfiletypes' => array(
index fb7e7ab..d3ffcff 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="mod/workshop/db" VERSION="20180427" COMMENT="XMLDB file for Moodle mod/workshop"
+<XMLDB PATH="mod/workshop/db" VERSION="20180626" COMMENT="XMLDB file for Moodle mod/workshop"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
 >
@@ -25,7 +25,9 @@
         <FIELD NAME="strategy" TYPE="char" LENGTH="30" NOTNULL="true" SEQUENCE="false" COMMENT="The type of the current grading strategy used in this workshop"/>
         <FIELD NAME="evaluation" TYPE="char" LENGTH="30" NOTNULL="true" SEQUENCE="false" COMMENT="The recently used grading evaluation method"/>
         <FIELD NAME="gradedecimals" TYPE="int" LENGTH="3" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="Number of digits that should be shown after the decimal point when displaying grades"/>
-        <FIELD NAME="nattachments" TYPE="int" LENGTH="3" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="Number of required submission attachments"/>
+        <FIELD NAME="submissiontypetext" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Can students enter text for their submissions? 0 for no, 1 for optional, 2 for required."/>
+        <FIELD NAME="submissiontypefile" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Can students attach files for their submissions? 0 for no, 1 for optional, 2 for required."/>
+        <FIELD NAME="nattachments" TYPE="int" LENGTH="3" NOTNULL="false" DEFAULT="1" SEQUENCE="false" COMMENT="Maximum number of submission attachments"/>
         <FIELD NAME="submissionfiletypes" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="comma separated list of file extensions"/>
         <FIELD NAME="latesubmissions" TYPE="int" LENGTH="2" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="Allow submitting the work after the deadline"/>
         <FIELD NAME="maxbytes" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="100000" SEQUENCE="false" COMMENT="Maximum size of the one attached file"/>
       </KEYS>
     </TABLE>
   </TABLES>
-</XMLDB>
\ No newline at end of file
+</XMLDB>
index 6c6e2c3..91dbde6 100644 (file)
@@ -68,5 +68,46 @@ function xmldb_workshop_upgrade($oldversion) {
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2018062600) {
+
+        // Define field submissiontypetext to be added to workshop.
+        $table = new xmldb_table('workshop');
+        $field = new xmldb_field('submissiontypetext', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '1', 'gradedecimals');
+
+        // Conditionally launch add field submissiontypetext.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        $field = new xmldb_field('submissiontypefile', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '1',
+                'submissiontypetext');
+
+        // Conditionally launch add field submissiontypefile.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Convert existing workshops with attachments disabled to use the new settings.
+        $workshops = $DB->get_records('workshop', ['nattachments' => 0]);
+        foreach ($workshops as $workshop) {
+            $update = (object) [
+                'id' => $workshop->id,
+                'submissiontypefile' => 0,
+                'submissiontypetext' => 2,
+                'nattachments' => 1
+            ];
+            $DB->update_record('workshop', $update);
+        }
+
+        // Changing the default of field nattachments on table workshop to 1.
+        $field = new xmldb_field('nattachments', XMLDB_TYPE_INTEGER, '3', null, null, null, '1', 'submissiontypefile');
+
+        // Launch change of default for field nattachments.
+        $dbman->change_field_default($table, $field);
+
+        // Workshop savepoint reached.
+        upgrade_mod_savepoint(true, 2018062600, 'workshop');
+    }
+
     return true;
 }
index f1f69aa..bf559e1 100644 (file)
@@ -204,6 +204,7 @@ $string['mysubmission'] = 'My submission';
 $string['nattachments'] = 'Maximum number of submission attachments';
 $string['noexamples'] = 'No examples yet in this workshop';
 $string['noexamplesformready'] = 'You must define the assessment form before providing example submissions';
+$string['nosubmissiontype'] = 'At least one submission type must be available';
 $string['nogradeyet'] = 'No grade yet';
 $string['nosubmissionfound'] = 'No submission found for this user';
 $string['nosubmissions'] = 'No submissions yet in this workshop';
@@ -333,6 +334,12 @@ $string['submissionstart'] = 'Open for submissions from';
 $string['submissionstartevent'] = '{$a} opens for submissions';
 $string['submissionstartdatetime'] = 'Open for submissions from {$a->daydatetime} ({$a->distanceday})';
 $string['submissiontitle'] = 'Title';
+$string['submissiontypefileavailable'] = 'File attachment<span class="accesshide"> available</span>';
+$string['submissiontypefilerequired'] = '<span class="accesshide">File attachment </span>Required';
+$string['submissiontypetextavailable'] = 'Online text<span class="accesshide"> available</span>';
+$string['submissiontypetextrequired'] = '<span class="accesshide">Online text </span>Required';
+$string['submissiontypedisabled'] = 'This submission type is disabled for this workshop.';
+$string['submissiontypes'] = 'Submission types';
 $string['submissionsreport'] = 'Workshop submissions report';
 $string['submittednotsubmitted'] = 'Submitted ({$a->submitted}) / not submitted ({$a->notsubmitted})';
 $string['subplugintype_workshopallocation'] = 'Submissions allocation method';
index 3ef9612..b4e3a41 100644 (file)
@@ -34,6 +34,9 @@ define('WORKSHOP_EVENT_TYPE_SUBMISSION_OPEN',   'opensubmission');
 define('WORKSHOP_EVENT_TYPE_SUBMISSION_CLOSE',  'closesubmission');
 define('WORKSHOP_EVENT_TYPE_ASSESSMENT_OPEN',   'openassessment');
 define('WORKSHOP_EVENT_TYPE_ASSESSMENT_CLOSE',  'closeassessment');
+define('WORKSHOP_SUBMISSION_TYPE_DISABLED', 0);
+define('WORKSHOP_SUBMISSION_TYPE_AVAILABLE', 1);
+define('WORKSHOP_SUBMISSION_TYPE_REQUIRED', 2);
 
 ////////////////////////////////////////////////////////////////////////////////
 // Moodle core API                                                            //
index 681eede..e928b3d 100644 (file)
@@ -171,6 +171,12 @@ class workshop {
     /** @var int maximum size of one file attached to the overall feedback */
     public $overallfeedbackmaxbytes;
 
+    /** @var int Should the submission form show the text field? */
+    public $submissiontypetext;
+
+    /** @var int Should the submission form show the file attachment field? */
+    public $submissiontypefile;
+
     /**
      * @var workshop_strategy grading strategy instance
      * Do not use directly, get the instance using {@link workshop::grading_strategy_instance()}
@@ -2870,9 +2876,39 @@ class workshop {
                 $errors['title'] = get_string('err_multiplesubmissions', 'mod_workshop');
             }
         }
+        // Get the workshop record by id or cmid, depending on whether we're creating or editing a submission.
+        if (empty($data['workshopid'])) {
+            $workshop = $DB->get_record_select('workshop', 'id = (SELECT instance FROM {course_modules} WHERE id = ?)',
+                    [$data['cmid']]);
+        } else {
+            $workshop = $DB->get_record('workshop', ['id' => $data['workshopid']]);
+        }
+
+        if (isset($data['attachment_filemanager'])) {
+            $getfiles = file_get_drafarea_files($data['attachment_filemanager']);
+            $attachments = $getfiles->list;
+        } else {
+            $attachments = array();
+        }
 
-        $getfiles = file_get_drafarea_files($data['attachment_filemanager']);
-        if (empty($getfiles->list) and html_is_blank($data['content_editor']['text'])) {
+        if ($workshop->submissiontypefile == WORKSHOP_SUBMISSION_TYPE_REQUIRED) {
+            if (empty($attachments)) {
+                $errors['attachment_filemanager'] = get_string('err_required', 'form');
+            }