Merge branch 'MDL-53140-master' of git://github.com/rezaies/moodle
authorAdrian Greeve <abgreeve@gmail.com>
Wed, 1 May 2019 02:30:22 +0000 (10:30 +0800)
committerAdrian Greeve <abgreeve@gmail.com>
Wed, 1 May 2019 02:30:22 +0000 (10:30 +0800)
229 files changed:
admin/settings/analytics.php
admin/settings/users.php
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/tests/behat/restoredefault.feature
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/usertours/lang/en/tool_usertours.php
analytics/classes/analysis.php
analytics/classes/local/time_splitting/periodic.php
analytics/classes/local/time_splitting/upcoming_periodic.php
analytics/tests/analysis_test.php [new file with mode: 0644]
analytics/tests/fixtures/test_timesplitting_upcoming_seconds.php [new file with mode: 0644]
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
backup/util/settings/setting_dependency.class.php
backup/util/settings/tests/settings_test.php
badges/classes/privacy/provider.php
badges/tests/privacy_test.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/navigation/lang/en/block_navigation.php
calendar/lib.php
completion/criteria/completion_criteria.php
completion/cron.php [deleted file]
course/classes/management/helper.php
course/tests/behat/behat_course.php
course/tests/behat/customfields_locked.feature
course/tests/behat/customfields_visibility.feature
customfield/field/checkbox/lang/en/customfield_checkbox.php
customfield/field/date/lang/en/customfield_date.php
customfield/field/select/lang/en/customfield_select.php
customfield/field/text/lang/en/customfield_text.php
customfield/field/text/tests/behat/field.feature
customfield/field/textarea/lang/en/customfield_textarea.php
customfield/tests/behat/edit_fields_settings.feature
customfield/tests/behat/required_field.feature
customfield/tests/behat/unique_field.feature
enrol/renderer.php
grade/grading/classes/privacy/gradingform_legacy_polyfill.php
grade/grading/tests/privacy_legacy_polyfill_test.php
grade/import/direct/lang/en/gradeimport_direct.php
install/lang/it_wp/langconfig.php [new file with mode: 0644]
install/lang/no_wp/langconfig.php [new file with mode: 0644]
install/lang/pt/install.php
lang/en/access.php
lang/en/admin.php
lang/en/analytics.php
lang/en/backup.php
lang/en/badges.php
lang/en/cache.php
lang/en/course.php
lang/en/customfield.php
lang/en/message.php
lang/en/moodle.php
lang/en/question.php
lib/badgeslib.php
lib/classes/event/question_base.php [new file with mode: 0644]
lib/classes/event/question_category_base.php [new file with mode: 0644]
lib/classes/event/question_category_created.php
lib/classes/event/question_category_deleted.php [new file with mode: 0644]
lib/classes/event/question_category_moved.php [new file with mode: 0644]
lib/classes/event/question_category_updated.php [new file with mode: 0644]
lib/classes/event/question_category_viewed.php [new file with mode: 0644]
lib/classes/event/question_created.php [new file with mode: 0644]
lib/classes/event/question_deleted.php [new file with mode: 0644]
lib/classes/event/question_moved.php [new file with mode: 0644]
lib/classes/event/question_updated.php [new file with mode: 0644]
lib/classes/event/question_viewed.php [new file with mode: 0644]
lib/classes/event/questions_exported.php [new file with mode: 0644]
lib/classes/event/questions_imported.php [new file with mode: 0644]
lib/classes/external/exporter.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/task/completion_daily_task.php
lib/classes/task/completion_regular_task.php
lib/completionlib.php
lib/db/caches.php
lib/db/install.xml
lib/db/upgrade.php
lib/form/course.php
lib/form/filemanager.php
lib/form/filepicker.js
lib/form/filepicker.php
lib/form/listing.php
lib/form/templatable_form_element.php
lib/form/templates/element-advcheckbox-inline.mustache
lib/form/templates/element-advcheckbox.mustache
lib/form/templates/element-autocomplete-inline.mustache
lib/form/templates/element-autocomplete.mustache
lib/form/templates/element-button-inline.mustache
lib/form/templates/element-button.mustache
lib/form/templates/element-checkbox-inline.mustache
lib/form/templates/element-checkbox.mustache
lib/form/templates/element-password.mustache
lib/form/templates/element-passwordunmask.mustache
lib/form/templates/element-radio-inline.mustache
lib/form/templates/element-radio.mustache
lib/form/templates/element-select-inline.mustache
lib/form/templates/element-select.mustache
lib/form/templates/element-selectgroups-inline.mustache
lib/form/templates/element-selectgroups.mustache
lib/form/templates/element-selectwithlink.mustache
lib/form/templates/element-submit.mustache
lib/form/templates/element-tags-inline.mustache
lib/form/templates/element-tags.mustache
lib/form/templates/element-template-inline.mustache
lib/form/templates/element-template.mustache
lib/form/templates/element-text-inline.mustache
lib/form/templates/element-text.mustache
lib/form/templates/element-textarea.mustache
lib/form/templates/element-url.mustache
lib/formslib.php
lib/outputrenderers.php
lib/pear/HTML/QuickForm/element.php
lib/questionlib.php
lib/tests/exporter_test.php
lib/tests/time_splittings_test.php
message/output/email/classes/output/email_digest.php
message/output/email/classes/task/send_email_task.php
message/output/email/lang/en/message_email.php
message/output/email/templates/email_digest_html.mustache
message/output/email/templates/email_digest_text.mustache
mod/assign/lang/en/assign.php
mod/chat/lang/en/chat.php
mod/choice/lang/en/choice.php
mod/choice/mod_form.php
mod/data/lang/en/data.php
mod/data/mod_form.php
mod/feedback/classes/completion.php
mod/feedback/classes/structure.php
mod/feedback/item/feedback_item_form_class.php
mod/feedback/item/multichoice/multichoice_form.php
mod/feedback/item/multichoicerated/multichoicerated_form.php
mod/feedback/lib.php
mod/feedback/tests/behat/question_types.feature
mod/feedback/tests/behat/question_types_non_anon.feature
mod/feedback/tests/lib_test.php
mod/feedback/tests/privacy_test.php
mod/forum/amd/build/discussion_list.min.js
mod/forum/amd/build/favourite_toggle.min.js [new file with mode: 0644]
mod/forum/amd/build/pin_toggle.min.js [new file with mode: 0644]
mod/forum/amd/build/repository.min.js
mod/forum/amd/build/selectors.min.js
mod/forum/amd/src/discussion_list.js
mod/forum/amd/src/favourite_toggle.js [new file with mode: 0644]
mod/forum/amd/src/pin_toggle.js [new file with mode: 0644]
mod/forum/amd/src/repository.js
mod/forum/amd/src/selectors.js
mod/forum/classes/local/builders/exported_discussion.php [new file with mode: 0644]
mod/forum/classes/local/builders/exported_discussion_summaries.php
mod/forum/classes/local/entities/discussion.php
mod/forum/classes/local/exporters/discussion.php
mod/forum/classes/local/exporters/discussion_summaries.php
mod/forum/classes/local/exporters/discussion_summary.php
mod/forum/classes/local/factories/builder.php
mod/forum/classes/local/factories/exporter.php
mod/forum/classes/local/factories/renderer.php
mod/forum/classes/local/factories/url.php
mod/forum/classes/local/managers/capability.php
mod/forum/classes/local/renderers/discussion.php
mod/forum/classes/local/renderers/discussion_list.php
mod/forum/classes/local/vaults/author.php
mod/forum/classes/local/vaults/db_table_vault.php
mod/forum/classes/local/vaults/discussion.php
mod/forum/classes/local/vaults/discussion_list.php
mod/forum/classes/local/vaults/forum.php
mod/forum/classes/local/vaults/post.php
mod/forum/classes/local/vaults/post_read_receipt_collection.php
mod/forum/db/access.php
mod/forum/db/services.php
mod/forum/externallib.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/mod_form.php
mod/forum/pix/t/star.png [new file with mode: 0644]
mod/forum/pix/t/star.svg [new file with mode: 0644]
mod/forum/styles.css
mod/forum/templates/discussion_favourite_toggle.mustache [new file with mode: 0644]
mod/forum/templates/discussion_list.mustache
mod/forum/templates/discussion_lock_toggle.mustache
mod/forum/templates/discussion_pin_toggle.mustache [new file with mode: 0644]
mod/forum/templates/discussion_subscription_toggle.mustache
mod/forum/templates/forum_action_menu.mustache [new file with mode: 0644]
mod/forum/templates/forum_discussion.mustache
mod/forum/templates/forum_discussion_favourite_toggle.mustache [new file with mode: 0644]
mod/forum/tests/behat/behat_mod_forum.php
mod/forum/tests/behat/discussion_lock.feature
mod/forum/tests/behat/favourite_discussion.feature [new file with mode: 0644]
mod/forum/tests/externallib_test.php
mod/forum/tests/vaults_discussion_list_test.php
mod/forum/version.php
mod/glossary/edit_form.php
mod/glossary/mod_form.php
mod/lesson/lang/en/lesson.php
mod/lesson/mod_form.php
mod/lti/mod_form.php
mod/page/mod_form.php
mod/quiz/lang/en/quiz.php
mod/quiz/mod_form.php
mod/resource/mod_form.php
mod/scorm/mod_form.php
mod/url/mod_form.php
mod/workshop/mod_form.php
pix/t/emptystar.png [new file with mode: 0644]
pix/t/emptystar.svg [new file with mode: 0644]
question/category.php
question/category_class.php
question/edit.php
question/export.php
question/format.php
question/import.php
question/preview.php
question/tests/events_test.php
question/type/multichoice/amd/build/clearchoice.min.js [new file with mode: 0644]
question/type/multichoice/amd/src/clearchoice.js [new file with mode: 0644]
question/type/multichoice/lang/en/qtype_multichoice.php
question/type/multichoice/question.php
question/type/multichoice/renderer.php
question/type/multichoice/tests/behat/preview.feature
question/type/multichoice/tests/question_multi_test.php
question/type/multichoice/tests/question_single_test.php
question/type/multichoice/tests/walkthrough_test.php
question/type/questiontypebase.php
repository/dropbox/lang/en/repository_dropbox.php
repository/filesystem/lang/en/repository_filesystem.php
tag/classes/external/tag_area_exporter.php
theme/boost/scss/moodle/modules.scss
theme/boost/scss/moodle/question.scss
theme/boost/style/moodle.css
theme/classic/lang/en/theme_classic.php
theme/classic/style/moodle.css
version.php

index dfbb1d6..5270745 100644 (file)
 defined('MOODLE_INTERNAL') || die();
 
 if ($hassiteconfig) {
+
+    $settings = new admin_settingpage('analyticssite', new lang_string('analyticssiteinfo', 'analytics'));
+    $ADMIN->add('analytics', $settings);
+
+    if ($ADMIN->fulltree) {
+        $modeinstructions = [
+            'facetoface' => get_string('modeinstructionfacetoface', 'analytics'),
+            'blendedhybrid' => get_string('modeinstructionblendedhybrid', 'analytics'),
+            'fullyonline' => get_string('modeinstructionfullyonline', 'analytics'),
+        ];
+        $settings->add(new admin_setting_configmultiselect('analytics/modeinstruction', get_string('modeinstruction', 'analytics'),
+            '', [], $modeinstructions));
+
+        $settings->add(new admin_setting_configtext_with_maxlength('analytics/percentonline',
+            get_string('percentonline', 'analytics'),
+            get_string('percentonline_help', 'analytics'), '', PARAM_INT, 3, 3));
+
+        $typeinstitutions = [
+            'typeinstitutionacademic' => get_string('typeinstitutionacademic', 'analytics'),
+            'typeinstitutiontraining' => get_string('typeinstitutiontraining', 'analytics'),
+            'typeinstitutionngo' => get_string('typeinstitutionngo', 'analytics'),
+        ];
+        $settings->add(new admin_setting_configmultiselect('analytics/typeinstitution', get_string('typeinstitution', 'analytics'),
+            '', [], $typeinstitutions));
+
+        $levelinstitutions = [
+            'levelinstitutionisced0' => get_string('levelinstitutionisced0', 'analytics'),
+            'levelinstitutionisced1' => get_string('levelinstitutionisced1', 'analytics'),
+            'levelinstitutionisced2' => get_string('levelinstitutionisced2', 'analytics'),
+            'levelinstitutionisced3' => get_string('levelinstitutionisced3', 'analytics'),
+            'levelinstitutionisced4' => get_string('levelinstitutionisced4', 'analytics'),
+            'levelinstitutionisced5' => get_string('levelinstitutionisced5', 'analytics'),
+            'levelinstitutionisced6' => get_string('levelinstitutionisced6', 'analytics'),
+            'levelinstitutionisced7' => get_string('levelinstitutionisced7', 'analytics'),
+            'levelinstitutionisced8' => get_string('levelinstitutionisced8', 'analytics'),
+        ];
+        $settings->add(new admin_setting_configmultiselect('analytics/levelinstitution',
+            get_string('levelinstitution', 'analytics'), '', [], $levelinstitutions));
+    }
+
     $settings = new admin_settingpage('analyticssettings', new lang_string('analyticssettings', 'analytics'));
     $ADMIN->add('analytics', $settings);
 
     if ($ADMIN->fulltree) {
+
+
         // Select the site prediction's processor.
         $predictionprocessors = \core_analytics\manager::get_all_prediction_processors();
         $predictors = array();
index 3adbf0b..901a38a 100644 (file)
@@ -174,12 +174,11 @@ if ($hassiteconfig
         // Options include fields from the user table that might be helpful to
         // distinguish when adding or listing users ('I want to add the John
         // Smith from Science faculty').
-        // Username is not included as an option because in some sites, it might
-        // be a security problem to reveal usernames even to trusted staff.
         // Custom user profile fields are not currently supported.
         $temp->add(new admin_setting_configmulticheckbox('showuseridentity',
                 new lang_string('showuseridentity', 'admin'),
                 new lang_string('showuseridentity_desc', 'admin'), array('email' => 1), array(
+                    'username'    => new lang_string('username'),
                     'idnumber'    => new lang_string('idnumber'),
                     'email'       => new lang_string('email'),
                     'phone1'      => new lang_string('phone1'),
@@ -255,4 +254,4 @@ if ($hassiteconfig) {
         new lang_string('sitepolicyguest_help', 'core_admin'), (isset($CFG->sitepolicy) ? $CFG->sitepolicy : ''), PARAM_RAW));
 
     $ADMIN->add('privacy', $temp);
-}
\ No newline at end of file
+}
index 7e4d208..9e7f30d 100644 (file)
@@ -43,7 +43,7 @@ $string['componentselectnone'] = 'Unselect all';
 $string['createmodel'] = 'Create model';
 $string['currenttimesplitting'] = 'Current time-splitting method';
 $string['delete'] = 'Delete';
-$string['deletemodelconfirmation'] = 'Are you sure you want to delete "{$a}"? These changes can not be reverted.';
+$string['deletemodelconfirmation'] = 'Are you sure you want to delete "{$a}"? These changes cannot be reverted.';
 $string['disabled'] = 'Disabled';
 $string['editmodel'] = 'Edit "{$a}" model';
 $string['edittrainedwarning'] = 'This model has already been trained. Note that changing its indicators or its time-splitting method will delete its previous predictions and start generating new predictions.';
@@ -98,7 +98,7 @@ $string['invalidindicatorsremoved'] = 'A new model has been added. Indicators th
 $string['invalidprediction'] = 'Invalid to get predictions';
 $string['invalidtraining'] = 'Invalid to train the model';
 $string['loginfo'] = 'Log extra info';
-$string['missingmoodleversion'] = 'Imported file does not define a moodle version number';
+$string['missingmoodleversion'] = 'Imported file doesn\'t define a version number';
 $string['modelid'] = 'Model ID';
 $string['modelinvalidanalysables'] = 'Invalid analysable elements for "{$a}" model';
 $string['modelname'] = 'Model name';
@@ -120,7 +120,7 @@ $string['previouspage'] = 'Previous page';
 $string['restoredefault'] = 'Restore default models';
 $string['restoredefaultempty'] = 'Please select models to be restored.';
 $string['restoredefaultinfo'] = 'These default models are missing or have changed since being installed. You can restore selected default models.';
-$string['restoredefaultnone'] = 'All default models provided by the Moodle core and installed plugins have been already created. No new models were found, there is nothing to restore.';
+$string['restoredefaultnone'] = 'All default models provided by core and installed plugins have been created. No new models were found; there is nothing to restore.';
 $string['restoredefaultsome'] = 'Succesfully re-created {$a->count} new model(s).';
 $string['restoredefaultsubmit'] = 'Restore selected';
 $string['samestartdate'] = 'Current start date is good';
@@ -135,7 +135,7 @@ $string['trainandpredictmodel'] = 'Training model and calculating predictions';
 $string['trainingprocessfinished'] = 'Training process finished';
 $string['trainingresults'] = 'Training results';
 $string['trainmodels'] = 'Train models';
-$string['versionnotsame'] = 'Imported file was from a different moodle version ({$a->importedversion}) than the current one ({$a->version})';
+$string['versionnotsame'] = 'Imported file was from a different version ({$a->importedversion}) than the current one ({$a->version})';
 $string['viewlog'] = 'Evaluation log';
 $string['weeksenddateautomaticallyset'] = 'End date automatically set based on start date and the number of sections';
 $string['weeksenddatedefault'] = 'End date automatically calculated from the course start date.';
index 74ff239..f819e5f 100644 (file)
@@ -65,7 +65,7 @@ Feature: Restoring default models
     And I should see "Analytics models"
     And I should see "No teaching"
     When I click on "Restore default models" "link"
-    Then I should see "All default models provided by the Moodle core and installed plugins have been already created. No new models were found, there is nothing to restore."
+    Then I should see "All default models provided by core and installed plugins have been created. No new models were found; there is nothing to restore."
     And I click on "Back" "link"
     And I should see "Analytics models"
 
index 1ce5887..0b6ba81 100644 (file)
@@ -87,7 +87,7 @@ $string['mobilesettings'] = 'Mobile settings';
 $string['offlineuse'] = 'Offline use';
 $string['pluginname'] = 'Moodle app tools';
 $string['pluginnotenabledorconfigured'] = 'Plugin not enabled or configured.';
-$string['readingthisemailgettheapp'] = 'Reading this in your e-mail? <a href="{$a}">Download the mobile app and receive notifications on your mobile devices</a>.';
+$string['readingthisemailgettheapp'] = 'Reading this in an email? <a href="{$a}">Download the mobile app and receive notifications on your mobile device</a>.';
 $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.';
 $string['setuplink'] = 'App download page';
index 8ebce53..f61d13c 100644 (file)
@@ -208,7 +208,7 @@ You can also choose to display the courses in a list, with summary information,
 
 // 3.6 Messaging tour.
 $string['tour4_title_messaging'] = 'New messaging interface';
-$string['tour4_content_messaging'] = 'Moodle 3.6 provides a new interface to messaging, ability for group messaging within a course, along with better control over who can message you.';
+$string['tour4_content_messaging'] = 'New messaging features include group messaging within a course and better control over who can message you.';
 $string['tour4_title_icon'] = 'Messaging';
 $string['tour4_content_icon'] = 'You can access your messages from any page using this icon.
 
index bcb621c..270a9f6 100644 (file)
@@ -67,6 +67,9 @@ class analysis {
         $this->analyser = $analyser;
         $this->includetarget = $includetarget;
         $this->result = $result;
+
+        // We cache the first time analysables were analysed because time-splitting methods can depend on these info.
+        self::fill_firstanalyses_cache($this->analyser->get_modelid());
     }
 
     /**
@@ -81,10 +84,6 @@ class analysis {
         // Time limit control.
         $modeltimelimit = intval(get_config('analytics', 'modeltimelimit'));
 
-        $filesbytimesplitting = array();
-
-        $alreadyprocessedanalysables = $this->get_processed_analysables();
-
         if ($this->includetarget) {
             $action = 'training';
         } else {
@@ -92,6 +91,8 @@ class analysis {
         }
         $analysables = $this->analyser->get_analysables_iterator($action);
 
+        $processedanalysables = $this->get_processed_analysables();
+
         $inittime = microtime(true);
         foreach ($analysables as $analysable) {
             $processed = false;
@@ -121,13 +122,16 @@ class analysis {
                 }
             }
 
-            // Updated regardless of how well the analysis went.
-            if ($this->analyser->get_target()->always_update_analysis_time() || $processed) {
-                $this->update_analysable_analysed_time($alreadyprocessedanalysables, $analysable->get_id());
-            }
-
-            // Apply time limit.
             if (!$options['evaluation']) {
+
+                if (empty($processedanalysables[$analysable->get_id()]) ||
+                        $this->analyser->get_target()->always_update_analysis_time() || $processed) {
+                    // We store the list of processed analysables even if the target does not always_update_analysis_time(),
+                    // what always_update_analysis_time controls is the update of the data.
+                    $this->update_analysable_analysed_time($processedanalysables, $analysable->get_id());
+                }
+
+                // Apply time limit.
                 $timespent = microtime(true) - $inittime;
                 if ($modeltimelimit <= $timespent) {
                     break;
@@ -150,7 +154,7 @@ class analysis {
 
         // Weird select fields ordering for performance (analysableid key matching, analysableid is also unique by modelid).
         return $DB->get_records_select('analytics_used_analysables', $select,
-            $params, 'timeanalysed DESC', 'analysableid, modelid, action, timeanalysed, id AS primarykey');
+            $params, 'timeanalysed DESC', 'analysableid, modelid, action, firstanalysis, timeanalysed, id AS primarykey');
     }
 
     /**
@@ -590,13 +594,16 @@ class analysis {
     protected function update_analysable_analysed_time(array $processedanalysables, int $analysableid) {
         global $DB;
 
+        $now = time();
+
         if (!empty($processedanalysables[$analysableid])) {
             $obj = $processedanalysables[$analysableid];
 
             $obj->id = $obj->primarykey;
             unset($obj->primarykey);
 
-            $obj->timeanalysed = time();
+            $obj->timeanalysed = $now;
+
             $DB->update_record('analytics_used_analysables', $obj);
 
         } else {
@@ -605,10 +612,54 @@ class analysis {
             $obj->modelid = $this->analyser->get_modelid();
             $obj->action = ($this->includetarget) ? 'training' : 'prediction';
             $obj->analysableid = $analysableid;
-            $obj->timeanalysed = time();
+            $obj->firstanalysis = $now;
+            $obj->timeanalysed = $now;
+
+            $obj->primarykey = $DB->insert_record('analytics_used_analysables', $obj);
+
+            // Update the cache just in case it is used in the same request.
+            $key = $this->analyser->get_modelid() . '_' . $analysableid;
+            $cache = \cache::make('core', 'modelfirstanalyses');
+            $cache->set($key, $now);
+        }
+    }
+
+    /**
+     * Fills a cache containing the first time each analysable in the provided model was analysed.
+     *
+     * @param int $modelid
+     * @param int|null $analysableid
+     * @return null
+     */
+    public static function fill_firstanalyses_cache(int $modelid, ?int $analysableid = null) {
+        global $DB;
 
-            $DB->insert_record('analytics_used_analysables', $obj);
+        // Using composed keys instead of cache $identifiers because of MDL-65358.
+        $primarykey = $DB->sql_concat($modelid, "'_'", 'analysableid');
+        $sql = "SELECT $primarykey AS id, MIN(firstanalysis) AS firstanalysis
+                  FROM {analytics_used_analysables} aua
+                 WHERE modelid = :modelid";
+        $params = ['modelid' => $modelid];
+
+        if ($analysableid) {
+            $sql .= " AND analysableid = :analysableid";
+            $params['analysableid'] = $analysableid;
         }
+
+        $sql .= " GROUP BY modelid, analysableid ORDER BY analysableid";
+
+        $firstanalyses = $DB->get_records_sql($sql, $params);
+        if ($firstanalyses) {
+            $cache = \cache::make('core', 'modelfirstanalyses');
+
+            $firstanalyses = array_map(function($record) {
+                return $record->firstanalysis;
+            }, $firstanalyses);
+
+            $cache->set_many($firstanalyses);
+        }
+
+        return $firstanalyses;
     }
 
     /**
index 4a52370..891820e 100644 (file)
@@ -64,12 +64,12 @@ abstract class periodic extends base {
 
         $periodicity = $this->periodicity();
 
-        $now = new \DateTimeImmutable('now', \core_date::get_server_timezone_object());
-
         if ($this->analysable->get_end()) {
             $end = (new \DateTimeImmutable())->setTimestamp($this->analysable->get_end());
         }
-        $next = (new \DateTimeImmutable())->setTimestamp($this->analysable->get_start());
+        $next = (new \DateTimeImmutable())->setTimestamp($this->get_first_start());
+
+        $now = new \DateTimeImmutable('now', \core_date::get_server_timezone_object());
 
         $ranges = [];
         while ($next < $now &&
@@ -140,4 +140,13 @@ abstract class periodic extends base {
             'time' => $end
         ];
     }
+
+    /**
+     * Get the start of the first time range.
+     *
+     * @return int A timestamp.
+     */
+    protected function get_first_start() {
+        return $this->analysable->get_start();
+    }
 }
index 7cc4054..4960c73 100644 (file)
@@ -68,4 +68,26 @@ abstract class upcoming_periodic extends periodic {
     public function valid_for_evaluation(): bool {
         return false;
     }
+
+    /**
+     * Get the start of the first time range.
+     *
+     * Overwriten to start generating predictions about upcoming stuff from time().
+     *
+     * @return int A timestamp.
+     */
+    protected function get_first_start() {
+        global $DB;
+
+        $cache = \cache::make('core', 'modelfirstanalyses');
+
+        $key = $this->modelid . '_' . $this->analysable->get_id();
+        $firstanalysis = $cache->get($key);
+        if (!empty($firstanalysis)) {
+            return $firstanalysis;
+        }
+
+        // This analysable has not yet been analysed, the start is therefore now (-1 so ready_to_predict can be executed).
+        return time() - 1;
+    }
 }
diff --git a/analytics/tests/analysis_test.php b/analytics/tests/analysis_test.php
new file mode 100644 (file)
index 0000000..9d763cd
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for the analysis class.
+ *
+ * @package   core_analytics
+ * @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Unit tests for the analysis class.
+ *
+ * @package   core_analytics
+ * @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class analytics_analysis_testcase extends advanced_testcase {
+
+    /**
+     * Test fill_firstanalyses_cache.
+     * @return null
+     */
+    public function test_fill_firstanalyses_cache() {
+        $this->resetAfterTest();
+
+        $this->insert_used(1, 1, 'training', 123);
+        $this->insert_used(1, 2, 'training', 124);
+        $this->insert_used(1, 1, 'prediction', 125);
+
+        $firstanalyses = \core_analytics\analysis::fill_firstanalyses_cache(1);
+        $this->assertCount(2, $firstanalyses);
+        $this->assertEquals(123, $firstanalyses['1_1']);
+        $this->assertEquals(124, $firstanalyses['1_2']);
+
+        // The cached elements gets refreshed.
+        $this->insert_used(1, 1, 'prediction', 122);
+        $firstanalyses = \core_analytics\analysis::fill_firstanalyses_cache(1, 1);
+        $this->assertCount(1, $firstanalyses);
+        $this->assertEquals(122, $firstanalyses['1_1']);
+    }
+
+    private function insert_used($modelid, $analysableid, $action, $timestamp) {
+        global $DB;
+
+        $obj = new \stdClass();
+        $obj->modelid = $modelid;
+        $obj->action = $action;
+        $obj->analysableid = $analysableid;
+        $obj->firstanalysis = $timestamp;
+        $obj->timeanalysed = $timestamp;
+        $obj->id = $DB->insert_record('analytics_used_analysables', $obj);
+    }
+}
diff --git a/analytics/tests/fixtures/test_timesplitting_upcoming_seconds.php b/analytics/tests/fixtures/test_timesplitting_upcoming_seconds.php
new file mode 100644 (file)
index 0000000..9309f75
--- /dev/null
@@ -0,0 +1,52 @@
+<?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/>.
+
+/**
+ * Test time splitting.
+ *
+ * @package   core_analytics
+ * @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Test time splitting.
+ *
+ * @package   core_analytics
+ * @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class test_timesplitting_upcoming_seconds extends \core_analytics\local\time_splitting\upcoming_periodic {
+
+    /**
+     * Every second.
+     * @return \DateInterval
+     */
+    public function periodicity() {
+        return new \DateInterval('PT1S');
+    }
+
+    /**
+     * Just to comply with the interface.
+     *
+     * @return \lang_string
+     */
+    public static function get_name() : \lang_string {
+        return new \lang_string('error');
+    }
+}
index 5f638ec..11dd4ce 100644 (file)
@@ -806,12 +806,25 @@ class backup_badges_structure_step extends backup_structure_step {
         $badge = new backup_nested_element('badge', array('id'), array('name', 'description',
                 'timecreated', 'timemodified', 'usercreated', 'usermodified', 'issuername',
                 'issuerurl', 'issuercontact', 'expiredate', 'expireperiod', 'type', 'courseid',
-                'message', 'messagesubject', 'attachment', 'notification', 'status', 'nextcron'));
+                'message', 'messagesubject', 'attachment', 'notification', 'status', 'nextcron',
+                'version', 'language', 'imageauthorname', 'imageauthoremail', 'imageauthorurl',
+                'imagecaption'));
 
         $criteria = new backup_nested_element('criteria');
         $criterion = new backup_nested_element('criterion', array('id'), array('badgeid',
                 'criteriatype', 'method', 'description', 'descriptionformat'));
 
+        $endorsement = new backup_nested_element('endorsement', array('id'), array('badgeid',
+                'issuername', 'issuerurl', 'issueremail', 'claimid', 'claimcomment', 'dateissued'));
+
+        $alignments = new backup_nested_element('alignments');
+        $alignment = new backup_nested_element('alignment', array('id'), array('badgeid',
+                'targetname', 'targeturl', 'targetdescription', 'targetframework', 'targetcode'));
+
+        $relatedbadges = new backup_nested_element('relatedbadges');
+        $relatedbadge = new backup_nested_element('relatedbadge', array('id'), array('badgeid',
+                'relatedbadgeid'));
+
         $parameters = new backup_nested_element('parameters');
         $parameter = new backup_nested_element('parameter', array('id'), array('critid',
                 'name', 'value', 'criteriatype'));
@@ -827,6 +840,11 @@ class backup_badges_structure_step extends backup_structure_step {
         $criteria->add_child($criterion);
         $criterion->add_child($parameters);
         $parameters->add_child($parameter);
+        $badge->add_child($endorsement);
+        $badge->add_child($alignments);
+        $alignments->add_child($alignment);
+        $badge->add_child($relatedbadges);
+        $relatedbadges->add_child($relatedbadge);
         $badge->add_child($manual_awards);
         $manual_awards->add_child($manual_award);
 
@@ -834,6 +852,10 @@ class backup_badges_structure_step extends backup_structure_step {
 
         $badge->set_source_table('badge', array('courseid' => backup::VAR_COURSEID));
         $criterion->set_source_table('badge_criteria', array('badgeid' => backup::VAR_PARENTID));
+        $endorsement->set_source_table('badge_endorsement', array('badgeid' => backup::VAR_PARENTID));
+
+        $alignment->set_source_table('badge_alignment', array('badgeid' => backup::VAR_PARENTID));
+        $relatedbadge->set_source_table('badge_related', array('badgeid' => backup::VAR_PARENTID));
 
         $parametersql = 'SELECT cp.*, c.criteriatype
                              FROM {badge_criteria_param} cp JOIN {badge_criteria} c
@@ -850,6 +872,10 @@ class backup_badges_structure_step extends backup_structure_step {
         $badge->annotate_ids('user', 'usermodified');
         $criterion->annotate_ids('badge', 'badgeid');
         $parameter->annotate_ids('criterion', 'critid');
+        $endorsement->annotate_ids('badge', 'badgeid');
+        $alignment->annotate_ids('badge', 'badgeid');
+        $relatedbadge->annotate_ids('badge', 'badgeid');
+        $relatedbadge->annotate_ids('badge', 'relatedbadgeid');
         $badge->annotate_files('badges', 'badgeimage', 'id');
         $manual_award->annotate_ids('badge', 'badgeid');
         $manual_award->annotate_ids('user', 'recipientid');
index ff9e937..fe699b3 100644 (file)
@@ -2517,6 +2517,9 @@ class restore_badges_structure_step extends restore_structure_step {
         $paths[] = new restore_path_element('badge', '/badges/badge');
         $paths[] = new restore_path_element('criterion', '/badges/badge/criteria/criterion');
         $paths[] = new restore_path_element('parameter', '/badges/badge/criteria/criterion/parameters/parameter');
+        $paths[] = new restore_path_element('endorsement', '/badges/badge/endorsement');
+        $paths[] = new restore_path_element('alignment', '/badges/badge/alignments/alignment');
+        $paths[] = new restore_path_element('relatedbadge', '/badges/badge/relatedbadges/relatedbadge');
         $paths[] = new restore_path_element('manual_award', '/badges/badge/manual_awards/manual_award');
 
         return $paths;
@@ -2561,13 +2564,87 @@ class restore_badges_structure_step extends restore_structure_step {
                 'attachment'     => $data->attachment,
                 'notification'   => $data->notification,
                 'status'         => BADGE_STATUS_INACTIVE,
-                'nextcron'       => $data->nextcron
+                'nextcron'       => $data->nextcron,
+                'version'        => $data->version,
+                'language'       => $data->language,
+                'imageauthorname' => $data->imageauthorname,
+                'imageauthoremail' => $data->imageauthoremail,
+                'imageauthorurl' => $data->imageauthorurl,
+                'imagecaption'   => $data->imagecaption
         );
 
         $newid = $DB->insert_record('badge', $params);
         $this->set_mapping('badge', $data->id, $newid, $restorefiles);
     }
 
+    /**
+     * Create an endorsement for a badge.
+     *
+     * @param mixed $data
+     * @return void
+     */
+    public function process_endorsement($data) {
+        global $DB;
+
+        $data = (object)$data;
+
+        $params = [
+            'badgeid' => $this->get_new_parentid('badge'),
+            'issuername' => $data->issuername,
+            'issuerurl' => $data->issuerurl,
+            'issueremail' => $data->issueremail,
+            'claimid' => $data->claimid,
+            'claimcomment' => $data->claimcomment,
+            'dateissued' => $this->apply_date_offset($data->dateissued)
+        ];
+        $newid = $DB->insert_record('badge_endorsement', $params);
+        $this->set_mapping('endorsement', $data->id, $newid);
+    }
+
+    /**
+     * Link to related badges for a badge. This relies on post processing in after_execute().
+     *
+     * @param mixed $data
+     * @return void
+     */
+    public function process_relatedbadge($data) {
+        global $DB;
+
+        $data = (object)$data;
+        $relatedbadgeid = $data->relatedbadgeid;
+
+        if ($relatedbadgeid) {
+            // Only backup and restore related badges if they are contained in the backup file.
+            $params = array(
+                    'badgeid'           => $this->get_new_parentid('badge'),
+                    'relatedbadgeid'    => $relatedbadgeid
+            );
+            $newid = $DB->insert_record('badge_related', $params);
+        }
+    }
+
+    /**
+     * Link to an alignment for a badge.
+     *
+     * @param mixed $data
+     * @return void
+     */
+    public function process_alignment($data) {
+        global $DB;
+
+        $data = (object)$data;
+        $params = array(
+                'badgeid'           => $this->get_new_parentid('badge'),
+                'targetname'        => $data->targetname,
+                'targeturl'         => $data->targeturl,
+                'targetdescription' => $data->targetdescription,
+                'targetframework'   => $data->targetframework,
+                'targetcode'        => $data->targetcode
+        );
+        $newid = $DB->insert_record('badge_alignment', $params);
+        $this->set_mapping('alignment', $data->id, $newid);
+    }
+
     public function process_criterion($data) {
         global $DB;
 
@@ -2580,6 +2657,7 @@ class restore_badges_structure_step extends restore_structure_step {
                 'description'       => isset($data->description) ? $data->description : '',
                 'descriptionformat' => isset($data->descriptionformat) ? $data->descriptionformat : 0,
         );
+
         $newid = $DB->insert_record('badge_criteria', $params);
         $this->set_mapping('criterion', $data->id, $newid);
     }
@@ -2613,6 +2691,14 @@ class restore_badges_structure_step extends restore_structure_step {
             } else {
                 return;
             }
+        } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_COMPETENCY) {
+            $competencyid = $this->get_mappingid('competency', $data->value);
+            if (!empty($competencyid)) {
+                $params['name'] = 'competency_' . $competencyid;
+                $params['value'] = $competencyid;
+            } else {
+                return;
+            }
         }
 
         if (!$DB->record_exists('badge_criteria_param', $params)) {
@@ -2645,8 +2731,38 @@ class restore_badges_structure_step extends restore_structure_step {
     }
 
     protected function after_execute() {
+        global $DB;
         // Add related files.
         $this->add_related_files('badges', 'badgeimage', 'badge');
+
+        $badgeid = $this->get_new_parentid('badge');
+        // Remap any related badges.
+        // We do this in the DB directly because this is backup/restore it is not valid to call into
+        // the component API.
+        $params = array('badgeid' => $badgeid);
+        $query = "SELECT DISTINCT br.id, br.badgeid, br.relatedbadgeid
+                    FROM {badge_related} br
+                   WHERE (br.badgeid = :badgeid)";
+        $relatedbadges = $DB->get_records_sql($query, $params);
+        $newrelatedids = [];
+        foreach ($relatedbadges as $relatedbadge) {
+            $relatedid = $this->get_mappingid('badge', $relatedbadge->relatedbadgeid);
+            $params['relatedbadgeid'] = $relatedbadge->relatedbadgeid;
+            $DB->delete_records_select('badge_related', '(badgeid = :badgeid AND relatedbadgeid = :relatedbadgeid)', $params);
+            if ($relatedid) {
+                $newrelatedids[] = $relatedid;
+            }
+        }
+        if (!empty($newrelatedids)) {
+            $relatedbadges = [];
+            foreach ($newrelatedids as $relatedid) {
+                $relatedbadge = new stdClass();
+                $relatedbadge->badgeid = $badgeid;
+                $relatedbadge->relatedbadgeid = $relatedid;
+                $relatedbadges[] = $relatedbadge;
+            }
+            $DB->insert_records('badge_related', $relatedbadges);
+        }
     }
 }
 
index 079537f..22ac0ed 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
@@ -82,7 +81,7 @@ abstract class setting_dependency {
      * Destroy all circular references. It helps PHP 5.2 a lot!
      */
     public function destroy() {
-        // No need to destroy anything recursively here, direct reset
+        // No need to destroy anything recursively here, direct reset.
         $this->setting = null;
         $this->dependentsetting = null;
     }
@@ -94,16 +93,19 @@ abstract class setting_dependency {
      * @return bool
      */
     final public function process_change($changetype, $oldvalue) {
-        // Check the type of change requested
+        // Check the type of change requested.
         switch ($changetype) {
-            // Process a status change
-            case base_setting::CHANGED_STATUS: return $this->process_status_change($oldvalue);
-            // Process a visibility change
-            case base_setting::CHANGED_VISIBILITY: return $this->process_visibility_change($oldvalue);
-            // Process a value change
-            case base_setting::CHANGED_VALUE: return $this->process_value_change($oldvalue);
+            // Process a status change.
+            case base_setting::CHANGED_STATUS:
+                return $this->process_status_change($oldvalue);
+            // Process a visibility change.
+            case base_setting::CHANGED_VISIBILITY:
+                return $this->process_visibility_change($oldvalue);
+            // Process a value change.
+            case base_setting::CHANGED_VALUE:
+                return $this->process_value_change($oldvalue);
         }
-        // Throw an exception if we get this far
+        // Throw an exception if we get this far.
         throw new backup_ui_exception('unknownchangetype');
     }
     /**
@@ -112,11 +114,11 @@ abstract class setting_dependency {
      * @return bool
      */
     protected function process_visibility_change($oldvisibility) {
-        // Store the current dependent settings visibility for comparison
+        // Store the current dependent settings visibility for comparison.
         $prevalue = $this->dependentsetting->get_visibility();
-        // Set it regardless of whether we need to
+        // Set it regardless of whether we need to.
         $this->dependentsetting->set_visibility($this->setting->get_visibility());
-        // Return true if it changed
+        // Return true if it changed.
         return ($prevalue != $this->dependentsetting->get_visibility());
     }
     /**
@@ -182,15 +184,16 @@ class setting_dependency_disabledif_equals extends setting_dependency {
      */
     public function __construct(base_setting $setting, base_setting $dependentsetting, $value, $defaultvalue = false) {
         parent::__construct($setting, $dependentsetting, $defaultvalue);
-        $this->value = ($value)?(string)$value:0;
+        $this->value = ($value) ? (string)$value : 0;
     }
     /**
      * Returns true if the dependent setting is locked by this setting_dependency.
      * @return bool
      */
     public function is_locked() {
-        // If the setting is locked or the dependent setting should be locked then return true
-        if ($this->setting->get_status() !== base_setting::NOT_LOCKED || $this->setting->get_value() == $this->value) {
+        // If the setting is locked or the dependent setting should be locked then return true.
+        if ($this->setting->get_status() !== base_setting::NOT_LOCKED ||
+                $this->evaluate_disabled_condition($this->setting->get_value())) {
             return true;
         }
         // Else the dependent setting is not locked by this setting_dependency.
@@ -208,17 +211,25 @@ class setting_dependency_disabledif_equals extends setting_dependency {
             return false;
         }
         $prevalue = $this->dependentsetting->get_value();
-        // If the setting is the desired value enact the dependency
-        if ($this->setting->get_value() == $this->value) {
+        // If the setting is the desired value enact the dependency.
+        $settingvalue = $this->setting->get_value();
+        if ($this->evaluate_disabled_condition($settingvalue)) {
             // The dependent setting needs to be locked by hierachy and set to the
             // default value.
             $this->dependentsetting->set_status(base_setting::LOCKED_BY_HIERARCHY);
-            $this->dependentsetting->set_value($this->defaultvalue);
+
+            // For checkboxes the default value is false, but when the setting is
+            // locked, the value should inherit from the parent setting.
+            if ($this->defaultvalue === false) {
+                $this->dependentsetting->set_value($settingvalue);
+            } else {
+                $this->dependentsetting->set_value($this->defaultvalue);
+            }
         } else if ($this->dependentsetting->get_status() == base_setting::LOCKED_BY_HIERARCHY) {
-            // We can unlock the dependent setting
+            // We can unlock the dependent setting.
             $this->dependentsetting->set_status(base_setting::NOT_LOCKED);
         }
-        // Return true if the value has changed for the dependent setting
+        // Return true if the value has changed for the dependent setting.
         return ($prevalue != $this->dependentsetting->get_value());
     }
     /**
@@ -227,17 +238,18 @@ class setting_dependency_disabledif_equals extends setting_dependency {
      * @return bool
      */
     protected function process_status_change($oldstatus) {
-        // Store the dependent status
+        // Store the dependent status.
         $prevalue = $this->dependentsetting->get_status();
-        // Store the current status
+        // Store the current status.
         $currentstatus = $this->setting->get_status();
         if ($currentstatus == base_setting::NOT_LOCKED) {
-            if ($prevalue == base_setting::LOCKED_BY_HIERARCHY && $this->setting->get_value() != $this->value) {
-                // Dependency has changes, is not fine, unlock the dependent setting
+            if ($prevalue == base_setting::LOCKED_BY_HIERARCHY &&
+                    !$this->evaluate_disabled_condition($this->setting->get_value())) {
+                // Dependency has changes, is not fine, unlock the dependent setting.
                 $this->dependentsetting->set_status(base_setting::NOT_LOCKED);
             }
         } else {
-            // Make sure the dependent setting is also locked, in this case by hierarchy
+            // Make sure the dependent setting is also locked, in this case by hierarchy.
             $this->dependentsetting->set_status(base_setting::LOCKED_BY_HIERARCHY);
         }
         // Return true if the dependent setting has changed.
@@ -248,17 +260,17 @@ class setting_dependency_disabledif_equals extends setting_dependency {
      * @return bool True if there were changes
      */
     public function enforce() {
-        // This will be set to true if ANYTHING changes
+        // This will be set to true if ANYTHING changes.
         $changes = false;
-        // First process any value changes
+        // First process any value changes.
         if ($this->process_value_change($this->setting->get_value())) {
             $changes = true;
         }
-        // Second process any status changes
+        // Second process any status changes.
         if ($this->process_status_change($this->setting->get_status())) {
             $changes = true;
         }
-        // Finally process visibility changes
+        // Finally process visibility changes.
         if ($this->process_visibility_change($this->setting->get_visibility())) {
             $changes = true;
         }
@@ -271,152 +283,72 @@ class setting_dependency_disabledif_equals extends setting_dependency {
      */
     public function get_moodleform_properties() {
         return array(
-            'setting'=>$this->dependentsetting->get_ui_name(),
-            'dependenton'=>$this->setting->get_ui_name(),
-            'condition'=>'eq',
-            'value'=>$this->value
+            'setting' => $this->dependentsetting->get_ui_name(),
+            'dependenton' => $this->setting->get_ui_name(),
+            'condition' => 'eq',
+            'value' => $this->value
         );
     }
+
+    /**
+     * Evaluate the current value of the setting and return true if the dependent setting should be locked or false.
+     * This function should be abstract, but there will probably be existing sub-classes so we must provide a default
+     * implementation.
+     * @param mixed $value The value of the parent setting.
+     * @return bool
+     */
+    protected function evaluate_disabled_condition($value) {
+        return $value == $this->value;
+    }
 }
 
 /**
-* A dependency that disables the secondary setting if the primary setting is
-* not equal to the provided value
-*
-* @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com>
-* @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
-*/
+ * A dependency that disables the secondary setting if the primary setting is
+ * not equal to the provided value
+ *
+ * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
 class setting_dependency_disabledif_not_equals extends setting_dependency_disabledif_equals {
+
     /**
-    * Enforces the dependency if required.
-    * @return bool True if there were changes
-    */
-    public function enforce() {
-        // This will be set to true if ANYTHING changes
-        $changes = false;
-        // First process any value changes
-        if (!$this->process_value_change($this->setting->get_value())) {
-            $changes = true;
-        }
-        // Second process any status changes
-        if ($this->process_status_change($this->setting->get_status())) {
-            $changes = true;
-        }
-        // Finally process visibility changes
-        if ($this->process_visibility_change($this->setting->get_visibility())) {
-            $changes = true;
-        }
-        return $changes;
+     * Evaluate the current value of the setting and return true if the dependent setting should be locked or false.
+     * @param mixed $value The value of the parent setting.
+     * @return bool
+     */
+    protected function evaluate_disabled_condition($value) {
+        return $value != $this->value;
     }
+
     /**
-    * Returns an array of properties suitable to be used to define a moodleforms
-    * disabled command
-    * @return array
-    */
+     * Returns an array of properties suitable to be used to define a moodleforms
+     * disabled command
+     * @return array
+     */
     public function get_moodleform_properties() {
         return array(
-                'setting'=>$this->dependentsetting->get_ui_name(),
-                'dependenton'=>$this->setting->get_ui_name(),
-                'condition'=>'notequal',
-                'value'=>$this->value
+                'setting' => $this->dependentsetting->get_ui_name(),
+                'dependenton' => $this->setting->get_ui_name(),
+                'condition' => 'notequal',
+                'value' => $this->value
         );
     }
 }
 
-//with array
-class setting_dependency_disabledif_equals2 extends setting_dependency {
-    /**
-     * The value to compare to
-     * @var mixed
-     */
-    protected $value;
-    /**
-     * Creates the dependency
-     *
-     * @param base_setting $setting
-     * @param base_setting $dependentsetting
-     * @param mixed $value
-     * @param mixed $defaultvalue
-     */
-    public function __construct(base_setting $setting, base_setting $dependentsetting, array $value, $defaultvalue = false) {
-        parent::__construct($setting, $dependentsetting, $defaultvalue);
-        $this->value = $value;
-    }
-    /**
-     * Returns true if the dependent setting is locked by this setting_dependency.
-     * @return bool
-     */
-    public function is_locked() {
-        // If the setting is locked or the dependent setting should be locked then return true
-        if ($this->setting->get_status() !== base_setting::NOT_LOCKED || in_array($this->setting->get_value(), $this->value)) {
-            return true;
-        }
-        // Else the dependent setting is not locked by this setting_dependency.
-        return false;
-    }
-    /**
-     * Processes a value change in the primary setting
-     * @param mixed $oldvalue
-     * @return bool
-     */
-    protected function process_value_change($oldvalue) {
-        $prevalue = $this->dependentsetting->get_value();
-        // If the setting is the desired value enact the dependency
-        if (in_array($this->setting->get_value(), $this->value)) {
-            // The dependent setting needs to be locked by hierachy and set to the
-            // default value.
-            $this->dependentsetting->set_status(base_setting::LOCKED_BY_HIERARCHY);
-            $this->dependentsetting->set_value($this->defaultvalue);
-        } else if ($this->dependentsetting->get_status() == base_setting::LOCKED_BY_HIERARCHY) {
-            // We can unlock the dependent setting
-            $this->dependentsetting->set_status(base_setting::NOT_LOCKED);
-        }
-        // Return true if the value has changed for the dependent setting
-        return ($prevalue != $this->dependentsetting->get_value());
-    }
+/**
+ * Disable if a value is in a list.
+ */
+class setting_dependency_disabledif_in_array extends setting_dependency_disabledif_equals {
+
     /**
-     * Processes a status change in the primary setting
-     * @param mixed $oldstatus
+     * Evaluate the current value of the setting and return true if the dependent setting should be locked or false.
+     * @param mixed $value The value of the parent setting.
      * @return bool
      */
-    protected function process_status_change($oldstatus) {
-        // Store the dependent status
-        $prevalue = $this->dependentsetting->get_status();
-        // Store the current status
-        $currentstatus = $this->setting->get_status();
-        if ($currentstatus == base_setting::NOT_LOCKED) {
-            if ($prevalue == base_setting::LOCKED_BY_HIERARCHY && !in_array($this->setting->get_value(), $this->value)) {
-                // Dependency has changes, is not fine, unlock the dependent setting
-                $this->dependentsetting->set_status(base_setting::NOT_LOCKED);
-            }
-        } else {
-            // Make sure the dependent setting is also locked, in this case by hierarchy
-            $this->dependentsetting->set_status(base_setting::LOCKED_BY_HIERARCHY);
-        }
-        // Return true if the dependent setting has changed.
-        return ($prevalue != $this->dependentsetting->get_status());
-    }
-    /**
-     * Enforces the dependency if required.
-     * @return bool True if there were changes
-     */
-    public function enforce() {
-        // This will be set to true if ANYTHING changes
-        $changes = false;
-        // First process any value changes
-        if ($this->process_value_change($this->setting->get_value())) {
-            $changes = true;
-        }
-        // Second process any status changes
-        if ($this->process_status_change($this->setting->get_status())) {
-            $changes = true;
-        }
-        // Finally process visibility changes
-        if ($this->process_visibility_change($this->setting->get_visibility())) {
-            $changes = true;
-        }
-        return $changes;
+    protected function evaluate_disabled_condition($value) {
+        return in_array($value, $this->value);
     }
+
     /**
      * Returns an array of properties suitable to be used to define a moodleforms
      * disabled command
@@ -424,14 +356,19 @@ class setting_dependency_disabledif_equals2 extends setting_dependency {
      */
     public function get_moodleform_properties() {
         return array(
-            'setting'=>$this->dependentsetting->get_ui_name(),
-            'dependenton'=>$this->setting->get_ui_name(),
-            'condition'=>'eq',
-            'value'=>$this->value
+            'setting' => $this->dependentsetting->get_ui_name(),
+            'dependenton' => $this->setting->get_ui_name(),
+            'condition' => 'eq',
+            'value' => $this->value
         );
     }
 }
 
+/**
+ * This class is here for backwards compatibility (terrible name).
+ */
+class setting_dependency_disabledif_equals2 extends setting_dependency_disabledif_in_array {
+}
 
 /**
  * A dependency that disables the secondary element if the primary element is
@@ -452,9 +389,9 @@ class setting_dependency_disabledif_checked extends setting_dependency_disabledi
      */
     public function get_moodleform_properties() {
         return array(
-            'setting'=>$this->dependentsetting->get_ui_name(),
-            'dependenton'=>$this->setting->get_ui_name(),
-            'condition'=>'checked'
+            'setting' => $this->dependentsetting->get_ui_name(),
+            'dependenton' => $this->setting->get_ui_name(),
+            'condition' => 'checked'
         );
     }
 }
@@ -478,9 +415,9 @@ class setting_dependency_disabledif_not_checked extends setting_dependency_disab
      */
     public function get_moodleform_properties() {
         return array(
-            'setting'=>$this->dependentsetting->get_ui_name(),
-            'dependenton'=>$this->setting->get_ui_name(),
-            'condition'=>'notchecked'
+            'setting' => $this->dependentsetting->get_ui_name(),
+            'dependenton' => $this->setting->get_ui_name(),
+            'condition' => 'notchecked'
         );
     }
 }
@@ -497,6 +434,16 @@ class setting_dependency_disabledif_not_empty extends setting_dependency_disable
         parent::__construct($setting, $dependentsetting, false, $defaultvalue);
         $this->value = false;
     }
+
+    /**
+     * Evaluate the current value of the setting and return true if the dependent setting should be locked or false.
+     * @param mixed $value The value of the parent setting.
+     * @return bool
+     */
+    protected function evaluate_disabled_condition($value) {
+        return !empty($value);
+    }
+
     /**
      * Returns an array of properties suitable to be used to define a moodleforms
      * disabled command
@@ -504,50 +451,12 @@ class setting_dependency_disabledif_not_empty extends setting_dependency_disable
      */
     public function get_moodleform_properties() {
         return array(
-            'setting'=>$this->dependentsetting->get_ui_name(),
-            'dependenton'=>$this->setting->get_ui_name(),
-            'condition'=>'notequal',
-            'value'=>''
+            'setting' => $this->dependentsetting->get_ui_name(),
+            'dependenton' => $this->setting->get_ui_name(),
+            'condition' => 'notequal',
+            'value' => ''
         );
     }
-    /**
-     * Processes a value change in the primary setting
-     * @param mixed $oldvalue
-     * @return bool
-     */
-    protected function process_value_change($oldvalue) {
-        $prevalue = $this->dependentsetting->get_value();
-        // If the setting is the desired value enact the dependency
-        $value = $this->setting->get_value();
-        if (!empty($value)) {
-            // The dependent setting needs to be locked by hierachy and set to the
-            // default value.
-            $this->dependentsetting->set_status(base_setting::LOCKED_BY_HIERARCHY);
-            if ($this->defaultvalue === false) {
-                $this->dependentsetting->set_value($value);
-            } else {
-                $this->dependentsetting->set_value($this->defaultvalue);
-            }
-        } else if ($this->dependentsetting->get_status() == base_setting::LOCKED_BY_HIERARCHY) {
-            // We can unlock the dependent setting
-            $this->dependentsetting->set_status(base_setting::NOT_LOCKED);
-        }
-        // Return true if the value has changed for the dependent setting
-        return ($prevalue != $this->dependentsetting->get_value());
-    }
-
-    /**
-     * Returns true if the dependent setting is locked by this setting_dependency.
-     * @return bool
-     */
-    public function is_locked() {
-        // If the setting is locked or the dependent setting should be locked then return true
-        if ($this->setting->get_status() !== base_setting::NOT_LOCKED || !empty($value)) {
-            return true;
-        }
-        // Else the dependent setting is not locked by this setting_dependency.
-        return false;
-    }
 }
 
 /**
@@ -562,6 +471,16 @@ class setting_dependency_disabledif_empty extends setting_dependency_disabledif_
         parent::__construct($setting, $dependentsetting, false, $defaultvalue);
         $this->value = false;
     }
+
+    /**
+     * Evaluate the current value of the setting and return true if the dependent setting should be locked or false.
+     * @param mixed $value The value of the parent setting.
+     * @return bool
+     */
+    protected function evaluate_disabled_condition($value) {
+        return empty($value);
+    }
+
     /**
      * Returns an array of properties suitable to be used to define a moodleforms
      * disabled command
@@ -569,47 +488,10 @@ class setting_dependency_disabledif_empty extends setting_dependency_disabledif_
      */
     public function get_moodleform_properties() {
         return array(
-            'setting'=>$this->dependentsetting->get_ui_name(),
-            'dependenton'=>$this->setting->get_ui_name(),
-            'condition'=>'notequal',
-            'value'=>''
+            'setting' => $this->dependentsetting->get_ui_name(),
+            'dependenton' => $this->setting->get_ui_name(),
+            'condition' => 'notequal',
+            'value' => ''
         );
     }
-    /**
-     * Processes a value change in the primary setting
-     * @param mixed $oldvalue
-     * @return bool
-     */
-    protected function process_value_change($oldvalue) {
-        $prevalue = $this->dependentsetting->get_value();
-        // If the setting is the desired value enact the dependency
-        $value = $this->setting->get_value();
-        if (empty($value)) {
-            // The dependent setting needs to be locked by hierachy and set to the
-            // default value.
-            $this->dependentsetting->set_status(base_setting::LOCKED_BY_HIERARCHY);
-            if ($this->defaultvalue === false) {
-                $this->dependentsetting->set_value($value);
-            } else {
-                $this->dependentsetting->set_value($this->defaultvalue);
-            }
-        } else if ($this->dependentsetting->get_status() == base_setting::LOCKED_BY_HIERARCHY) {
-            // We can unlock the dependent setting
-            $this->dependentsetting->set_status(base_setting::NOT_LOCKED);
-        }
-        // Return true if the value has changed for the dependent setting
-        return ($prevalue != $this->dependentsetting->get_value());
-    }
-    /**
-     * Returns true if the dependent setting is locked by this setting_dependency.
-     * @return bool
-     */
-    public function is_locked() {
-        // If the setting is locked or the dependent setting should be locked then return true
-        if ($this->setting->get_status() !== base_setting::NOT_LOCKED || empty($value)) {
-            return true;
-        }
-        // Else the dependent setting is not locked by this setting_dependency.
-        return false;
-    }
 }
index 45bc4ec..63999fe 100644 (file)
@@ -45,7 +45,7 @@ class backp_settings_testcase extends basic_testcase {
     /**
      * test base_setting class
      */
-    function test_base_setting() {
+    public function test_base_setting() {
         // Instantiate base_setting and check everything
         $bs = new mock_base_setting('test', base_setting::IS_BOOLEAN);
         $this->assertTrue($bs instanceof base_setting);
@@ -290,10 +290,54 @@ class backp_settings_testcase extends basic_testcase {
         $this->assertEquals($ubs3->get_status(), $ubs1->get_status());
     }
 
+    /**
+     * Test that locked and unlocked states on dependent backup settings at the same level
+     * correctly do not flow from the parent to the child setting when the setting is locked by permissions.
+     */
+    public function test_dependency_empty_locked_by_permission_child_is_not_unlocked() {
+        // Check dependencies are working ok.
+        $bs1 = new mock_backup_setting('test1', base_setting::IS_INTEGER, 2);
+        $bs1->set_level(1);
+        $bs2 = new mock_backup_setting('test2', base_setting::IS_INTEGER, 2);
+        $bs2->set_level(1); // Same level *must* work.
+        $bs1->add_dependency($bs2, setting_dependency::DISABLED_EMPTY);
+
+        $bs1->set_status(base_setting::LOCKED_BY_PERMISSION);
+        $this->assertEquals(base_setting::LOCKED_BY_HIERARCHY, $bs2->get_status());
+        $this->assertEquals(base_setting::LOCKED_BY_PERMISSION, $bs1->get_status());
+        $bs2->set_status(base_setting::LOCKED_BY_PERMISSION);
+        $this->assertEquals(base_setting::LOCKED_BY_PERMISSION, $bs1->get_status());
+
+        // Unlocking the parent should NOT unlock the child.
+        $bs1->set_status(base_setting::NOT_LOCKED);
+
+        $this->assertEquals(base_setting::LOCKED_BY_PERMISSION, $bs2->get_status());
+    }
+
+    /**
+     * Test that locked and unlocked states on dependent backup settings at the same level
+     * correctly do flow from the parent to the child setting when the setting is locked by config.
+     */
+    public function test_dependency_not_empty_locked_by_config_parent_is_unlocked() {
+        $bs1 = new mock_backup_setting('test1', base_setting::IS_INTEGER, 0);
+        $bs1->set_level(1);
+        $bs2 = new mock_backup_setting('test2', base_setting::IS_INTEGER, 0);
+        $bs2->set_level(1); // Same level *must* work.
+        $bs1->add_dependency($bs2, setting_dependency::DISABLED_NOT_EMPTY);
+
+        $bs1->set_status(base_setting::LOCKED_BY_CONFIG);
+        $this->assertEquals(base_setting::LOCKED_BY_HIERARCHY, $bs2->get_status());
+        $this->assertEquals(base_setting::LOCKED_BY_CONFIG, $bs1->get_status());
+
+        // Unlocking the parent should unlock the child.
+        $bs1->set_status(base_setting::NOT_LOCKED);
+        $this->assertEquals(base_setting::NOT_LOCKED, $bs2->get_status());
+    }
+
     /**
      * test backup_setting class
      */
-    function test_backup_setting() {
+    public function test_backup_setting() {
         // Instantiate backup_setting class and set level
         $bs = new mock_backup_setting('test', base_setting::IS_INTEGER, null);
         $bs->set_level(1);
@@ -340,7 +384,7 @@ class backp_settings_testcase extends basic_testcase {
     /**
      * test activity_backup_setting class
      */
-    function test_activity_backup_setting() {
+    public function test_activity_backup_setting() {
         $bs = new mock_activity_backup_setting('test', base_setting::IS_INTEGER, null);
         $this->assertEquals($bs->get_level(), backup_setting::ACTIVITY_LEVEL);
 
@@ -355,7 +399,7 @@ class backp_settings_testcase extends basic_testcase {
     /**
      * test section_backup_setting class
      */
-    function test_section_backup_setting() {
+    public function test_section_backup_setting() {
         $bs = new mock_section_backup_setting('test', base_setting::IS_INTEGER, null);
         $this->assertEquals($bs->get_level(), backup_setting::SECTION_LEVEL);
 
@@ -370,7 +414,7 @@ class backp_settings_testcase extends basic_testcase {
     /**
      * test course_backup_setting class
      */
-    function test_course_backup_setting() {
+    public function test_course_backup_setting() {
         $bs = new mock_course_backup_setting('test', base_setting::IS_INTEGER, null);
         $this->assertEquals($bs->get_level(), backup_setting::COURSE_LEVEL);
 
index 7bb97b0..a0bafb4 100644 (file)
@@ -377,18 +377,40 @@ class provider implements
 
             // Export the badges.
             $uniqueid = $DB->sql_concat_join("'-'", ['b.id', 'COALESCE(bc.id, 0)', 'COALESCE(bi.id, 0)',
-                'COALESCE(bma.id, 0)', 'COALESCE(bcm.id, 0)']);
+                'COALESCE(bma.id, 0)', 'COALESCE(bcm.id, 0)', 'COALESCE(brb.id, 0)', 'COALESCE(ba.id, 0)']);
             $sql = "
                 SELECT $uniqueid AS uniqueid, b.id,
                        bi.id AS biid, bi.dateissued, bi.dateexpire, bi.uniquehash,
                        bma.id AS bmaid, bma.datemet, bma.issuerid,
                        bcm.id AS bcmid,
                        c.fullname AS coursename,
+                       be.id AS beid,
+                       be.issuername AS beissuername,
+                       be.issuerurl AS beissuerurl,
+                       be.issueremail AS beissueremail,
+                       be.claimid AS beclaimid,
+                       be.claimcomment AS beclaimcomment,
+                       be.dateissued AS bedateissued,
+                       brb.id as rbid,
+                       brb.badgeid as rbbadgeid,
+                       brb.relatedbadgeid as rbrelatedbadgeid,
+                       ba.id as baid,
+                       ba.targetname as batargetname,
+                       ba.targeturl as batargeturl,
+                       ba.targetdescription as batargetdescription,
+                       ba.targetframework as batargetframework,
+                       ba.targetcode as batargetcode,
                        $ctxfields
                   FROM {badge} b
              LEFT JOIN {badge_issued} bi
                     ON bi.badgeid = b.id
                    AND bi.userid = :userid1
+            LEFT JOIN {badge_related} brb
+                    ON ( b.id = brb.badgeid OR b.id = brb.relatedbadgeid )
+             LEFT JOIN {badge_alignment} ba
+                    ON ( b.id = ba.badgeid )
+             LEFT JOIN {badge_endorsement} be
+                    ON be.badgeid = b.id
              LEFT JOIN {badge_manual_award} bma
                     ON bma.badgeid = b.id
                    AND bma.recipientid = :userid2
@@ -422,9 +444,16 @@ class provider implements
                 if ($carry === null) {
                     $carry = [
                         'name' => $badge->name,
+                        'version' => $badge->version,
+                        'language' => $badge->language,
+                        'imageauthorname' => $badge->imageauthorname,
+                        'imageauthoremail' => $badge->imageauthoremail,
+                        'imageauthorurl' => $badge->imageauthorurl,
+                        'imagecaption' => $badge->imagecaption,
                         'issued' => null,
                         'manual_award' => null,
-                        'criteria_met' => []
+                        'criteria_met' => [],
+                        'endorsement' => null,
                     ];
 
                     if ($badge->type == BADGE_TYPE_COURSE) {
@@ -432,6 +461,17 @@ class provider implements
                         $carry['course'] = format_string($record->coursename, true, ['context' => $badge->get_context()]);
                     }
 
+                    if (!empty($record->beid)) {
+                        $carry['endorsement'] = [
+                            'issuername' => $record->beissuername,
+                            'issuerurl' => $record->beissuerurl,
+                            'issueremail' => $record->beissueremail,
+                            'claimid' => $record->beclaimid,
+                            'claimcomment' => $record->beclaimcomment,
+                            'dateissued' => $record->bedateissued ? transform::datetime($record->bedateissued) : null
+                        ];
+                    }
+
                     if (!empty($record->biid)) {
                         $carry['issued'] = [
                             'issued_on' => transform::datetime($record->dateissued),
@@ -447,6 +487,52 @@ class provider implements
                         ];
                     }
                 }
+                if (!empty($record->rbid)) {
+                    if (empty($carry['related_badge'])) {
+                        $carry['related_badge'] = [];
+                    }
+                    $rbid = $record->rbbadgeid;
+                    if ($rbid == $record->id) {
+                        $rbid = $record->rbrelatedbadgeid;
+                    }
+                    $exists = false;
+                    foreach ($carry['related_badge'] as $related) {
+                        if ($related['badgeid'] == $rbid) {
+                            $exists = true;
+                            break;
+                        }
+                    }
+                    if (!$exists) {
+                        $relatedbadge = new badge($rbid);
+                        $carry['related_badge'][] = [
+                            'badgeid' => $rbid,
+                            'badgename' => $relatedbadge->name
+                        ];
+                    }
+                }
+
+                if (!empty($record->baid)) {
+                    if (empty($carry['alignment'])) {
+                        $carry['alignment'] = [];
+                    }
+                    $exists = false;
+                    $newalignment = [
+                        'targetname' => $record->batargetname,
+                        'targeturl' => $record->batargeturl,
+                        'targetdescription' => $record->batargetdescription,
+                        'targetframework' => $record->batargetframework,
+                        'targetcode' => $record->batargetcode,
+                    ];
+                    foreach ($carry['alignment'] as $alignment) {
+                        if ($alignment == $newalignment) {
+                            $exists = true;
+                            break;
+                        }
+                    }
+                    if (!$exists) {
+                        $carry['alignment'][] = $newalignment;
+                    }
+                }
 
                 // Export the details of the criteria met.
                 // We only do that once, when we find that a least one criteria was met.
index 1bfb93a..fb75770 100644 (file)
@@ -306,7 +306,11 @@ class core_badges_privacy_testcase extends provider_testcase {
         $u2ctx = context_user::instance($u2->id);
 
         $b1 = $this->create_badge(['usercreated' => $u3->id]);
+        $this->endorse_badge(['badgeid' => $b1->id]);
+        $this->align_badge(['badgeid' => $b1->id], ' (1)');
+        $this->align_badge(['badgeid' => $b1->id], ' (2)');
         $b2 = $this->create_badge(['type' => BADGE_TYPE_COURSE, 'courseid' => $c1->id, 'usermodified' => $u3->id]);
+        $this->relate_badge($b1->id, $b2->id);
         $b3 = $this->create_badge();
         $b3crit = $this->create_criteria_manual($b3->id);
         $b4 = $this->create_badge();
@@ -333,6 +337,12 @@ class core_badges_privacy_testcase extends provider_testcase {
         $path = [get_string('badges', 'core_badges'), "{$b1->name} ({$b1->id})"];
         $data = writer::with_context($u1ctx)->get_data($path);
         $this->assertEquals($b1->name, $data->name);
+        $this->assertEquals($b1->version, $data->version);
+        $this->assertEquals($b1->language, $data->language);
+        $this->assertEquals($b1->imageauthorname, $data->imageauthorname);
+        $this->assertEquals($b1->imageauthoremail, $data->imageauthoremail);
+        $this->assertEquals($b1->imageauthorurl, $data->imageauthorurl);
+        $this->assertEquals($b1->imagecaption, $data->imagecaption);
         $this->assertNotEmpty($data->issued);
         $this->assertEmpty($data->manual_award);
         $this->assertEmpty($data->criteria_met);
@@ -340,6 +350,33 @@ class core_badges_privacy_testcase extends provider_testcase {
         $this->assertEquals('yoohoo', $data->issued['unique_hash']);
         $this->assertNull($data->issued['expires_on']);
 
+        $this->assertNotEmpty($data->endorsement);
+        $this->assertNotEmpty($data->endorsement['issuername']);
+        $this->assertNotEmpty($data->endorsement['issuerurl']);
+        $this->assertNotEmpty($data->endorsement['issueremail']);
+        $this->assertNotEmpty($data->endorsement['claimid']);
+        $this->assertNotEmpty($data->endorsement['claimcomment']);
+        $this->assertNotEmpty($data->endorsement['dateissued']);
+
+        $this->assertNotEmpty($data->related_badge);
+        $this->assertNotEmpty($data->related_badge[0]);
+        $this->assertEquals($data->related_badge[0]['badgeid'], $b2->id);
+        $this->assertEquals($data->related_badge[0]['badgename'], $b2->name);
+
+        $this->assertNotEmpty($data->alignment);
+        $this->assertNotEmpty($data->alignment[0]);
+        $this->assertNotEmpty($data->alignment[0]['targetname']);
+        $this->assertNotEmpty($data->alignment[0]['targeturl']);
+        $this->assertNotEmpty($data->alignment[0]['targetdescription']);
+        $this->assertNotEmpty($data->alignment[0]['targetframework']);
+        $this->assertNotEmpty($data->alignment[0]['targetcode']);
+        $this->assertNotEmpty($data->alignment[1]);
+        $this->assertNotEmpty($data->alignment[1]['targetname']);
+        $this->assertNotEmpty($data->alignment[1]['targeturl']);
+        $this->assertNotEmpty($data->alignment[1]['targetdescription']);
+        $this->assertNotEmpty($data->alignment[1]['targetframework']);
+        $this->assertNotEmpty($data->alignment[1]['targetcode']);
+
         $path = [get_string('badges', 'core_badges'), "{$b2->name} ({$b2->id})"];
         $data = writer::with_context($u1ctx)->get_data($path);
         $this->assertEquals($b2->name, $data->name);
@@ -598,12 +635,79 @@ class core_badges_privacy_testcase extends provider_testcase {
             'attachment' => 1,
             'notification' => 0,
             'status' => BADGE_STATUS_ACTIVE,
+            'version' => OPEN_BADGES_V2,
+            'language' => 'en',
+            'imageauthorname' => 'Image author',
+            'imageauthoremail' => 'author@example.com',
+            'imageauthorurl' => 'http://image.example.com/',
+            'imagecaption' => 'Image caption'
         ], $params);
         $record->id = $DB->insert_record('badge', $record);
 
         return $record;
     }
 
+    /**
+     * Relate a badge.
+     *
+     * @param int $badgeid The badge ID.
+     * @param int $relatedbadgeid The related badge ID.
+     * @return object
+     */
+    protected function relate_badge(int $badgeid, int $relatedbadgeid) {
+        global $DB;
+        $record = (object) [
+            'badgeid' => $badgeid,
+            'relatedbadgeid' => $relatedbadgeid
+        ];
+        $record->id = $DB->insert_record('badge_related', $record);
+
+        return $record;
+    }
+
+    /**
+     * Align a badge.
+     *
+     * @param array $params Parameters.
+     * @return object
+     */
+    protected function align_badge(array $params = [], $suffix = '') {
+        global $DB;
+        $record = (object) array_merge([
+            'badgeid' => null,
+            'targetname' => "Alignment name" . $suffix,
+            'targeturl' => "http://issuer-url.domain.co.nz",
+            'targetdescription' => "Description" . $suffix,
+            'targetframework' => "Framework" . $suffix,
+            'targetcode' => "Code . $suffix"
+        ], $params);
+        $record->id = $DB->insert_record('badge_alignment', $record);
+
+        return $record;
+    }
+
+    /**
+     * Endorse a badge.
+     *
+     * @param array $params Parameters.
+     * @return object
+     */
+    protected function endorse_badge(array $params = []) {
+        global $DB;
+        $record = (object) array_merge([
+            'badgeid' => null,
+            'issuername' => "External issuer name",
+            'issuerurl' => "http://issuer-url.domain.co.nz",
+            'issueremail' => "issuer@example.com",
+            'claimid' => "Claim ID",
+            'claimcomment' => "Claim comment",
+            'dateissued' => time()
+        ], $params);
+        $record->id = $DB->insert_record('badge_endorsement', $record);
+
+        return $record;
+    }
+
     /**
      * Create a backpack.
      *
index b6f51b8..79a0b78 100644 (file)
@@ -49,8 +49,8 @@ $string['card'] = 'Card';
 $string['cards'] = 'Cards';
 $string['courseprogress'] = 'Course progress:';
 $string['completepercent'] = '{$a}% complete';
-$string['displaycategories'] = 'Display Categories';
-$string['displaycategories_help'] = 'Display the Course Category on dashboard course items including cards, list items and summary items';
+$string['displaycategories'] = 'Display categories';
+$string['displaycategories_help'] = 'Display the course category on dashboard course items including cards, list items and summary items.';
 $string['favourites'] = 'Starred';
 $string['future'] = 'Future';
 $string['inprogress'] = 'In progress';
index 89c95e6..94e10ac 100644 (file)
@@ -27,7 +27,7 @@
 $string['everything'] = 'Everything';
 $string['courses'] = 'Categories and courses';
 $string['coursestructures'] = 'Categories, courses, and course structures';
-$string['courseactivities'] = 'Categories, courses, and course Activities';
+$string['courseactivities'] = 'Categories, courses, and course activities';
 $string['enabledockdesc'] = 'Allow the user to dock this block';
 $string['expansionlimit'] = 'Generate navigation for the following';
 $string['linkcategoriesdesc'] = 'Display categories as links';
index 12bc80b..6ef434f 100644 (file)
@@ -608,49 +608,44 @@ class calendar_event {
             $updaterepeated = (!empty($this->properties->repeatid) && !empty($this->properties->repeateditall));
 
             if ($updaterepeated) {
-                // Update all.
+
+                $sqlset = 'name = ?,
+                           description = ?,
+                           timeduration = ?,
+                           timemodified = ?,
+                           groupid = ?,
+                           courseid = ?';
+
+                // Note: Group and course id may not be set. If not, keep their current values.
+                $params = [
+                    $this->properties->name,
+                    $this->properties->description,
+                    $this->properties->timeduration,
+                    time(),
+                    isset($this->properties->groupid) ? $this->properties->groupid : $event->groupid,
+                    isset($this->properties->courseid) ? $this->properties->courseid : $event->courseid,
+                ];
+
+                // Note: Only update start date, if it was changed by the user.
                 if ($this->properties->timestart != $event->timestart) {
                     $timestartoffset = $this->properties->timestart - $event->timestart;
-                    $sql = "UPDATE {event}
-                               SET name = ?,
-                                   description = ?,
-                                   timestart = timestart + ?,
-                                   timeduration = ?,
-                                   timemodified = ?,
-                                   groupid = ?,
-                                   courseid = ?
-                             WHERE repeatid = ?";
-                    // Note: Group and course id may not be set. If not, keep their current values.
-                    $params = [
-                        $this->properties->name,
-                        $this->properties->description,
-                        $timestartoffset,
-                        $this->properties->timeduration,
-                        time(),
-                        isset($this->properties->groupid) ? $this->properties->groupid : $event->groupid,
-                        isset($this->properties->courseid) ? $this->properties->courseid : $event->courseid,
-                        $event->repeatid
-                    ];
-                } else {
-                    $sql = "UPDATE {event}
-                               SET name = ?,
-                                   description = ?,
-                                   timeduration = ?,
-                                   timemodified = ?,
-                                   groupid = ?,
-                                   courseid = ?
-                            WHERE repeatid = ?";
-                    // Note: Group and course id may not be set. If not, keep their current values.
-                    $params = [
-                        $this->properties->name,
-                        $this->properties->description,
-                        $this->properties->timeduration,
-                        time(),
-                        isset($this->properties->groupid) ? $this->properties->groupid : $event->groupid,
-                        isset($this->properties->courseid) ? $this->properties->courseid : $event->courseid,
-                        $event->repeatid
-                    ];
+                    $sqlset .= ', timestart = timestart + ?';
+                    $params[] = $timestartoffset;
+                }
+
+                // Note: Only update location, if it was changed by the user.
+                $updatelocation = (!empty($this->properties->location) && $this->properties->location !== $event->location);
+                if ($updatelocation) {
+                    $sqlset .= ', location = ?';
+                    $params[] = $this->properties->location;
                 }
+
+                // Update all.
+                $sql = "UPDATE {event}
+                           SET $sqlset
+                         WHERE repeatid = ?";
+
+                $params[] = $event->repeatid;
                 $DB->execute($sql, $params);
 
                 // Trigger an update event for each of the calendar event.
index bec45a2..baa3977 100644 (file)
@@ -77,7 +77,9 @@ define('COMPLETION_CRITERIA_TYPE_ROLE',         7);
 define('COMPLETION_CRITERIA_TYPE_COURSE',       8);
 
 /**
- * Criteria type constant to class name mapping
+ * Criteria type constant to class name mapping.
+ *
+ * This global variable would be improved if it was implemented as a cache.
  */
 global $COMPLETION_CRITERIA_TYPES;
 $COMPLETION_CRITERIA_TYPES = array(
diff --git a/completion/cron.php b/completion/cron.php
deleted file mode 100644 (file)
index f56e5dd..0000000
+++ /dev/null
@@ -1,393 +0,0 @@
-<?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/>.
-
-/**
- * Code used by scheduled tasks for reviewing and aggregating course completion criteria.
- *
- * @package core_completion
- * @category completion
- * @copyright 2009 Catalyst IT Ltd
- * @author Aaron Barnes <aaronb@catalyst.net.nz>
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-require_once($CFG->libdir.'/completionlib.php');
-
-/**
- * Mark users as started if the config option is set
- *
- * @return void
- */
-function completion_cron_mark_started() {
-    global $CFG, $DB;
-
-    if (debugging()) {
-        mtrace('Marking users as started');
-    }
-
-    if (!empty($CFG->gradebookroles)) {
-        $roles = ' AND ra.roleid IN ('.$CFG->gradebookroles.')';
-    } else {
-        // This causes it to default to everyone (if there is no student role)
-        $roles = '';
-    }
-
-    /**
-     * A quick explaination of this horrible looking query
-     *
-     * It's purpose is to locate all the active participants
-     * of a course with course completion enabled.
-     *
-     * We also only want the users with no course_completions
-     * record as this functions job is to create the missing
-     * ones :)
-     *
-     * We want to record the user's enrolment start time for the
-     * course. This gets tricky because there can be multiple
-     * enrolment plugins active in a course, hence the possibility
-     * of multiple records for each couse/user in the results
-     */
-    $sql = "
-        SELECT
-            c.id AS course,
-            u.id AS userid,
-            crc.id AS completionid,
-            ue.timestart AS timeenrolled,
-            ue.timecreated
-        FROM
-            {user} u
-        INNER JOIN
-            {user_enrolments} ue
-         ON ue.userid = u.id
-        INNER JOIN
-            {enrol} e
-         ON e.id = ue.enrolid
-        INNER JOIN
-            {course} c
-         ON c.id = e.courseid
-        INNER JOIN
-            {role_assignments} ra
-         ON ra.userid = u.id
-        LEFT JOIN
-            {course_completions} crc
-         ON crc.course = c.id
-        AND crc.userid = u.id
-        WHERE
-            c.enablecompletion = 1
-        AND crc.timeenrolled IS NULL
-        AND ue.status = 0
-        AND e.status = 0
-        AND u.deleted = 0
-        AND ue.timestart < ?
-        AND (ue.timeend > ? OR ue.timeend = 0)
-            $roles
-        ORDER BY
-            course,
-            userid
-    ";
-
-    $now = time();
-    $rs = $DB->get_recordset_sql($sql, array($now, $now, $now, $now));
-
-    // Check if result is empty
-    if (!$rs->valid()) {
-        $rs->close(); // Not going to iterate (but exit), close rs
-        return;
-    }
-
-    /**
-     * An explaination of the following loop
-     *
-     * We are essentially doing a group by in the code here (as I can't find
-     * a decent way of doing it in the sql).
-     *
-     * Since there can be multiple enrolment plugins for each course, we can have
-     * multiple rows for each particpant in the query result. This isn't really
-     * a problem until you combine it with the fact that the enrolment plugins
-     * can save the enrol start time in either timestart or timeenrolled.
-     *
-     * The purpose of this loop is to find the earliest enrolment start time for
-     * each participant in each course.
-     */
-    $prev = null;
-    while ($rs->valid() || $prev) {
-
-        $current = $rs->current();
-
-        if (!isset($current->course)) {
-            $current = false;
-        }
-        else {
-            // Not all enrol plugins fill out timestart correctly, so use whichever
-            // is non-zero
-            $current->timeenrolled = max($current->timecreated, $current->timeenrolled);
-        }
-
-        // If we are at the last record,
-        // or we aren't at the first and the record is for a diff user/course
-        if ($prev &&
-            (!$rs->valid() ||
-            ($current->course != $prev->course || $current->userid != $prev->userid))) {
-
-            $completion = new completion_completion();
-            $completion->userid = $prev->userid;
-            $completion->course = $prev->course;
-            $completion->timeenrolled = (string) $prev->timeenrolled;
-            $completion->timestarted = 0;
-            $completion->reaggregate = time();
-
-            if ($prev->completionid) {
-                $completion->id = $prev->completionid;
-            }
-
-            $completion->mark_enrolled();
-
-            if (debugging()) {
-                mtrace('Marked started user '.$prev->userid.' in course '.$prev->course);
-            }
-        }
-        // Else, if this record is for the same user/course
-        elseif ($prev && $current) {
-            // Use oldest timeenrolled
-            $current->timeenrolled = min($current->timeenrolled, $prev->timeenrolled);
-        }
-
-        // Move current record to previous
-        $prev = $current;
-
-        // Move to next record
-        $rs->next();
-    }
-
-    $rs->close();
-}
-
-/**
- * Run installed criteria's data aggregation methods
- *
- * Loop through each installed criteria and run the
- * cron() method if it exists
- *
- * @return void
- */
-function completion_cron_criteria() {
-
-    // Process each criteria type
-    global $CFG, $COMPLETION_CRITERIA_TYPES;
-
-    foreach ($COMPLETION_CRITERIA_TYPES as $type) {
-
-        $object = 'completion_criteria_'.$type;
-        require_once $CFG->dirroot.'/completion/criteria/'.$object.'.php';
-
-        $class = new $object();
-
-        // Run the criteria type's cron method, if it has one
-        if (method_exists($class, 'cron')) {
-
-            if (debugging()) {
-                mtrace('Running '.$object.'->cron()');
-            }
-            $class->cron();
-        }
-    }
-}
-
-/**
- * Aggregate each user's criteria completions
- */
-function completion_cron_completions() {
-    global $DB;
-
-    if (debugging()) {
-        mtrace('Aggregating completions');
-    }
-
-    // Save time started
-    $timestarted = time();
-
-    // Grab all criteria and their associated criteria completions
-    $sql = '
-        SELECT DISTINCT
-            c.id AS course,
-            cr.id AS criteriaid,
-            crc.userid AS userid,
-            cr.criteriatype AS criteriatype,
-            cc.timecompleted AS timecompleted
-        FROM
-            {course_completion_criteria} cr
-        INNER JOIN
-            {course} c
-         ON cr.course = c.id
-        INNER JOIN
-            {course_completions} crc
-         ON crc.course = c.id
-        LEFT JOIN
-            {course_completion_crit_compl} cc
-         ON cc.criteriaid = cr.id
-        AND crc.userid = cc.userid
-        WHERE
-            c.enablecompletion = 1
-        AND crc.timecompleted IS NULL
-        AND crc.reaggregate > 0
-        AND crc.reaggregate < :timestarted
-        ORDER BY
-            course,
-            userid
-    ';
-
-    $rs = $DB->get_recordset_sql($sql, array('timestarted' => $timestarted));
-
-    // Check if result is empty
-    if (!$rs->valid()) {
-        $rs->close(); // Not going to iterate (but exit), close rs
-        return;
-    }
-
-    $current_user = null;
-    $current_course = null;
-    $completions = array();
-
-    while (1) {
-
-        // Grab records for current user/course
-        foreach ($rs as $record) {
-            // If we are still grabbing the same users completions
-            if ($record->userid === $current_user && $record->course === $current_course) {
-                $completions[$record->criteriaid] = $record;
-            } else {
-                break;
-            }
-        }
-
-        // Aggregate
-        if (!empty($completions)) {
-
-            if (debugging()) {
-                mtrace('Aggregating completions for user '.$current_user.' in course '.$current_course);
-            }
-
-            // Get course info object
-            $info = new completion_info((object)array('id' => $current_course));
-
-            // Setup aggregation
-            $overall = $info->get_aggregation_method();
-            $activity = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ACTIVITY);
-            $prerequisite = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_COURSE);
-            $role = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ROLE);
-
-            $overall_status = null;
-            $activity_status = null;
-            $prerequisite_status = null;
-            $role_status = null;
-
-            // Get latest timecompleted
-            $timecompleted = null;
-
-            // Check each of the criteria
-            foreach ($completions as $params) {
-                $timecompleted = max($timecompleted, $params->timecompleted);
-
-                $completion = new completion_criteria_completion((array)$params, false);
-
-                // Handle aggregation special cases
-                if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
-                    completion_cron_aggregate($activity, $completion->is_complete(), $activity_status);
-                } else if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_COURSE) {
-                    completion_cron_aggregate($prerequisite, $completion->is_complete(), $prerequisite_status);
-                } else if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_ROLE) {
-                    completion_cron_aggregate($role, $completion->is_complete(), $role_status);
-                } else {
-                    completion_cron_aggregate($overall, $completion->is_complete(), $overall_status);
-                }
-            }
-
-            // Include role criteria aggregation in overall aggregation
-            if ($role_status !== null) {
-                completion_cron_aggregate($overall, $role_status, $overall_status);
-            }
-
-            // Include activity criteria aggregation in overall aggregation
-            if ($activity_status !== null) {
-                completion_cron_aggregate($overall, $activity_status, $overall_status);
-            }
-
-            // Include prerequisite criteria aggregation in overall aggregation
-            if ($prerequisite_status !== null) {
-                completion_cron_aggregate($overall, $prerequisite_status, $overall_status);
-            }
-
-            // If aggregation status is true, mark course complete for user
-            if ($overall_status) {
-                if (debugging()) {
-                    mtrace('Marking complete');
-                }
-
-                $ccompletion = new completion_completion(array('course' => $params->course, 'userid' => $params->userid));
-                $ccompletion->mark_complete($timecompleted);
-            }
-        }
-
-        // If this is the end of the recordset, break the loop
-        if (!$rs->valid()) {
-            $rs->close();
-            break;
-        }
-
-        // New/next user, update user details, reset completions
-        $current_user = $record->userid;
-        $current_course = $record->course;
-        $completions = array();
-        $completions[$record->criteriaid] = $record;
-    }
-
-    // Mark all users as aggregated
-    $sql = "
-        UPDATE
-            {course_completions}
-        SET
-            reaggregate = 0
-        WHERE
-            reaggregate < :timestarted
-        AND reaggregate > 0
-    ";
-
-    $DB->execute($sql, array('timestarted' => $timestarted));
-}
-
-/**
- * Aggregate criteria status's as per configured aggregation method
- *
- * @param int $method COMPLETION_AGGREGATION_* constant
- * @param bool $data Criteria completion status
- * @param bool|null $state Aggregation state
- */
-function completion_cron_aggregate($method, $data, &$state) {
-    if ($method == COMPLETION_AGGREGATION_ALL) {
-        if ($data && $state !== false) {
-            $state = true;
-        } else {
-            $state = false;
-        }
-    } elseif ($method == COMPLETION_AGGREGATION_ANY) {
-        if ($data) {
-            $state = true;
-        } else if (!$data && $state === null) {
-            $state = false;
-        }
-    }
-}
index 39c2c5e..1a96fbf 100644 (file)
@@ -764,7 +764,7 @@ class helper {
      */
     public static function get_management_viewmodes() {
         return array(
-            'combined' => new \lang_string('categoriesandcoures'),
+            'combined' => new \lang_string('categoriesandcourses'),
             'categories' => new \lang_string('categories'),
             'courses' => new \lang_string('courses')
         );
index 7f2b057..3546584 100644 (file)
@@ -83,7 +83,7 @@ class behat_course extends behat_base {
 
         // Select Miscellaneous category.
         $this->i_click_on_category_in_the_management_interface(get_string('miscellaneous'));
-        $this->execute("behat_course::i_should_see_the_courses_management_page", get_string('categoriesandcoures'));
+        $this->execute("behat_course::i_should_see_the_courses_management_page", get_string('categoriesandcourses'));
 
         // Click create new course.
         $this->execute('behat_general::i_click_on_in_the',
index a3c73b6..45d55dd 100644 (file)
@@ -23,7 +23,7 @@ Feature: Fields locked control where they are displayed
     When I log in as "admin"
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field |
       | Short name | testfield  |
index e8650a3..802f6fa 100644 (file)
@@ -23,7 +23,7 @@ Feature: The visibility of fields control where they are displayed
     When I log in as "admin"
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field |
       | Short name | testfield  |
@@ -43,7 +43,7 @@ Feature: The visibility of fields control where they are displayed
     When I log in as "admin"
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field  |
       | Short name | testfield   |
@@ -63,7 +63,7 @@ Feature: The visibility of fields control where they are displayed
     When I log in as "admin"
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field     |
       | Short name | testfield      |
index 74a41d9..cffdc85 100644 (file)
@@ -27,4 +27,4 @@ $string['checkedbydefault'] = 'Checked by default';
 $string['errorconfigunique'] = 'The checkbox field cannot be defined as unique.';
 $string['pluginname'] = 'Checkbox';
 $string['privacy:metadata'] = 'The Checkbox field type plugin doesn\'t store any personal data; it uses tables defined in core.';
-$string['specificsettings'] = 'Settings for the checkbox field';
+$string['specificsettings'] = 'Checkbox field settings';
index fd5b3cc..d23c2e2 100644 (file)
@@ -32,4 +32,4 @@ $string['mindate'] = 'Minimum value';
 $string['mindateaftermax'] = 'The minimum value cannot be bigger than the maximum value.';
 $string['pluginname'] = 'Date and time';
 $string['privacy:metadata'] = 'The Date and time field type plugin doesn\'t store any personal data; it uses tables defined in core.';
-$string['specificsettings'] = 'Settings for the date and time field';
+$string['specificsettings'] = 'Date and time field settings';
index 81dc118..52f9809 100644 (file)
@@ -30,4 +30,4 @@ $string['invalidoption'] = 'Invalid option selected';
 $string['menuoptions'] = 'Menu options (one per line)';
 $string['pluginname'] = 'Dropdown menu';
 $string['privacy:metadata'] = 'The Dropdown menu field type plugin doesn\'t store any personal data; it uses tables defined in core.';
-$string['specificsettings'] = 'Settings for the dropdown menu field';
+$string['specificsettings'] = 'Dropdown menu field settings';
index c9e47a7..de273bf 100644 (file)
@@ -28,17 +28,17 @@ $string['displaysize'] = 'Form input size';
 $string['errorconfigdisplaysize'] = 'The form input size must be between 1 and 200 characters.';
 $string['errorconfiglinkplaceholder'] = 'The link must contain a placeholder $$.';
 $string['errorconfiglinksyntax'] = 'The link must be a valid URL starting with either http:// or https://.';
-$string['errorconfigmaxlen'] = 'The maximum length must be between 1 and 1333.';
-$string['errormaxlength'] = 'This field maximum length is {$a}.';
+$string['errorconfigmaxlen'] = 'The maximum number of characters allowed must be between 1 and 1333.';
+$string['errormaxlength'] = 'The maximum number of characters allowed in this field is {$a}.';
 $string['islink'] = 'Link field';
 $string['islink_help'] = 'To transform the text into a link, enter a URL containing $$ as a placeholder, where $$ will be replaced with the text. For example, to transform a Twitter ID to a link, enter http://twitter.com/$$.';
 $string['ispassword'] = 'Password field';
 $string['linktarget'] = 'Link target';
-$string['maxlength'] = 'Maximum length';
+$string['maxlength'] = 'Maximum number of characters';
 $string['newwindow'] = 'New window';
 $string['none'] = 'None';
-$string['pluginname'] = 'Text field';
-$string['privacy:metadata'] = 'The Text field field type plugin doesn\'t store any personal data; it uses tables defined in core.';
+$string['pluginname'] = 'Short text';
+$string['privacy:metadata'] = 'The Short text field type plugin doesn\'t store any personal data; it uses tables defined in core.';
 $string['sameframe'] = 'Same frame';
 $string['samewindow'] = 'Same window';
-$string['specificsettings'] = 'Settings for the text field';
+$string['specificsettings'] = 'Short text field settings';
index e1bed82..da4ad41 100644 (file)
@@ -13,7 +13,7 @@ Feature: Managers can manage course custom fields text
 
   Scenario: Create a custom course text field
     When I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field |
       | Short name | testfield  |
@@ -23,7 +23,7 @@ Feature: Managers can manage course custom fields text
 
   Scenario: Edit a custom course text field
     When I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field |
       | Short name | testfield  |
@@ -40,7 +40,7 @@ Feature: Managers can manage course custom fields text
   @javascript
   Scenario: Delete a custom course text field
     When I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field |
       | Short name | testfield  |
@@ -64,7 +64,7 @@ Feature: Managers can manage course custom fields text
       | teacher1 | C1     | editingteacher |
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | See more on website       |
       | Short name | testfield                 |
@@ -94,11 +94,11 @@ Feature: Managers can manage course custom fields text
       | teacher1 | C1     | editingteacher |
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field |
       | Short name | testfield  |
-      | Maximum length | 3          |
+      | Maximum number of characters | 3          |
     And I press "Save changes"
     And I log out
     Then I log in as "teacher1"
@@ -107,7 +107,7 @@ Feature: Managers can manage course custom fields text
     And I set the following fields to these values:
       | Test field | 1234 |
     And I press "Save and display"
-    Then I should see "This field maximum length is 3"
+    Then I should see "The maximum number of characters allowed in this field is 3."
 
   Scenario: A text field with a default value must be shown on listing but allow empty values that will not be shown
     Given the following "users" exist:
@@ -121,7 +121,7 @@ Feature: Managers can manage course custom fields text
       | teacher1 | C1     | editingteacher |
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name          | Test field  |
       | Short name    | testfield   |
index 7149604..ca601f4 100644 (file)
@@ -26,4 +26,4 @@ defined('MOODLE_INTERNAL') || die();
 
 $string['pluginname'] = 'Text area';
 $string['privacy:metadata'] = 'The Text area field type plugin doesn\'t store any personal data; it uses tables defined in core.';
-$string['specificsettings'] = 'Settings for the text area field';
+$string['specificsettings'] = 'Text area field settings';
index 247f5ce..90bfacd 100644 (file)
@@ -101,7 +101,7 @@ Feature: Teachers can edit course custom fields
     When I log in as "admin"
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field |
     And I press "Save changes"
index d0aea96..56c0f23 100644 (file)
@@ -22,7 +22,7 @@ Feature: Requiredness The course custom fields can be mandatory or not
     When I log in as "admin"
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field |
       | Short name | testfield  |
@@ -43,7 +43,7 @@ Feature: Requiredness The course custom fields can be mandatory or not
     When I log in as "admin"
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name       | Test field |
       | Short name | testfield  |
index 8fe7063..e834eaa 100644 (file)
@@ -22,7 +22,7 @@ Feature: Uniqueness The course custom fields can be mandatory or not
     When I log in as "admin"
     And I navigate to "Courses > Course custom fields" in site administration
     And I click on "Add a new custom field" "link"
-    And I click on "Text field" "link"
+    And I click on "Short text" "link"
     And I set the following fields to these values:
       | Name        | Test field |
       | Short name  | testfield  |
index 9bf0182..bb88d11 100644 (file)
@@ -353,7 +353,8 @@ class course_enrolment_table extends html_table implements renderable {
      * @var array
      */
     protected static $sortablefields = array('firstname', 'lastname', 'firstnamephonetic', 'lastnamephonetic', 'middlename',
-            'alternatename', 'idnumber', 'email', 'phone1', 'phone2', 'institution', 'department', 'lastaccess', 'lastcourseaccess' );
+            'alternatename', 'username', 'idnumber', 'email', 'phone1', 'phone2',
+            'institution', 'department', 'lastaccess', 'lastcourseaccess');
 
     /**
      * Constructs the table
index 799e827..5973857 100644 (file)
@@ -58,7 +58,7 @@ trait gradingform_legacy_polyfill {
      * This method is used to export any user data this sub-plugin has using the object to get the context and userid.
      *
      * @deprecated Since Moodle 3.6 MDL-62535 Please use the methods in the gradingform_provider_v2 interface.
-     * @todo MDL-63137 remove this method.
+     * @todo MDL-63167 remove this method.
      *
      * @param context $context Context owner of the data.
      * @param stdClass $definition Grading definition entry to export.
@@ -75,7 +75,7 @@ trait gradingform_legacy_polyfill {
      * Any call to this method should delete all user data for the context defined.
      *
      * @deprecated Since Moodle 3.6 MDL-62535 Please use the methods in the gradingform_provider_v2 interface.
-     * @todo MDL-63137 remove this method.
+     * @todo MDL-63167 remove this method.
      *
      * @param context $context Context owner of the data.
      */
@@ -88,7 +88,7 @@ trait gradingform_legacy_polyfill {
      * A call to this method should delete user data (where practicle) from the userid and context.
      *
      * @deprecated Since Moodle 3.6 MDL-62535 Please use the methods in the gradingform_provider_v2 interface.
-     * @todo MDL-63137 remove this method.
+     * @todo MDL-63167 remove this method.
      *
      * @param int $userid The user whose information is to be deleted.
      * @param context $context Context owner of the data.
index 241224a..63465c8 100644 (file)
@@ -174,7 +174,7 @@ class test_legacy_polyfill_gradingform_provider implements
      * This method is used to export any user data this sub-plugin has using the object to get the context and userid.
      *
      * @deprecated Since Moodle 3.6 MDL-62535 Please use the methods in the gradingform_provider_v2 interface.
-     * @todo MDL-63137 remove this method.
+     * @todo MDL-63167 remove this method.
      *
      * @param context $context Context owner of the data.
      * @param stdClass $definition Grading definition entry to export.
@@ -190,7 +190,7 @@ class test_legacy_polyfill_gradingform_provider implements
      * Any call to this method should delete all user data for the context defined.
      *
      * @deprecated Since Moodle 3.6 MDL-62535 Please use the methods in the gradingform_provider_v2 interface.
-     * @todo MDL-63137 remove this method.
+     * @todo MDL-63167 remove this method.
      *
      * @param context $context Context owner of the data.
      */
@@ -202,7 +202,7 @@ class test_legacy_polyfill_gradingform_provider implements
      * A call to this method should delete user data (where practicle) from the userid and context.
      *
      * @deprecated Since Moodle 3.6 MDL-62535 Please use the methods in the gradingform_provider_v2 interface.
-     * @todo MDL-63137 remove this method.
+     * @todo MDL-63167 remove this method.
      *
      * @param int $userid The user whose information is to be deleted.
      * @param context $context Context owner of the data.
index 516c5fc..4ed05d1 100644 (file)
@@ -22,7 +22,7 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['direct:view'] = 'Import grades from CSV';
+$string['direct:view'] = 'Import grades from spreadsheet';
 $string['pluginname'] = 'Paste from spreadsheet';
 $string['privacy:metadata'] = 'The import grades from copied spreadsheed plugin does not store any personal data.';
 $string['userdata'] = 'Help copying data into this form.';
diff --git a/install/lang/it_wp/langconfig.php b/install/lang/it_wp/langconfig.php
new file mode 100644 (file)
index 0000000..7592849
--- /dev/null
@@ -0,0 +1,34 @@
+<?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/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['parentlanguage'] = 'it';
+$string['thislanguage'] = 'Italiano per Workplace';
diff --git a/install/lang/no_wp/langconfig.php b/install/lang/no_wp/langconfig.php
new file mode 100644 (file)
index 0000000..4b5771a
--- /dev/null
@@ -0,0 +1,34 @@
+<?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/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['parentlanguage'] = 'no';
+$string['thislanguage'] = 'Norsk Workplace';
index 8a6fa41..4ff4110 100644 (file)
@@ -49,7 +49,7 @@ $string['environmentsub2'] = 'Cada nova versão do Moodle tem pré-requisitos m
 $string['errorsinenvironment'] = 'A verificação do sistema falhou!';
 $string['installation'] = 'Instalação';
 $string['langdownloaderror'] = 'Não foi possível descarregar o idioma <b>{$a}</b> . O processo de instalação continuará em Inglês.';
-$string['memorylimithelp'] = '<p>O limite de memória para o PHP definido atualmente no servidor é <b>{$a}</b>.</p><p>Um número elevado de módulos em utilização ou de utilizadores registados pode fazer com que o Moodle apresente problemas de falta de memória.</p><p>É recomendado que o PHP seja configurado com um limite de memória de pelo menos 40MB. Esta configuração pode ser definida de diversas formas:</p><ol><li>Compilação do PHP com o parâmetro <b>--enable-memory-limit</b>. Esta definição permitirá ao próprio Moodle definir o valor a utilizar.</li><li>Alteração do parâmetro <b>memory_limit</b> no ficheiro de configuração do PHP para um valor igual ou superior a 40MB.</li><li>Criação de um ficheiro <b>.htaccess</b> na raiz da pasta do Moodle com a linha <b>php_value memory_limit 40M</b><p>ATENÇÃO: Em alguns servidores esta configuração impedirá o funcionamento de <b>todas</b> as páginas PHP. Nestes casos, não poderá ser utilizado o ficheiro <b>.htaccess</b>.</p></li></ol>';
+$string['memorylimithelp'] = '<p>O limite de memória para o PHP definido atualmente no servidor é <b>{$a}</b>.</p><p>Um número elevado de módulos em utilização ou de utilizadores registados pode fazer com que o Moodle apresente problemas de falta de memória.</p><p>É recomendado que o PHP seja configurado com um limite de memória de pelo menos 40MB. Esta configuração pode ser definida de diversas formas:</p><ol><li>Compilação do PHP com o parâmetro <b>--enable-memory-limit</b>. Esta definição permitirá ao próprio Moodle definir o valor a utilizar.</li><li>Alteração do parâmetro <b>memory_limit</b> no ficheiro de configuração do PHP para um valor igual ou superior a 40MB.</li><li>Criação de um ficheiro <b>.htaccess</b> na raiz da pasta do Moodle com a linha <b>php_value memory_limit 40M</b><p><strong>Atenção</strong>: Em alguns servidores esta configuração impedirá o funcionamento de <b>todas</b> as páginas PHP. Nestes casos, não poderá ser utilizado o ficheiro <b>.htaccess</b>.</p></li></ol>';
 $string['paths'] = 'Caminhos';
 $string['pathserrcreatedataroot'] = 'O programa de instalação não conseguiu criar a pasta de dados <b>{$a->dataroot}</b>.';
 $string['pathshead'] = 'Confirmar caminhos';
index 9eb6d21..6fe3616 100644 (file)
@@ -28,7 +28,7 @@ $string['accesskey'] = 'Access key, {$a}';
 $string['accessstatement'] = 'Accessibility statement';
 $string['activitynext'] = 'Next activity';
 $string['activityprev'] = 'Previous activity';
-$string['breadcrumb'] = 'Breadcrumb trail';
+$string['breadcrumb'] = 'Navigation bar';
 $string['hideblocka'] = 'Hide {$a} block';
 $string['showblocka'] = 'Show {$a} block';
 $string['sitemap'] = 'Site map';
index 9b43f46..c475f96 100644 (file)
@@ -1111,7 +1111,7 @@ $string['sessioncookiedomain'] = 'Cookie domain';
 $string['sessioncookiepath'] = 'Cookie path';
 $string['sessionhandling'] = 'Session handling';
 $string['sessiontimeout'] = 'Timeout';
-$string['settingdependenton'] = 'This setting may be hidden, based on the value of <strong>{$a}</strong>';
+$string['settingdependenton'] = 'This setting may be hidden, based on the value of <strong>{$a}</strong>.';
 $string['settingfileuploads'] = 'File uploading is required for normal operation, please enable it in PHP configuration.';
 $string['settingmemorylimit'] = 'Insufficient memory detected, please set higher memory limit in PHP settings.';
 $string['settingsafemode'] = 'Moodle is not fully compatible with safe mode, please ask server administrator to turn it off. Running Moodle under safe mode is not supported, please expect various problems if you do so.';
@@ -1237,7 +1237,7 @@ $string['taskfiletrashcleanup'] = 'Cleanup files in trash';
 $string['taskglobalsearchindex'] = 'Global search indexing';
 $string['taskglobalsearchoptimize'] = 'Global search index optimization';
 $string['taskgradecron'] = 'Background processing for gradebook';
-$string['taskgradehistorycleanup'] = 'Background processing for clean grade history tables';
+$string['taskgradehistorycleanup'] = 'Background processing for cleaning grade history tables';
 $string['tasklegacycron'] = 'Legacy cron processing for plugins';
 $string['tasklogcleanup'] = 'Cleanup of task logs';
 $string['tasklogs'] = 'Task logs';
@@ -1246,8 +1246,8 @@ $string['taskpasswordresetcleanup'] = 'Cleanup password reset attempts';
 $string['taskplagiarismcron'] = 'Background processing for legacy cron in plagiarism plugins';
 $string['taskportfoliocron'] = 'Background processing for portfolio plugins';
 $string['taskprocessing'] = 'Task processing';
-$string['taskquestioncron'] = 'Background processing for cleaning up the old question previews';
-$string['taskquestionstatscleanupcron'] = 'Background processing for cleaning up the old question statistics cache';
+$string['taskquestioncron'] = 'Background processing for cleaning up question previews';
+$string['taskquestionstatscleanupcron'] = 'Background processing for cleaning up question statistics caches';
 $string['taskrefreshsystemtokens'] = 'Refresh OAuth tokens for service accounts';
 $string['taskregistrationcron'] = 'Site registration';
 $string['tasksendfailedloginnotifications'] = 'Send failed login notifications';
@@ -1258,11 +1258,11 @@ $string['tasktagcron'] = 'Background processing for tags';
 $string['tasktempfilecleanup'] = 'Delete stale temp files';
 $string['tempdatafoldercleanup'] = 'Clean up temporary data files older than';
 $string['testoutgoingmailconf'] = 'Test outgoing mail configuration';
-$string['testoutgoingmaildetail'] = 'Before testing you have to save the configuration.<br />{$a}';
-$string['testoutgoingmailconf_message'] = 'This is a test message. Please disregard. If you received this email, it means that you have successfully configured your Moodle site\'s email settings.';
-$string['testoutgoingmailconf_errorcommunications'] = 'Moodle could not communicate with your mail server. Start by checking your Moodle Outgoing mail configuration.';
+$string['testoutgoingmaildetail'] = 'Note: Before testing, please save your configuration.<br />{$a}';
+$string['testoutgoingmailconf_message'] = 'This is a test message to confirm that you have successfully configured your site\'s outgoing mail.';
+$string['testoutgoingmailconf_errorcommunications'] = 'Your site couldn\'t communicate with your mail server. Please check your outgoing mail configuration.';
 $string['testoutgoingmailconf_sendtest'] = 'Send a test message';
-$string['testoutgoingmailconf_sentmail'] = 'Moodle successfully delivered the test message to the mail server.<br />From: {$a->fromemail}<br />To: {$a->toemail}';
+$string['testoutgoingmailconf_sentmail'] = 'This site has successfully sent a test message to the mail server.<br />From: {$a->fromemail}<br />To: {$a->toemail}';
 $string['testoutgoingmailconf_subject'] = '{$a}: test message';
 $string['testoutgoingmailconf_toemail'] = 'To email address';
 $string['themedesignermode'] = 'Theme designer mode';
index 47d14d5..b0f5244 100644 (file)
@@ -29,6 +29,7 @@ $string['analytics'] = 'Analytics';
 $string['analyticslogstore'] = 'Log store used for analytics';
 $string['analyticslogstore_help'] = 'The log store that will be used by the analytics API to read users\' activity';
 $string['analyticssettings'] = 'Analytics settings';
+$string['analyticssiteinfo'] = 'Site information';
 $string['defaulttimesplittingmethods'] = 'Default time-splitting methods for model\'s evaluation';
 $string['defaulttimesplittingmethods_help'] = 'The time-splitting method divides the course duration into parts; the predictions engine will run at the end of these parts. The model evaluation process will iterate through these time-splitting methods unless a specific time-splitting method is specified (the ability to specify a time-splitting method is only available when evaluating models using the command line script).';
 $string['defaultpredictionsprocessor'] = 'Default predictions processor';
@@ -38,15 +39,15 @@ $string['erroralreadypredict'] = 'File {$a} has already been used to generate pr
 $string['errorcannotreaddataset'] = 'Dataset file {$a} can not be read';
 $string['errorcannotwritedataset'] = 'Dataset file {$a} cannot be written';
 $string['errorexportmodelresult'] = 'The machine learning model can not be exported.';
-$string['errorimport'] = 'Error importing the provided json file.';
-$string['errorimportmissingcomponents'] = 'The provided model requires the following plugins to be installed: {$a}. Note that the versions do not necessarily need to match with the versions installed in your system. To install the same or a newer version of the plugin should be enough in most cases.';
-$string['errorimportversionmismatches'] = 'The version of the following components differ from the version installed in this site: {$a}. You can use "Ignore version mismatches" option to ignore these differences.';
+$string['errorimport'] = 'Error importing the provided JSON file.';
+$string['errorimportmissingcomponents'] = 'The provided model requires the following plugins to be installed: {$a}. Note that the versions do not necessarily need to match with the versions installed on your site. Installing the same or a newer version of the plugin should be fine in most cases.';
+$string['errorimportversionmismatches'] = 'The version of the following components differs from the version installed on this site: {$a}. You can use the option \'Ignore version mismatches\' to ignore these differences.';
 $string['errorimportmissingclasses'] = 'The following analytics components are not available in this site: {$a->missingclasses}. ';
 $string['errorinvalidindicator'] = 'Invalid {$a} indicator';
 $string['errorinvalidtarget'] = 'Invalid {$a} target';
-$string['errorinvalidtimesplitting'] = 'Invalid time splitting; please ensure you add the class fully qualified class name.';
+$string['errorinvalidtimesplitting'] = 'Invalid time splitting; please ensure you add the fully qualified class name.';
 $string['errornoexportconfig'] = 'There was a problem exporting the model configuration.';
-$string['errornoexportconfigrequirements'] = 'Only non static models with timeplitting methods can be exported.';
+$string['errornoexportconfigrequirements'] = 'Only non-static models with time-splitting methods can be exported.';
 $string['errornoindicators'] = 'This model does not have any indicators.';
 $string['errornopredictresults'] = 'No results returned from the predictions processor. Check the output directory contents for more information.';
 $string['errornotimesplittings'] = 'This model does not have any time-splitting method.';
@@ -71,7 +72,21 @@ $string['insightinfomessagehtml'] = 'The system generated an insight for you.';
 $string['insightinfomessageaction'] = '{$a->text}: {$a->url}';
 $string['invalidtimesplitting'] = 'Model with ID {$a} needs a time-splitting method before it can be used to train.';
 $string['invalidanalysablefortimesplitting'] = 'It cannot be analysed using {$a} time-splitting method.';
+$string['levelinstitution'] = 'Level of education';
+$string['levelinstitutionisced0'] = 'Early childhood education (‘less than primary’ for educational attainment)';
+$string['levelinstitutionisced1'] = 'Primary education';
+$string['levelinstitutionisced2'] = 'Lower secondary education';
+$string['levelinstitutionisced3'] = 'Upper secondary education';
+$string['levelinstitutionisced4'] = 'Post-secondary non-tertiary education (may include corporate or community/NGO training)';
+$string['levelinstitutionisced5'] = 'Short-cycle tertiary education (may include corporate or community/NGO training)';
+$string['levelinstitutionisced6'] = 'Bachelor’s or equivalent level';
+$string['levelinstitutionisced7'] = 'Master’s or equivalent level';
+$string['levelinstitutionisced8'] = 'Doctoral or equivalent level';
 $string['nocourses'] = 'No courses to analyse';
+$string['modeinstruction'] = 'Modes of instruction';
+$string['modeinstructionfacetoface'] = 'Face to Face';
+$string['modeinstructionblendedhybrid'] = 'Blended/Hybrid';
+$string['modeinstructionfullyonline'] = 'Fully Online';
 $string['modeloutputdir'] = 'Models output directory';
 $string['modeloutputdirinfo'] = 'Directory where prediction processors store all evaluation info. Useful for debugging and research.';
 $string['modeltimelimit'] = 'Analysis time limit per model';
@@ -91,6 +106,8 @@ $string['novaliddata'] = 'No valid data available';
 $string['novalidsamples'] = 'No valid samples available';
 $string['onlycli'] = 'Analytics processes execution via command line only';
 $string['onlycliinfo'] = 'Analytics processes like evaluating models, training machine learning algorithms or getting predictions can take some time, they will run as cron tasks and they can be forced via command line. Disable this setting if you want your site managers to be able to run these processes manually via web interface';
+$string['percentonline'] = 'Percent online';
+$string['percentonline_help'] = 'If your institution offers blended or hybrid courses, what percentage of the student work is conducted online in Moodle? Use a numeric value from 0 to 100.';
 $string['predictionsprocessor'] = 'Predictions processor';
 $string['predictionsprocessor_help'] = 'A predictions processor is the machine-learning backend that processes the datasets generated by calculating models\' indicators and targets. Each model can use a different processor. The one specified here will be the default.';
 $string['privacy:metadata:analytics:indicatorcalc'] = 'Indicator calculations';
@@ -106,7 +123,7 @@ $string['privacy:metadata:analytics:predictions'] = 'Predictions';
 $string['privacy:metadata:analytics:predictions:modelid'] = 'The model ID';
 $string['privacy:metadata:analytics:predictions:contextid'] = 'The context';
 $string['privacy:metadata:analytics:predictions:sampleid'] = 'The sample ID';
-$string['privacy:metadata:analytics:predictions:rangeindex'] = 'The index of the time splitting method';
+$string['privacy:metadata:analytics:predictions:rangeindex'] = 'The index of the time-splitting method';
 $string['privacy:metadata:analytics:predictions:prediction'] = 'The prediction';
 $string['privacy:metadata:analytics:predictions:predictionscore'] = 'The prediction score';
 $string['privacy:metadata:analytics:predictions:calculations'] = 'Indicator calculations';
@@ -121,8 +138,12 @@ $string['privacy:metadata:analytics:predictionactions:timecreated'] = 'When the
 $string['processingsitecontents'] = 'Processing site contents';
 $string['successfullyanalysed'] = 'Successfully analysed';
 $string['timesplittingmethod'] = 'Time-splitting method';
-$string['timesplittingmethod_help'] = 'The time-splitting method is what defines when the system will calculate predictions and the portion of activity logs that will be considered for those predictions. E.g. they can divide the course duration in parts and generate a prediction at the end of these parts.';
+$string['timesplittingmethod_help'] = 'The time-splitting method is what defines when the system will calculate predictions and the portion of activity logs that will be considered for those predictions. E.g. They can divide the course duration in parts and generate a prediction at the end of these parts.';
 $string['timesplittingmethod_link'] = 'Time_splitting_methods';
+$string['typeinstitution'] = 'Type of institution';
+$string['typeinstitutionacademic'] = 'Academic';
+$string['typeinstitutiontraining'] = 'Corporate training';
+$string['typeinstitutionngo'] = 'Non-governmental organization (NGO)';
 $string['viewdetails'] = 'View details';
 $string['viewinsight'] = 'View insight';
 $string['viewinsightdetails'] = 'View insight details';
index c518dab..de681c6 100644 (file)
 
 $string['asyncbackupcomplete'] = 'The backup process has completed';
 $string['asyncbackupcompletebutton'] = 'Continue';
-$string['asyncbackupcompletedetail'] = 'The backup process has completed successfully completed. <br/> You can access the backup in the <a href="{$a}">restore page.</a>';
+$string['asyncbackupcompletedetail'] = 'The backup process has completed successfully. <br/> You can access the backup on the <a href="{$a}">restore page.</a>';
 $string['asyncbackuperror'] = 'The backup process has failed';
 $string['asyncbackuperrordetail'] = 'The backup process has failed. Please contact your system administrator.';
 $string['asyncbackuppending'] = 'The backup process is pending';
 $string['asyncbackupprocessing'] = 'The backup is in progress';
-$string['asyncbadexecution'] = 'Bad backup controller execution, is {$a} should be 2';
-$string['asynccheckprogress'] = ' You can check the progress at anytime at the <a href="{$a}">restore page.</a>';
-$string['asyncgeneralsettings'] = 'Asynchronous backup/restore general settings';
+$string['asyncbadexecution'] = 'Bad backup controller execution. It is {$a} and should be 2.';
+$string['asynccheckprogress'] = 'You can check the progress at any time on the <a href="{$a}">restore page</a>.';
+$string['asyncgeneralsettings'] = 'Asynchronous backup/restore';
 $string['asyncemailenable'] = 'Enable message notifications';
-$string['asyncemailenabledetail'] = 'When enabled users will receive a message when an asynchronous restore/backup completes';
+$string['asyncemailenabledetail'] = 'If enabled, users will receive a message when an asynchronous backup or restore completes.';
 $string['asyncmessagebody'] = 'Message';
-$string['asyncmessagebodydetail'] = 'Message to send when an asynchronous restore/backup completes';
-$string['asyncmessagebodydefault'] = 'Dear {user_firstname} {user_lastname}, <br/> Your {operation} (ID: {backupid}) has completed successfully! <br/><br/>You can view it here {link}.<br/>Kind Regards,<br/>Your Moodle Administrator.';
+$string['asyncmessagebodydetail'] = 'Message to send when an asynchronous backup or restore completes.';
+$string['asyncmessagebodydefault'] = 'Hi {user_firstname},<br/> Your {operation} (ID: {backupid}) has completed successfully. <br/><br/>You can access it here: {link}.';
 $string['asyncmessagesubject'] = 'Subject';
 $string['asyncmessagesubjectdetail'] = 'Message subject';
 $string['asyncmessagesubjectdefault'] = 'Moodle {operation} completed successfully';
-$string['asyncnowait'] = 'You don\'t need to wait here, the process will continue in the background.';
+$string['asyncnowait'] = 'You don\'t need to wait here, as the process will continue in the background.';
 $string['asyncprocesspending'] = 'Process pending';
 $string['asyncrestorecomplete'] = 'The restore process has completed';
 $string['asyncrestorecompletebutton'] = 'Continue';
-$string['asyncrestorecompletedetail'] = 'The restore process has completed successfully completed. Clicking continue will take you to the <a href="{$a}">course for the restored item.</a>';
+$string['asyncrestorecompletedetail'] = 'The restore process has completed successfully. Clicking continue will take you to the <a href="{$a}">course for the restored item.</a>';
 $string['asyncrestoreerror'] = 'The restore process has failed';
 $string['asyncrestoreerrordetail'] = 'The restore process has failed. Please contact your system administrator.';
 $string['asyncrestorepending'] = 'The restore process is pending';
@@ -169,7 +169,7 @@ $string['currentstage4'] = 'Confirmation and review';
 $string['currentstage8'] = 'Perform backup';
 $string['currentstage16'] = 'Complete';
 $string['enableasyncbackup'] = 'Enable asynchronous backups';
-$string['enableasyncbackup_help'] = 'If enabled, all backup and restore operations will be done asynchronously. This does not effect imports and exports. Asynchronous backups and restores allow users to do other operations while a backup or restore is in progress.';
+$string['enableasyncbackup_help'] = 'If enabled, all backup and restore operations will be done asynchronously. This does not affect imports and exports. Asynchronous backups and restores allow users to do other operations while a backup or restore is in progress.';
 $string['enterasearch'] = 'Enter a search';
 $string['error_block_for_module_not_found'] = 'Orphan block instance (id: {$a->bid}) for course module (id: {$a->mid}) found. This block will not be backed up';
 $string['error_course_module_not_found'] = 'Orphan course module (id: {$a}) found. This module will not be backed up.';
@@ -258,7 +258,7 @@ $string['nomatchingcourses'] = 'There are no courses to display';
 $string['norestoreoptions'] = 'There are no categories or existing courses you can restore to.';
 $string['originalwwwroot'] = 'URL of backup';
 $string['overwrite'] = 'Overwrite';
-$string['pendingasyncdetail'] = 'Asynchronous backups only allow a user to have one pending backup for a resource at a time. <br/> Muliple asynchronous backups of the same resource can\'t be queued, as this would likely result in multiple backups with the same content.';
+$string['pendingasyncdetail'] = 'Asynchronous backups only allow a user to have one pending backup for a resource at a time. <br/> Multiple asynchronous backups of the same resource can\'t be queued, as this would likely result in multiple backups with the same content.';
 $string['pendingasyncdeletedetail'] = 'This course has an asynchronous backup pending. <br/> Courses can\'t be deleted until this backup finishes.';
 $string['pendingasyncedit'] = 'There is a pending asynchronous backup for this course. Please do not edit this course until backup is complete.';
 $string['pendingasyncerror'] = 'Backup pending for this resource';
@@ -374,4 +374,4 @@ $string['unnamedsection'] = 'Unnamed section';
 $string['userinfo'] = 'Userinfo';
 $string['module'] = 'Module';
 $string['morecoursesearchresults'] = 'More than {$a} courses found, showing first {$a} results';
-$string['recyclebin_desc'] = 'These settings will be also applied to recycle bin';
+$string['recyclebin_desc'] = 'Note that these settings will be also be used for the recycle bin.';
index 8e1c503..7ee2620 100644 (file)
@@ -245,7 +245,7 @@ $string['criteria_7_help'] = 'Allows a badge to be awarded to users based on the
 $string['criteria_8'] = 'Cohort membership';
 $string['criteria_8_help'] = 'Allows a badge to be awarded to users based on cohort membership.';
 $string['criteria_9'] = 'Competencies';
-$string['criteria_9_help'] = 'Allows a badge to be awarded to users based on the competencies thay have completed.';
+$string['criteria_9_help'] = 'Allows a badge to be awarded to users based on the competencies they have completed.';
 $string['criterror'] = 'Current parameters issues';
 $string['criterror_help'] = 'This fieldset shows all parameters that were initially added to this badge requirement but are no longer available. It is recommended that you un-check such parameters to make sure that users can earn this badge in the future.';
 $string['currentimage'] = 'Current image';
index eaef860..441b191 100644 (file)
@@ -56,6 +56,7 @@ $string['cachedef_groupdata'] = 'Course group information';
 $string['cachedef_htmlpurifier'] = 'HTML Purifier - cleaned content';
 $string['cachedef_langmenu'] = 'List of available languages';
 $string['cachedef_message_time_last_message_between_users'] = 'Time created for most recent message in a conversation';
+$string['cachedef_modelfirstanalyses'] = 'First analysis by model and analysable';
 $string['cachedef_locking'] = 'Locking';
 $string['cachedef_message_processors_enabled'] = "Message processors enabled status";
 $string['cachedef_contextwithinsights'] = 'Context with insights';
index d7930bb..d33e396 100644 (file)
@@ -38,7 +38,7 @@ $string['customfield_visibility'] = 'Visible to';
 $string['customfield_visibility_help'] = 'This setting determines who can view the custom field name and value in the list of courses.';
 $string['customfield_visibletoall'] = 'Everyone';
 $string['customfield_visibletoteachers'] = 'Teachers';
-$string['customfieldsettings'] = 'Settings for course custom fields';
+$string['customfieldsettings'] = 'Common course custom fields settings';
 $string['errorendbeforestart'] = 'The end date ({$a}) is before the course start date.';
 $string['favourite'] = 'Starred course';
 $string['gradetopassnotset'] = 'This course does not have a grade to pass set. It may be set in the grade item of the course (Gradebook setup).';
@@ -58,8 +58,8 @@ $string['target:coursecompetencies'] = 'Students at risk of not achieving the co
 $string['target:coursecompetencies_help'] = 'This target describes whether a student is at risk of not achieving the competencies assigned to a course. This target considers that all competencies assigned to the course must be achieved by the end of the course.';
 $string['target:coursedropout'] = 'Students at risk of dropping out';
 $string['target:coursedropout_help'] = 'This target describes whether the student is considered at risk of dropping out.';
-$string['target:coursegradetopass'] = 'Students at risk of not getting the minimum grade to pass the course.';
-$string['target:coursegradetopass_help'] = 'This target describes whether the student is at risk of not getting the minimum grade to pass the course.';
+$string['target:coursegradetopass'] = 'Students at risk of not achieving the minimum grade to pass the course';
+$string['target:coursegradetopass_help'] = 'This target describes whether the student is at risk of not achieving the minimum grade to pass the course.';
 $string['target:noteachingactivity'] = 'No teaching';
 $string['target:noteachingactivity_help'] = 'This target describes whether courses due to start in the coming week will have teaching activity.';
 $string['targetlabelstudentcompletionno'] = 'Student who is likely to meet the course completion conditions';
@@ -70,5 +70,5 @@ $string['targetlabelstudentdropoutyes'] = 'Student at risk of dropping out';
 $string['targetlabelstudentdropoutno'] = 'Not at risk';
 $string['targetlabelstudentgradetopassno'] = 'Student who is likely to meet the minimum grade to pass the course.';
 $string['targetlabelstudentgradetopassyes'] = 'Student at risk of not meeting the minimum grade to pass the course.';
-$string['targetlabelteachingyes'] = 'Users with teaching capabilities have access to the course';
+$string['targetlabelteachingyes'] = 'Users with teaching capabilities who have access to the course';
 $string['targetlabelteachingno'] = 'No teaching';
index a916c31..698680d 100644 (file)
@@ -28,7 +28,7 @@ $string['addnewcategory'] = 'Add a new category';
 $string['afterfield'] = 'After field {$a}';
 $string['categorynotfound'] = 'Category not found';
 $string['checked'] = 'Checked';
-$string['commonsettings'] = 'Common settings';
+$string['commonsettings'] = 'General';
 $string['componentsettings'] = 'Component settings';
 $string['confirmdeletecategory'] = 'Are you sure you want to delete this category? All fields inside the category will also be deleted and all data associated with them. This action cannot be undone.';
 $string['confirmdeletefield'] = 'Are you sure you want to delete this field and all associated data? This action cannot be undone.';
index 8c18ed7..5451eda 100644 (file)
@@ -157,7 +157,7 @@ $string['privacy:metadata:messages:useridfrom'] = 'The ID of the user who sent t
 $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 time when the message was created';
-$string['privacy:metadata:messages:customdata'] = 'Custom data, usually contains internal ids and a public URL of the sender image (user or group).';
+$string['privacy:metadata:messages:customdata'] = 'Custom data, usually containing internal IDs and a public URL of the sender image (user or group)';
 $string['privacy:metadata:message_contacts'] = 'The list of contacts';
 $string['privacy:metadata:message_contacts:contactid'] = 'The ID of the user who is a contact';
 $string['privacy:metadata:message_contacts:timecreated'] = 'The time when the contact was created';
@@ -198,7 +198,7 @@ $string['privacy:metadata:notifications:timeread'] = 'The time when the notifica
 $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:notifications:customdata'] = 'Custom data, usually contains internal ids and a public URL of the sender picture (if any).';
+$string['privacy:metadata:notifications:customdata'] = 'Custom data, usually containing internal IDs and a public URL of the sender picture (if any)';
 $string['privacy:metadata:preference:core_message_settings'] = 'Settings related to messaging';
 $string['privacy:request:preference:set'] = 'The value of the setting \'{$a->name}\' was \'{$a->value}\'';
 $string['privacy:export:conversationprefix'] = 'Conversation: ';
index 01ecbc8..df841d6 100644 (file)
@@ -225,7 +225,7 @@ $string['cachecontrols'] = 'Cache controls';
 $string['cancel'] = 'Cancel';
 $string['cancelled'] = 'Cancelled';
 $string['categories'] = 'Course categories';
-$string['categoriesandcoures'] = 'Course categories and courses';
+$string['categoriesandcourses'] = 'Course categories and courses';
 $string['category'] = 'Category';
 $string['categoryadded'] = 'The category \'{$a}\' was added';
 $string['categorybulkaction'] = 'Bulk actions for selected categories';
@@ -1807,7 +1807,7 @@ $string['separateandconnected'] = 'Separate and Connected ways of knowing';
 $string['separateandconnectedinfo'] = 'The scale based on the theory of separate and connected knowing. This theory describes two different ways that we can evaluate and learn about the things we see and hear.<ul><li><strong>Separate knowers</strong> remain as objective as possible without including feelings and emotions. In a discussion with other people, they like to defend their own ideas, using logic to find holes in opponent\'s ideas.</li><li><strong>Connected knowers</strong> are more sensitive to other people. They are skilled at empathy and tend to listen and ask questions until they feel they can connect and "understand things from their point of view". They learn by trying to share the experiences that led to the knowledge they find in other people.</li></ul>';
 $string['servererror'] = 'An error occurred whilst communicating with the server';
 $string['serverlocaltime'] = 'Server\'s local time';
-$string['sessionforceclean'] = 'As a security precaution, user generated scripts have been disabled within this session';
+$string['sessionforceclean'] = 'As a security precaution, user-generated scripts have been disabled within this session.';
 $string['setcategorytheme'] = 'Set category theme';
 $string['setpassword'] = 'Set password';
 $string['setpasswordinstructions'] = 'Please enter your new password below, then save changes.';
index d9df15e..7dc290e 100644 (file)
@@ -142,6 +142,17 @@ $string['errorprocessingresponses'] = 'An error occurred while processing your r
 $string['errorsavingcomment'] = 'Error saving the comment for question {$a->name} in the database.';
 $string['errorupdatingattempt'] = 'Error updating attempt {$a->id} in the database.';
 $string['eventquestioncategorycreated'] = 'Question category created';
+$string['eventquestioncategorydeleted'] = 'Question category deleted';
+$string['eventquestioncategorymoved'] = 'Question category moved';
+$string['eventquestioncategoryupdated'] = 'Question category updated';
+$string['eventquestioncategoryviewed'] = 'Question category viewed';
+$string['eventquestioncreated'] = 'Question created';
+$string['eventquestiondeleted'] = 'Question deleted';
+$string['eventquestionmoved'] = 'Question moved';
+$string['eventquestionviewed'] = 'Question viewed';
+$string['eventquestionsexported'] = 'Questions exported';
+$string['eventquestionsimported'] = 'Questions imported';
+$string['eventquestionupdated'] = 'Question updated';
 $string['export'] = 'Export';
 $string['exportcategory'] = 'Export category';
 $string['exportcategory_help'] = 'This setting determines the category from which the exported questions will be taken.
index 3b05402..4c1bb90 100644 (file)
@@ -324,6 +324,28 @@ class badge {
                 $crit->make_clone($new);
             }
 
+            // Copy endorsement.
+            $endorsement = $this->get_endorsement();
+            if ($endorsement) {
+                unset($endorsement->id);
+                $endorsement->badgeid = $new;
+                $newbadge->save_endorsement($endorsement);
+            }
+
+            // Copy alignments.
+            $alignments = $this->get_alignments();
+            foreach ($alignments as $alignment) {
+                unset($alignment->id);
+                $alignment->badgeid = $new;
+                $newbadge->save_alignment($alignment);
+            }
+
+            // Copy related badges.
+            $related = $this->get_related_badges(true);
+            if (!empty($related)) {
+                $newbadge->add_related_badges(array_keys($related));
+            }
+
             // Trigger event, badge duplicated.
             $eventparams = array('objectid' => $new, 'context' => $PAGE->context);
             $event = \core\event\badge_duplicated::create($eventparams);
diff --git a/lib/classes/event/question_base.php b/lib/classes/event/question_base.php
new file mode 100644 (file)
index 0000000..2f07b85
--- /dev/null
@@ -0,0 +1,129 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Base class for question events.
+ *
+ * @package    core
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Base class for question events.
+ *
+ * @package    core
+ * @since      Moodle 3.7
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class question_base extends base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'question';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        if ($this->courseid) {
+            $cat = $this->other['categoryid'] . ',' . $this->contextid;
+            if ($this->contextlevel == CONTEXT_MODULE) {
+                return new \moodle_url('/question/edit.php',
+                        ['cmid' => $this->contextinstanceid, 'cat' => $cat, 'lastchanged' => $this->objectid]);
+            }
+            return new \moodle_url('/question/edit.php',
+                    ['courseid' => $this->courseid, 'cat' => $cat, 'lastchanged' => $this->objectid]);
+        }
+        // Lets try viewing from the frontpage for contexts above course.
+        return new \moodle_url('/question/category.php',
+                ['courseid' => SITEID, 'edit' => $this->other['categoryid'], 'lastchanged' => $this->objectid]);
+    }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['categoryid'])) {
+            throw new \coding_exception('The \'categoryid\' must be set in \'other\'.');
+        }
+    }
+
+    /**
+     * Returns DB mappings used with backup / restore.
+     *
+     * @return array
+     */
+    public static function get_objectid_mapping() {
+        return ['db' => 'question', 'restore' => 'question'];
+    }
+
+    /**
+     * Used for maping events on restore
+     *
+     * @return array
+     */
+    public static function get_other_mapping() {
+
+        $othermapped = [];
+        $othermapped['categoryid'] = ['db' => 'question_categories', 'restore' => 'question_categories'];
+        return $othermapped;
+    }
+
+    /**
+     * Create a event from question object
+     *
+     * @param object $question
+     * @param object|null $context
+     * @param array|null $other will override the categoryid pre-filled out on the first line.
+     * @return base
+     * @throws \coding_exception
+     */
+    public static function create_from_question_instance($question, $context = null, $other = null) {
+
+        $params = ['objectid' => $question->id, 'other' => ['categoryid' => $question->category]];
+
+        if (!empty($question->contextid)) {
+            $params['contextid'] = $question->contextid;
+        }
+
+        $params['context'] = $context;
+
+        if (!empty($other)) {
+            $params['other'] = $other;
+        }
+
+        $event = self::create($params);
+        return $event;
+    }
+}
+
diff --git a/lib/classes/event/question_category_base.php b/lib/classes/event/question_category_base.php
new file mode 100644 (file)
index 0000000..fa1ffc7
--- /dev/null
@@ -0,0 +1,95 @@
+<?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/>.
+
+/**
+ * Base class for question category events.
+ *
+ * @package    core
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Base class for question category events
+ *
+ * @package    core
+ * @since      Moodle 3.7
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class question_category_base extends base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'question_categories';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        if ($this->courseid) {
+            $cat = $this->objectid . ',' . $this->contextid;
+            if ($this->contextlevel == CONTEXT_MODULE) {
+                return new \moodle_url('/question/edit.php', ['cmid' => $this->contextinstanceid, 'cat' => $cat]);
+            }
+            return new \moodle_url('/question/edit.php', ['courseid' => $this->courseid, 'cat' => $cat]);
+        }
+        // Lets try viewing from the frontpage for contexts above course.
+        return new \moodle_url('/question/category.php', ['courseid' => SITEID, 'edit' => $this->objectid]);
+    }
+
+    /**
+     * Returns DB mappings used with backup / restore.
+     *
+     * @return array
+     */
+    public static function get_objectid_mapping() {
+        return ['db' => 'question_categories', 'restore' => 'question_categories'];
+    }
+
+    /**
+     * Create a event from question category object
+     *
+     * @param object $category
+     * @param object|null $context
+     * @return base
+     * @throws \coding_exception
+     */
+    public static function create_from_question_category_instance($category, $context = null) {
+
+        $params = ['objectid' => $category->id];
+
+        if (!empty($category->contextid)) {
+            $params['contextid'] = $category->contextid;
+        }
+
+        $params['context'] = $context;
+
+        $event = self::create($params);
+        return $event;
+    }
+}
+
index dd011ec..2b6baf1 100644 (file)
@@ -34,15 +34,14 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2014 Mark Nelson <markn@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class question_category_created extends base {
+class question_category_created extends question_category_base {
 
     /**
      * Init method.
      */
     protected function init() {
-        $this->data['objecttable'] = 'question_categories';
+        parent::init();
         $this->data['crud'] = 'c';
-        $this->data['edulevel'] = self::LEVEL_TEACHING;
     }
 
     /**
@@ -63,26 +62,6 @@ class question_category_created extends base {
         return "The user with id '$this->userid' created the question category with id '$this->objectid'.";
     }
 
-    /**
-     * Returns relevant URL.
-     *
-     * @return \moodle_url
-     */
-    public function get_url() {
-        if ($this->courseid) {
-            $cat = $this->objectid . ',' . $this->contextid;
-            if ($this->contextlevel == CONTEXT_MODULE) {
-                return new \moodle_url('/question/edit.php', array('cmid' => $this->contextinstanceid, 'cat' => $cat));
-            }
-            return new \moodle_url('/question/edit.php', array('courseid' => $this->courseid, 'cat' => $cat));
-        }
-
-        // Bad luck, there does not seem to be any simple intelligent way
-        // to go to specific question category in context above course,
-        // let's try to edit it from frontpage which may surprisingly work.
-        return new \moodle_url('/question/category.php', array('courseid' => SITEID, 'edit' => $this->objectid));
-    }
-
     /**
      * Return the legacy event log data.
      *
@@ -97,7 +76,4 @@ class question_category_created extends base {
         return null;
     }
 
-    public static function get_objectid_mapping() {
-        return array('db' => 'question_categories', 'restore' => 'question_category');
-    }
 }
diff --git a/lib/classes/event/question_category_deleted.php b/lib/classes/event/question_category_deleted.php
new file mode 100644 (file)
index 0000000..94c5e76
--- /dev/null
@@ -0,0 +1,65 @@
+<?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/>.
+
+/**
+ * Question category deleted event.
+ *
+ * @package    core
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Question category deleted event class.
+ *
+ * @package    core
+ * @since      Moodle 3.7
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_category_deleted extends question_category_base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        parent::init();
+        $this->data['crud'] = 'd';
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventquestioncategorydeleted', 'question');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' deleted the question category with id '$this->objectid'.";
+    }
+
+}
diff --git a/lib/classes/event/question_category_moved.php b/lib/classes/event/question_category_moved.php
new file mode 100644 (file)
index 0000000..05e867b
--- /dev/null
@@ -0,0 +1,65 @@
+<?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/>.
+
+/**
+ * Question category moved event.
+ *
+ * @package    core
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Question category moved event class.
+ *
+ * @package    core
+ * @since      Moodle 3.7
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_category_moved extends question_category_base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        parent::init();
+        $this->data['crud'] = 'u';
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventquestioncategorymoved', 'question');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' moved the question category with id '$this->objectid'.";
+    }
+
+}
diff --git a/lib/classes/event/question_category_updated.php b/lib/classes/event/question_category_updated.php
new file mode 100644 (file)
index 0000000..e5b0e24
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Question category updated event.
+ *
+ * @package    core
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Question category updated event class.
+ *
+ * @package    core
+ * @since      Moodle 3.7
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_category_updated extends question_category_base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+         parent::init();
+        $this->data['crud'] = 'u';
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventquestioncategoryupdated', 'question');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' updated the question category with id '$this->objectid'.";
+    }
+}
diff --git a/lib/classes/event/question_category_viewed.php b/lib/classes/event/question_category_viewed.php
new file mode 100644 (file)
index 0000000..d6595eb
--- /dev/null
@@ -0,0 +1,65 @@
+<?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/>.
+
+/**
+ * Question category viewed event.
+ *
+ * @package    core
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Question category viewed event class.
+ *
+ * @package    core
+ * @since      Moodle 3.7
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_category_viewed extends question_category_base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        parent::init();
+        $this->data['crud'] = 'r';
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventquestioncategoryviewed', 'question');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' viewed the question category with id '$this->objectid'.";
+    }
+
+}
diff --git a/lib/classes/event/question_created.php b/lib/classes/event/question_created.php
new file mode 100644 (file)
index 0000000..b05e1fb
--- /dev/null
@@ -0,0 +1,87 @@
+<?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/>.
+
+/**
+ * Question created event.
+ *
+ * @package    core
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Question created event class.
+ *
+ * @property-read array $other {
+ *      Extra information about the event.
+ *
+ *      - int categoryid: The ID of the category where the question resides
+ * }
+ *
+ * @package    core
+ * @since      Moodle 3.7
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_created extends question_base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        parent::init();
+        $this->data['crud'] = 'c';
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventquestioncreated', 'question');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' created a question with the id of '$this->objectid'" .
+                " in the category with the id '" . $this->other['categoryid'] . "'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        if ($this->courseid) {
+            if ($this->contextlevel == CONTEXT_MODULE) {
+                return new \moodle_url('/question/preview.php', ['cmid' => $this->contextinstanceid, 'id' => $this->objectid]);
+            }
+            return new \moodle_url('/question/preview.php', ['courseid' => $this->courseid, 'id' => $this->objectid]);
+        }
+        // Lets try editing from the frontpage for contexts above course.
+        return new \moodle_url('/question/preview.php', ['courseid' => SITEID, 'id' => $this->objectid]);
+    }
+}
diff --git a/lib/classes/event/question_deleted.php b/lib/classes/event/question_deleted.php
new file mode 100644 (file)
index 0000000..17363d1
--- /dev/null
@@ -0,0 +1,82 @@
+<?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/>.
+
+/**
+ * Question deleted event.
+ *
+ * @package    core
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Question deleted event class.
+ *
+ * @property-read array $other {
+ *      Extra information about the event.
+ *
+ *      - int categoryid: The ID of the category where the question resides
+ * }
+ *
+ * @package    core
+ * @since      Moodle 3.7
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_deleted extends question_base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        parent::init();
+        $this->data['crud'] = 'd';
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventquestiondeleted', 'question');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' deleted the question with id '$this->objectid'" .
+                " from the category with the id '" . $this->other['categoryid'] . "'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     * This is needed to override the function in question_base
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        // No URL for delete.
+        return null;
+    }
+}
diff --git a/lib/classes/event/question_moved.php b/lib/classes/event/question_moved.php
new file mode 100644 (file)
index 0000000..a798f81
--- /dev/null
@@ -0,0 +1,132 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Question moved event.
+ *
+ * @package    core
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Question moved event class.
+ *
+ * @property-read array $other {
+ *      Extra information about the event.
+ *
+ *      - int newcategoryid: The ID of the new category of the question
+ *      - int oldcategoryid: The ID of the old category of the question
+ * }
+ *
+ * @package    core
+ * @since      Moodle 3.7
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_moved extends question_base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        parent::init();
+        $this->data['crud'] = 'u';
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventquestionmoved', 'question');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' moved the question with the id of '$this->objectid'" .
+                " from the category with the id of '" . $this->other['oldcategoryid'] .
+                "' to the category with the id of '" . $this->other['newcategoryid'] . "'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        if ($this->courseid) {
+            $cat = $this->other['newcategoryid'] . ',' . $this->contextid;
+            if ($this->contextlevel == CONTEXT_MODULE) {
+                return new \moodle_url('/question/edit.php',
+                        ['cmid' => $this->contextinstanceid, 'cat' => $cat, 'lastchanged' => $this->objectid]);
+            }
+            return new \moodle_url('/question/edit.php',
+                    ['courseid' => $this->courseid, 'cat' => $cat, 'lastchanged' => $this->objectid]);
+        }
+        // Lets try viewing from the frontpage for contexts above course.
+        return new \moodle_url('/question/category.php',
+                ['courseid' => SITEID, 'edit' => $this->other['newcategoryid'], 'lastchanged' => $this->objectid]);
+    }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+
+        if (!isset($this->other['oldcategoryid'])) {
+            throw new \coding_exception('The \'oldcategoryid\' must be set in \'other\'.');
+        }
+        if (!isset($this->other['newcategoryid'])) {
+            throw new \coding_exception('The \'newcategoryid\' must be set in \'other\'.');
+        }
+    }
+
+    /**
+     * Returns DB mappings used with backup / restore.
+     *
+     * @return array
+     */
+    public static function get_objectid_mapping() {
+        return ['db' => 'question', 'restore' => 'question'];
+    }
+
+    /**
+     * Used for maping events on restore
+     *
+     * @return array
+     */
+    public static function get_other_mapping() {
+
+        $othermapped = [];
+        $othermapped['newcategoryid'] = ['db' => 'question_categories', 'restore' => 'question_categories'];
+        $othermapped['oldcategoryid'] = ['db' => 'question_categories', 'restore' => 'question_categories'];
+
+        return $othermapped;
+    }
+}
diff --git a/lib/classes/event/question_updated.php b/lib/classes/event/question_updated.php
new file mode 100644 (file)
index 0000000..4e5bdf9
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Question updated event.
+ *
+ * @package    core
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Question updated event class.
+ *
+ * @property-read array $other {
+ *      Extra information about the event.
+ *
+ *      - int categoryid: The ID of the category where the question resides
+ * }
+ *
+ * @package    core
+ * @since      Moodle 3.7
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_updated extends question_base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        parent::init();
+        $this->data['crud'] = 'u';
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventquestionupdated', 'question');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' updated the question with the id of '$this->objectid'.";
+    }
+
+}
diff --git a/lib/classes/event/question_viewed.php b/lib/classes/event/question_viewed.php
new file mode 100644 (file)
index 0000000..9ef0d37
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Question previewed event.
+ *
+ * @package    core
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Question previewed event class.
+ *
+ * @property-read array $other {
+ *      Extra information about the event.
+ *
+ *      - int categoryid: The ID of the category where the question resides
+ * }
+ *
+ * @package    core
+ * @since      Moodle 3.7
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_viewed extends question_base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        parent::init();
+        $this->data['crud'] = 'r';
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventquestionviewed', 'question');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' viewed the question with the id of '$this->objectid'.";
+    }
+
+}
diff --git a/lib/classes/event/questions_exported.php b/lib/classes/event/questions_exported.php
new file mode 100644 (file)
index 0000000..adc3117
--- /dev/null
@@ -0,0 +1,116 @@
+<?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/>.
+
+/**
+ * Questions exported event.
+ *
+ * @package    core
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Question category exported event class.
+ *
+ * @property-read array $other {
+ *      Extra information about the event.
+ *
+ *      - int categoryid: The ID of the category where the question resides
+ *      - string format: The format of file export
+ * }
+ *
+ * @package    core
+ * @since      Moodle 3.7
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class questions_exported extends question_base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['crud'] = 'r';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventquestionsexported', 'question');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' exported questions in '" . $this->other['format'] .
+                "' format from the category with id '" . $this->other['categoryid'] . "'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        if ($this->courseid) {
+            $cat = $this->other['categoryid'] . ',' . $this->contextid;
+            if ($this->contextlevel == CONTEXT_MODULE) {
+                return new \moodle_url('/question/edit.php', ['cmid' => $this->contextinstanceid, 'cat' => $cat]);
+            }
+            return new \moodle_url('/question/edit.php', ['courseid' => $this->courseid, 'cat' => $cat]);
+        }
+        return new \moodle_url('/question/category.php', ['courseid' => SITEID, 'edit' => $this->other['categoryid']]);
+    }
+
+    /**
+     * Custom validations.
+     *
+     * other['categoryid'] and other['format'] is required.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['format'])) {
+            throw new \coding_exception('The \'format\' must be set in \'other\'.');
+        }
+    }
+
+    /**
+     * Returns DB mappings used with backup / restore.
+     * This is needed to override the function in question_base
+     *
+     * @return array
+     */
+    public static function get_objectid_mapping() {
+        // No mappings.
+        return [];
+    }
+
+}
diff --git a/lib/classes/event/questions_imported.php b/lib/classes/event/questions_imported.php
new file mode 100644 (file)
index 0000000..d774c40
--- /dev/null
@@ -0,0 +1,115 @@
+<?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/>.
+
+/**
+ * Questions imported event.
+ *
+ * @package    core
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Question category imported event class.
+ *
+ * @property-read array $other {
+ *      Extra information about the event.
+ *
+ *      - int categoryid: The ID of the category where the question resides
+ *      - string format: The format of file import
+ * }
+ *
+ * @package    core
+ * @since      Moodle 3.7
+ * @copyright  2019 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class questions_imported extends question_base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventquestionsimported', 'question');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' imported questions in '" . $this->other['format'] .
+                "' format into the category with id '" . $this->other['categoryid'] . "'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        if ($this->courseid) {
+            $cat = $this->other['categoryid'] . ',' . $this->contextid;
+            if ($this->contextlevel == CONTEXT_MODULE) {
+                return new \moodle_url('/question/edit.php', ['cmid' => $this->contextinstanceid, 'cat' => $cat]);
+            }
+            return new \moodle_url('/question/edit.php', ['courseid' => $this->courseid, 'cat' => $cat]);
+        }
+        return new \moodle_url('/question/category.php', ['courseid' => SITEID, 'edit' => $this->other['categoryid']]);
+    }
+
+    /**
+     * Custom validations.
+     *
+     * other['categoryid'] and other['format'] is required.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['format'])) {
+            throw new \coding_exception('The \'format\' must be set in \'other\'.');
+        }
+    }
+
+    /**
+     * Returns DB mappings used with backup / restore.
+     * This is needed to override the function in question_base
+     *
+     * @return array
+     */
+    public static function get_objectid_mapping() {
+        // No mappings.
+        return [];
+    }
+}
index 2eee814..6195288 100644 (file)
@@ -79,14 +79,16 @@ abstract class exporter {
             }
 
             $missingdataerr = 'Exporter class is missing required related data: (' . get_called_class() . ') ';
+            $scalartypes = ['string', 'int', 'bool', 'float'];
+            $scalarcheck = 'is_' . $classname;
 
-            if ($nullallowed && array_key_exists($key, $related) && $related[$key] === null) {
-                $this->related[$key] = $related[$key];
+            if ($nullallowed && (!array_key_exists($key, $related) || $related[$key] === null)) {
+                $this->related[$key] = null;
 
             } else if ($isarray) {
                 if (array_key_exists($key, $related) && is_array($related[$key])) {
                     foreach ($related[$key] as $index => $value) {
-                        if (!$value instanceof $classname) {
+                        if (!$value instanceof $classname && !$scalarcheck($value)) {
                             throw new coding_exception($missingdataerr . $key . ' => ' . $classname . '[]');
                         }
                     }
@@ -96,8 +98,6 @@ abstract class exporter {
                 }
 
             } else {
-                $scalartypes = ['string', 'int', 'bool', 'float'];
-                $scalarcheck = 'is_' . $classname;
                 if (array_key_exists($key, $related) &&
                         ((in_array($classname, $scalartypes) && $scalarcheck($related[$key])) ||
                         ($related[$key] instanceof $classname))) {
index 249961e..aa138d6 100644 (file)
@@ -356,6 +356,7 @@ class icon_system_fontawesome extends icon_system_font {
             'core:t/edit' => 'fa-cog',
             'core:t/emailno' => 'fa-ban',
             'core:t/email' => 'fa-envelope-o',
+            'core:t/emptystar' => 'fa-star-o',
             'core:t/enrolusers' => 'fa-user-plus',
             'core:t/expanded' => 'fa-caret-down',
             'core:t/go' => 'fa-play',
index a6597d8..2457200 100644 (file)
@@ -44,13 +44,97 @@ class completion_daily_task extends scheduled_task {
      * Throw exceptions on errors (the job will be retried).
      */
     public function execute() {
-        global $CFG;
+        global $CFG, $DB;
 
         if ($CFG->enablecompletion) {
-            // Daily Completion cron.
-            require_once($CFG->dirroot.'/completion/cron.php');
-            completion_cron_mark_started();
+            require_once($CFG->libdir . "/completionlib.php");
+
+            if (debugging()) {
+                mtrace('Marking users as started');
+            }
+
+            // This causes it to default to everyone (if there is no student role).
+            $sqlroles = '';
+            if (!empty($CFG->gradebookroles)) {
+                $sqlroles = ' AND ra.roleid IN (' . $CFG->gradebookroles.')';
+            }
+
+            // It's purpose is to locate all the active participants of a course with course completion enabled.
+            // We also only want the users with no course_completions record as this functions job is to create
+            // the missing ones :)
+            // We want to record the user's enrolment start time for the course. This gets tricky because there can be
+            // multiple enrolment plugins active in a course, hence the possibility of multiple records for each
+            // couse/user in the results.
+            $sql = "SELECT c.id AS course, u.id AS userid, crc.id AS completionid, ue.timestart AS timeenrolled,
+                           ue.timecreated
+                      FROM {user} u
+                INNER JOIN {user_enrolments} ue ON ue.userid = u.id
+                INNER JOIN {enrol} e ON e.id = ue.enrolid
+                INNER JOIN {course} c ON c.id = e.courseid
+                INNER JOIN {role_assignments} ra ON ra.userid = u.id
+                 LEFT JOIN {course_completions} crc ON crc.course = c.id AND crc.userid = u.id
+                     WHERE c.enablecompletion = 1
+                       AND crc.timeenrolled IS NULL
+                       AND ue.status = 0
+                       AND e.status = 0
+                       AND u.deleted = 0
+                       AND ue.timestart < ?
+                       AND (ue.timeend > ? OR ue.timeend = 0)
+                       $sqlroles
+                  ORDER BY course, userid";
+            $now = time();
+            $rs = $DB->get_recordset_sql($sql, [$now, $now, $now, $now]);
+
+            // Check if result is empty.
+            if (!$rs->valid()) {
+                // Not going to iterate (but exit), close rs.
+                $rs->close();
+                return;
+            }
+
+            // We are essentially doing a group by in the code here (as I can't find a decent way of doing it
+            // in the sql). Since there can be multiple enrolment plugins for each course, we can have multiple rows
+            // for each participant in the query result. This isn't really a problem until you combine it with the fact
+            // that the enrolment plugins can save the enrol start time in either timestart or timeenrolled.
+            // The purpose of the loop is to find the earliest enrolment start time for each participant in each course.
+            $prev = null;
+            while ($rs->valid() || $prev) {
+                $current = $rs->current();
+                if (!isset($current->course)) {
+                    $current = false;
+                } else {
+                    // Not all enrol plugins fill out timestart correctly, so use whichever is non-zero.
+                    $current->timeenrolled = max($current->timecreated, $current->timeenrolled);
+                }
+
+                // If we are at the last record, or we aren't at the first and the record is for a diff user/course.
+                if ($prev && (!$rs->valid() ||
+                        ($current->course != $prev->course || $current->userid != $prev->userid))) {
+
+                    $completion = new \completion_completion();
+                    $completion->userid = $prev->userid;
+                    $completion->course = $prev->course;
+                    $completion->timeenrolled = (string) $prev->timeenrolled;
+                    $completion->timestarted = 0;
+                    $completion->reaggregate = time();
+                    if ($prev->completionid) {
+                        $completion->id = $prev->completionid;
+                    }
+                    $completion->mark_enrolled();
+
+                    if (debugging()) {
+                        mtrace('Marked started user ' . $prev->userid . ' in course ' . $prev->course);
+                    }
+                } else if ($prev && $current) {
+                    // Else, if this record is for the same user/course use oldest timeenrolled.
+                    $current->timeenrolled = min($current->timeenrolled, $prev->timeenrolled);
+                }
+                // Move current record to previous.
+                $prev = $current;
+                // Move to next record.
+                $rs->next();
+            }
+            $rs->close();
         }
     }
-
 }
index 19be43f..40d4572 100644 (file)
@@ -44,13 +44,155 @@ class completion_regular_task extends scheduled_task {
      * Throw exceptions on errors (the job will be retried).
      */
     public function execute() {
-        global $CFG;
+        global $CFG, $COMPLETION_CRITERIA_TYPES, $DB;
 
         if ($CFG->enablecompletion) {
-            // Regular Completion cron.
-            require_once($CFG->dirroot.'/completion/cron.php');
-            completion_cron_criteria();
-            completion_cron_completions();
+            require_once($CFG->libdir . "/completionlib.php");
+
+            // Process each criteria type.
+            foreach ($COMPLETION_CRITERIA_TYPES as $type) {
+                $object = 'completion_criteria_' . $type;
+                require_once($CFG->dirroot . '/completion/criteria/' . $object . '.php');
+
+                $class = new $object();
+                // Run the criteria type's cron method, if it has one.
+                if (method_exists($class, 'cron')) {
+                    if (debugging()) {
+                        mtrace('Running '.$object.'->cron()');
+                    }
+                    $class->cron();
+                }
+            }
+
+            if (debugging()) {
+                mtrace('Aggregating completions');
+            }
+
+            // Save time started.
+            $timestarted = time();
+
+            // Grab all criteria and their associated criteria completions.
+            $sql = 'SELECT DISTINCT c.id AS course, cr.id AS criteriaid, crc.userid AS userid,
+                                    cr.criteriatype AS criteriatype, cc.timecompleted AS timecompleted
+                      FROM {course_completion_criteria} cr
+                INNER JOIN {course} c ON cr.course = c.id
+                INNER JOIN {course_completions} crc ON crc.course = c.id
+                 LEFT JOIN {course_completion_crit_compl} cc ON cc.criteriaid = cr.id AND crc.userid = cc.userid
+                     WHERE c.enablecompletion = 1
+                       AND crc.timecompleted IS NULL
+                       AND crc.reaggregate > 0
+                       AND crc.reaggregate < :timestarted
+                  ORDER BY course, userid';
+            $rs = $DB->get_recordset_sql($sql, ['timestarted' => $timestarted]);
+
+            // Check if result is empty.
+            if (!$rs->valid()) {
+                $rs->close();
+                return;
+            }
+
+            $currentuser = null;
+            $currentcourse = null;
+            $completions = [];
+            while (1) {
+                // Grab records for current user/course.
+                foreach ($rs as $record) {
+                    // If we are still grabbing the same users completions.
+                    if ($record->userid === $currentuser && $record->course === $currentcourse) {
+                        $completions[$record->criteriaid] = $record;
+                    } else {
+                        break;
+                    }
+                }
+
+                // Aggregate.
+                if (!empty($completions)) {
+                    if (debugging()) {
+                        mtrace('Aggregating completions for user ' . $currentuser . ' in course ' . $currentcourse);
+                    }
+
+                    // Get course info object.
+                    $info = new \completion_info((object)['id' => $currentcourse]);
+
+                    // Setup aggregation.
+                    $overall = $info->get_aggregation_method();
+                    $activity = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ACTIVITY);
+                    $prerequisite = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_COURSE);
+                    $role = $info->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ROLE);
+
+                    $overallstatus = null;
+                    $activitystatus = null;
+                    $prerequisitestatus = null;
+                    $rolestatus = null;
+
+                    // Get latest timecompleted.
+                    $timecompleted = null;
+
+                    // Check each of the criteria.
+                    foreach ($completions as $params) {
+                        $timecompleted = max($timecompleted, $params->timecompleted);
+                        $completion = new \completion_criteria_completion((array)$params, false);
+
+                        // Handle aggregation special cases.
+                        if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
+                            completion_cron_aggregate($activity, $completion->is_complete(), $activitystatus);
+                        } else if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_COURSE) {
+                            completion_cron_aggregate($prerequisite, $completion->is_complete(), $prerequisitestatus);
+                        } else if ($params->criteriatype == COMPLETION_CRITERIA_TYPE_ROLE) {
+                            completion_cron_aggregate($role, $completion->is_complete(), $rolestatus);
+                        } else {
+                            completion_cron_aggregate($overall, $completion->is_complete(), $overallstatus);
+                        }
+                    }
+
+                    // Include role criteria aggregation in overall aggregation.
+                    if ($rolestatus !== null) {
+                        completion_cron_aggregate($overall, $rolestatus, $overallstatus);
+                    }
+
+                    // Include activity criteria aggregation in overall aggregation.
+                    if ($activitystatus !== null) {
+                        completion_cron_aggregate($overall, $activitystatus, $overallstatus);
+                    }
+
+                    // Include prerequisite criteria aggregation in overall aggregation.
+                    if ($prerequisitestatus !== null) {
+                        completion_cron_aggregate($overall, $prerequisitestatus, $overallstatus);
+                    }
+
+                    // If aggregation status is true, mark course complete for user.
+                    if ($overallstatus) {
+                        if (debugging()) {
+                            mtrace('Marking complete');
+                        }
+
+                        $ccompletion = new \completion_completion([
+                            'course' => $params->course,
+                            'userid' => $params->userid
+                        ]);
+                        $ccompletion->mark_complete($timecompleted);
+                    }
+                }
+
+                // If this is the end of the recordset, break the loop.
+                if (!$rs->valid()) {
+                    $rs->close();
+                    break;
+                }
+
+                // New/next user, update user details, reset completions.
+                $currentuser = $record->userid;
+                $currentcourse = $record->course;
+                $completions = [];
+                $completions[$record->criteriaid] = $record;
+            }
+
+            // Mark all users as aggregated.
+            $sql = "UPDATE {course_completions}
+                       SET reaggregate = 0
+                     WHERE reaggregate < :timestarted
+                       AND reaggregate > 0";
+            $DB->execute($sql, ['timestarted' => $timestarted]);
         }
     }
 
index cac848f..db3abeb 100644 (file)
@@ -1370,3 +1370,26 @@ class completion_info {
             $CFG->wwwroot.'/course/view.php?id='.$this->course->id,null,$error);
     }
 }
+
+/**
+ * Aggregate criteria status's as per configured aggregation method.
+ *
+ * @param int $method COMPLETION_AGGREGATION_* constant.
+ * @param bool $data Criteria completion status.
+ * @param bool|null $state Aggregation state.
+ */
+function completion_cron_aggregate($method, $data, &$state) {
+    if ($method == COMPLETION_AGGREGATION_ALL) {
+        if ($data && $state !== false) {
+            $state = true;
+        } else {
+            $state = false;
+        }
+    } else if ($method == COMPLETION_AGGREGATION_ANY) {
+        if ($data) {
+            $state = true;
+        } else if (!$data && $state === null) {
+            $state = false;
+        }
+    }
+}
index 567a146..71ebe33 100644 (file)
@@ -384,4 +384,11 @@ $definitions = array(
         'simpledata' => true,
         'ttl' => 1800
     ),
+
+    // Caches the first time we analysed models' analysables.
+    'modelfirstanalyses' => array(
+        'mode' => cache_store::MODE_REQUEST,
+        'simplekeys' => true,
+        'simpledata' => true,
+    ),
 );
index 434974d..e901acb 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20190403" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20190412" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <FIELD NAME="modelid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="action" TYPE="char" LENGTH="50" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="analysableid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="firstanalysis" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="timeanalysed" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
       </FIELDS>
       <KEYS>
index dea5215..744561c 100644 (file)
@@ -3128,12 +3128,13 @@ function xmldb_main_upgrade($oldversion) {
             $mua->timecreated = $message->timeread;
 
             $DB->insert_record('message_user_actions', $mua);
+
+            // The self-conversation message has been migrated. Delete the record from the legacy table as soon as possible
+            // to avoid migrate it twice.
+            $DB->delete_records('message_read', ['id' => $message->id]);
         }
         $legacyselfmessagesrs->close();
 
-        // We can now delete the records from legacy table because the self-conversations have been migrated from the legacy tables.
-        $DB->delete_records_select('message_read', $select);
-
         // STEP 3. For existing users without self-conversations, create and star it.
 
         // Get all the users without a self-conversation.
@@ -3270,5 +3271,35 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2019042300.03);
     }
 
+    if ($oldversion < 2019042700.01) {
+
+        // Define field firstanalysis to be added to analytics_used_analysables.
+        $table = new xmldb_table('analytics_used_analysables');
+
+        // Declaring it as null initially (although it is NOT NULL).
+        $field = new xmldb_field('firstanalysis', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'analysableid');
+
+        // Conditionally launch add field firstanalysis.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+
+            // Set existing values to the current timeanalysed value.
+            $recordset = $DB->get_recordset('analytics_used_analysables');
+            foreach ($recordset as $record) {
+                $record->firstanalysis = $record->timeanalysed;
+                $DB->update_record('analytics_used_analysables', $record);
+            }
+            $recordset->close();
+
+            // Now make the field 'NOT NULL'.
+            $field = new xmldb_field('firstanalysis', XMLDB_TYPE_INTEGER, '10',
+                null, XMLDB_NOTNULL, null, null, 'analysableid');
+            $dbman->change_field_notnull($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2019042700.01);
+    }
+
     return true;
 }
index b75d7d8..f651398 100644 (file)
@@ -64,8 +64,7 @@ class MoodleQuickForm_course extends MoodleQuickForm_autocomplete {
      *
      * @param string $elementname Element name
      * @param mixed $elementlabel Label(s) for an element
-     * @param array $options Options to control the element's display
-     *                       Valid options are:
+     * @param mixed $attributes Array of typical HTML attributes plus additional options, such as:
      *                       'multiple' - boolean multi select
      *                       'exclude' - array or int, list of course ids to never show
      *                       'requiredcapabilities' - array of capabilities. Uses ANY to combine them.
@@ -73,46 +72,44 @@ class MoodleQuickForm_course extends MoodleQuickForm_autocomplete {
      *                       'includefrontpage' - boolean Enables the frontpage to be selected.
      *                       'onlywithcompletion' - only courses where completion is enabled
      */
-    public function __construct($elementname = null, $elementlabel = null, $options = array()) {
-        if (isset($options['multiple'])) {
-            $this->multiple = $options['multiple'];
+    public function __construct($elementname = null, $elementlabel = null, $attributes = array()) {
+        if (!is_array($attributes)) {
+            $attributes = [];
         }
-        if (isset($options['exclude'])) {
-            $this->exclude = $options['exclude'];
+        if (isset($attributes['multiple'])) {
+            $this->multiple = $attributes['multiple'];
+        }
+        if (isset($attributes['exclude'])) {
+            $this->exclude = $attributes['exclude'];
             if (!is_array($this->exclude)) {
                 $this->exclude = array($this->exclude);
             }
+            unset($attributes['exclude']);
         }
-        if (isset($options['requiredcapabilities'])) {
-            $this->requiredcapabilities = $options['requiredcapabilities'];
+        if (isset($attributes['requiredcapabilities'])) {
+            $this->requiredcapabilities = $attributes['requiredcapabilities'];
+            unset($attributes['requiredcapabilities']);
         }
-        if (isset($options['limittoenrolled'])) {
-            $this->limittoenrolled = $options['limittoenrolled'];
+        if (isset($attributes['limittoenrolled'])) {
+            $this->limittoenrolled = $attributes['limittoenrolled'];
+            unset($attributes['limittoenrolled']);
         }
 
-        $validattributes = array(
+        $attributes += array(
             'ajax' => 'core/form-course-selector',
             'data-requiredcapabilities' => implode(',', $this->requiredcapabilities),
             'data-exclude' => implode(',', $this->exclude),
             'data-limittoenrolled' => (int)$this->limittoenrolled
         );
-        if ($this->multiple) {
-            $validattributes['multiple'] = 'multiple';
-        }
-        if (isset($options['noselectionstring'])) {
-            $validattributes['noselectionstring'] = $options['noselectionstring'];
-        }
-        if (isset($options['placeholder'])) {
-            $validattributes['placeholder'] = $options['placeholder'];
-        }
-        if (!empty($options['includefrontpage'])) {
-            $validattributes['data-includefrontpage'] = SITEID;
+        if (!empty($attributes['includefrontpage'])) {
+            $attributes['data-includefrontpage'] = SITEID;
+            unset($attributes['includefrontpage']);
         }
         if (!empty($options['onlywithcompletion'])) {
             $validattributes['data-onlywithcompletion'] = 1;
         }
 
-        parent::__construct($elementname, $elementlabel, array(), $validattributes);
+        parent::__construct($elementname, $elementlabel, array(), $attributes);
     }
 
     /**
index af191a0..62670e5 100644 (file)
@@ -296,9 +296,7 @@ class MoodleQuickForm_filemanager extends HTML_QuickForm_element implements temp
         $output = $PAGE->get_renderer('core', 'files');
         $html .= $output->render($fm);
 
-        $html .= html_writer::empty_tag('input', array('value' => $draftitemid, 'name' => $elname, 'type' => 'hidden'));
-        // label element needs 'for' attribute work
-        $html .= html_writer::empty_tag('input', array('value' => '', 'id' => 'id_'.$elname, 'type' => 'hidden'));
+        $html .= html_writer::empty_tag('input', array('value' => $draftitemid, 'name' => $elname, 'type' => 'hidden', 'id' => $id));
 
         if (!empty($options->accepted_types) && $options->accepted_types != '*') {
             $html .= html_writer::tag('p', get_string('filesofthesetypes', 'form'));
index d13a9cd..fa48a7a 100644 (file)
@@ -8,10 +8,10 @@ M.form_filepicker.callback = function(params) {
     html += '<div class="dndupload-progressbars"></div>';
     M.form_filepicker.Y.one('#file_info_'+params['client_id'] + ' .filepicker-filename').setContent(html);
     //When file is added then set status of global variable to true
-    var elementname = M.core_filepicker.instances[params['client_id']].options.elementname;
-    M.form_filepicker.instances[elementname].fileadded = true;
+    var elementid = M.core_filepicker.instances[params['client_id']].options.elementid;
+    M.form_filepicker.instances[elementid].fileadded = true;
     //generate event to indicate changes which will be used by disable if or validation code
-    M.form_filepicker.Y.one('#id_'+elementname).simulate('change');
+    M.form_filepicker.Y.one('#'+elementid).simulate('change');
 };
 
 /**
@@ -22,8 +22,8 @@ M.form_filepicker.init = function(Y, options) {
     M.form_filepicker.Y = Y;
 
     //For client side validation, initialize file status for this filepicker
-    M.form_filepicker.instances[options.elementname] = {};
-    M.form_filepicker.instances[options.elementname].fileadded = false;
+    M.form_filepicker.instances[options.elementid] = {};
+    M.form_filepicker.instances[options.elementid].fileadded = false;
 
     //Set filepicker callback
     options.formcallback = M.form_filepicker.callback;
index a376459..35b08fe 100644 (file)
@@ -155,7 +155,7 @@ class MoodleQuickForm_filepicker extends HTML_QuickForm_input implements templat
         $args->maxbytes = $this->_options['maxbytes'];
         $args->context = $PAGE->context;
         $args->buttonname = $elname.'choose';
-        $args->elementname = $elname;
+        $args->elementid = $id;
 
         $html = $this->_getTabs();
         $fp = new file_picker($args);
index cd9477c..dc520b7 100644 (file)
@@ -110,19 +110,21 @@ class MoodleQuickForm_listing extends HTML_QuickForm_input {
     function toHtml() {
         global $CFG, $PAGE;
 
+        $this->_generateId();
+        $elementid = $this->getAttribute('id');
         $mainhtml = html_writer::tag('div', $this->items[$this->getValue()]->mainhtml,
-                array('id' => $this->getName().'_items_main', 'class' => 'formlistingmain'));
+                array('id' => $elementid . '_items_main', 'class' => 'formlistingmain'));
 
         // Add the main div containing the selected item (+ the caption: "More items").
         $html = html_writer::tag('div', $mainhtml .
                     html_writer::tag('div', $this->showall,
-                        array('id' => $this->getName().'_items_caption', 'class' => 'formlistingmore')),
-                    array('id'=>$this->getName().'_items', 'class' => 'formlisting hide'));
+                        array('id' => $elementid . '_items_caption', 'class' => 'formlistingmore')),
+                    array('id' => $elementid . '_items', 'class' => 'formlisting hide'));
 
         // Add collapsible region: all the items.
         $itemrows = '';
         $html .= html_writer::tag('div', $itemrows,
-                array('id' => $this->getName().'_items_all', 'class' => 'formlistingall'));
+                array('id' => $elementid . '_items_all', 'class' => 'formlistingall'));
 
         // Add radio buttons for non javascript support.
         $radiobuttons = '';
@@ -139,19 +141,19 @@ class MoodleQuickForm_listing extends HTML_QuickForm_input {
 
         // Container for the hidden hidden input which will contain the selected item.
         $html .= html_writer::tag('div', $radiobuttons,
-                array('id' => 'formlistinginputcontainer_' . $this->getName(), 'class' => 'formlistinginputcontainer'));
+                array('id' => 'formlistinginputcontainer_' . $elementid, 'class' => 'formlistinginputcontainer'));
 
         $module = array('name'=>'form_listing', 'fullpath'=>'/lib/form/yui/listing/listing.js',
             'requires'=>array('node', 'event', 'transition', 'escape'));
 
         $PAGE->requires->js_init_call('M.form_listing.init',
                  array(array(
-                'elementid' => $this->getName().'_items',
+                'elementid' => $elementid.'_items',
                 'hideall' => $this->hideall,
                 'showall' => $this->showall,
                 'hiddeninputid' => $this->getAttribute('id'),
                 'items' => $this->items,
-                'inputname' => $this->getName(),
+                'inputname' => $elementid,
                 'currentvalue' => $this->getValue())), true, $module);
 
         return $html;
index e0b235a..447a822 100644 (file)
@@ -82,6 +82,7 @@ trait templatable_form_element {
         $context['type'] = $this->getType();
         $context['attributes'] = implode(' ', $otherattributes);
         $context['emptylabel'] = ($this->getLabel() === '');
+        $context['iderror'] = preg_replace('/^id_/', 'id_error_', $context['id']);
 
         // Elements with multiple values need array syntax.
         if ($this->getAttribute('multiple')) {
index 94d2fd6..0584acc 100644 (file)
@@ -15,7 +15,7 @@
     {{#element.checked}}checked{{/element.checked}}
     size="{{element.size}}"
     {{#error}}
-        autofocus aria-describedby="id_error_{{element.name}}"
+        autofocus aria-describedby="{{element.iderror}}"
     {{/error}}
     {{#element.frozen}}
         disabled
@@ -27,7 +27,7 @@
     <em>{{{.}}}</em>
 {{/text}}
 {{{helpbutton}}}
-<span class="form-control-feedback invalid-feedback" id="id_error_{{element.name}}" {{#error}} style="display: block;"{{/error}}>
+<span class="form-control-feedback invalid-feedback" id="{{element.iderror}}" {{#error}} style="display: block;"{{/error}}>
     {{{error}}}
 </span>
 {{^element.frozen}}
index 8e1a472..e775939 100644 (file)
@@ -25,7 +25,7 @@
                 {{/element.selectedvalue}}
                 id="{{element.id}}" {{#element.checked}}checked{{/element.checked}}
                 {{#error}}
-                    autofocus aria-describedby="id_error_{{element.name}}"
+                    autofocus aria-describedby="{{element.iderror}}"
                 {{/error}}
                 {{#element.frozen}}
                     disabled
@@ -43,7 +43,7 @@
                 {{{helpbutton}}}
             </span>
         </div>
-        <div class="form-control-feedback invalid-feedback" id="id_error_{{element.name}}" {{#error}} style="display: block;"{{/error}}>
+        <div class="form-control-feedback invalid-feedback" id="{{element.iderror}}" {{#error}} style="display: block;"{{/error}}>
             {{{error}}}
         </div>
     </div>
index 175280e..127fa77 100644 (file)
@@ -8,7 +8,7 @@
             id="{{element.id}}"
             {{#element.multiple}}multiple{{/element.multiple}}
             {{#error}}
-                autofocus aria-describedby="id_error_{{element.name}}"
+                autofocus aria-describedby="{{element.iderror}}"
             {{/error}}
             {{{element.attributes}}} >
             {{#element.options}}
index 8736f0a..ead064d 100644 (file)
@@ -8,7 +8,7 @@
             id="{{element.id}}"
             {{#element.multiple}}multiple{{/element.multiple}}
             {{#error}}
-                autofocus aria-describedby="id_error_{{element.name}}"
+                autofocus aria-describedby="{{element.iderror}}"
             {{/error}}
             {{{element.attributes}}} >
             {{#element.options}}
index fff81f9..3806649 100644 (file)
@@ -7,7 +7,7 @@
                 id="{{element.id}}"
                 type="button"
                 {{#error}}
-                    autofocus aria-describedby="id_error_{{element.name}}"
+                    autofocus aria-describedby="{{element.iderror}}"
                 {{/error}}
                 {{{element.attributes}}}
                 >
index 3ce46e2..c3e29cd 100644 (file)
@@ -7,7 +7,7 @@
                 id="{{element.id}}"
                 type="button"
                 {{#error}}
-                    autofocus aria-describedby="id_error_{{element.name}}"
+                    autofocus aria-describedby="{{element.iderror}}"
                 {{/error}}
                 {{{element.attributes}}}>
                 {{{element.value}}}
index 279affb..994ebc5 100644 (file)
@@ -15,7 +15,7 @@
     {{#element.checked}}checked{{/element.checked}}
     size="{{element.size}}"
     {{#error}}
-        autofocus aria-describedby="id_error_{{element.name}}"
+        autofocus aria-describedby="{{element.iderror}}"
     {{/error}}
     {{#element.frozen}}
         disabled
@@ -27,7 +27,7 @@
     <em>{{{.}}}</em>
 {{/text}}
 {{{helpbutton}}}
-<span class="form-control-feedback invalid-feedback" id="id_error_{{element.name}}" {{#error}} style="display: block;"{{/error}}>
+<span class="form-control-feedback invalid-feedback" id="{{element.iderror}}" {{#error}} style="display: block;"{{/error}}>
     {{{error}}}
 </span>
 {{^element.frozen}}
index 9277f0b..7852905 100644 (file)
@@ -25,7 +25,7 @@
                 {{/element.value}}
                 id="{{element.id}}" {{#element.checked}}checked{{/element.checked}}
                 {{#error}}
-                    autofocus aria-describedby="id_error_{{element.name}}"
+                    autofocus aria-describedby="{{element.iderror}}"
                 {{/error}}
                 {{#element.frozen}}
                     disabled
@@ -43,7 +43,7 @@
                 {{{helpbutton}}}
             </span>
         </div>
-        <div class="form-control-feedback invalid-feedback" id="id_error_{{element.name}}" {{#error}} style="display: block;"{{/error}}>
+        <div class="form-control-feedback invalid-feedback" id="{{element.iderror}}" {{#error}} style="display: block;"{{/error}}>
             {{{error}}}
         </div>
     </div>
index 09f779f..fdd5cdb 100644 (file)
@@ -8,7 +8,7 @@
                 value="{{element.value}}"
                 size="{{element.size}}"
                 {{#error}}
-                    autofocus aria-describedby="id_error_{{element.name}}"
+                    autofocus aria-describedby="{{element.iderror}}"
                 {{/error}} {{{element.attributes}}}>
         {{/element.frozen}}
     {{/element}}
index 2fa7e5f..999ee72 100644 (file)
@@ -57,7 +57,7 @@
                         class="form-control d-inline-block {{# error }}is-invalid{{/ error }}"
                         data-size="{{ element.size }}"
                         {{# error }}
-                            autofocus aria-describedby="id_error_{{ element.name }}"
+                            autofocus aria-describedby="{{ element.iderror }}"
                         {{/ error }}
                         {{{ attributes }}}
                         >
index bac7a26..7f99a2e 100644 (file)
@@ -19,7 +19,7 @@
     {{#element.checked}}checked{{/element.checked}}
     {{#element.frozen}}disabled{{/element.frozen}}
     {{#error}}
-        autofocus aria-describedby="id_error_{{element.name}}"
+        autofocus aria-describedby="{{element.iderror}}"
     {{/error}}
     {{{element.attributes}}} >
     {{{label}}}
@@ -28,7 +28,7 @@
     <em>{{{.}}}</em>
 {{/text}}
 {{{helpbutton}}}
-<span class="form-control-feedback invalid-feedback" id="id_error_{{element.name}}" {{#error}} style="display: block;"{{/error}}>
+<span class="form-control-feedback invalid-feedback" id="{{element.iderror}}" {{#error}} style="display: block;"{{/error}}>
     {{{error}}}
 </span>
 {{^element.frozen}}
index 87b0472..b052bda 100644 (file)
@@ -30,7 +30,7 @@
                     {{#element.checked}}checked{{/element.checked}}
                     {{#element.frozen}}disabled{{/element.frozen}}
                     {{#error}}
-                        autofocus aria-describedby="id_error_{{element.name}}"
+                        autofocus aria-describedby="{{element.iderror}}"
                     {{/error}} {{{element.attributes}}} >
                 {{#text}}
                     {{{.}}}
@@ -44,7 +44,7 @@
             {{{helpbutton}}}
             </span>
         </div>
-        <div class="form-control-feedback invalid-feedback" id="id_error_{{element.name}}" {{#error}} style="display: block;"{{/error}}>
+        <div class="form-control-feedback invalid-feedback" id="{{element.iderror}}" {{#error}} style="display: block;"{{/error}}>
             {{{error}}}
         </div>
     </div>
index bd5ae84..1dcb34f 100644 (file)
@@ -11,7 +11,7 @@
         id="{{element.id}}"
         {{#element.multiple}}multiple size="{{element.size}}"{{/element.multiple}}
         {{#error}}
-            autofocus aria-describedby="id_error_{{element.name}}"
+            autofocus aria-describedby="{{element.iderror}}"
         {{/error}}
         {{{element.attributes}}} >
         {{#element.options}}
index 4966408..f701053 100644 (file)
             id="{{element.id}}"
             {{#element.multiple}}multiple size="{{element.size}}"{{/element.multiple}}
             {{#error}}
-                autofocus aria-describedby="id_error_{{element.name}}"
+                autofocus aria-describedby="{{element.iderror}}"
             {{/error}}
             {{{element.attributes}}} >
             {{#element.options}}
index 1fbb0c1..0164335 100644 (file)
@@ -5,7 +5,7 @@
         id="{{element.id}}"
         {{#element.multiple}}multiple{{/element.multiple}}
         {{#error}}
-            autofocus aria-describedby="id_error_{{element.name}}"
+            autofocus aria-describedby="{{element.iderror}}"
         {{/error}}
         {{{element.attributes}}} >
         {{#element.optiongroups}}
index 783f028..15738f7 100644 (file)
@@ -5,7 +5,7 @@
             id="{{element.id}}"
             {{#element.multiple}}multiple{{/element.multiple}}
             {{#error}}
-                autofocus aria-describedby="id_error_{{element.name}}"
+                autofocus aria-describedby="{{element.iderror}}"
             {{/error}}
             {{{element.attributes}}} >
             {{#element.optiongroups}}
index a0bf6d0..b49e0a5 100644 (file)
@@ -5,7 +5,7 @@
             id="{{element.id}}"
             {{#element.multiple}}multiple{{/element.multiple}}
             {{#error}}
-                autofocus aria-describedby="id_error_{{element.name}}"
+                autofocus aria-describedby="{{element.iderror}}"
             {{/error}}
             {{{element.attributes}}} >
             {{#element.options}}
index cd8b8fa..058e414 100644 (file)
@@ -13,7 +13,7 @@
                 name="{{element.name}}"
                 id="{{element.id}}"
                 {{#error}}
-                    autofocus aria-describedby="id_error_{{element.name}}"
+                    autofocus aria-describedby="{{element.iderror}}"
                 {{/error}}
                 value="{{element.value}}"
                 {{{element.attributes}}} >
index e388742..eb12f78 100644 (file)
@@ -6,7 +6,7 @@
             id="{{element.id}}"
             {{#element.multiple}}multiple{{/element.multiple}}
             {{#error}}
-                autofocus aria-describedby="id_error_{{element.name}}"
+                autofocus aria-describedby="{{element.iderror}}"
             {{/error}}
             {{{element.attributes}}} >
             {{#element.options}}
diff --git