Merge branch 'MDL-41878-master' of git://github.com/andrewnicols/moodle
authorSam Hemelryk <sam@moodle.com>
Tue, 1 Oct 2013 19:14:36 +0000 (08:14 +1300)
committerSam Hemelryk <sam@moodle.com>
Tue, 1 Oct 2013 19:14:36 +0000 (08:14 +1300)
150 files changed:
admin/settings/courses.php
admin/tool/generator/tests/maketestcourse_test.php
admin/webservice/service.php
admin/webservice/service_users.php
backup/moodle2/backup_activity_task.class.php
backup/moodle2/backup_course_task.class.php
backup/moodle2/backup_root_task.class.php
backup/util/dbops/backup_controller_dbops.class.php
backup/util/helper/backup_cron_helper.class.php
badges/assertion.php
badges/badge.php
badges/classes/assertion.php [new file with mode: 0644]
badges/lib/bakerlib.php
badges/renderer.php
badges/tests/badgeslib_test.php
blocks/course_list/block_course_list.php
blocks/course_overview/renderer.php
cache/classes/definition.php
cache/classes/dummystore.php
cache/tests/cache_test.php
calendar/export.php
calendar/view.php
course/lib.php
course/modedit.php
course/renderer.php
course/yui/toolboxes/toolboxes.js
enrol/category/version.php
enrol/cohort/db/events.php
enrol/cohort/locallib.php
enrol/cohort/version.php
enrol/imsenterprise/lib.php
enrol/meta/version.php
files/renderer.php
grade/edit/tree/lib.php
grade/tests/edittreelib_test.php
lang/en/backup.php
lang/en/repository.php
lang/en/webservice.php
lib/badgeslib.php
lib/behat/behat_base.php
lib/classes/event/webservice_function_called.php [new file with mode: 0644]
lib/classes/event/webservice_login_failed.php [new file with mode: 0644]
lib/classes/event/webservice_service_created.php [new file with mode: 0644]
lib/classes/event/webservice_service_deleted.php [new file with mode: 0644]
lib/classes/event/webservice_service_updated.php [new file with mode: 0644]
lib/classes/event/webservice_service_user_added.php [new file with mode: 0644]
lib/classes/event/webservice_service_user_removed.php [new file with mode: 0644]
lib/classes/event/webservice_token_created.php [new file with mode: 0644]
lib/classes/event/webservice_token_sent.php [new file with mode: 0644]
lib/classes/user.php
lib/componentlib.class.php
lib/coursecatlib.php
lib/db/install.xml
lib/db/upgrade.php
lib/deprecatedlib.php
lib/editor/atto/plugins/link/lang/en/atto_link.php
lib/editor/atto/plugins/link/lib.php
lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-debug.js
lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-min.js
lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button.js
lib/editor/atto/plugins/link/yui/src/button/js/button.js
lib/editor/atto/styles.css
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/src/editor/js/editor.js
lib/eventslib.php
lib/filestorage/file_storage.php
lib/filestorage/stored_file.php
lib/filestorage/tests/file_storage_test.php
lib/moodlelib.php
lib/navigationlib.php
lib/phpunit/classes/advanced_testcase.php
lib/phpunit/classes/util.php
lib/phpunit/tests/advanced_test.php
lib/questionlib.php
lib/tests/componentlib_test.php
lib/tests/eventslib_test.php
lib/tests/filelib_test.php
lib/tests/rsslib_test.php
lib/tests/weblib_test.php
lib/upgrade.txt
login/token.php
message/lib.php
mod/chat/db/events.php
mod/chat/gui_header_js/jsupdate.php
mod/chat/gui_header_js/jsupdated.php
mod/chat/gui_header_js/users.php
mod/chat/lib.php
mod/chat/version.php
mod/feedback/show_entries.php
mod/forum/classes/observer.php
mod/forum/db/events.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/mod_form.php
mod/forum/settings.php
mod/forum/version.php
mod/quiz/lib.php
mod/quiz/report/attemptsreport_table.php
mod/quiz/report/statistics/classes/calculated.php [new file with mode: 0644]
mod/quiz/report/statistics/classes/calculator.php [new file with mode: 0644]
mod/quiz/report/statistics/db/install.php
mod/quiz/report/statistics/db/upgrade.php
mod/quiz/report/statistics/lib.php
mod/quiz/report/statistics/report.php
mod/quiz/report/statistics/statistics_form.php
mod/quiz/report/statistics/statistics_graph.php
mod/quiz/report/statistics/statistics_question_table.php
mod/quiz/report/statistics/statistics_table.php
mod/quiz/report/statistics/tests/statistics_test.php
mod/quiz/report/statistics/tests/stats_from_steps_walkthrough_test.php
mod/quiz/version.php
mod/scorm/lib.php
mod/upgrade.txt
mod/url/mod_form.php
mod/workshop/allocation/scheduled/version.php
mod/workshop/eval/best/lib.php
mod/workshop/eval/best/tests/lib_test.php
phpunit.xml.dist
question/classes/statistics/questions/calculated.php [new file with mode: 0644]
question/classes/statistics/questions/calculated_for_subquestion.php [new file with mode: 0644]
question/classes/statistics/questions/calculator.php [moved from question/engine/statistics.php with 50% similarity]
question/classes/statistics/responses/analyser.php [moved from question/engine/responseanalysis.php with 91% similarity]
question/engine/statisticslib.php
question/type/multianswer/questiontype.php
question/type/multichoice/backup/moodle2/backup_qtype_multichoice_plugin.class.php
question/type/multichoice/backup/moodle2/restore_qtype_multichoice_plugin.class.php
question/type/multichoice/db/install.xml
question/type/multichoice/db/upgrade.php
question/type/multichoice/questiontype.php
question/type/multichoice/tests/questiontype_test.php
question/type/multichoice/version.php
question/upgrade.txt
repository/upgrade.txt
theme/base/style/course.css
theme/base/style/filemanager.css
theme/bootstrapbase/config.php
theme/bootstrapbase/layout/maintenance.php [new file with mode: 0644]
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/less/moodle/filemanager.less
theme/bootstrapbase/less/moodle/modules.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/upgrade.txt
theme/clean/layout/maintenance.php [new file with mode: 0644]
user/edit.php
user/editadvanced.php
version.php
webservice/lib.php
webservice/tests/events.php [new file with mode: 0644]

index 377034b..4005c7c 100644 (file)
@@ -162,6 +162,7 @@ if ($hassiteconfig
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_userscompletion', new lang_string('generaluserscompletion','backup'), new lang_string('configgeneraluserscompletion','backup'), array('value'=>1, 'locked'=>0)));
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_logs', new lang_string('generallogs','backup'), new lang_string('configgenerallogs','backup'), array('value'=>0, 'locked'=>0)));
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_histories', new lang_string('generalhistories','backup'), new lang_string('configgeneralhistories','backup'), array('value'=>0, 'locked'=>0)));
+    $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_questionbank', new lang_string('generalquestionbank','backup'), new lang_string('configgeneralquestionbank','backup'), array('value'=>1, 'locked'=>0)));
     $ADMIN->add('backups', $temp);
 
     // Create a page for general import configuration and defaults.
@@ -236,6 +237,7 @@ if ($hassiteconfig
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_userscompletion', new lang_string('generaluserscompletion','backup'), new lang_string('configgeneraluserscompletion','backup'), 1));
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_logs', new lang_string('generallogs', 'backup'), new lang_string('configgenerallogs', 'backup'), 0));
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_histories', new lang_string('generalhistories','backup'), new lang_string('configgeneralhistories','backup'), 0));
+    $temp->add(new admin_setting_configcheckbox('backup/backup_auto_questionbank', new lang_string('generalquestionbank','backup'), new lang_string('configgeneralquestionbank','backup'), 1));
 
 
     //$temp->add(new admin_setting_configcheckbox('backup/backup_auto_messages', new lang_string('messages', 'message'), new lang_string('backupmessageshelp','message'), 0));
index b5afea0..a5150b1 100644 (file)
@@ -112,7 +112,6 @@ class tool_generator_maketestcourse_testcase extends advanced_testcase {
      * Creates an small test course with fixed data set and checks the used sections and users.
      */
     public function test_fixed_data_set() {
-        global $DB;
 
         $this->resetAfterTest();
         $this->setAdminUser();
@@ -127,14 +126,12 @@ class tool_generator_maketestcourse_testcase extends advanced_testcase {
 
         // Check module instances belongs to section 1.
         $instances = $modinfo->get_instances_of('page');
-        $npageinstances = count($instances);
         foreach ($instances as $instance) {
             $this->assertEquals(1, $instance->sectionnum);
         }
 
         // Users that started discussions are the same.
         $forums = $modinfo->get_instances_of('forum');
-        $nforuminstances = count($forums);
         $discussions = forum_get_discussions(reset($forums), 'd.timemodified ASC');
         $lastusernumber = 0;
         $discussionstarters = array();
index a933ec4..13f3c8d 100644 (file)
@@ -58,7 +58,12 @@ if ($action == 'delete' and confirm_sesskey() and $service and empty($service->c
     }
     //The user has confirmed the deletion, delete and redirect
     $webservicemanager->delete_service($service->id);
-    add_to_log(SITEID, 'webservice', 'delete', $returnurl, get_string('deleteservice', 'webservice', $service));
+    $params = array(
+        'objectid' => $service->id
+    );
+    $event = \core\event\webservice_service_deleted::create($params);
+    $event->add_record_snapshot('external_services', $service);
+    $event->trigger();
     redirect($returnurl);
 }
 
@@ -75,7 +80,12 @@ if ($mform->is_cancelled()) {
     //create operation
     if (empty($servicedata->id)) {
         $servicedata->id = $webservicemanager->add_external_service($servicedata);
-        add_to_log(SITEID, 'webservice', 'add', $returnurl, get_string('addservice', 'webservice', $servicedata));
+        $params = array(
+            'objectid' => $servicedata->id
+        );
+        $event = \core\event\webservice_service_updated::create($params);
+        $event->add_record_snapshot('external_services', $servicedata);
+        $event->trigger();
 
         //redirect to the 'add functions to service' page
         $addfunctionpage = new moodle_url(
@@ -85,7 +95,12 @@ if ($mform->is_cancelled()) {
     } else {
         //update operation
         $webservicemanager->update_external_service($servicedata);
-        add_to_log(SITEID, 'webservice', 'edit', $returnurl, get_string('editservice', 'webservice', $servicedata));
+        $params = array(
+            'objectid' => $servicedata->id
+        );
+        $event = \core\event\webservice_service_created::create($params);
+        $event->add_record_snapshot('external_services', $servicedata);
+        $event->trigger();
     }
 
     redirect($returnurl);
index ad67c66..907a4cc 100644 (file)
@@ -57,8 +57,13 @@ if (optional_param('add', false, PARAM_BOOL) && confirm_sesskey()) {
             $serviceuser->externalserviceid = $id;
             $serviceuser->userid = $adduser->id;
             $webservicemanager->add_ws_authorised_user($serviceuser);
-            add_to_log(SITEID, 'core', 'assign', $CFG->admin . '/webservice/service_users.php?id='
-                    . $id, 'add', '', $adduser->id);
+
+            $params = array(
+                'objectid' => $serviceuser->externalserviceid,
+                'relateduserid' => $serviceuser->userid
+            );
+            $event = \core\event\webservice_service_user_added::create($params);
+            $event->trigger();
         }
         $potentialuserselector->invalidate_selected_users();
         $alloweduserselector->invalidate_selected_users();
@@ -71,8 +76,13 @@ if (optional_param('remove', false, PARAM_BOOL) && confirm_sesskey()) {
     if (!empty($userstoremove)) {
         foreach ($userstoremove as $removeuser) {
             $webservicemanager->remove_ws_authorised_user($removeuser, $id);
-            add_to_log(SITEID, 'core', 'assign', $CFG->admin . '/webservice/service_users.php?id='
-                    . $id, 'remove', '', $removeuser->id);
+
+            $params = array(
+                'objectid' => $id,
+                'relateduserid' => $removeuser->id
+            );
+            $event = \core\event\webservice_service_user_removed::create($params);
+            $event->trigger();
         }
         $potentialuserselector->invalidate_selected_users();
         $alloweduserselector->invalidate_selected_users();
index a1ada38..2dc9353 100644 (file)
@@ -246,6 +246,8 @@ abstract class backup_activity_task extends backup_task {
      * Defines the common setting that any backup activity will have
      */
     protected function define_settings() {
+        global $CFG;
+        require_once($CFG->libdir.'/questionlib.php');
 
         // All the settings related to this activity will include this prefix
         $settingprefix = $this->modulename . '_' . $this->moduleid . '_';
@@ -264,6 +266,12 @@ abstract class backup_activity_task extends backup_task {
         // Look for "activities" root setting
         $activities = $this->plan->get_setting('activities');
         $activities->add_dependency($activity_included);
+
+        if (question_module_uses_questions($this->modulename)) {
+            $questionbank = $this->plan->get_setting('questionbank');
+            $questionbank->add_dependency($activity_included);
+        }
+
         // Look for "section_included" section setting (if exists)
         $settingname = 'section_' . $this->sectionid . '_included';
         if ($this->plan->setting_exists($settingname)) {
index 5d4cd9b..7e37f99 100644 (file)
@@ -90,8 +90,10 @@ class backup_course_task extends backup_task {
         // course->defaultgroupingid
         $this->add_step(new backup_annotate_groups_from_groupings('annotate_groups_from_groupings'));
 
-        // Annotate the question_categories belonging to the course context
-        $this->add_step(new backup_calculate_question_categories('course_question_categories'));
+        // Annotate the question_categories belonging to the course context (conditionally).
+        if ($this->get_setting_value('questionbank')) {
+            $this->add_step(new backup_calculate_question_categories('course_question_categories'));
+        }
 
         // Generate the roles file (optionally role assignments and always role overrides)
         $this->add_step(new backup_roles_structure_step('course_roles', 'roles.xml'));
index a4d1782..3e97f68 100644 (file)
@@ -152,5 +152,10 @@ class backup_root_task extends backup_task {
         $gradehistories->set_ui(new backup_setting_ui_checkbox($gradehistories, get_string('rootsettinggradehistories', 'backup')));
         $this->add_setting($gradehistories);
         $users->add_dependency($gradehistories);
+
+        // Define question bank inclusion setting.
+        $questionbank = new backup_generic_setting('questionbank', base_setting::IS_BOOLEAN, true);
+        $questionbank->set_ui(new backup_setting_ui_checkbox($questionbank, get_string('rootsettingquestionbank', 'backup')));
+        $this->add_setting($questionbank);
     }
 }
index 409be32..9fd95ac 100644 (file)
@@ -544,11 +544,18 @@ abstract class backup_controller_dbops extends backup_dbops {
             'backup_general_badges'             => 'badges',
             'backup_general_userscompletion'    => 'userscompletion',
             'backup_general_logs'               => 'logs',
-            'backup_general_histories'          => 'grade_histories'
+            'backup_general_histories'          => 'grade_histories',
+            'backup_general_questionbank'       => 'questionbank'
         );
         $plan = $controller->get_plan();
         foreach ($settings as $config=>$settingname) {
             $value = get_config('backup', $config);
+            if ($value === false) {
+                // Ignore this because the config has not been set. get_config
+                // returns false if a setting doesn't exist, '0' is returned when
+                // the configuration is set to false.
+                continue;
+            }
             $locked = (get_config('backup', $config.'_locked') == true);
             if ($plan->setting_exists($settingname)) {
                 $setting = $plan->get_setting($settingname);
index 5913324..97d3334 100644 (file)
@@ -386,11 +386,14 @@ abstract class backup_cron_automated_helper {
                 'badges' => 'backup_auto_badges',
                 'completion_information' => 'backup_auto_userscompletion',
                 'logs' => 'backup_auto_logs',
-                'histories' => 'backup_auto_histories'
+                'histories' => 'backup_auto_histories',
+                'questionbank' => 'backup_auto_questionbank'
             );
             foreach ($settings as $setting => $configsetting) {
                 if ($bc->get_plan()->setting_exists($setting)) {
-                    $bc->get_plan()->get_setting($setting)->set_value($config->{$configsetting});
+                    if (isset($config->{$configsetting})) {
+                        $bc->get_plan()->get_setting($setting)->set_value($config->{$configsetting});
+                    }
                 }
             }
 
index 24b8e60..0bd944e 100644 (file)
  */
 
 require_once(dirname(dirname(__FILE__)) . '/config.php');
-require_once($CFG->libdir . '/badgeslib.php');
 
 if (empty($CFG->enablebadges)) {
     print_error('badgesdisabled', 'badges');
 }
 
-$hash = required_param('b', PARAM_ALPHANUM);
+$hash = required_param('b', PARAM_ALPHANUM); // Issued badge unique hash for badge assertion.
+$action = optional_param('action', null, PARAM_BOOL); // Generates badge class if true.
 
-$badge = badges_get_issued_badge_info($hash);
+$assertion = new core_badges_assertion($hash);
+
+if (!is_null($action)) {
+    // Get badge class or issuer information depending on $action.
+    $json = ($action) ? $assertion->get_badge_class() : $assertion->get_issuer();
+} else {
+    // Otherwise, get badge assertion.
+    $json = $assertion->get_badge_assertion();
+}
 
 header('Content-type: application/json; charset=utf-8');
 
-echo json_encode($badge);
\ No newline at end of file
+echo json_encode($json);
index 339b147..c2f7a63 100644 (file)
@@ -35,8 +35,8 @@ $output = $PAGE->get_renderer('core', 'badges');
 
 $badge = new issued_badge($id);
 
-if ($bake && ($badge->recipient == $USER->id)) {
-    $name = str_replace(' ', '_', $badge->issued['badge']['name']) . '.png';
+if ($bake && ($badge->recipient->id == $USER->id)) {
+    $name = str_replace(' ', '_', $badge->badgeclass['name']) . '.png';
     ob_start();
     $file = badges_bake($id, $badge->badgeid);
     header('Content-Type: image/png');
@@ -50,8 +50,8 @@ $PAGE->set_pagelayout('base');
 $PAGE->set_title(get_string('issuedbadge', 'badges'));
 
 if (isloggedin()) {
-    $PAGE->set_heading($badge->issued['badge']['name']);
-    $PAGE->navbar->add($badge->issued['badge']['name']);
+    $PAGE->set_heading($badge->badgeclass['name']);
+    $PAGE->navbar->add($badge->badgeclass['name']);
     $url = new moodle_url('/badges/mybadges.php');
     navigation_node::override_active_url($url);
 }
diff --git a/badges/classes/assertion.php b/badges/classes/assertion.php
new file mode 100644 (file)
index 0000000..ba0efb5
--- /dev/null
@@ -0,0 +1,158 @@
+<?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/>.
+
+/**
+ * Badge assertion library.
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Open Badges Assertions specification 1.0 {@link https://github.com/mozilla/openbadges/wiki/Assertions}
+ *
+ * Badge asserion is defined by three parts:
+ * - Badge Assertion (information regarding a specific badge that was awarded to a badge earner)
+ * - Badge Class (general information about a badge and what it is intended to represent)
+ * - Issuer Class (general information of an issuing organisation)
+ */
+
+/**
+ * Class that represents badge assertion.
+ *
+ */
+class core_badges_assertion {
+    /** @var object Issued badge information from database */
+    private $_data;
+
+    /** @var moodle_url Issued badge url */
+    private $_url;
+
+    /**
+     * Constructs with issued badge unique hash.
+     *
+     * @param string $hash Badge unique hash from badge_issued table.
+     */
+    public function __construct($hash) {
+        global $DB;
+
+        $this->_data = $DB->get_record_sql('
+            SELECT
+                bi.dateissued,
+                bi.dateexpire,
+                bi.uniquehash,
+                u.email,
+                b.*,
+                bb.email as backpackemail
+            FROM
+                {badge} b
+                JOIN {badge_issued} bi
+                    ON b.id = bi.badgeid
+                JOIN {user} u
+                    ON u.id = bi.userid
+                LEFT JOIN {badge_backpack} bb
+                    ON bb.userid = bi.userid
+            WHERE ' . $DB->sql_compare_text('bi.uniquehash', 40) . ' = ' . $DB->sql_compare_text(':hash', 40),
+            array('hash' => $hash), IGNORE_MISSING);
+
+        $this->_url = new moodle_url('/badges/badge.php', array('hash' => $this->_data->uniquehash));
+    }
+
+    /**
+     * Get badge assertion.
+     *
+     * @return array Badge assertion.
+     */
+    public function get_badge_assertion() {
+        global $CFG;
+        $assertion = array();
+        if ($this->_data) {
+            $hash = $this->_data->uniquehash;
+            $email = empty($this->_data->backpackemail) ? $this->_data->email : $this->_data->backpackemail;
+            $assertionurl = new moodle_url('/badges/assertion.php', array('b' => $hash));
+            $classurl = new moodle_url('/badges/assertion.php', array('b' => $hash, 'action' => 1));
+
+            // Required.
+            $assertion['uid'] = $hash;
+            $assertion['recipient'] = array();
+            $assertion['recipient']['identity'] = 'sha256$' . hash('sha256', $email . $CFG->badges_badgesalt);
+            $assertion['recipient']['type'] = 'email'; // Currently the only supported type.
+            $assertion['recipient']['hashed'] = true; // We are always hashing recipient.
+            $assertion['recipient']['salt'] = $CFG->badges_badgesalt;
+            $assertion['badge'] = $classurl->out(false);
+            $assertion['verify'] = array();
+            $assertion['verify']['type'] = 'hosted'; // 'Signed' is not implemented yet.
+            $assertion['verify']['url'] = $assertionurl->out(false);
+            $assertion['issuedOn'] = $this->_data->dateissued;
+            // Optional.
+            $assertion['evidence'] = $this->_url->out(false); // Currently issued badge URL.
+            if (!empty($this->_data->dateexpire)) {
+                $assertion['expires'] = $this->_data->dateexpire;
+            }
+        }
+        return $assertion;
+    }
+
+    /**
+     * Get badge class information.
+     *
+     * @return array Badge Class information.
+     */
+    public function get_badge_class() {
+        $class = array();
+        if ($this->_data) {
+            if (empty($this->_data->courseid)) {
+                $context = context_system::instance();
+            } else {
+                $context = context_course::instance($this->_data->courseid);
+            }
+            $issuerurl = new moodle_url('/badges/assertion.php', array('b' => $this->_data->uniquehash, 'action' => 0));
+
+            // Required.
+            $class['name'] = $this->_data->name;
+            $class['description'] = $this->_data->description;
+            $class['image'] = moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $this->_data->id, '/', 'f1')->out(false);
+            $class['criteria'] = $this->_url->out(false); // Currently issued badge URL.
+            $class['issuer'] = $issuerurl->out(false);
+        }
+        return $class;
+    }
+
+    /**
+     * Get badge issuer information.
+     *
+     * @return array Issuer information.
+     */
+    public function get_issuer() {
+        $issuer = array();
+        if ($this->_data) {
+            // Required.
+            $issuer['name'] = $this->_data->issuername;
+            $issuer['url'] = $this->_data->issuerurl;
+            // Optional.
+            if (!empty($this->_data->issuercontact)) {
+                $issuer['email'] = $this->_data->issuercontact;
+            }
+        }
+        return $issuer;
+    }
+
+}
index 8a0c45e..9507bfa 100644 (file)
@@ -120,25 +120,11 @@ class PNG_MetaDataHandler
             debugging('Key is too big');
         }
 
-        if ($type == 'iTXt') {
-            // iTXt International textual data.
-            // Keyword:             1-79 bytes (character string)
-            // Null separator:      1 byte
-            // Compression flag:    1 byte
-            // Compression method:  1 byte
-            // Language tag:        0 or more bytes (character string)
-            // Null separator:      1 byte
-            // Translated keyword:  0 or more bytes
-            // Null separator:      1 byte
-            // Text:                0 or more bytes
-            $data = $key . "\000'json'\0''\0\"{'method': 'hosted', 'assertionUrl': '" . $value . "'}\"";
-        } else {
-            // tEXt Textual data.
-            // Keyword:        1-79 bytes (character string)
-            // Null separator: 1 byte
-            // Text:           n bytes (character string)
-            $data = $key . "\0" . $value;
-        }
+        // tEXt Textual data.
+        // Keyword:        1-79 bytes (character string)
+        // Null separator: 1 byte
+        // Text:           n bytes (character string)
+        $data = $key . "\0" . $value;
         $crc = pack("N", crc32($type . $data));
         $len = pack("N", strlen($data));
 
index de4894e..b3fa735 100644 (file)
@@ -282,24 +282,24 @@ class core_badges_renderer extends plugin_renderer_base {
         global $USER, $CFG, $DB;
         $issued = $ibadge->issued;
         $userinfo = $ibadge->recipient;
+        $badgeclass = $ibadge->badgeclass;
         $badge = new badge($ibadge->badgeid);
-        $today_date = date('Y-m-d');
-        $today = strtotime($today_date);
+        $now = time();
 
         $table = new html_table();
         $table->id = 'issued-badge-table';
 
         $imagetable = new html_table();
         $imagetable->attributes = array('class' => 'clearfix badgeissuedimage');
-        $imagetable->data[] = array(html_writer::empty_tag('img', array('src' => $issued['badge']['image'])));
+        $imagetable->data[] = array(html_writer::empty_tag('img', array('src' => $badgeclass['image'])));
         if ($USER->id == $userinfo->id && !empty($CFG->enablebadges)) {
             $imagetable->data[] = array($this->output->single_button(
-                        new moodle_url('/badges/badge.php', array('hash' => $ibadge->hash, 'bake' => true)),
+                        new moodle_url('/badges/badge.php', array('hash' => $issued['uid'], 'bake' => true)),
                         get_string('download'),
                         'POST'));
-            $expiration = isset($issued['expires']) ? strtotime($issued['expires']) : $today + 1;
-            if (!empty($CFG->badges_allowexternalbackpack) && ($expiration > $today) && badges_user_has_backpack($USER->id)) {
-                $assertion = new moodle_url('/badges/assertion.php', array('b' => $ibadge->hash));
+            $expiration = isset($issued['expires']) ? $issued['expires'] : $now + 86400;
+            if (!empty($CFG->badges_allowexternalbackpack) && ($expiration > $now) && badges_user_has_backpack($USER->id)) {
+                $assertion = new moodle_url('/badges/assertion.php', array('b' => $issued['uid']));
                 $action = new component_action('click', 'addtobackpack', array('assertion' => $assertion->out(false)));
                 $attributes = array(
                         'type'  => 'button',
@@ -339,24 +339,23 @@ class core_badges_renderer extends plugin_renderer_base {
 
         $datatable->data[] = array(get_string('bcriteria', 'badges'), self::print_badge_criteria($badge));
         $datatable->data[] = array($this->output->heading(get_string('issuancedetails', 'badges'), 3), '');
-        $datatable->data[] = array(get_string('dateawarded', 'badges'), $issued['issued_on']);
+        $datatable->data[] = array(get_string('dateawarded', 'badges'), userdate($issued['issuedOn']));
         if (isset($issued['expires'])) {
-            $expiration = strtotime($issued['expires']);
-            if ($expiration < $today) {
-                $cell = new html_table_cell($issued['expires'] . get_string('warnexpired', 'badges'));
+            if ($issued['expires'] < $now) {
+                $cell = new html_table_cell(userdate($issued['expires']) . get_string('warnexpired', 'badges'));
                 $cell->attributes = array('class' => 'notifyproblem warning');
                 $datatable->data[] = array(get_string('expirydate', 'badges'), $cell);
 
                 $image = html_writer::start_tag('div', array('class' => 'badge'));
-                $image .= html_writer::empty_tag('img', array('src' => $issued['badge']['image']));
+                $image .= html_writer::empty_tag('img', array('src' => $badgeclass['image']));
                 $image .= $this->output->pix_icon('i/expired',
-                                get_string('expireddate', 'badges', $issued['expires']),
+                                get_string('expireddate', 'badges', userdate($issued['expires'])),
                                 'moodle',
                                 array('class' => 'expireimage'));
                 $image .= html_writer::end_tag('div');
                 $imagetable->data[0] = array($image);
             } else {
-                $datatable->data[] = array(get_string('expirydate', 'badges'), $issued['expires']);
+                $datatable->data[] = array(get_string('expirydate', 'badges'), userdate($issued['expires']));
             }
         }
 
@@ -909,15 +908,15 @@ class issued_badge implements renderable {
     /** @var badge recipient */
     public $recipient;
 
+    /** @var badge class */
+    public $badgeclass;
+
     /** @var badge visibility to others */
     public $visible = 0;
 
     /** @var badge class */
     public $badgeid = 0;
 
-    /** @var issued badge unique hash */
-    public $hash = "";
-
     /**
      * Initializes the badge to display
      *
@@ -925,8 +924,10 @@ class issued_badge implements renderable {
      */
     public function __construct($hash) {
         global $DB;
-        $this->issued = badges_get_issued_badge_info($hash);
-        $this->hash = $hash;
+
+        $assertion = new core_badges_assertion($hash);
+        $this->issued = $assertion->get_badge_assertion();
+        $this->badgeclass = $assertion->get_badge_class();
 
         $rec = $DB->get_record_sql('SELECT userid, visible, badgeid
                 FROM {badge_issued}
index a3b2878..a571569 100644 (file)
@@ -35,6 +35,7 @@ class core_badgeslib_testcase extends advanced_testcase {
     protected $user;
     protected $module;
     protected $coursebadge;
+    protected $assertion;
 
     protected function setUp() {
         global $DB, $CFG;
@@ -52,6 +53,7 @@ class core_badgeslib_testcase extends advanced_testcase {
         $fordb->usermodified = $user->id;
         $fordb->issuername = "Test issuer";
         $fordb->issuerurl = "http://issuer-url.domain.co.nz";
+        $fordb->issuercontact = "issuer@example.com";
         $fordb->expiredate = null;
         $fordb->expireperiod = null;
         $fordb->type = BADGE_TYPE_SITE;
@@ -86,6 +88,10 @@ class core_badgeslib_testcase extends advanced_testcase {
         $fordb->status = BADGE_STATUS_ACTIVE;
 
         $this->coursebadge = $DB->insert_record('badge', $fordb, true);
+        $this->assertion = new stdClass();
+        $this->assertion->badge = '{"uid":"%s","recipient":{"identity":"%s","type":"email","hashed":true,"salt":"%s"},"badge":"%s","verify":{"type":"hosted","url":"%s"},"issuedOn":"%d","evidence":"%s"}';
+        $this->assertion->class = '{"name":"%s","description":"%s","image":"%s","criteria":"%s","issuer":"%s"}';
+        $this->assertion->issuer = '{"name":"%s","url":"%s","email":"%s"}';
     }
 
     public function test_create_badge() {
@@ -103,6 +109,7 @@ class core_badgeslib_testcase extends advanced_testcase {
         $this->assertEquals($badge->description, $cloned_badge->description);
         $this->assertEquals($badge->issuercontact, $cloned_badge->issuercontact);
         $this->assertEquals($badge->issuername, $cloned_badge->issuername);
+        $this->assertEquals($badge->issuercontact, $cloned_badge->issuercontact);
         $this->assertEquals($badge->issuerurl, $cloned_badge->issuerurl);
         $this->assertEquals($badge->expiredate, $cloned_badge->expiredate);
         $this->assertEquals($badge->expireperiod, $cloned_badge->expireperiod);
@@ -279,4 +286,34 @@ class core_badgeslib_testcase extends advanced_testcase {
         $this->assertDebuggingCalled('Error baking badge image!');
         $this->assertTrue($badge->is_issued($this->user->id));
     }
+
+    /**
+     * Test badges assertion generated when a badge is issued.
+     */
+    public function test_badges_assertion() {
+        $badge = new badge($this->coursebadge);
+        $this->assertFalse($badge->is_issued($this->user->id));
+
+        $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
+        $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
+        $criteria_overall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $badge->id));
+        $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address'));
+
+        $this->user->address = 'Test address';
+        user_update_user($this->user, false);
+        // Check if badge is awarded.
+        $this->assertDebuggingCalled('Error baking badge image!');
+        $awards = $badge->get_awards();
+        $this->assertCount(1, $awards);
+
+        // Get assertion.
+        $award = reset($awards);
+        $assertion = new core_badges_assertion($award->uniquehash);
+        $testassertion = $this->assertion;
+
+        // Make sure JSON strings have the same structure.
+        $this->assertStringMatchesFormat($testassertion->badge, json_encode($assertion->get_badge_assertion()));
+        $this->assertStringMatchesFormat($testassertion->class, json_encode($assertion->get_badge_class()));
+        $this->assertStringMatchesFormat($testassertion->issuer, json_encode($assertion->get_issuer()));
+    }
 }
index de35644..aaf6570 100644 (file)
@@ -40,7 +40,7 @@ class block_course_list extends block_list {
                     $coursecontext = context_course::instance($course->id);
                     $linkcss = $course->visible ? "" : " class=\"dimmed\" ";
                     $this->content->items[]="<a $linkcss title=\"" . format_string($course->shortname, true, array('context' => $coursecontext)) . "\" ".
-                               "href=\"$CFG->wwwroot/course/view.php?id=$course->id\">".$icon.format_string($course->fullname). "</a>";
+                               "href=\"$CFG->wwwroot/course/view.php?id=$course->id\">".$icon.format_string(get_course_display_name_for_list($course)). "</a>";
                 }
                 $this->title = get_string('mycourses');
             /// If we can update any course of the view all isn't hidden, show the view all courses link
@@ -79,7 +79,7 @@ class block_course_list extends block_list {
                         $this->content->items[]="<a $linkcss title=\""
                                    . format_string($course->shortname, true, array('context' => $coursecontext))."\" ".
                                    "href=\"$CFG->wwwroot/course/view.php?id=$course->id\">"
-                                   .$icon. format_string($course->fullname, true, array('context' => context_course::instance($course->id))) . "</a>";
+                                   .$icon. format_string(get_course_display_name_for_list($course), true, array('context' => context_course::instance($course->id))) . "</a>";
                     }
                 /// If we can update any course of the view all isn't hidden, show the view all courses link
                     if (has_capability('moodle/course:update', context_system::instance()) || empty($CFG->block_course_list_hideallcourseslink)) {
@@ -124,7 +124,7 @@ class block_course_list extends block_list {
                 $coursecontext = context_course::instance($course->id);
                 $this->content->items[]="<a title=\"" . format_string($course->shortname, true, array('context' => $coursecontext)) . "\" ".
                     "href=\"{$CFG->wwwroot}/auth/mnet/jump.php?hostid={$course->hostid}&amp;wantsurl=/course/view.php?id={$course->remoteid}\">"
-                    .$icon. format_string($course->fullname) . "</a>";
+                    .$icon. format_string(get_course_display_name_for_list($course)) . "</a>";
             }
             // if we listed courses, we are done
             return true;
index 6f33899..8db1e34 100644 (file)
@@ -108,7 +108,7 @@ class block_course_overview_renderer extends plugin_renderer_base {
                     $attributes['class'] = 'dimmed';
                 }
                 $courseurl = new moodle_url('/course/view.php', array('id' => $course->id));
-                $coursefullname = format_string($course->fullname, true, $course->id);
+                $coursefullname = format_string(get_course_display_name_for_list($course), true, $course->id);
                 $link = html_writer::link($courseurl, $coursefullname, $attributes);
                 $html .= $this->output->heading($link, 2, 'title');
             } else {
index 302bc17..281fa30 100644 (file)
@@ -824,7 +824,7 @@ class cache_definition {
             // Request caches should never use static acceleration - it just doesn't make sense.
             return false;
         }
-        return $this->staticacceleration || $this->mode === cache_store::MODE_SESSION;
+        return $this->staticacceleration;
     }
 
     /**
index 13c3017..55ee3ad 100644 (file)
@@ -113,7 +113,12 @@ class cachestore_dummy extends cache_store {
         //     store things in its static array.
         //   - If the definition is not using static acceleration then the cache loader won't try to store anything
         //     and we will need to store it here in order to make sure it is accessible.
-        $this->persist = !$definition->use_static_acceleration();
+        if ($definition->get_mode() !== self::MODE_APPLICATION) {
+            // Neither the request cache nor the session cache provide static acceleration.
+            $this->persist = true;
+        } else {
+            $this->persist = !$definition->use_static_acceleration();
+        }
     }
 
     /**
index 6295f55..2267afa 100644 (file)
@@ -1058,28 +1058,63 @@ class core_cache_testcase extends advanced_testcase {
      */
     public function test_disable_stores() {
         $instance = cache_config_phpunittest::instance();
-        $instance->phpunit_add_definition('phpunit/disabletest', array(
+        $instance->phpunit_add_definition('phpunit/disabletest1', array(
             'mode' => cache_store::MODE_APPLICATION,
             'component' => 'phpunit',
-            'area' => 'disabletest'
+            'area' => 'disabletest1'
+        ));
+        $instance->phpunit_add_definition('phpunit/disabletest2', array(
+            'mode' => cache_store::MODE_SESSION,
+            'component' => 'phpunit',
+            'area' => 'disabletest2'
+        ));
+        $instance->phpunit_add_definition('phpunit/disabletest3', array(
+            'mode' => cache_store::MODE_REQUEST,
+            'component' => 'phpunit',
+            'area' => 'disabletest3'
         ));
-        $cache = cache::make('phpunit', 'disabletest');
-        $this->assertInstanceOf('cache_phpunit_application', $cache);
-        $this->assertEquals('cachestore_file', $cache->phpunit_get_store_class());
 
-        $this->assertFalse($cache->get('test'));
-        $this->assertTrue($cache->set('test', 'test'));
-        $this->assertEquals('test', $cache->get('test'));
+        $caches = array(
+            'disabletest1' => cache::make('phpunit', 'disabletest1'),
+            'disabletest2' => cache::make('phpunit', 'disabletest2'),
+            'disabletest3' => cache::make('phpunit', 'disabletest3')
+        );
+
+        $this->assertInstanceOf('cache_phpunit_application', $caches['disabletest1']);
+        $this->assertInstanceOf('cache_phpunit_session', $caches['disabletest2']);
+        $this->assertInstanceOf('cache_phpunit_request', $caches['disabletest3']);
+
+        $this->assertEquals('cachestore_file', $caches['disabletest1']->phpunit_get_store_class());
+        $this->assertEquals('cachestore_session', $caches['disabletest2']->phpunit_get_store_class());
+        $this->assertEquals('cachestore_static', $caches['disabletest3']->phpunit_get_store_class());
+
+        foreach ($caches as $cache) {
+            $this->assertFalse($cache->get('test'));
+            $this->assertTrue($cache->set('test', 'test'));
+            $this->assertEquals('test', $cache->get('test'));
+        }
 
         cache_factory::disable_stores();
 
-        $cache = cache::make('phpunit', 'disabletest');
-        $this->assertInstanceOf('cache_phpunit_application', $cache);
-        $this->assertEquals('cachestore_dummy', $cache->phpunit_get_store_class());
+        $caches = array(
+            'disabletest1' => cache::make('phpunit', 'disabletest1'),
+            'disabletest2' => cache::make('phpunit', 'disabletest2'),
+            'disabletest3' => cache::make('phpunit', 'disabletest3')
+        );
 
-        $this->assertFalse($cache->get('test'));
-        $this->assertTrue($cache->set('test', 'test'));
-        $this->assertEquals('test', $cache->get('test'));
+        $this->assertInstanceOf('cache_phpunit_application', $caches['disabletest1']);
+        $this->assertInstanceOf('cache_phpunit_session', $caches['disabletest2']);
+        $this->assertInstanceOf('cache_phpunit_request', $caches['disabletest3']);
+
+        $this->assertEquals('cachestore_dummy', $caches['disabletest1']->phpunit_get_store_class());
+        $this->assertEquals('cachestore_dummy', $caches['disabletest2']->phpunit_get_store_class());
+        $this->assertEquals('cachestore_dummy', $caches['disabletest3']->phpunit_get_store_class());
+
+        foreach ($caches as $cache) {
+            $this->assertFalse($cache->get('test'));
+            $this->assertTrue($cache->set('test', 'test'));
+            $this->assertEquals('test', $cache->get('test'));
+        }
     }
 
     /**
@@ -1108,6 +1143,7 @@ class core_cache_testcase extends advanced_testcase {
         $cache = cache::make('phpunit', 'disable');
         $this->assertInstanceOf('cache_disabled', $cache);
 
+        // Test an application cache.
         $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'phpunit', 'disable');
         $this->assertInstanceOf('cache_disabled', $cache);
 
@@ -1118,6 +1154,28 @@ class core_cache_testcase extends advanced_testcase {
         $this->assertFalse($cache->delete('test'));
         $this->assertTrue($cache->purge());
 
+        // Test a session cache.
+        $cache = cache::make_from_params(cache_store::MODE_SESSION, 'phpunit', 'disable');
+        $this->assertInstanceOf('cache_disabled', $cache);
+
+        $this->assertFalse(file_exists($configfile));
+
+        $this->assertFalse($cache->get('test'));
+        $this->assertFalse($cache->set('test', 'test'));
+        $this->assertFalse($cache->delete('test'));
+        $this->assertTrue($cache->purge());
+
+        // Finally test a request cache.
+        $cache = cache::make_from_params(cache_store::MODE_REQUEST, 'phpunit', 'disable');
+        $this->assertInstanceOf('cache_disabled', $cache);
+
+        $this->assertFalse(file_exists($configfile));
+
+        $this->assertFalse($cache->get('test'));
+        $this->assertFalse($cache->set('test', 'test'));
+        $this->assertFalse($cache->delete('test'));
+        $this->assertTrue($cache->purge());
+
         cache_factory::reset();
 
         $factory = cache_factory::instance(true);
index 1426271..d6a7981 100644 (file)
@@ -106,8 +106,8 @@ $PAGE->navbar->add($pagetitle);
 
 $PAGE->set_title($course->shortname.': '.get_string('calendar', 'calendar').': '.$pagetitle);
 $PAGE->set_heading($course->fullname);
+$PAGE->set_pagelayout('standard');
 $PAGE->set_button(calendar_preferences_button($course));
-$PAGE->set_pagelayout('base');
 
 $renderer = $PAGE->get_renderer('core_calendar');
 $calendar->add_sidecalendar_blocks($renderer);
@@ -125,7 +125,7 @@ switch($action) {
             $weekend = intval($CFG->calendar_weekend);
         }
 
-        $authtoken = sha1($USER->id . $USER->password . $CFG->calendar_exportsalt);
+        $authtoken = sha1($USER->id . $DB->get_field('user', 'password', array('id'=>$USER->id)). $CFG->calendar_exportsalt);
         // Let's populate some vars to let "common tasks" be somewhat smart...
         // If today it's weekend, give the "next week" option
         $allownextweek  = $weekend & (1 << $now['wday']);
index 40a2d82..73e5924 100644 (file)
@@ -153,7 +153,7 @@ if (!empty($CFG->enablecalendarexport)) {
         echo $OUTPUT->single_button(new moodle_url('/calendar/managesubscriptions.php', array('course'=>$courseid)), get_string('managesubscriptions', 'calendar'));
     }
     if (isloggedin()) {
-        $authtoken = sha1($USER->id . $USER->password . $CFG->calendar_exportsalt);
+        $authtoken = sha1($USER->id . $DB->get_field('user', 'password', array('id'=>$USER->id)) . $CFG->calendar_exportsalt);
         $link = new moodle_url('/calendar/export_execute.php', array('preset_what'=>'all', 'preset_time'=>'recentupcoming', 'userid' => $USER->id, 'authtoken'=>$authtoken));
         $icon = html_writer::empty_tag('img', array('src'=>$OUTPUT->pix_url('i/ical'), 'height'=>'14', 'width'=>'36', 'alt'=>get_string('ical', 'calendar'), 'title'=>get_string('quickdownloadcalendar', 'calendar')));
         echo html_writer::tag('a', $icon, array('href'=>$link));
index 19988e2..e331e2f 100644 (file)
@@ -1289,8 +1289,11 @@ function get_module_metadata($course, $modnames, $sectionreturn = null) {
 
         // NOTE: this is legacy stuff, module subtypes are very strongly discouraged!!
         $gettypesfunc =  $modname.'_get_types';
+        $types = MOD_SUBTYPE_NO_CHILDREN;
         if (function_exists($gettypesfunc)) {
             $types = $gettypesfunc();
+        }
+        if ($types !== MOD_SUBTYPE_NO_CHILDREN) {
             if (is_array($types) && count($types) > 0) {
                 $group = new stdClass();
                 $group->name = $modname;
@@ -1314,7 +1317,9 @@ function get_module_metadata($course, $modnames, $sectionreturn = null) {
                     // should have the same archetype
                     $group->archetype = $subtype->archetype;
 
-                    if (get_string_manager()->string_exists('help' . $subtype->name, $modname)) {
+                    if (!empty($type->help)) {
+                        $subtype->help = $type->help;
+                    } else if (get_string_manager()->string_exists('help' . $subtype->name, $modname)) {
                         $subtype->help = get_string('help' . $subtype->name, $modname);
                     }
                     $subtype->link = new moodle_url($urlbase, array('add' => $modname, 'type' => $subtype->name));
index db70a9f..bf088d8 100644 (file)
@@ -56,6 +56,11 @@ if (!empty($add)) {
     $course = $DB->get_record('course', array('id'=>$course), '*', MUST_EXIST);
     require_login($course);
 
+    // There is no page for this in the navigation. The closest we'll have is the course section.
+    // If the course section isn't displayed on the navigation this will fall back to the course which
+    // will be the closest match we have.
+    navigation_node::override_active_url(course_get_url($course, $section));
+
     list($module, $context, $cw) = can_add_moduleinfo($course, $add, $section);
 
     $cm = null;
@@ -114,6 +119,7 @@ if (!empty($add)) {
     } else {
         $pageheading = get_string('addinganew', 'moodle', $fullmodulename);
     }
+    $navbaraddition = $pageheading;
 
 } else if (!empty($update)) {
 
@@ -220,6 +226,7 @@ if (!empty($add)) {
     } else {
         $pageheading = get_string('updatinga', 'moodle', $fullmodulename);
     }
+    $navbaraddition = null;
 
 } else {
     require_login();
@@ -290,6 +297,11 @@ if ($mform->is_cancelled()) {
     $PAGE->set_heading($course->fullname);
     $PAGE->set_title($streditinga);
     $PAGE->set_cacheable(false);
+
+    if (isset($navbaraddition)) {
+        $PAGE->navbar->add($navbaraddition);
+    }
+
     echo $OUTPUT->header();
 
     if (get_string_manager()->string_exists('modulename_help', $module->name)) {
index 659383a..abfc963 100644 (file)
@@ -674,7 +674,8 @@ class core_course_renderer extends plugin_renderer_base {
             $conditionalhidden = $mod->availablefrom > time() ||
                 ($mod->availableuntil && $mod->availableuntil < time()) ||
                 count($mod->conditionsgrade) > 0 ||
-                count($mod->conditionscompletion) > 0;
+                count($mod->conditionscompletion) > 0 ||
+                count($mod->conditionsfield);
         }
         return $conditionalhidden;
     }
@@ -725,25 +726,27 @@ class core_course_renderer extends plugin_renderer_base {
         // viewhiddenactivities, so that teachers see 'items which might not
         // be available to some students' dimmed but students do not see 'item
         // which is actually available to current student' dimmed.
-        $conditionalhidden = $this->is_cm_conditionally_hidden($mod);
-        $accessiblebutdim = (!$mod->visible || $conditionalhidden) &&
-                (!$mod->uservisible || has_capability('moodle/course:viewhiddenactivities',
-                        context_course::instance($mod->course)));
-
         $linkclasses = '';
         $accesstext = '';
         $textclasses = '';
-        if ($accessiblebutdim) {
-            $linkclasses .= ' dimmed';
-            $textclasses .= ' dimmed_text';
-            if ($conditionalhidden) {
-                $linkclasses .= ' conditionalhidden';
-                $textclasses .= ' conditionalhidden';
-            }
-            if ($mod->uservisible) {
-                // show accessibility note only if user can access the module himself
+        if ($mod->uservisible) {
+            $conditionalhidden = $this->is_cm_conditionally_hidden($mod);
+            $accessiblebutdim = (!$mod->visible || $conditionalhidden) &&
+                has_capability('moodle/course:viewhiddenactivities',
+                        context_course::instance($mod->course));
+            if ($accessiblebutdim) {
+                $linkclasses .= ' dimmed';
+                $textclasses .= ' dimmed_text';
+                if ($conditionalhidden) {
+                    $linkclasses .= ' conditionalhidden';
+                    $textclasses .= ' conditionalhidden';
+                }
+                // Show accessibility note only if user can access the module himself.
                 $accesstext = get_accesshide(get_string('hiddenfromstudents').':'. $mod->modfullname);
             }
+        } else {
+            $linkclasses .= ' dimmed';
+            $textclasses .= ' dimmed_text';
         }
 
         // Get on-click attribute value if specified and decode the onclick - it
@@ -788,28 +791,23 @@ class core_course_renderer extends plugin_renderer_base {
             return $output;
         }
         $content = $mod->get_formatted_content(array('overflowdiv' => true, 'noclean' => true));
-        if ($this->page->user_is_editing()) {
-            // In editing mode, when an item is conditionally hidden from some users
-            // we show it as greyed out.
+        $accesstext = '';
+        $textclasses = '';
+        if ($mod->uservisible) {
             $conditionalhidden = $this->is_cm_conditionally_hidden($mod);
-            $dim = !$mod->visible || $conditionalhidden;
+            $accessiblebutdim = (!$mod->visible || $conditionalhidden) &&
+                has_capability('moodle/course:viewhiddenactivities',
+                        context_course::instance($mod->course));
+            if ($accessiblebutdim) {
+                $textclasses .= ' dimmed_text';
+                if ($conditionalhidden) {
+                    $textclasses .= ' conditionalhidden';
+                }
+                // Show accessibility note only if user can access the module himself.
+                $accesstext = get_accesshide(get_string('hiddenfromstudents').':'. $mod->modfullname);
+            }
         } else {
-            // When not in editing mode, we only show item as hidden if it is
-            // actually not available to the user
-            $conditionalhidden = false;
-            $dim = !$mod->uservisible;
-        }
-        $textclasses = '';
-        $accesstext = '';
-        if ($dim) {
             $textclasses .= ' dimmed_text';
-            if ($conditionalhidden) {
-                $textclasses .= ' conditionalhidden';
-            }
-            if ($mod->uservisible) {
-                // show accessibility note only if user can access the module himself
-                $accesstext = get_accesshide(get_string('hiddenfromstudents').': ');
-            }
         }
         if ($mod->get_url()) {
             if ($content) {
index 2dad9f0..372652a 100644 (file)
@@ -432,24 +432,24 @@ YUI.add('moodle-course-toolboxes', function(Y) {
          * @method handle_resource_dim
          * @param {Node} button The button that triggered the action.
          * @param {Node} activity The activity node that this action will be performed on.
-         * @param {String} status Whether the activity was shown or hidden.
-         * @returns {number} 1 if we were changing to visible, 0 if we were hiding.
+         * @param {String} action 'show' or 'hide'.
+         * @returns {number} 1 if we changed to visible, 0 if we were hiding.
          */
-        handle_resource_dim : function(button, activity, status) {
+        handle_resource_dim : function(button, activity, action) {
             var toggleclass = CSS.DIMCLASS,
                 dimarea = activity.one('a'),
                 availabilityinfo = activity.one(CSS.AVAILABILITYINFODIV),
-                newstatus = (status === 'hide') ? 'show' : 'hide',
-                newstring = M.util.get_string(newstatus, 'moodle');
+                nextaction = (action === 'hide') ? 'show' : 'hide';
+                newstring = M.util.get_string(nextaction, 'moodle');
 
             // Update button info.
             button.one('img').setAttrs({
                 'alt' : newstring,
-                'src'   : M.util.image_url('t/' + newstatus)
+                'src'   : M.util.image_url('t/' + nextaction)
             });
             button.set('title', newstring);
-            button.replaceClass('editing_'+status, 'editing_'+newstatus)
-            button.setData('action', newstatus);
+            button.replaceClass('editing_'+action, 'editing_'+nextaction);
+            button.setData('action', nextaction);
 
             // If activity is conditionally hidden, then don't toggle.
             if (Y.Moodle.core_course.util.cm.getName(activity) == null) {
@@ -466,7 +466,7 @@ YUI.add('moodle-course-toolboxes', function(Y) {
             if (availabilityinfo) {
                 availabilityinfo.toggleClass(CSS.HIDE);
             }
-            return (status === 'hide') ? 0 : 1;
+            return (action === 'hide') ? 0 : 1;
         },
 
         /**
@@ -683,13 +683,13 @@ YUI.add('moodle-course-toolboxes', function(Y) {
                 shouldbevisible = args.visible,
                 buttonnode = element.one(SELECTOR.SHOW),
                 visible = (buttonnode === null),
-                status = 'hide';
+                action = 'show';
             if (visible) {
                 buttonnode = element.one(SELECTOR.HIDE);
-                status = 'show'
+                action = 'hide';
             }
             if (visible != shouldbevisible) {
-                this.handle_resource_dim(buttonnode, buttonnode.getData('activity'), status);
+                this.handle_resource_dim(buttonnode, buttonnode.getData('activity'), action);
             }
         }
     }, {
@@ -752,26 +752,26 @@ YUI.add('moodle-course-toolboxes', function(Y) {
 
             // The value to submit
             var value;
-            // The status text for strings and images
-            var status,
-                oldstatus;
+            // The text for strings and images. Also determines the icon to display.
+            var action,
+                nextaction;
 
             if (!section.hasClass(CSS.SECTIONHIDDENCLASS)) {
                 section.addClass(CSS.SECTIONHIDDENCLASS);
                 value = 0;
-                status = 'show';
-                oldstatus = 'hide';
+                action = 'hide';
+                nextaction = 'show';
             } else {
                 section.removeClass(CSS.SECTIONHIDDENCLASS);
                 value = 1;
-                status = 'hide';
-                oldstatus = 'show';
+                action = 'show';
+                nextaction = 'hide';
             }
 
-            var newstring = M.util.get_string(status + 'fromothers', 'format_' + this.get('format'));
+            var newstring = M.util.get_string(nextaction + 'fromothers', 'format_' + this.get('format'));
             hideicon.setAttrs({
                 'alt' : newstring,
-                'src'   : M.util.image_url('i/' + status)
+                'src'   : M.util.image_url('i/' + nextaction)
             });
             button.set('title', newstring);
 
@@ -800,7 +800,7 @@ YUI.add('moodle-course-toolboxes', function(Y) {
                 // NOTE: resourcestotoggle is returned as a string instead
                 // of a Number so we must cast our activityid to a String.
                 if (Y.Array.indexOf(response.resourcestotoggle, "" + activityid) != -1) {
-                    node.getData('toolbox').handle_resource_dim(button, node, oldstatus);
+                    node.getData('toolbox').handle_resource_dim(button, node, action);
                 }
             }, this);
         },
index e655c7c..b8d1173 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2013050100;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2013092600;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2013050100;        // Requires this Moodle version
 $plugin->component = 'enrol_category';  // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 60;
index 9de7dcf..7b902fd 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-/* List of handlers. */
-$handlers = array (
-    'cohort_member_added' => array (
-        'handlerfile'      => '/enrol/cohort/locallib.php',
-        'handlerfunction'  => array('enrol_cohort_handler', 'member_added'),
-        'schedule'         => 'instant',
-        'internal'         => 1,
+$observers = array(
+
+    array(
+        'eventname' => '\core\event\cohort_member_added',
+        'callback' => 'enrol_cohort_handler::member_added',
+        'includefile' => '/enrol/cohort/locallib.php'
     ),
 
-    'cohort_member_removed' => array (
-        'handlerfile'      => '/enrol/cohort/locallib.php',
-        'handlerfunction'  => array('enrol_cohort_handler', 'member_removed'),
-        'schedule'         => 'instant',
-        'internal'         => 1,
+    array(
+        'eventname' => '\core\event\cohort_member_removed',
+        'callback' => 'enrol_cohort_handler::member_removed',
+        'includefile' => '/enrol/cohort/locallib.php'
     ),
 
-    'cohort_deleted' => array (
-        'handlerfile'      => '/enrol/cohort/locallib.php',
-        'handlerfunction'  => array('enrol_cohort_handler', 'deleted'),
-        'schedule'         => 'instant',
-        'internal'         => 1,
+    array(
+        'eventname' => '\core\event\cohort_deleted',
+        'callback' => 'enrol_cohort_handler::deleted',
+        'includefile' => '/enrol/cohort/locallib.php'
     ),
 );
index 377584e..e07c20b 100644 (file)
@@ -36,10 +36,10 @@ require_once($CFG->dirroot . '/enrol/locallib.php');
 class enrol_cohort_handler {
     /**
      * Event processor - cohort member added.
-     * @param stdClass $ca
+     * @param \core\event\cohort_member_added $event
      * @return bool
      */
-    public static function member_added($ca) {
+    public static function member_added(\core\event\cohort_member_added $event) {
         global $DB, $CFG;
         require_once("$CFG->dirroot/group/lib.php");
 
@@ -53,7 +53,7 @@ class enrol_cohort_handler {
              LEFT JOIN {role} r ON (r.id = e.roleid)
                  WHERE e.customint1 = :cohortid AND e.enrol = 'cohort'
               ORDER BY e.id ASC";
-        if (!$instances = $DB->get_records_sql($sql, array('cohortid'=>$ca->cohortid))) {
+        if (!$instances = $DB->get_records_sql($sql, array('cohortid'=>$event->objectid))) {
             return true;
         }
 
@@ -68,13 +68,13 @@ class enrol_cohort_handler {
             }
             unset($instance->roleexists);
             // No problem if already enrolled.
-            $plugin->enrol_user($instance, $ca->userid, $instance->roleid, 0, 0, ENROL_USER_ACTIVE);
+            $plugin->enrol_user($instance, $event->relateduserid, $instance->roleid, 0, 0, ENROL_USER_ACTIVE);
 
             // Sync groups.
             if ($instance->customint2) {
-                if (!groups_is_member($instance->customint2, $ca->userid)) {
+                if (!groups_is_member($instance->customint2, $event->relateduserid)) {
                     if ($group = $DB->get_record('groups', array('id'=>$instance->customint2, 'courseid'=>$instance->courseid))) {
-                        groups_add_member($group->id, $ca->userid, 'enrol_cohort', $instance->id);
+                        groups_add_member($group->id, $event->relateduserid, 'enrol_cohort', $instance->id);
                     }
                 }
             }
@@ -85,14 +85,14 @@ class enrol_cohort_handler {
 
     /**
      * Event processor - cohort member removed.
-     * @param stdClass $ca
+     * @param \core\event\cohort_member_removed $event
      * @return bool
      */
-    public static function member_removed($ca) {
+    public static function member_removed(\core\event\cohort_member_removed $event) {
         global $DB;
 
         // Does anything want to sync with this cohort?
-        if (!$instances = $DB->get_records('enrol', array('customint1'=>$ca->cohortid, 'enrol'=>'cohort'), 'id ASC')) {
+        if (!$instances = $DB->get_records('enrol', array('customint1'=>$event->objectid, 'enrol'=>'cohort'), 'id ASC')) {
             return true;
         }
 
@@ -100,11 +100,11 @@ class enrol_cohort_handler {
         $unenrolaction = $plugin->get_config('unenrolaction', ENROL_EXT_REMOVED_UNENROL);
 
         foreach ($instances as $instance) {
-            if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$ca->userid))) {
+            if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$event->relateduserid))) {
                 continue;
             }
             if ($unenrolaction == ENROL_EXT_REMOVED_UNENROL) {
-                $plugin->unenrol_user($instance, $ca->userid);
+                $plugin->unenrol_user($instance, $event->relateduserid);
 
             } else {
                 if ($ue->status != ENROL_USER_SUSPENDED) {
@@ -120,14 +120,14 @@ class enrol_cohort_handler {
 
     /**
      * Event processor - cohort deleted.
-     * @param stdClass $cohort
+     * @param \core\event\cohort_deleted $event
      * @return bool
      */
-    public static function deleted($cohort) {
+    public static function deleted(\core\event\cohort_deleted $event) {
         global $DB;
 
         // Does anything want to sync with this cohort?
-        if (!$instances = $DB->get_records('enrol', array('customint1'=>$cohort->id, 'enrol'=>'cohort'), 'id ASC')) {
+        if (!$instances = $DB->get_records('enrol', array('customint1'=>$event->objectid, 'enrol'=>'cohort'), 'id ASC')) {
             return true;
         }
 
index 2024bbd..e5df70d 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2013050100;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2013092600;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2013050100;        // Requires this Moodle version
 $plugin->component = 'enrol_cohort';    // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 60*60;             // run cron every hour by default, it is not out-of-sync often
index f9ac4ca..f3dcdc0 100644 (file)
@@ -85,6 +85,7 @@ function cron() {
         $this->logfp = fopen($logtolocation, 'a');
     }
 
+    $fileisnew = false;
     if ( file_exists($filename) ) {
         @set_time_limit(0);
         $starttime = time();
@@ -107,10 +108,8 @@ function cron() {
         if(empty($prev_path)  || ($filename != $prev_path)) {
             $fileisnew = true;
         } elseif(isset($prev_time) && ($filemtime <= $prev_time)) {
-            $fileisnew = false;
             $this->log_line('File modification time is not more recent than last update - skipping processing.');
         } elseif(isset($prev_md5) && ($md5 == $prev_md5)) {
-            $fileisnew = false;
             $this->log_line('File MD5 hash is same as on last update - skipping processing.');
         } else {
             $fileisnew = true; // Let's process it!
@@ -217,7 +216,7 @@ function cron() {
         $this->log_line('File not found: '.$filename);
     }
 
-    if (!empty($mailadmins)) {
+    if (!empty($mailadmins) && $fileisnew) {
         $msg = "An IMS enrolment has been carried out within Moodle.\nTime taken: $timeelapsed seconds.\n\n";
         if(!empty($logtolocation)){
             if($this->logfp){
@@ -234,7 +233,7 @@ function cron() {
 
         $eventdata = new stdClass();
         $eventdata->modulename        = 'moodle';
-        $eventdata->component         = 'imsenterprise';
+        $eventdata->component         = 'enrol_imsenterprise';
         $eventdata->name              = 'imsenterprise_enrolment';
         $eventdata->userfrom          = get_admin();
         $eventdata->userto            = get_admin();
@@ -400,7 +399,7 @@ function process_group_tag($tagcontents) {
                     // Insert default names for teachers/students, from the current language
 
                     // Handle course categorisation (taken from the group.org.orgunit field if present)
-                    if (strlen($group->category)>0) {
+                    if (!empty($group->category)) {
                         // If the category is defined and exists in Moodle, we want to store it in that one
                         if ($catid = $DB->get_field('course_categories', 'id', array('name'=>$group->category))) {
                             $course->category = $catid;
index 841022b..5ee34c3 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2013050100;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2013092600;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2013050100;        // Requires this Moodle version
 $plugin->component = 'enrol_meta';      // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 60*60;             // run cron every hour by default, it is not out-of-sync often
\ No newline at end of file
index 64353f0..4017ade 100644 (file)
@@ -367,6 +367,7 @@ class core_files_renderer extends plugin_renderer_base {
      * @return string
      */
     private function fm_js_template_fileselectlayout() {
+        global $OUTPUT;
         $strloading  = get_string('loading', 'repository');
         $icon_progress = $this->pix_icon('i/loading_small', $strloading).'';
         $rv = '
@@ -377,7 +378,7 @@ class core_files_renderer extends plugin_renderer_base {
     <form>
         <button class="{!}fp-file-download">'.get_string('download').'</button>
         <button class="{!}fp-file-delete">'.get_string('delete').'</button>
-        <button class="{!}fp-file-setmain">'.get_string('setmainfile', 'repository').'</button>
+        <button class="{!}fp-file-setmain">'.get_string('setmainfile', 'repository').'</button><span class="fp-file-setmain-help">'.$OUTPUT->help_icon('setmainfile', 'repository').'</span>
         <button class="{!}fp-file-zip">'.get_string('zip', 'editor').'</button>
         <button class="{!}fp-file-unzip">'.get_string('unzip').'</button>
         <div class="fp-hr"></div>
index e782303..c05eb92 100644 (file)
@@ -793,9 +793,12 @@ class grade_edit_tree_column_range extends grade_edit_tree_column {
     public function get_item_cell($item, $params) {
         global $DB, $OUTPUT;
 
-        // If the parent aggregation is Sum of Grades, this cannot be changed
+        // If the parent aggregation is Sum of Grades, we should show the number, even for scales, as that value is used...
+        // ...in the computation. For text grades, the grademax is not used, so we can still show the no value string.
         $parent_cat = $item->get_parent_category();
-        if ($parent_cat->aggregation == GRADE_AGGREGATE_SUM) {
+        if ($item->gradetype == GRADE_TYPE_TEXT) {
+            $grademax = ' - ';
+        } else if ($parent_cat->aggregation == GRADE_AGGREGATE_SUM) {
             $grademax = format_float($item->grademax, $item->get_decimals());
         } elseif ($item->gradetype == GRADE_TYPE_SCALE) {
             $scale = $DB->get_record('scale', array('id' => $item->scaleid));
index 59e4a23..07c59e7 100644 (file)
@@ -32,21 +32,86 @@ require_once($CFG->dirroot.'/grade/edit/tree/lib.php');
 /**
  * Tests grade_edit_tree (deals with the data on the categories and items page in the gradebook)
  */
-class core_grade_edittreelib_testcase extends basic_testcase {
-    var $courseid = 1;
-    var $context = null;
-    var $grade_edit_tree = null;
-
+class core_grade_edittreelib_testcase extends advanced_testcase {
     public function test_format_number() {
-        $numinput = array( 0,   1,   1.01, '1.010', 1.2345);
+        $numinput = array(0,   1,   1.01, '1.010', 1.2345);
         $numoutput = array(0.0, 1.0, 1.01,  1.01,   1.2345);
 
-        for ($i=0; $i<sizeof($numinput); $i++) {
+        for ($i = 0; $i < count($numinput); $i++) {
             $msg = 'format_number() testing '.$numinput[$i].' %s';
-            $this->assertEquals(grade_edit_tree::format_number($numinput[$i]),$numoutput[$i],$msg);
+            $this->assertEquals(grade_edit_tree::format_number($numinput[$i]), $numoutput[$i], $msg);
         }
     }
 
+    public function test_grade_edit_tree_column_range_get_item_cell() {
+        global $DB, $CFG;
+
+        $this->resetAfterTest(true);
+
+        // Make some things we need.
+        $scale = $this->getDataGenerator()->create_scale();
+        $course = $this->getDataGenerator()->create_course();
+        $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id));
+        $modulecontext = context_module::instance($assign->id);
+        // The generator returns a dummy object, lets get the real assign object.
+        $assign = new assign($modulecontext, false, false);
+        $cm = $assign->get_course_module();
+
+        // Get range column.
+        $column = grade_edit_tree_column::factory('range');
+
+        $gradeitemparams = array(
+            'itemtype'     => 'mod',
+            'itemmodule'   => $cm->modname,
+            'iteminstance' => $cm->instance,
+            'courseid'     => $cm->course,
+            'itemnumber'   => 0
+        );
+
+        // Lets set the grade to something we know.
+        $instance = $assign->get_instance();
+        $instance->grade = 70;
+        $instance->instance = $instance->id;
+        $assign->update_instance($instance);
+
+        $gradeitem = grade_item::fetch($gradeitemparams);
+        $cell = $column->get_item_cell($gradeitem, array());
+
+        $this->assertEquals(GRADE_TYPE_VALUE, $gradeitem->gradetype);
+        $this->assertEquals(null, $gradeitem->scaleid);
+        $this->assertEquals(70.0, (float) $cell->text, "Grade text is 70", 0.01);
+
+        // Now change it to a scale.
+        $instance = $assign->get_instance();
+        $instance->grade = -($scale->id);
+        $instance->instance = $instance->id;
+        $assign->update_instance($instance);
+
+        $gradeitem = grade_item::fetch($gradeitemparams);
+        $cell = $column->get_item_cell($gradeitem, array());
+
+        // Make the expected scale text.
+        $scaleitems = null;
+        $scaleitems = explode(',', $scale->scale);
+        $scalestring = end($scaleitems) . ' (' . count($scaleitems) . ')';
+
+        $this->assertEquals(GRADE_TYPE_SCALE, $gradeitem->gradetype);
+        $this->assertEquals($scale->id, $gradeitem->scaleid);
+        $this->assertEquals($scalestring, $cell->text, "Grade text matches scale");
+
+        // Now change it to no grade.
+        $instance = $assign->get_instance();
+        $instance->grade = 0;
+        $instance->instance = $instance->id;
+        $assign->update_instance($instance);
+
+        $gradeitem = grade_item::fetch($gradeitemparams);
+        $cell = $column->get_item_cell($gradeitem, array());
+
+        $this->assertEquals(GRADE_TYPE_TEXT, $gradeitem->gradetype);
+        $this->assertEquals(null, $gradeitem->scaleid);
+        $this->assertEquals(' - ', $cell->text, 'Grade text matches empty value of " - "');
+    }
 }
 
 
index 1a092c9..54a7297 100644 (file)
@@ -86,6 +86,7 @@ $string['configgeneralcomments'] = 'Sets the default for including comments in a
 $string['configgeneralfilters'] = 'Sets the default for including filters in a backup.';
 $string['configgeneralhistories'] = 'Sets the default for including user history within a backup.';
 $string['configgenerallogs'] = 'If enabled logs will be included in backups by default.';
+$string['configgeneralquestionbank'] = 'If enabled the question bank will be included in backups by default. PLEASE NOTE: Disabling this setting with disable the backup of activities which use the question bank, such as the quiz.';
 $string['configgeneralroleassignments'] = 'If enabled by default roles assignments will also be backed up.';
 $string['configgeneraluserscompletion'] = 'If enabled user completion information will be included in backups by default.';
 $string['configgeneralusers'] = 'Sets the default for whether to include users in backups.';
@@ -136,7 +137,9 @@ $string['generalfilters'] = 'Include filters';
 $string['generalhistories'] = 'Include histories';
 $string['generalgradehistories'] = 'Include histories';
 $string['generallogs'] = 'Include logs';
+$string['generalquestionbank'] = 'Include question bank';
 $string['generalroleassignments'] = 'Include role assignments';
+$string['generalquestionbank'] = 'Include question bank';
 $string['generalsettings'] = 'General backup settings';
 $string['generaluserscompletion'] = 'Include user completion information';
 $string['generalusers'] = 'Include users';
@@ -229,6 +232,7 @@ $string['rootsettingfilters'] = 'Include filters';
 $string['rootsettingcomments'] = 'Include comments';
 $string['rootsettingcalendarevents'] = 'Include calendar events';
 $string['rootsettinguserscompletion'] = 'Include user completion details';
+$string['rootsettingquestionbank'] = 'Include question bank';
 $string['rootsettinglogs'] = 'Include course logs';
 $string['rootsettinggradehistories'] = 'Include grade history';
 $string['rootsettingimscc1'] = 'Convert to IMS Common Cartridge 1.0';
index a6734bf..db2c118 100644 (file)
@@ -205,6 +205,7 @@ $string['select'] = 'Select';
 $string['settings'] = 'Settings';
 $string['setupdefaultplugins'] = 'Setting up default repository plugins';
 $string['setmainfile'] = 'Set main file';
+$string['setmainfile_help'] = 'If there are multiple files in the folder, the main file is the one that appears on the view page. Other files such as images or videos may be embedded in it. In filemanager the main file is indicated with a title in bold.';
 $string['siteinstances'] = 'Repositories instances of the site';
 $string['size'] = 'Size';
 $string['submit'] = 'Submit';
index 83cdb58..52966b2 100644 (file)
@@ -82,6 +82,14 @@ $string['errorcoursecontextnotvalid'] = 'You cannot execute functions in the cou
 $string['errorinvalidparam'] = 'The param "{$a}" is invalid.';
 $string['errornotemptydefaultparamarray'] = 'The web service description parameter named \'{$a}\' is an single or multiple structure. The default can only be empty array. Check web service description.';
 $string['erroroptionalparamarray'] = 'The web service description parameter named \'{$a}\' is an single or multiple structure. It can not be set as VALUE_OPTIONAL. Check web service description.';
+$string['event_webservice_function_called'] = 'Web service function called';
+$string['event_webservice_login_failed'] = 'Web service login failed';
+$string['event_webservice_service_created'] = 'Web service service created';
+$string['event_webservice_service_updated'] = 'Web service service updated';
+$string['event_webservice_service_user_added'] = 'Web service service user added';
+$string['event_webservice_service_user_removed'] = 'Web service service user removed';
+$string['event_webservice_token_created'] = 'Web service token created';
+$string['event_webservice_token_sent'] = 'Web service token sent';
 $string['execute'] = 'Execute';
 $string['executewarnign'] = 'WARNING: If you press execute your database will be modified and changes can not be reverted automatically!';
 $string['externalservice'] = 'External service';
index 1a300b9..815fb2a 100644 (file)
@@ -821,69 +821,6 @@ function badges_get_user_badges($userid, $courseid = 0, $page = 0, $perpage = 0,
     return $badges;
 }
 
-/**
- * Get issued badge details for assertion URL
- *
- * @param string $hash
- */
-function badges_get_issued_badge_info($hash) {
-    global $DB, $CFG;
-
-    $a = array();
-
-    $record = $DB->get_record_sql('
-            SELECT
-                bi.dateissued,
-                bi.dateexpire,
-                u.email,
-                b.*,
-                bb.email as backpackemail
-            FROM
-                {badge} b
-                JOIN {badge_issued} bi
-                    ON b.id = bi.badgeid
-                JOIN {user} u
-                    ON u.id = bi.userid
-                LEFT JOIN {badge_backpack} bb
-                    ON bb.userid = bi.userid
-            WHERE ' . $DB->sql_compare_text('bi.uniquehash', 40) . ' = ' . $DB->sql_compare_text(':hash', 40),
-            array('hash' => $hash), IGNORE_MISSING);
-
-    if ($record) {
-        if ($record->type == BADGE_TYPE_SITE) {
-            $context = context_system::instance();
-        } else {
-            $context = context_course::instance($record->courseid);
-        }
-
-        $url = new moodle_url('/badges/badge.php', array('hash' => $hash));
-        $email = empty($record->backpackemail) ? $record->email : $record->backpackemail;
-
-        // Recipient's email is hashed: <algorithm>$<hash(email + salt)>.
-        $a['recipient'] = 'sha256$' . hash('sha256', $email . $CFG->badges_badgesalt);
-        $a['salt'] = $CFG->badges_badgesalt;
-
-        if ($record->dateexpire) {
-            $a['expires'] = date('Y-m-d', $record->dateexpire);
-        }
-
-        $a['issued_on'] = date('Y-m-d', $record->dateissued);
-        $a['evidence'] = $url->out(); // Issued badge URL.
-        $a['badge'] = array();
-        $a['badge']['version'] = '0.5.0'; // Version of OBI specification, 0.5.0 - current beta.
-        $a['badge']['name'] = $record->name;
-        $a['badge']['image'] = moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $record->id, '/', 'f1')->out();
-        $a['badge']['description'] = $record->description;
-        $a['badge']['criteria'] = $url->out(); // Issued badge URL.
-        $a['badge']['issuer'] = array();
-        $a['badge']['issuer']['origin'] = $record->issuerurl;
-        $a['badge']['issuer']['name'] = $record->issuername;
-        $a['badge']['issuer']['contact'] = $record->issuercontact;
-    }
-
-    return $a;
-}
-
 /**
  * Extends the course administration navigation with the Badges page
  *
index 667273f..6278ff7 100644 (file)
@@ -241,48 +241,56 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
      * closure exception will be used, but you must provide an exception if the closure does not throws
      * an exception.
      *
-     * @throws Exception            If it timeouts without receiving something != false from the closure
-     * @param  Closure   $lambda    The function to execute.
-     * @param  mixed     $args      Arguments to pass to the closure
-     * @param  int       $timeout   Timeout
-     * @param  Exception $exception The exception to throw in case it time outs.
+     * @throws Exception If it timeouts without receiving something != false from the closure
+     * @param Function|array|string $lambda The function to execute or an array passed to call_user_func (maps to a class method)
+     * @param mixed $args Arguments to pass to the closure
+     * @param int $timeout Timeout in seconds
+     * @param Exception $exception The exception to throw in case it time outs.
+     * @param bool $microsleep If set to true it'll sleep micro seconds rather than seconds.
      * @return mixed The value returned by the closure
      */
-    protected function spin($lambda, $args = false, $timeout = false, $exception = false) {
+    protected function spin($lambda, $args = false, $timeout = false, $exception = false, $microsleep = false) {
 
         // Using default timeout which is pretty high.
         if (!$timeout) {
             $timeout = self::TIMEOUT;
         }
+        if ($microsleep) {
+            // Will sleep 1/10th of a second by default for self::TIMEOUT seconds.
+            $loops = $timeout * 10;
+        } else {
+            // Will sleep for self::TIMEOUT seconds.
+            $loops = $timeout;
+        }
 
-        for ($i = 0; $i < $timeout; $i++) {
-
+        for ($i = 0; $i < $loops; $i++) {
             // We catch the exception thrown by the step definition to execute it again.
             try {
-
                 // We don't check with !== because most of the time closures will return
                 // direct Behat methods returns and we are not sure it will be always (bool)false
                 // if it just runs the behat method without returning anything $return == null.
-                if ($return = $lambda($this, $args)) {
+                if ($return = call_user_func($lambda, $this, $args)) {
                     return $return;
                 }
             } catch (Exception $e) {
-
                 // We would use the first closure exception if no exception has been provided.
                 if (!$exception) {
                     $exception = $e;
                 }
-
                 // We wait until no exception is thrown or timeout expires.
                 continue;
             }
 
-            sleep(1);
+            if ($microsleep) {
+                usleep(100000);
+            } else {
+                sleep(1);
+            }
         }
 
         // Using coding_exception as is a development issue if no exception has been provided.
         if (!$exception) {
-            $exception = new coding_exception('spin method requires an exception if the closure doesn\'t throw an exception itself');
+            $exception = new coding_exception('spin method requires an exception if the callback does not throw an exception');
         }
 
         // Throwing exception to the user.
diff --git a/lib/classes/event/webservice_function_called.php b/lib/classes/event/webservice_function_called.php
new file mode 100644 (file)
index 0000000..9914385
--- /dev/null
@@ -0,0 +1,101 @@
+<?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/>.
+
+/**
+ * core webservice function_called event.
+ *
+ * @package    core
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * core webservice function_called event class.
+ *
+ * @package    core
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class webservice_function_called extends \core\event\base {
+
+    /**
+     * Legacy log data.
+     */
+    protected $legacylogdata;
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The web service function '{$this->other['function']}' has been called.";
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        return $this->legacylogdata;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('event_webservice_function_called', 'webservice');
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'r';
+        $this->data['level'] = self::LEVEL_OTHER;
+        $this->context = \context_system::instance();
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return void
+     */
+    public function set_legacy_logdata($legacydata) {
+        $this->legacylogdata = $legacydata;
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        if (!isset($this->other['function'])) {
+           throw new \coding_exception('The key \'function\' needs to be set in $other.');
+        }
+    }
+
+}
diff --git a/lib/classes/event/webservice_login_failed.php b/lib/classes/event/webservice_login_failed.php
new file mode 100644 (file)
index 0000000..02c49a3
--- /dev/null
@@ -0,0 +1,107 @@
+<?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/>.
+
+/**
+ * core web service login failed event.
+ *
+ * @package    core
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * core web service login_failed event class.
+ *
+ * @package    core
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class webservice_login_failed extends \core\event\base {
+
+    /**
+     * Legacy log data.
+     *
+     * @var null|array
+     */
+    protected $legacylogdata;
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "Web service authentication failed with code: {$this->other['reason']}.";
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        return $this->legacylogdata;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('event_webservice_login_failed', 'webservice');
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'r';
+        $this->data['level'] = self::LEVEL_OTHER;
+        $this->context = \context_system::instance();
+    }
+
+    /**
+     * Set the legacy event log data.
+     *
+     * @param array $logdata The log data.
+     * @return void
+     */
+    public function set_legacy_logdata($logdata) {
+        $this->legacylogdata = $logdata;
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        if (!isset($this->other['reason'])) {
+           throw new \coding_exception('The key \'reason\' needs to be set in $other.');
+        } else if (!isset($this->other['method'])) {
+           throw new \coding_exception('The key \'method\' needs to be set in $other.');
+        } else if (!isset($this->other['token']) && !isset($this->other['tokenid']) && !isset($this->other['username'])) {
+           throw new \coding_exception('The keys \'username\', \'token\' or \'tokenid\' need to be set in $other.');
+        }
+    }
+}
diff --git a/lib/classes/event/webservice_service_created.php b/lib/classes/event/webservice_service_created.php
new file mode 100644 (file)
index 0000000..436a236
--- /dev/null
@@ -0,0 +1,88 @@
+<?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/>.
+
+/**
+ * core webservice service created event.
+ *
+ * @package    core
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * core webservice service created event class.
+ *
+ * @package    core
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class webservice_service_created extends \core\event\base {
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The web service service $this->objectid has been created by user $this->userid.";
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        global $CFG;
+        $service = $this->get_record_snapshot('external_services', $this->objectid);
+        return array(SITEID, 'webservice', 'add', $CFG->wwwroot . "/" . $CFG->admin . "/settings.php?section=externalservices",
+            get_string('addservice', 'webservice', $service));
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('event_webservice_service_created', 'webservice');
+    }
+
+    /**
+     * Get URL related to the action.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/admin/settings.php', array('section' => 'externalservices'));
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->context = \context_system::instance();
+        $this->data['crud'] = 'c';
+        $this->data['level'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'external_services';
+    }
+
+}
diff --git a/lib/classes/event/webservice_service_deleted.php b/lib/classes/event/webservice_service_deleted.php
new file mode 100644 (file)
index 0000000..4c497d9
--- /dev/null
@@ -0,0 +1,88 @@
+<?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/>.
+
+/**
+ * core webservice service deleted event.
+ *
+ * @package    core
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * core webservice service deleted event class.
+ *
+ * @package    core
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class webservice_service_deleted extends \core\event\base {
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The web service service $this->objectid has been deleted by user $this->userid.";
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        global $CFG;
+        $service = $this->get_record_snapshot('external_services', $this->objectid);
+        return array(SITEID, 'webservice', 'delete', $CFG->wwwroot . "/" . $CFG->admin . "/settings.php?section=externalservices",
+            get_string('deleteservice', 'webservice', $service));
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('event_webservice_service_deleted', 'webservice');
+    }
+
+    /**
+     * Get URL related to the action.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/admin/settings.php', array('section' => 'externalservices'));
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->context = \context_system::instance();
+        $this->data['crud'] = 'd';
+        $this->data['level'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'external_services';
+    }
+
+}
diff --git a/lib/classes/event/webservice_service_updated.php b/lib/classes/event/webservice_service_updated.php
new file mode 100644 (file)
index 0000000..0b82da7
--- /dev/null
@@ -0,0 +1,97 @@
+<?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/>.
+
+/**
+ * core webservice service updated event.
+ *
+ * @package    core
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * core webservice service updated event class.
+ *
+ * @package    core
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class webservice_service_updated extends \core\event\base {
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The web service service $this->objectid has been updated by user $this->userid.";
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        global $CFG;
+        $service = $this->get_record_snapshot('external_services', $this->objectid);
+        return array(SITEID, 'webservice', 'edit', $CFG->wwwroot . "/" . $CFG->admin . "/settings.php?section=externalservices",
+            get_string('editservice', 'webservice', $service));
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('event_webservice_service_updated', 'webservice');
+    }
+
+    /**
+     * Get URL related to the action.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/admin/settings.php', array('section' => 'externalservices'));
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->context = \context_system::instance();
+        $this->data['crud'] = 'u';
+        $this->data['level'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'external_services';
+    }
+
+    /**
+     * Set the legacy event log data.
+     *
+     * @return void
+     */
+    public function set_legacy_logdata($legacylogdata) {
+        $this->legacylogdata = $legacylogdata;
+    }
+
+}
diff --git a/lib/classes/event/webservice_service_user_added.php b/lib/classes/event/webservice_service_user_added.php
new file mode 100644 (file)
index 0000000..38309fb
--- /dev/null
@@ -0,0 +1,98 @@
+<?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/>.
+
+/**
+ * core webservice service user added event.
+ *
+ * @package    core
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * core webservice service user added event class.
+ *
+ * @package    core
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class webservice_service_user_added extends \core\event\base {
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user $this->relateduserid has been added to the web service service $this->objectid.";
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        global $CFG;
+        return array(SITEID, 'core', 'assign', $CFG->admin . '/webservice/service_users.php?id=' . $this->objectid, 'add', '',
+            $this->relateduserid);
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('event_webservice_service_user_added', 'webservice');
+    }
+
+    /**
+     * Get URL related to the action.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/admin/webservice/service_users.php', array('id' => $this->objectid));
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->context = \context_system::instance();
+        $this->data['crud'] = 'c';
+        $this->data['level'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'external_services';
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @return void
+     */
+    protected function validate_data() {
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The relateduserid must be set.');
+        }
+    }
+
+}
diff --git a/lib/classes/event/webservice_service_user_removed.php b/lib/classes/event/webservice_service_user_removed.php
new file mode 100644 (file)
index 0000000..a0f7eec
--- /dev/null
@@ -0,0 +1,98 @@
+<?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/>.
+
+/**
+ * core webservice service user removed event.
+ *
+ * @package    core
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * core webservice service user removed event class.
+ *
+ * @package    core
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class webservice_service_user_removed extends \core\event\base {
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user $this->relateduserid has been removed to the web service service $this->objectid.";
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        global $CFG;
+        return array(SITEID, 'core', 'assign', $CFG->admin . '/webservice/service_users.php?id=' . $this->objectid, 'remove', '',
+            $this->relateduserid);
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('event_webservice_service_user_removed', 'webservice');
+    }
+
+    /**
+     * Get URL related to the action.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/admin/webservice/service_users.php', array('id' => $this->objectid));
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->context = \context_system::instance();
+        $this->data['crud'] = 'd';
+        $this->data['level'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'external_services';
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @return void
+     */
+    protected function validate_data() {
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The relateduserid must be set.');
+        }
+    }
+
+}
diff --git a/lib/classes/event/webservice_token_created.php b/lib/classes/event/webservice_token_created.php
new file mode 100644 (file)
index 0000000..5f18336
--- /dev/null
@@ -0,0 +1,100 @@
+<?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/>.
+
+/**
+ * core webservice token_created event.
+ *
+ * @package    core
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * core webservice token_created event class.
+ *
+ * @package    core
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class webservice_token_created extends \core\event\base {
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "A web service token has been created for the user $this->relateduserid.";
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        if (!empty($this->other['auto'])) {
+            // The token has been automatically created.
+            return array(SITEID, 'webservice', 'automatically create user token', '' , 'User ID: ' . $this->relateduserid);
+        }
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('event_webservice_token_created', 'webservice');
+    }
+
+    /**
+     * Get URL related to the action.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/admin/settings.php', array('section' => 'webservicetokens'));
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->context = \context_system::instance();
+        $this->data['crud'] = 'c';
+        $this->data['level'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'external_tokens';
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        if (!isset($this->relateduserid)) {
+           throw new \coding_exception('The property \'relateduserid\' must be set.');
+        }
+    }
+
+}
diff --git a/lib/classes/event/webservice_token_sent.php b/lib/classes/event/webservice_token_sent.php
new file mode 100644 (file)
index 0000000..51775bd
--- /dev/null
@@ -0,0 +1,76 @@
+<?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/>.
+
+/**
+ * core webservice token_sent event.
+ *
+ * @package    core
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * core webservice token sent event class.
+ *
+ * @package    core
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class webservice_token_sent extends \core\event\base {
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The token $this->objectid has been sent to the user $this->userid.";
+    }
+
+    /**
+     * Return the legacy event log data.
+     *
+     * @return array|null
+     */
+    protected function get_legacy_logdata() {
+        return array(SITEID, 'webservice', 'sending requested user token', '' , 'User ID: ' . $this->userid);
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('event_webservice_token_sent', 'webservice');
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->context = \context_system::instance();
+        $this->data['crud'] = 'r';
+        $this->data['level'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'external_tokens';
+    }
+
+}
index 4032884..badf854 100644 (file)
@@ -39,7 +39,7 @@ class core_user {
     const NOREPLY_USER = -10;
 
     /**
-     * Suppport user id.
+     * Support user id.
      */
     const SUPPORT_USER = -20;
 
@@ -160,9 +160,11 @@ class core_user {
             self::$supportuser = self::get_dummy_user_record();
             self::$supportuser->id = self::SUPPORT_USER;
             self::$supportuser->email = $CFG->supportemail;
-            self::$supportuser->firstname = $CFG->supportname ? $CFG->supportname : $supportuser->firstname;
+            if ($CFG->supportname) {
+                self::$supportuser->firstname = $CFG->supportname;
+            }
             self::$supportuser->username = 'support';
-            self::$supportuser->maildisplay = true;
+            self::$supportuser->maildisplay = '1'; // Show to all.
         }
 
         // Send support msg to admin user if nothing is set above.
@@ -191,7 +193,7 @@ class core_user {
 
     /**
      * Return true is user id is greater than self::NOREPLY_USER and
-     * alternetely check db.
+     * alternatively check db.
      *
      * @param int $userid user id.
      * @param bool $checkdb if true userid will be checked in db. By default it's false, and
@@ -199,6 +201,8 @@ class core_user {
      * @return bool true is real user else false.
      */
     public static function is_real_user($userid, $checkdb = false) {
+        global $DB;
+
         if ($userid < 0) {
             return false;
         }
index c10389e..55fc643 100644 (file)
@@ -220,12 +220,12 @@ class component_installer {
         $this->requisitesok = false;
 
     /// Check that everything we need is present
-        if (empty($this->sourcebase) || empty($this->zippath) || empty($this->zipfilename)) {
+        if (empty($this->sourcebase) || empty($this->zipfilename)) {
             $this->errorstring='missingrequiredfield';
             return false;
         }
     /// Check for correct sourcebase (this will be out in the future)
-        if ($this->sourcebase != 'http://download.moodle.org') {
+        if (!PHPUNIT_TEST and $this->sourcebase != 'http://download.moodle.org') {
             $this->errorstring='wrongsourcebase';
             return false;
         }
@@ -286,7 +286,12 @@ class component_installer {
              return COMPONENT_ERROR;
         }
     /// Download zip file and save it to temp
-        $source = $this->sourcebase.'/'.$this->zippath.'/'.$this->zipfilename;
+        if ($this->zippath) {
+            $source = $this->sourcebase.'/'.$this->zippath.'/'.$this->zipfilename;
+        } else {
+            $source = $this->sourcebase.'/'.$this->zipfilename;
+        }
+
         $zipfile= $CFG->tempdir.'/'.$this->zipfilename;
 
         if($contents = download_file_content($source)) {
@@ -474,7 +479,11 @@ class component_installer {
         $comp_arr = array();
 
     /// Define and retrieve the full md5 file
-        $source = $this->sourcebase.'/'.$this->zippath.'/'.$this->md5filename;
+        if ($this->zippath) {
+            $source = $this->sourcebase.'/'.$this->zippath.'/'.$this->md5filename;
+        } else {
+            $source = $this->sourcebase.'/'.$this->md5filename;
+        }
 
     /// Check if we have downloaded the md5 file before (per request cache)
         if (!empty($this->cachedmd5components[$source])) {
index 5348112..520840e 100644 (file)
@@ -572,12 +572,6 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
         if ($rv !== false) {
             return $rv;
         }
-        // We did not find the entry in cache but it also can mean that tree is not built.
-        // The keys 0 and 'countall' must always be present if tree is built.
-        if ($id !== 0 && $id !== 'countall' && $coursecattreecache->has('countall')) {
-            // Tree was built, it means the non-existing $id was requested.
-            return false;
-        }
         // Re-build the tree.
         $sql = "SELECT cc.id, cc.parent, cc.visible
                 FROM {course_categories} cc
@@ -619,7 +613,8 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
         if (array_key_exists($id, $all)) {
             return $all[$id];
         }
-        return false;
+        // Requested non-existing category.
+        return array();
     }
 
     /**
@@ -1187,8 +1182,13 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
         $coursecatcache = cache::make('core', 'coursecat');
         $cntcachekey = 'scnt-'. serialize($search);
         if (($cnt = $coursecatcache->get($cntcachekey)) === false) {
-            self::search_courses($search, $options);
-            $cnt = $coursecatcache->get($cntcachekey);
+            // Cached value not found. Retrieve ALL courses and return their count.
+            unset($options['offset']);
+            unset($options['limit']);
+            unset($options['summary']);
+            unset($options['coursecontacts']);
+            $courses = self::search_courses($search, $options);
+            $cnt = count($courses);
         }
         return $cnt;
     }
@@ -1311,8 +1311,13 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
         $cntcachekey = 'lcnt-'. $this->id. '-'. (!empty($options['recursive']) ? 'r' : '');
         $coursecatcache = cache::make('core', 'coursecat');
         if (($cnt = $coursecatcache->get($cntcachekey)) === false) {
-            $this->get_courses($options);
-            $cnt = $coursecatcache->get($cntcachekey);
+            // Cached value not found. Retrieve ALL courses and return their count.
+            unset($options['offset']);
+            unset($options['limit']);
+            unset($options['summary']);
+            unset($options['coursecontacts']);
+            $courses = $this->get_courses($options);
+            $cnt = count($courses);
         }
         return $cnt;
     }
index b0c04d8..f8f65cc 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20130921" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20130927" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="sortorder" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="order of files"/>
         <FIELD NAME="referencefileid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Use to indicate file is a proxy for repository file"/>
-        <FIELD NAME="referencelastsync" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Last time the proxy file was synced with repository, defined for performance reasons"/>
-        <FIELD NAME="referencelifetime" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="How often do we have to sync proxy file with repository, defined for performance reasons"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
index d68ce28..5409977 100644 (file)
@@ -864,11 +864,9 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2012062000.01);
     }
 
-
     // Moodle v2.3.0 release upgrade line
     // Put any upgrade step following this
 
-
     if ($oldversion < 2012062500.02) {
         // Drop some old backup tables, not used anymore
 
@@ -1497,7 +1495,6 @@ function xmldb_main_upgrade($oldversion) {
     // Moodle v2.4.0 release upgrade line
     // Put any upgrade step following this
 
-
     if ($oldversion < 2012120300.01) {
         // Make sure site-course has format='site' //MDL-36840
 
@@ -2555,5 +2552,29 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2013092001.02);
     }
 
+    if ($oldversion < 2013092700.01) {
+
+        $table = new xmldb_table('files');
+
+        // Define field referencelastsync to be dropped from files.
+        $field = new xmldb_field('referencelastsync');
+
+        // Conditionally launch drop field referencelastsync.
+        if ($dbman->field_exists($table, $field)) {
+            $dbman->drop_field($table, $field);
+        }
+
+        // Define field referencelifetime to be dropped from files.
+        $field = new xmldb_field('referencelifetime');
+
+        // Conditionally launch drop field referencelifetime.
+        if ($dbman->field_exists($table, $field)) {
+            $dbman->drop_field($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2013092700.01);
+    }
+
     return true;
 }
index 6ef7851..9709302 100644 (file)
@@ -4678,4 +4678,17 @@ function get_browser_version_classes() {
 function generate_email_supportuser() {
     debugging('generate_email_supportuser is deprecated, please use core_user::get_support_user');
     return core_user::get_support_user();
-}
\ No newline at end of file
+}
+
+/**
+ * Get issued badge details for assertion URL
+ *
+ * @deprecated since Moodle 2.6
+ * @param string $hash Unique hash of a badge
+ * @return array Information about issued badge.
+ */
+function badges_get_issued_badge_info($hash) {
+    debugging('Function badges_get_issued_badge_info() is deprecated. Please use core_badges_assertion class and methods to generate badge assertion.', DEBUG_DEVELOPER);
+    $assertion = new core_badges_assertion($hash);
+    return $assertion->get_badge_assertion();
+}
index 9735788..ec7a027 100644 (file)
@@ -27,4 +27,5 @@ $string['link'] = 'Link';
 $string['createlink'] = 'Create link';
 $string['enterurl'] = 'Enter a URL';
 $string['browserepositories'] = 'Browse repositories...';
+$string['openinnewwindow'] = 'Open in new window';
 $string['accessibilityhint'] = '<p>Web content accessibility guidelines (WCAG):<br/><ul><li><a href="http://www.w3.org/TR/WCAG20/#navigation-mechanisms-refs" target="_blank">2.4.4 Link Purpose (In Context)</a></li><li><a href="http://www.w3.org/TR/WCAG20/#navigation-mechanisms-link" target="_blank">2.4.9 Link Purpose (Link Only)</a></li></ul></p>';
index 90b953c..1276905 100644 (file)
@@ -39,6 +39,7 @@ function atto_link_init_editor($elementid) {
     $PAGE->requires->strings_for_js(array('createlink',
                                           'enterurl',
                                           'browserepositories',
+                                          'openinnewwindow',
                                           'accessibilityhint'),
                                     'atto_link');
 
index 69cc689..2a4d90b 100644 (file)
Binary files a/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-debug.js and b/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-debug.js differ
index 68806af..cf87a38 100644 (file)
Binary files a/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-min.js and b/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-min.js differ
index 69cc689..2a4d90b 100644 (file)
Binary files a/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button.js and b/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button.js differ
index 329e1f7..24f7594 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 M.atto_link = M.atto_link || {
+    /**
+     * The window used to get the link details.
+     *
+     * @property dialogue
+     * @type M.core.dialogue
+     * @default null
+     */
     dialogue : null,
+
+    /**
+     * The selection object returned by the browser.
+     *
+     * @property selection
+     * @type Range
+     * @default null
+     */
     selection : null,
-    init : function(params) {
-        var display_chooser = function(e, elementid) {
-            e.preventDefault();
-            if (!M.editor_atto.is_active(elementid)) {
-                M.editor_atto.focus(elementid);
+
+    /**
+     * Display the chooser dialogue.
+     *
+     * @method init
+     * @param Event e
+     * @param string elementid
+     */
+    display_chooser : function(e, elementid) {
+        e.preventDefault();
+        if (!M.editor_atto.is_active(elementid)) {
+            M.editor_atto.focus(elementid);
+        }
+        M.atto_link.selection = M.editor_atto.get_selection();
+        if (M.atto_link.selection !== false && (!M.atto_link.selection.collapsed)) {
+            var dialogue;
+            if (!M.atto_link.dialogue) {
+                dialogue = new M.core.dialogue({
+                    visible: false,
+                    modal: true,
+                    close: true,
+                    draggable: true
+                });
+            } else {
+                dialogue = M.atto_link.dialogue;
             }
-            M.atto_link.selection = M.editor_atto.get_selection();
-            if (M.atto_link.selection !== false && (!M.atto_link.selection.collapsed)) {
-                var dialogue;
-                if (!M.atto_link.dialogue) {
-                    dialogue = new M.core.dialogue({
-                        visible: false,
-                        modal: true,
-                        close: true,
-                        draggable: true
-                    });
-                } else {
-                    dialogue = M.atto_link.dialogue;
-                }
 
-                dialogue.render();
-                dialogue.set('bodyContent', M.atto_link.get_form_content(elementid));
-                dialogue.set('headerContent', M.util.get_string('createlink', 'atto_link'));
+            dialogue.render();
+            dialogue.set('bodyContent', M.atto_link.get_form_content(elementid));
+            dialogue.set('headerContent', M.util.get_string('createlink', 'atto_link'));
 
-                M.atto_link.resolve_anchors();
+            M.atto_link.resolve_anchors();
 
-                dialogue.show();
-                M.atto_link.dialogue = dialogue;
-            }
-        };
+            dialogue.show();
+            M.atto_link.dialogue = dialogue;
+        }
+    },
 
-        M.editor_atto.add_toolbar_button(params.elementid, 'link', params.icon, params.group, display_chooser, this);
+    /**
+     * Add this button to the form.
+     *
+     * @method init
+     * @param {Object} params
+     */
+    init : function(params) {
+        M.editor_atto.add_toolbar_button(params.elementid, 'link', params.icon, params.group, this.display_chooser, this);
     },
+
+    /**
+     * If there is selected text and it is part of an anchor link,
+     * extract the url (and target) from the link (and set them in the form).
+     *
+     * @method resolve_anchors
+     */
     resolve_anchors : function() {
         // Find the first anchor tag in the selection.
         var selectednode = M.editor_atto.get_selection_parent_node(),
@@ -70,19 +106,39 @@ M.atto_link = M.atto_link || {
         anchornode = Y.one(selectednode).ancestor('a');
 
         if (anchornode) {
+            M.atto_link.selection = M.editor_atto.get_selection_from_node(anchornode);
             url = anchornode.getAttribute('href');
+            target = anchornode.getAttribute('target');
             if (url !== '') {
-                M.atto_link.selection = M.editor_atto.get_selection_from_node(anchornode);
                 Y.one('#atto_link_urlentry').set('value', url);
             }
+            if (target === '_blank') {
+                Y.one('#atto_link_openinnewwindow').set('checked', 'checked');
+            } else {
+                Y.one('#atto_link_openinnewwindow').set('checked', '');
+            }
         }
     },
+
+    /**
+     * Open the repository file picker.
+     *
+     * @method open_filepicker
+     * @param Event e
+     */
     open_filepicker : function(e) {
         var elementid = this.getAttribute('data-editor');
         e.preventDefault();
 
         M.editor_atto.show_filepicker(elementid, 'link', M.atto_link.filepicker_callback);
     },
+
+    /**
+     * Called by the file picker when a link has been chosen.
+     *
+     * @method filepicker_callback
+     * @param {Object} params - contains selected url.
+     */
     filepicker_callback : function(params) {
         M.atto_link.dialogue.hide();
         if (params.url !== '') {
@@ -91,25 +147,69 @@ M.atto_link = M.atto_link || {
             document.execCommand('createLink', false, params.url);
         }
     },
+
+    /**
+     * The OK button has been pressed - make the changes to the source.
+     *
+     * @method set_link
+     * @param Event e
+     */
     set_link : function(e) {
+        var input,
+            target,
+            selectednode,
+            anchornode,
+            value;
+
         e.preventDefault();
         M.atto_link.dialogue.hide();
 
-        var input = e.currentTarget.get('parentNode').one('input');
+        input = e.currentTarget.get('parentNode').one('input[type=url]');
+        target = e.currentTarget.get('parentNode').one('input[type=checkbox]');
 
-        var value = input.get('value');
+        value = input.get('value');
         if (value !== '') {
             M.editor_atto.set_selection(M.atto_link.selection);
             document.execCommand('unlink', false, null);
             document.execCommand('createLink', false, value);
+
+            // Now set the target.
+            selectednode = M.editor_atto.get_selection_parent_node();
+
+            // Note this is a document fragment and YUI doesn't like them.
+            if (!selectednode) {
+                return;
+            }
+
+            anchornode = Y.one(selectednode).ancestor('a');
+            if (anchornode) {
+                target = e.currentTarget.get('parentNode').one('input[type=checkbox]');
+                if (target.get('checked')) {
+                    anchornode.setAttribute('target', '_blank');
+                } else {
+                    anchornode.removeAttribute('target');
+                }
+            }
         }
     },
+
+    /**
+     * Return the HTML of the form to show in the dialogue.
+     *
+     * @method get_form_content
+     * @param string elementid
+     * @return string
+     */
     get_form_content : function(elementid) {
         var content = Y.Node.create('<form>' +
                              '<label for="atto_link_urlentry">' + M.util.get_string('enterurl', 'atto_link') +
                              '</label><br/>' +
                              '<input type="url" value="" id="atto_link_urlentry" size="32"/>' +
                              '<br/>' +
+                             '<label for="atto_link_openinnewwindow">' + M.util.get_string('openinnewwindow', 'atto_link') +
+                             '</label><br/>' +
+                             '<input type="checkbox" id="atto_link_openinnewwindow"/>' +
+                             '<br/>' +
                              '<button id="openlinkbrowser" data-editor="' + Y.Escape.html(elementid) + '">' +
                              M.util.get_string('browserepositories', 'atto_link') +
                              '</button>' +
index 6511ee9..4cbd3a3 100644 (file)
@@ -1,34 +1,40 @@
-div.editor_atto {
+.editor_atto_content_wrap {
     background-color: white;
-    border: 1px solid #BBB;
-    width: 100%;
+}
+.editor_atto_content {
+    padding: 4px;
 }
 
+.editor_atto_content_wrap,
 .editor_atto + textarea {
     width: 100%;
     padding: 0;
-    resize: vertical;
-    border-radius: 0;
     border: 1px solid #BBB;
+    border-top: none;
+}
+
+.editor_atto + textarea {
+    border-radius: 0;
+    resize: vertical;
+    margin-top: -1px;
 }
 
 div.editor_atto_toolbar {
     display: block;
     background: #F2F2F2;
-    min-height: 42px;
-    border-top: 1px solid #BBB;
-    border-left: 1px solid #BBB;
-    border-right: 1px solid #BBB;
+    min-height: 35px;
+    border: 1px solid #BBB;
     width: 100%;
     padding: 0 0 9px 0;
 }
 
 div.editor_atto_toolbar button {
-    padding: 5px 9px 4px 9px;
+    padding: 3px 9px;
     background: none;
     border: 0px;
     margin: 0;
     border-radius: 0;
+    cursor: pointer;
  }
 
 div.editor_atto_toolbar button + button {
@@ -39,6 +45,10 @@ div.editor_atto_toolbar button[disabled] {
     opacity: .45;
 }
 
+div.editor_atto_toolbar button:active {
+    background: #B7D6FF;
+}
+
 div.editor_atto_toolbar button img {
     padding: 1px;
 }
@@ -53,7 +63,7 @@ div.editor_atto_toolbar div.atto_group {
     background: #FFF;
 }
 
-.editor_atto img {
+.editor_atto_content img {
     resize: both; overflow: auto;
 }
 
index 96ccd49..ccdc137 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js differ
index 21a2ace..6bf193f 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js differ
index 96ccd49..ccdc137 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js differ
index 30e1892..be956f0 100644 (file)
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
+/**
+ * Atto editor.
+ *
+ * @package    editor_atto
+ * @copyright  2013 Damyon Wiese  <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Classes constants.
+ */
+CSS = {
+    CONTENT: 'editor_atto_content',
+    CONTENTWRAPPER: 'editor_atto_content_wrap',
+    TOOLBAR: 'editor_atto_toolbar',
+    WRAPPER: 'editor_atto'
+};
+
 /**
  * Atto editor main class.
  * Common functions required by editor plugins.
  *
- * @package    editor-atto
+ * @package    editor_atto
  * @copyright  2013 Damyon Wiese  <damyon@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 M.editor_atto = M.editor_atto || {
+
     /**
      * List of attached button handlers to prevent duplicates.
      */
@@ -307,12 +326,22 @@ M.editor_atto = M.editor_atto || {
      */
     init : function(params) {
         var textarea = Y.one('#' +params.elementid);
+        var wrapper = Y.Node.create('<div class="' + CSS.WRAPPER + '" />');
         var atto = Y.Node.create('<div id="' + params.elementid + 'editable" ' +
                                             'contenteditable="true" ' +
                                             'spellcheck="true" ' +
-                                            'class="editor_atto"/>');
+                                            'class="' + CSS.CONTENT + '" />');
+
         var cssfont = '';
-        var toolbar = Y.Node.create('<div class="editor_atto_toolbar" id="' + params.elementid + '_toolbar" role="toolbar"/>');
+        var toolbar = Y.Node.create('<div class="' + CSS.TOOLBAR + '" id="' + params.elementid + '_toolbar" role="toolbar"/>');
+
+        // Editable content wrapper.
+        var content = Y.Node.create('<div class="' + CSS.CONTENTWRAPPER + '" />');
+        content.appendChild(atto);
+
+        // Add everything to the wrapper.
+        wrapper.appendChild(toolbar);
+        wrapper.appendChild(content);
 
         // Bleh - why are we sent a url and not the css to apply directly?
         var css = Y.io(params.content_css, { sync: true });
@@ -326,10 +355,8 @@ M.editor_atto = M.editor_atto || {
         // Copy text to editable div.
         atto.append(textarea.get('value'));
 
-        // Add the toolbar to the page.
-        textarea.get('parentNode').insert(toolbar, textarea);
-        // Add the editable div to the page.
-        textarea.get('parentNode').insert(atto, textarea);
+        // Add the toolbar and editable zone to the page.
+        textarea.get('parentNode').insert(wrapper, textarea);
         atto.setStyle('color', textarea.getStyle('color'));
         atto.setStyle('lineHeight', textarea.getStyle('lineHeight'));
         atto.setStyle('fontSize', textarea.getStyle('fontSize'));
index 3bd83b1..567cc6c 100644 (file)
@@ -363,6 +363,8 @@ function events_process_queued_handler($qhandler) {
     $qh->status       = $qhandler->status + 1;
     $DB->update_record('events_queue_handlers', $qh);
 
+    debugging($errormessage);
+
     return false;
 }
 
index 3783eee..f05a70a 100644 (file)
@@ -1400,10 +1400,6 @@ class file_storage {
             $filerecord->sortorder = 0;
         }
 
-        // TODO MDL-33416 [2.4] fields referencelastsync and referencelifetime to be removed from {files} table completely
-        unset($filerecord->referencelastsync);
-        unset($filerecord->referencelifetime);
-
         $filerecord->mimetype          = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
         $filerecord->userid            = empty($filerecord->userid) ? null : $filerecord->userid;
         $filerecord->source            = empty($filerecord->source) ? null : $filerecord->source;
@@ -2345,7 +2341,7 @@ class file_storage {
                     'lastsync' => $lastsync,
                     'lifetime' => $lifetime);
         $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize,
-            status = :status, referencelastsync = :lastsync, referencelifetime = :lifetime
+            status = :status
             WHERE referencefileid = :referencefileid', $params);
         $data = array('id' => $referencefileid, 'lastsync' => $lastsync, 'lifetime' => $lifetime);
         $DB->update_record('files_reference', (object)$data);
index 93b7cda..e937405 100644 (file)
@@ -156,12 +156,6 @@ class stored_file {
                     }
                 }
 
-                if ($field === 'referencelastsync' or $field === 'referencelifetime') {
-                    // do not update those fields
-                    // TODO MDL-33416 [2.4] fields referencelastsync and referencelifetime to be removed from {files} table completely
-                    continue;
-                }
-
                 // adding the field
                 $this->file_record->$field = $value;
             } else {
index 15f8d33..76a4b63 100644 (file)
@@ -865,7 +865,7 @@ class core_files_file_storage_testcase extends advanced_testcase {
             'itemid'    => 0,
             'filepath'  => '/downloadtest/',
         );
-        $url = 'http://download.moodle.org/unittest/test.html';
+        $url = $this->getExternalTestFileUrl('/test.html');
 
         $fs = get_file_storage();
 
index 9d1b609..5e714a4 100644 (file)
@@ -433,6 +433,9 @@ define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2');
 /** True if module can show description on course main page */
 define('FEATURE_SHOW_DESCRIPTION', 'showdescription');
 
+/** True if module uses the question bank */
+define('FEATURE_USES_QUESTIONS', 'usesquestions');
+
 /** Unspecified module archetype */
 define('MOD_ARCHETYPE_OTHER', 0);
 /** Resource-like type module */
@@ -442,6 +445,9 @@ define('MOD_ARCHETYPE_ASSIGNMENT', 2);
 /** System (not user-addable) module archetype */
 define('MOD_ARCHETYPE_SYSTEM', 3);
 
+/** Return this from modname_get_types callback to use default display in activity chooser */
+define('MOD_SUBTYPE_NO_CHILDREN', 'modsubtypenochildren');
+
 /**
  * Security token used for allowing access
  * from external application such as web services.
@@ -7247,10 +7253,35 @@ function plugin_callback($type, $name, $feature, $action, $params = null, $defau
  * @param array $params parameters of callback function
  * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
  * @return mixed
- * @throws coding_exception
  */
 function component_callback($component, $function, array $params = array(), $default = null) {
-    global $CFG; // This is needed for require_once() below.
+
+    $functionname = component_callback_exists($component, $function);
+
+    if ($functionname) {
+        // Function exists, so just return function result.
+        $ret = call_user_func_array($functionname, $params);
+        if (is_null($ret)) {
+            return $default;
+        } else {
+            return $ret;
+        }
+    }
+    return $default;
+}
+
+/**
+ * Determine if a component callback exists and return the function name to call. Note that this
+ * function will include the required library files so that the functioname returned can be
+ * called directly.
+ *
+ * @param string $component frankenstyle component name, e.g. 'mod_quiz'
+ * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
+ * @return mixed Complete function name to call if the callback exists or false if it doesn't.
+ * @throws coding_exception if invalid component specfied
+ */
+function component_callback_exists($component, $function) {
+    global $CFG; // This is needed for the inclusions.
 
     $cleancomponent = clean_param($component, PARAM_COMPONENT);
     if (empty($cleancomponent)) {
@@ -7276,21 +7307,15 @@ function component_callback($component, $function, array $params = array(), $def
 
     if (!function_exists($function) and function_exists($oldfunction)) {
         if ($type !== 'mod' and $type !== 'core') {
-            debugging("Please use new function name $function instead of legacy $oldfunction");
+            debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER);
         }
         $function = $oldfunction;
     }
 
     if (function_exists($function)) {
-        // Function exists, so just return function result.
-        $ret = call_user_func_array($function, $params);
-        if (is_null($ret)) {
-            return $default;
-        } else {
-            return $ret;
-        }
+        return $function;
     }
-    return $default;
+    return false;
 }
 
 /**
index 506d2d7..63ad252 100644 (file)
@@ -3522,55 +3522,6 @@ class settings_navigation extends navigation_node {
         return $node;
     }
 
-    /**
-     * Generate the list of modules for the given course.
-     *
-     * @param stdClass $course The course to get modules for
-     */
-    protected function get_course_modules($course) {
-        global $CFG;
-        // This function is included when we include course/lib.php at the top
-        // of this file
-        $modnames = get_module_types_names();
-        $resources = array();
-        $activities = array();
-        foreach($modnames as $modname=>$modnamestr) {
-            if (!course_allowed_module($course, $modname)) {
-                continue;
-            }
-
-            $libfile = "$CFG->dirroot/mod/$modname/lib.php";
-            if (!file_exists($libfile)) {
-                continue;
-            }
-            include_once($libfile);
-            $gettypesfunc =  $modname.'_get_types';
-            if (function_exists($gettypesfunc)) {
-                $types = $gettypesfunc();
-                foreach($types as $type) {
-                    if (!isset($type->modclass) || !isset($type->typestr)) {
-                        debugging('Incorrect activity type in '.$modname);
-                        continue;
-                    }
-                    if ($type->modclass == MOD_CLASS_RESOURCE) {
-                        $resources[html_entity_decode($type->type, ENT_QUOTES, 'UTF-8')] = $type->typestr;
-                    } else {
-                        $activities[html_entity_decode($type->type, ENT_QUOTES, 'UTF-8')] = $type->typestr;
-                    }
-                }
-            } else {
-                $archetype = plugin_supports('mod', $modname, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER);
-                if ($archetype == MOD_ARCHETYPE_RESOURCE) {
-                    $resources[$modname] = $modnamestr;
-                } else {
-                    // all other archetypes are considered activity
-                    $activities[$modname] = $modnamestr;
-                }
-            }
-        }
-        return array($resources, $activities);
-    }
-
     /**
      * This function loads the course settings that are available for the user
      *
index 42d4dd3..9c275e3 100644 (file)
@@ -452,6 +452,46 @@ abstract class advanced_testcase extends PHPUnit_Framework_TestCase {
         return phpunit_util::get_data_generator();
     }
 
+    /**
+     * Returns UTL of the external test file.
+     *
+     * The result depends on the value of following constants:
+     *  - TEST_EXTERNAL_FILES_HTTP_URL
+     *  - TEST_EXTERNAL_FILES_HTTPS_URL
+     *
+     * They should point to standard external test files repository,
+     * it defaults to 'http://download.moodle.org/unittest'.
+     *
+     * False value means skip tests that require external files.
+     *
+     * @param string $path
+     * @param bool $https true if https required
+     * @return string url
+     */
+    public function getExternalTestFileUrl($path, $https = false) {
+        $path = ltrim($path, '/');
+        if ($path) {
+            $path = '/'.$path;
+        }
+        if ($https) {
+            if (defined('TEST_EXTERNAL_FILES_HTTPS_URL')) {
+                if (!TEST_EXTERNAL_FILES_HTTPS_URL) {
+                    $this->markTestSkipped('Tests using external https test files are disabled');
+                }
+                return TEST_EXTERNAL_FILES_HTTPS_URL.$path;
+            }
+            return 'https://download.moodle.org/unittest'.$path;
+        }
+
+        if (defined('TEST_EXTERNAL_FILES_HTTP_URL')) {
+            if (!TEST_EXTERNAL_FILES_HTTP_URL) {
+                $this->markTestSkipped('Tests using external http test files are disabled');
+            }
+            return TEST_EXTERNAL_FILES_HTTP_URL.$path;
+        }
+        return 'http://download.moodle.org/unittest'.$path;
+    }
+
     /**
      * Recursively visit all the files in the source tree. Calls the callback
      * function with the pathname of each file found.
index b0c5bed..7461fb4 100644 (file)
@@ -200,6 +200,7 @@ class phpunit_util extends testing_util {
         reset_text_filters_cache(true);
         events_get_handlers('reset');
         core_text::reset_caches();
+        get_message_processors(false, true);
         if (class_exists('repository')) {
             repository::reset_caches();
         }
index 52328af..e64b516 100644 (file)
@@ -334,6 +334,33 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
         $this->assertTrue($DB->record_exists('user', array('username'=>'onemore')));
     }
 
+    public function test_message_processors_reset() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        // Get all processors first.
+        $processors1 = get_message_processors();
+
+        // Add a new message processor and get all processors again.
+        $processor = new stdClass();
+        $processor->name = 'test_processor';
+        $processor->enabled = 1;
+        $DB->insert_record('message_processors', $processor);
+
+        $processors2 = get_message_processors();
+
+        // Assert that new processor still haven't been added to the list.
+        $this->assertSame($processors1, $processors2);
+
+        // Reset message processors data.
+        $processors3 = get_message_processors(false, true);
+        // Now, list of processors should not be the same any more,
+        // And we should have one more message processor in the list.
+        $this->assertNotSame($processors1, $processors3);
+        $this->assertEquals(count($processors1) + 1, count($processors3));
+    }
+
     public function test_message_redirection() {
         $this->preventResetByRollback(); // Messaging is not compatible with transactions...
         $this->resetAfterTest(false);
index 9984b71..5cdfb69 100644 (file)
@@ -2054,3 +2054,23 @@ function question_page_type_list($pagetype, $parentcontext, $currentcontext) {
         return $types;
     }
 }
+
+/**
+ * Does an activity module use the question bank?
+ *
+ * @param string $modname The name of the module (without mod_ prefix).
+ * @return bool true if the module uses questions.
+ */
+function question_module_uses_questions($modname) {
+    if (plugin_supports('mod', $modname, FEATURE_USES_QUESTIONS)) {
+        return true;
+    }
+
+    $component = 'mod_'.$modname;
+    if (component_callback_exists($component, 'question_pluginfile')) {
+        debugging("{$component} uses questions but doesn't declare FEATURE_USES_QUESTIONS", DEBUG_DEVELOPER);
+        return true;
+    }
+
+    return false;
+}
index 1096e30..53a401c 100644 (file)
@@ -33,7 +33,8 @@ class core_componentlib_testcase extends advanced_testcase {
     public function test_component_installer() {
         global $CFG;
 
-        $ci = new component_installer('http://download.moodle.org', 'unittest', 'downloadtests.zip');
+        $url = $this->getExternalTestFileUrl('');
+        $ci = new component_installer($url, '', 'downloadtests.zip');
         $this->assertTrue($ci->check_requisites());
 
         $destpath = $CFG->dataroot.'/downloadtests';
index c0eb6a7..aefa4c2 100644 (file)
@@ -126,9 +126,18 @@ class core_eventslib_testcase extends advanced_testcase {
      * Tests events_trigger() function when instant handler fails.
      */
     public function test_events_trigger__failed_instant() {
+        global $CFG;
+        $olddebug = $CFG->debug;
+
         $this->assertEquals(1, events_trigger('test_instant', 'fail'), 'fail first event: %s');
         $this->assertEquals(1, events_trigger('test_instant', 'ok'), 'this one should fail too: %s');
+
+        // We disable debugging for this next test. It'll make some noise when it fails to dispatch
+        // so that problems don't go permanently unnoticed.
+        $CFG->debug = 0;
         $this->assertEquals(0, events_cron('test_instant'), 'all events should stay in queue: %s');
+        $CFG->debug = $olddebug;
+
         $this->assertEquals(2, events_pending_count('test_instant'), 'two events should in queue: %s');
         $this->assertEquals(0, eventslib_sample_function_handler('status'), 'verify no event dispatched yet: %s');
         eventslib_sample_function_handler('ignorefail'); // Ignore "fail" eventdata from now on.
index 57e4197..0dc7a64 100644 (file)
@@ -84,7 +84,7 @@ class core_filelib_testcase extends advanced_testcase {
         global $CFG;
 
         // Test http success first.
-        $testhtml = "http://download.moodle.org/unittest/test.html";
+        $testhtml = $this->getExternalTestFileUrl('/test.html');
 
         $contents = download_file_content($testhtml);
         $this->assertSame('47250a973d1b88d9445f94db4ef2c97a', md5($contents));
@@ -109,7 +109,7 @@ class core_filelib_testcase extends advanced_testcase {
         $this->assertSame('', $response->error);
 
         // Test https success.
-        $testhtml = "https://download.moodle.org/unittest/test.html";
+        $testhtml = $this->getExternalTestFileUrl('/test.html', true);
 
         $contents = download_file_content($testhtml, null, null, false, 300, 20, true);
         $this->assertSame('47250a973d1b88d9445f94db4ef2c97a', md5($contents));
@@ -118,7 +118,7 @@ class core_filelib_testcase extends advanced_testcase {
         $this->assertSame('47250a973d1b88d9445f94db4ef2c97a', md5($contents));
 
         // Now 404.
-        $testhtml = "http://download.moodle.org/unittest/test.html_nonexistent";
+        $testhtml = $this->getExternalTestFileUrl('/test.html_nonexistent');
 
         $contents = download_file_content($testhtml);
         $this->assertFalse($contents);
@@ -133,13 +133,14 @@ class core_filelib_testcase extends advanced_testcase {
         $this->assertSame('', $response->error);
 
         // Invalid url.
-        $testhtml = "ftp://download.moodle.org/unittest/test.html";
+        $testhtml = $this->getExternalTestFileUrl('/test.html');
+        $testhtml = str_replace('http://', 'ftp://', $testhtml);
 
         $contents = download_file_content($testhtml);
         $this->assertFalse($contents);
 
         // Test standard redirects.
-        $testurl = 'http://download.moodle.org/unittest/test_redir.php';
+        $testurl = $this->getExternalTestFileUrl('/test_redir.php');
 
         $contents = download_file_content("$testurl?redir=2");
         $this->assertSame('done', $contents);
@@ -152,13 +153,11 @@ class core_filelib_testcase extends advanced_testcase {
         $this->assertSame('done', $response->results);
         $this->assertSame('', $response->error);
 
+        // Commented out this block if there are performance problems.
         /*
-        // Commented out for performance reasons.
-
         $contents = download_file_content("$testurl?redir=6");
         $this->assertFalse(false, $contents);
         $this->assertDebuggingCalled();
-
         $response = download_file_content("$testurl?redir=6", null, null, true);
         $this->assertInstanceOf('stdClass', $response);
         $this->assertSame('0', $response->status);
@@ -168,7 +167,7 @@ class core_filelib_testcase extends advanced_testcase {
         */
 
         // Test relative redirects.
-        $testurl = 'http://download.moodle.org/unittest/test_relative_redir.php';
+        $testurl = $this->getExternalTestFileUrl('/test_relative_redir.php');
 
         $contents = download_file_content("$testurl");
         $this->assertSame('done', $contents);
@@ -184,7 +183,7 @@ class core_filelib_testcase extends advanced_testcase {
         global $CFG;
 
         // Test https success.
-        $testhtml = "https://download.moodle.org/unittest/test.html";
+        $testhtml = $this->getExternalTestFileUrl('/test.html');
 
         $curl = new curl();
         $contents = $curl->get($testhtml);
@@ -212,7 +211,7 @@ class core_filelib_testcase extends advanced_testcase {
         @unlink($tofile);
 
         // Test full URL redirects.
-        $testurl = 'http://download.moodle.org/unittest/test_redir.php';
+        $testurl = $this->getExternalTestFileUrl('/test_redir.php');
 
         $curl = new curl();
         $contents = $curl->get("$testurl?redir=2", array(), array('CURLOPT_MAXREDIRS'=>2));
@@ -294,7 +293,7 @@ class core_filelib_testcase extends advanced_testcase {
         @unlink($tofile);
 
         // Test relative location redirects.
-        $testurl = 'http://download.moodle.org/unittest/test_relative_redir.php';
+        $testurl = $this->getExternalTestFileUrl('/test_relative_redir.php');
 
         $curl = new curl();
         $contents = $curl->get($testurl);
@@ -310,7 +309,7 @@ class core_filelib_testcase extends advanced_testcase {
         $this->assertSame('done', $contents);
 
         // Test different redirect types.
-        $testurl = 'http://download.moodle.org/unittest/test_relative_redir.php';
+        $testurl = $this->getExternalTestFileUrl('/test_relative_redir.php');
 
         $curl = new curl();
         $contents = $curl->get("$testurl?type=301");
index 34fe337..a10d05a 100644 (file)
@@ -35,14 +35,8 @@ global $CFG;
 require_once($CFG->libdir.'/simplepie/moodle_simplepie.php');
 
 
-class core_rsslib_testcase extends basic_testcase {
-
-    // A url we know exists and is valid.
-    const VALIDURL = 'http://download.moodle.org/unittest/rsstest.xml';
-    // A url which we know doesn't exist.
-    const INVALIDURL = 'http://download.moodle.org/unittest/rsstest-which-doesnt-exist.xml';
-    // This tinyurl redirects to th rsstest.xml file.
-    const REDIRECTURL = 'http://tinyurl.com/lvyslv';
+class core_rsslib_testcase extends advanced_testcase {
+
     // The number of seconds tests should wait for the server to respond (high to prevent false positives).
     const TIMEOUT = 10;
 
@@ -51,7 +45,7 @@ class core_rsslib_testcase extends basic_testcase {
     }
 
     public function test_getfeed() {
-        $feed = new moodle_simplepie(self::VALIDURL, self::TIMEOUT);
+        $feed = new moodle_simplepie($this->getExternalTestFileUrl('/rsstest.xml'), self::TIMEOUT);
 
         $this->assertInstanceOf('moodle_simplepie', $feed);
 
@@ -105,7 +99,7 @@ EOD;
      * Test retrieving a url which doesn't exist.
      */
     public function test_failurl() {
-        $feed = @new moodle_simplepie(self::INVALIDURL, self::TIMEOUT); // We do not want this in php error log.
+        $feed = @new moodle_simplepie($this->getExternalTestFileUrl('/rsstest-which-doesnt-exist.xml'), self::TIMEOUT); // We do not want this in php error log.
 
         $this->assertNotEmpty($feed->error());
     }
@@ -119,7 +113,7 @@ EOD;
         $oldproxy = $CFG->proxyhost;
         $CFG->proxyhost = 'xxxxxxxxxxxxxxx.moodle.org';
 
-        $feed = new moodle_simplepie(self::VALIDURL);
+        $feed = new moodle_simplepie($this->getExternalTestFileUrl('/rsstest.xml'));
 
         $this->assertNotEmpty($feed->error());
         $this->assertEmpty($feed->get_title());
@@ -130,7 +124,7 @@ EOD;
      * Test retrieving a url which sends a redirect to another valid feed.
      */
     public function test_redirect() {
-        $feed = new moodle_simplepie(self::REDIRECTURL, self::TIMEOUT);
+        $feed = new moodle_simplepie($this->getExternalTestFileUrl('/rss_redir.php'), self::TIMEOUT);
 
         $this->assertNull($feed->error());
         $this->assertSame('Moodle News', $feed->get_title());
index 8705dcc..8718534 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * These tests rely on the rsstest.xml file on download.moodle.org,
- * from eloys listing:
- *   rsstest.xml: One valid rss feed.
- *   md5:  8fd047914863bf9b3a4b1514ec51c32c
- *   size: 32188
- *
- * If networking/proxy configuration is wrong these tests will fail..
+ * Weblib tests.
  *
  * @package    core
  * @category   phpunit
index 9f4809d..4643fc1 100644 (file)
@@ -40,6 +40,7 @@ information provided here is intended especially for developers.
 * Implement new method get_enabled_plugins() method in subplugin info classes.
 * Each plugin should include version information in version.php.
 * Module and block tables do not contain version column any more, use get_config('xx_yy', 'version') instead.
+* $USER->password field is intentionally unset so that session data does not contain password hashes.
 
 DEPRECATIONS:
 Various previously deprecated functions have now been altered to throw DEBUG_DEVELOPER debugging notices
@@ -96,6 +97,8 @@ Navigation:
     * print_navigation()                    -> $OUTPUT->navbar()
     * build_navigation()                    -> $PAGE->navbar methods
     * navmenu()                             -> (no replacement)
+    * settings_navigation::
+          get_course_modules()              -> (no replacement)
 
 Calendar:
     * add_event()                           -> calendar_event::create()
index 9fd88b0..919b97c 100644 (file)
@@ -150,7 +150,8 @@ if (!empty($user)) {
                 or (!is_siteadmin($user) && has_capability('moodle/webservice:createtoken', context_system::instance()))) {
             // if service doesn't exist, dml will throw exception
             $service_record = $DB->get_record('external_services', array('shortname'=>$serviceshortname, 'enabled'=>1), '*', MUST_EXIST);
-            // create a new token
+
+            // Create a new token.
             $token = new stdClass;
             $token->token = md5(uniqid(rand(), 1));
             $token->userid = $user->id;
@@ -159,9 +160,18 @@ if (!empty($user)) {
             $token->creatorid = $user->id;
             $token->timecreated = time();
             $token->externalserviceid = $service_record->id;
-            $tokenid = $DB->insert_record('external_tokens', $token);
-            add_to_log(SITEID, 'webservice', 'automatically create user token', '' , 'User ID: ' . $user->id);
-            $token->id = $tokenid;
+            $token->id = $DB->insert_record('external_tokens', $token);
+
+            $params = array(
+                'objectid' => $token->id,
+                'relateduserid' => $user->id,
+                'other' => array(
+                    'auto' => true
+                )
+            );
+            $event = \core\event\webservice_token_created::create($params);
+            $event->add_record_snapshot('external_tokens', $token);
+            $event->trigger();
         } else {
             throw new moodle_exception('cannotcreatetoken', 'webservice', '', $serviceshortname);
         }
@@ -170,7 +180,12 @@ if (!empty($user)) {
     // log token access
     $DB->set_field('external_tokens', 'lastaccess', time(), array('id'=>$token->id));
 
-    add_to_log(SITEID, 'webservice', 'sending requested user token', '' , 'User ID: ' . $user->id);
+    $params = array(
+        'objectid' => $token->id,
+    );
+    $event = \core\event\webservice_token_sent::create($params);
+    $event->add_record_snapshot('external_tokens', $token);
+    $event->trigger();
 
     $usertoken = new stdClass;
     $usertoken->token = $token->token;
index f0d8816..c3fa84a 100644 (file)
@@ -2289,12 +2289,16 @@ function message_print_heading($title, $colspan=3) {
  * system configuration
  *
  * @param bool $ready only return ready-to-use processors
+ * @param bool $reset Reset list of message processors (used in unit tests)
  * @return mixed $processors array of objects containing information on message processors
  */
-function get_message_processors($ready = false) {
+function get_message_processors($ready = false, $reset = false) {
     global $DB, $CFG;
 
     static $processors;
+    if ($reset) {
+        $processors = array();
+    }
 
     if (empty($processors)) {
         // Get all processors, ensure the name column is the first so it will be the array key
index a33062f..85b9d9b 100644 (file)
@@ -15,7 +15,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * User logout event handler definition.
+ * Event observers definition.
  *
  * @package mod_chat
  * @category event
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-/* List of handlers */
-$handlers = array (
-    'user_logout' => array (
-        'handlerfile'      => '/mod/chat/lib.php',
-        'handlerfunction'  => 'chat_user_logout',
-        'schedule'         => 'instant',
-        'internal'         => 1,
-    ),
+$observers = array(
+
+    // User logging out.
+    array(
+        'eventname' => '\core\event\user_loggedout',
+        'callback' => 'chat_user_logout',
+        'includefile' => '/mod/chat/lib.php'
+    )
 );
index cbff840..a278120 100644 (file)
@@ -29,10 +29,10 @@ if (!$course = $DB->get_record('course', array('id'=>$chatuser->course))) {
 }
 
 //Get the user theme and enough info to be used in chat_format_message() which passes it along to
-if (!$USER = $DB->get_record('user', array('id'=>$chatuser->userid))) { // no optimisation here, it would break again in future!
+if (!$user = $DB->get_record('user', array('id'=>$chatuser->userid, 'deleted'=>0, 'suspended'=>0))) { // no optimisation here, it would break again in future!
     print_error('invaliduser');
 }
-$USER->description = '';
+\core\session\manager::set_user($user);
 
 //Setup course, lang and theme
 $PAGE->set_course($course);
index 9a98185..22bdb58 100644 (file)
@@ -55,10 +55,10 @@ if (!$course = $DB->get_record('course', array('id'=>$chatuser->course))) {
 
 //Get the user theme and enough info to be used in chat_format_message() which passes it along to
 // chat_format_message_manually() -- and only id and timezone are used.
-if (!$USER = $DB->get_record('user', array('id'=>$chatuser->userid))) { // no optimisation here, it would break again in future!
+if (!$user = $DB->get_record('user', array('id'=>$chatuser->userid, 'deleted'=>0, 'suspended'=>0))) { // no optimisation here, it would break again in future!
     print_error('invaliduser');
 }
-$USER->description = '';
+\core\session\manager::set_user($user);
 
 //Setup course, lang and theme
 $PAGE->set_course($course);
index 35fff5d..e523de8 100644 (file)
@@ -21,14 +21,13 @@ if (!$course = $DB->get_record('course', array('id'=>$chatuser->course))) {
 }
 
 //Get the user theme and enough info to be used in chat_format_message() which passes it along to
-if (!$USER = $DB->get_record('user', array('id'=>$chatuser->userid))) { // no optimisation here, it would break again in future!
+if (!$user = $DB->get_record('user', array('id'=>$chatuser->userid, 'deleted'=>0, 'suspended'=>0))) { // no optimisation here, it would break again in future!
     print_error('invaliduser');
 }
+\core\session\manager::set_user($user);
 
 $PAGE->set_pagelayout('embedded');
 
-$USER->description = '';
-
 //Setup course, lang and theme
 $PAGE->set_course($course);
 
index 0089feb..b48df08 100644 (file)
@@ -1315,11 +1315,12 @@ function chat_extend_settings_navigation(settings_navigation $settings, navigati
 /**
  * user logout event handler
  *
- * @param object $user full $USER object
+ * @param \core\event\user_loggedout $event The event.
+ * @return void
  */
-function chat_user_logout($user) {
+function chat_user_logout(\core\event\user_loggedout $event) {
     global $DB;
-    $DB->delete_records('chat_users', array('userid'=>$user->id));
+    $DB->delete_records('chat_users', array('userid' => $event->objectid));
 }
 
 /**
index 5be16c2..a9a39f8 100644 (file)
@@ -25,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$module->version   = 2013050100;       // The current module version (Date: YYYYMMDDXX)
+$module->version   = 2013092600;       // The current module version (Date: YYYYMMDDXX)
 $module->requires  = 2013050100;    // Requires this Moodle version
 $module->component = 'mod_chat';       // Full name of the plugin (used for diagnostics)
 $module->cron      = 300;
index b698c37..7b19ff8 100644 (file)
@@ -41,10 +41,6 @@ $current_tab = $do_show;
 //get the objects
 ////////////////////////////////////////////////////////
 
-if ($userid) {
-    $formdata->userid = intval($userid);
-}
-
 if (! $cm = get_coursemodule_from_id('feedback', $id)) {
     print_error('invalidcoursemodule');
 }
@@ -65,10 +61,6 @@ $context = context_module::instance($cm->id);
 
 require_login($course, true, $cm);
 
-if (($formdata = data_submitted()) AND !confirm_sesskey()) {
-    print_error('invalidsesskey');
-}
-
 require_capability('mod/feedback:viewreports', $context);
 
 ////////////////////////////////////////////////////////
index a41efff..2b5f2e9 100644 (file)
@@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die();
  * Event observer for mod_forum.
  */
 class mod_forum_observer {
+
     /**
      * Triggered via user_enrolment_deleted event.
      *
@@ -49,4 +50,45 @@ class mod_forum_observer {
             $DB->delete_records_select('forum_read', 'userid = :userid AND forumid '.$forumselect, $params);
         }
     }
+
+    /**
+     * Observer for role_assigned event.
+     *
+     * @param \core\event\role_assigned $event
+     * @return void
+     */
+    public static function role_assigned(\core\event\role_assigned $event) {
+        global $CFG, $DB;
+
+        $context = context::instance_by_id($event->contextid, MUST_EXIST);
+
+        // If contextlevel is course then only subscribe user. Role assignment
+        // at course level means user is enroled in course and can subscribe to forum.
+        if ($context->contextlevel != CONTEXT_COURSE) {
+            return;
+        }
+
+        // Forum lib required for the constant used below.
+        require_once($CFG->dirroot . '/mod/forum/lib.php');
+
+        $userid = $event->relateduserid;
+        $sql = "SELECT f.id, cm.id AS cmid
+                  FROM {forum} f
+                  JOIN {course_modules} cm ON (cm.instance = f.id)
+                  JOIN {modules} m ON (m.id = cm.module)
+             LEFT JOIN {forum_subscriptions} fs ON (fs.forum = f.id AND fs.userid = :userid)
+                 WHERE f.course = :courseid
+                   AND f.forcesubscribe = :initial
+                   AND m.name = 'forum'
+                   AND fs.id IS NULL";
+        $params = array('courseid' => $context->instanceid, 'userid' => $userid, 'initial' => FORUM_INITIALSUBSCRIBE);
+
+        $forums = $DB->get_records_sql($sql, $params);
+        foreach ($forums as $forum) {
+            // If user doesn't have allowforcesubscribe capability then don't subscribe.
+            if (has_capability('mod/forum:allowforcesubscribe', context_module::instance($forum->cmid), $userid)) {
+                forum_subscribe($userid, $forum->id);
+            }
+        }
+    }
 }
index 5421b5a..15f5248 100644 (file)
@@ -15,7 +15,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Meta course enrolment plugin event handler definition.
+ * Forum event handler definition.
  *
  * @package mod_forum
  * @category event
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-/* List of handlers */
-$handlers = array (
-    'role_assigned' => array (
-        'handlerfile'      => '/mod/forum/lib.php',
-        'handlerfunction'  => 'forum_user_role_assigned',
-        'schedule'         => 'instant',
-        'internal'         => 1,
-    ),
-);
-
 // List of observers.
 $observers = array(
 
@@ -40,4 +30,9 @@ $observers = array(
         'eventname'   => '\core\event\user_enrolment_deleted',
         'callback'    => 'mod_forum_observer::user_enrolment_deleted',
     ),
+
+    array(
+        'eventname' => '\core\event\role_assigned',
+        'callback' => 'mod_forum_observer::role_assigned'
+    ),
 );
index c483ec6..0ab864f 100644 (file)
@@ -100,6 +100,7 @@ $string['configmaxbytes'] = 'Default maximum size for all forum attachments on t
 $string['configoldpostdays'] = 'Number of days old any post is considered read.';
 $string['configreplytouser'] = 'When a forum post is mailed out, should it contain the user\'s email address so that recipients can reply personally rather than via the forum? Even if set to \'Yes\' users can choose in their profile to keep their email address secret.';
 $string['configshortpost'] = 'Any post under this length (in characters not including HTML) is considered short (see below).';
+$string['configtrackingtype'] = 'Default setting for read tracking.';
 $string['configtrackreadposts'] = 'Set to \'yes\' if you want to track read/unread for each user.';
 $string['configusermarksread'] = 'If \'yes\', the user must manually mark a post as read. If \'no\', when the post is viewed it is marked as read.';
 $string['confirmsubscribe'] = 'Do you really want to subscribe to forum \'{$a}\'?';
index a62d3e0..a63bd09 100644 (file)
@@ -6193,7 +6193,7 @@ function forum_update_subscriptions_button($courseid, $forumid) {
 /**
  * This function gets run whenever user is enrolled into course
  *
- * @deprecated deprecating this function as we will be using forum_user_role_assigned
+ * @deprecated deprecating this function as we will be using \mod_forum\observer::role_assigned()
  * @param stdClass $cp
  * @return void
  */
@@ -6216,43 +6216,6 @@ function forum_user_enrolled($cp) {
     }
 }
 
-/**
- * This function gets run whenever user is assigned role in course
- *
- * @param stdClass $cp
- * @return void
- */
-function forum_user_role_assigned($cp) {
-    global $DB;
-
-    $context = context::instance_by_id($cp->contextid, MUST_EXIST);
-
-    // If contextlevel is course then only subscribe user. Role assignment
-    // at course level means user is enroled in course and can subscribe to forum.
-    if ($context->contextlevel != CONTEXT_COURSE) {
-        return;
-    }
-
-    $sql = "SELECT f.id, cm.id AS cmid
-              FROM {forum} f
-              JOIN {course_modules} cm ON (cm.instance = f.id)
-              JOIN {modules} m ON (m.id = cm.module)
-         LEFT JOIN {forum_subscriptions} fs ON (fs.forum = f.id AND fs.userid = :userid)
-             WHERE f.course = :courseid
-               AND f.forcesubscribe = :initial
-               AND m.name = 'forum'
-               AND fs.id IS NULL";
-    $params = array('courseid'=>$context->instanceid, 'userid'=>$cp->userid, 'initial'=>FORUM_INITIALSUBSCRIBE);
-
-    $forums = $DB->get_records_sql($sql, $params);
-    foreach ($forums as $forum) {
-        // If user doesn't have allowforcesubscribe capability then don't subscribe.
-        if (has_capability('mod/forum:allowforcesubscribe', context_module::instance($forum->cmid), $cp->userid)) {
-            forum_subscribe($cp->userid, $forum->id);
-        }
-    }
-}
-
 // Functions to do with read tracking.
 
 /**
index f9f0358..70c8f62 100644 (file)
@@ -89,6 +89,7 @@ class mod_forum_mod_form extends moodleform_mod {
         $options[FORUM_TRACKING_ON] = get_string('trackingon', 'forum');
         $mform->addElement('select', 'trackingtype', get_string('trackingtype', 'forum'), $options);
         $mform->addHelpButton('trackingtype', 'trackingtype', 'forum');
+        $mform->setDefault('trackingtype', $CFG->forum_trackingtype);
 
         if ($CFG->enablerssfeeds && isset($CFG->forum_enablerssfeeds) && $CFG->forum_enablerssfeeds) {
 //-------------------------------------------------------------------------------
index fade8f1..b6bf51d 100644 (file)
@@ -57,6 +57,14 @@ if ($ADMIN->fulltree) {
     $settings->add(new admin_setting_configtext('forum_maxattachments', get_string('maxattachments', 'forum'),
                        get_string('configmaxattachments', 'forum'), 9, PARAM_INT));
 
+    // Default Read Tracking setting.
+    $options = array();
+    $options[FORUM_TRACKING_OPTIONAL] = get_string('trackingoptional', 'forum');
+    $options[FORUM_TRACKING_OFF] = get_string('trackingoff', 'forum');
+    $options[FORUM_TRACKING_ON] = get_string('trackingon', 'forum');
+    $settings->add(new admin_setting_configselect('forum_trackingtype', get_string('trackingtype', 'forum'),
+                       get_string('configtrackingtype', 'forum'), FORUM_TRACKING_OPTIONAL, $options));
+
     // Default whether user needs to mark a post as read
     $settings->add(new admin_setting_configcheckbox('forum_trackreadposts', get_string('trackforum', 'forum'),
                        get_string('configtrackreadposts', 'forum'), 1));
index 4c62e97..437438f 100644 (file)
@@ -25,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$module->version   = 2013071000;       // The current module version (Date: YYYYMMDDXX)
+$module->version   = 2013092600;       // The current module version (Date: YYYYMMDDXX)
 $module->requires  = 2013050100;       // Requires this Moodle version
 $module->component = 'mod_forum';      // Full name of the plugin (used for diagnostics)
 $module->cron      = 60;
index c6394ad..7aa48c5 100644 (file)
@@ -1547,6 +1547,7 @@ function quiz_supports($feature) {
         case FEATURE_BACKUP_MOODLE2:            return true;
         case FEATURE_SHOW_DESCRIPTION:          return true;
         case FEATURE_CONTROLS_GRADE_VISIBILITY: return true;
+        case FEATURE_USES_QUESTIONS:            return true;
 
         default: return null;
     }
index 52c34e3..6c19c50 100644 (file)
@@ -138,7 +138,7 @@ abstract class quiz_attempts_report_table extends table_sql {
      */
     public function col_fullname($attempt) {
         $html = parent::col_fullname($attempt);
-        if ($this->is_downloading()) {
+        if ($this->is_downloading() || empty($attempt->attempt)) {
             return $html;
         }
 
diff --git a/mod/quiz/report/statistics/classes/calculated.php b/mod/quiz/report/statistics/classes/calculated.php
new file mode 100644 (file)
index 0000000..8103b7c
--- /dev/null
@@ -0,0 +1,217 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The statistics calculator returns an instance of this class which contains the calculated statistics.
+ *
+ * These quiz statistics calculations are described here :
+ *
+ * http://docs.moodle.org/dev/Quiz_statistics_calculations#Test_statistics
+ *
+ * @package    quiz_statistics
+ * @copyright  2013 The Open University
+ * @author     James Pratt me@jamiep.org
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quiz_statistics_calculated {
+
+    public function __construct($allattempts = null) {
+        if ($allattempts !== null) {
+            $this->allattempts = $allattempts;
+        }
+    }
+
+    /**
+     * @var bool whether we are calculating calculate stats from all attempts.
+     */
+    public $allattempts;
+
+    /* Following stats all described here : http://docs.moodle.org/dev/Quiz_statistics_calculations#Test_statistics  */
+
+    public $firstattemptscount = 0;
+
+    public $allattemptscount = 0;
+
+    public $firstattemptsavg;
+
+    public $allattemptsavg;
+
+    public $median;
+
+    public $standarddeviation;
+
+    public $skewness;
+
+    public $kurtosis;
+
+    public $cic;
+
+    public $errorratio;
+
+    public $standarderror;
+
+    /**
+     * @var int time these stats where calculated and cached.
+     */
+    public $timemodified;
+
+    public function s() {
+        if ($this->allattempts) {
+            return $this->allattemptscount;
+        } else {
+            return $this->firstattemptscount;
+        }
+    }
+
+    public function avg() {
+        if ($this->allattempts) {
+            return $this->allattemptsavg;
+        } else {
+            return $this->firstattemptsavg;
+        }
+    }
+
+    /**
+     * @param $course
+     * @param $cm
+     * @param $quiz
+     * @return array to display in table or spreadsheet.
+     */
+    public function get_formatted_quiz_info_data($course, $cm, $quiz) {
+
+        // You can edit this array to control which statistics are displayed.
+        $todisplay = array('firstattemptscount' => 'number',
+                           'allattemptscount' => 'number',
+                           'firstattemptsavg' => 'summarks_as_percentage',
+                           'allattemptsavg' => 'summarks_as_percentage',
+                           'median' => 'summarks_as_percentage',
+                           'standarddeviation' => 'summarks_as_percentage',
+                           'skewness' => 'number_format',
+                           'kurtosis' => 'number_format',
+                           'cic' => 'number_format_percent',
+                           'errorratio' => 'number_format_percent',
+                           'standarderror' => 'summarks_as_percentage');
+
+        // General information about the quiz.
+        $quizinfo = array();
+        $quizinfo[get_string('quizname', 'quiz_statistics')] = format_string($quiz->name);
+        $quizinfo[get_string('coursename', 'quiz_statistics')] = format_string($course->fullname);
+        if ($cm->idnumber) {
+            $quizinfo[get_string('idnumbermod')] = $cm->idnumber;
+        }
+        if ($quiz->timeopen) {
+            $quizinfo[get_string('quizopen', 'quiz')] = userdate($quiz->timeopen);
+        }
+        if ($quiz->timeclose) {
+            $quizinfo[get_string('quizclose', 'quiz')] = userdate($quiz->timeclose);
+        }
+        if ($quiz->timeopen && $quiz->timeclose) {
+            $quizinfo[get_string('duration', 'quiz_statistics')] =
+                format_time($quiz->timeclose - $quiz->timeopen);
+        }
+
+        // The statistics.
+        foreach ($todisplay as $property => $format) {
+            if (!isset($this->$property) || !$format) {
+                continue;
+            }
+            $value = $this->$property;
+
+            switch ($format) {
+                case 'summarks_as_percentage':
+                    $formattedvalue = quiz_report_scale_summarks_as_percentage($value, $quiz);
+                    break;
+                case 'number_format_percent':
+                    $formattedvalue = quiz_format_grade($quiz, $value) . '%';
+                    break;
+                case 'number_format':
+                    // 2 extra decimal places, since not a percentage,
+                    // and we want the same number of sig figs.
+                    $formattedvalue = format_float($value, $quiz->decimalpoints + 2);
+                    break;
+                case 'number':
+                    $formattedvalue = $value + 0;
+                    break;
+                default:
+                    $formattedvalue = $value;
+            }
+
+            $quizinfo[get_string($property, 'quiz_statistics', $this->using_attempts_string())] = $formattedvalue;
+        }
+
+        return $quizinfo;
+    }
+
+    /**
+     * @return string the appropriate lang string to describe this option.
+     */
+    protected function using_attempts_string() {
+        if ($this->allattempts) {
+            return get_string('allattempts', 'quiz_statistics');
+        } else {
+            return get_string('firstattempts', 'quiz_statistics');
+        }
+    }
+
+    /**
+     * @var array of names of properties of this class that are cached in db record.
+     */
+    protected $fieldsindb = array('allattempts', 'firstattemptscount', 'allattemptscount', 'firstattemptsavg', 'allattemptsavg',
+                                    'median', 'standarddeviation', 'skewness',
+                                    'kurtosis', 'cic', 'errorratio', 'standarderror');
+
+    /**
+     * Cache the stats contained in this class.
+     *
+     * @param $qubaids qubaid_condition
+     */
+    public function cache($qubaids) {
+        global $DB;
+
+        $toinsert = new stdClass();
+
+        foreach ($this->fieldsindb as $field) {
+            $toinsert->{$field} = $this->{$field};
+        }
+
+        $toinsert->hashcode = $qubaids->get_hash_code();
+        $toinsert->timemodified = time();
+
+        // Fix up some dodgy data.
+        if (isset($toinsert->errorratio) && is_nan($toinsert->errorratio)) {
+            $toinsert->errorratio = null;
+        }
+        if (isset($toinsert->standarderror) && is_nan($toinsert->standarderror)) {
+            $toinsert->standarderror = null;
+        }
+
+        // Store the data.
+        $DB->insert_record('quiz_statistics', $toinsert);
+
+    }
+
+    /**
+     * Given a record from 'quiz_statistics' table load the data into the properties of this class.
+     *
+     * @param $record from db.
+     */
+    public function populate_from_record($record) {
+        foreach ($this->fieldsindb as $field) {
+            $this->$field = $record->$field;
+        }
+        $this->timemodified = $record->timemodified;
+    }
+}
diff --git a/mod/quiz/report/statistics/classes/calculator.php b/mod/quiz/report/statistics/classes/calculator.php
new file mode 100644 (file)
index 0000000..85c8cb7
--- /dev/null
@@ -0,0 +1,247 @@
+<?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/>.
+
+/**
+ * Class to calculate and also manage caching of quiz statistics.
+ *
+ * These quiz statistics calculations are described here :
+ *
+ * http://docs.moodle.org/dev/Quiz_statistics_calculations#Test_statistics
+ *
+ * @package    quiz_statistics
+ * @copyright  2013 The Open University
+ * @author     James Pratt me@jamiep.org
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quiz_statistics_calculator {
+
+    /**
+     * Compute the quiz statistics.
+     *
+     * @param int   $quizid            the quiz id.
+     * @param int   $currentgroup      the current group. 0 for none.
+     * @param bool  $useallattempts    use all attempts, or just first attempts.
+     * @param array $groupstudents     students in this group.
+     * @param int   $p                 number of positions (slots).
+     * @param float $sumofmarkvariance sum of mark variance, calculated as part of question statistics
+     * @return quiz_statistics_calculated $quizstats The statistics for overall attempt scores.
+     */
+    public function calculate($quizid, $currentgroup, $useallattempts, $groupstudents, $p, $sumofmarkvariance) {
+
+        $quizstats = $this->attempt_counts_and_averages($quizid, $currentgroup, $useallattempts, $groupstudents);
+
+        $s = $quizstats->s();
+
+        if ($s == 0) {
+            return $quizstats;
+        }
+
+        // Recalculate sql again this time possibly including test for first attempt.
+        list($fromqa, $whereqa, $qaparams) =
+            quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents, $useallattempts);
+
+        $quizstats->median = $this->median($s, $fromqa, $whereqa, $qaparams);
+
+        if ($s > 1) {
+
+            $powers = $this->sum_of_powers_of_difference_to_mean($quizstats->avg(), $fromqa, $whereqa, $qaparams);
+
+            $quizstats->standarddeviation = sqrt($powers->power2 / ($s - 1));
+
+            // Skewness.
+            if ($s > 2) {
+                // See http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise#Skewness_and_Kurtosis.
+                $m2 = $powers->power2 / $s;
+                $m3 = $powers->power3 / $s;
+                $m4 = $powers->power4 / $s;
+
+                $k2 = $s * $m2 / ($s - 1);
+                $k3 = $s * $s * $m3 / (($s - 1) * ($s - 2));
+                if ($k2 != 0) {
+                    $quizstats->skewness = $k3 / (pow($k2, 3 / 2));
+
+                    // Kurtosis.
+                    if ($s > 3) {
+                        $k4 = $s * $s * ((($s + 1) * $m4) - (3 * ($s - 1) * $m2 * $m2)) / (($s - 1) * ($s - 2) * ($s - 3));
+                        $quizstats->kurtosis = $k4 / ($k2 * $k2);
+                    }
+
+                    if ($p > 1) {
+                        $quizstats->cic = (100 * $p / ($p -1)) * (1 - ($sumofmarkvariance / $k2));
+                        $quizstats->errorratio = 100 * sqrt(1 - ($quizstats->cic / 100));
+                        $quizstats->standarderror = $quizstats->errorratio *
+                            $quizstats->standarddeviation / 100;
+                    }
+                }
+
+            }
+        }
+
+        $quizstats->cache(quiz_statistics_qubaids_condition($quizid, $currentgroup, $groupstudents, $useallattempts));
+
+        return $quizstats;
+    }
+
+    /** @var integer Time after which statistics are automatically recomputed. */
+    const TIME_TO_CACHE = 900; // 15 minutes.
+
+    /**
+     * Load cached statistics from the database.
+     *
+     * @param $qubaids qubaid_condition
+     * @return quiz_statistics_calculated The statistics for overall attempt scores or false if not cached.
+     */
+    public function get_cached($qubaids) {
+        global $DB;
+
+        $timemodified = time() - self::TIME_TO_CACHE;
+        $fromdb = $DB->get_record_select('quiz_statistics', 'hashcode = ? AND timemodified > ?',
+                                         array($qubaids->get_hash_code(), $timemodified));
+        $stats = new quiz_statistics_calculated();
+        $stats->populate_from_record($fromdb);
+        return $stats;
+    }
+
+    /**
+     * Find time of non-expired statistics in the database.
+     *
+     * @param $qubaids qubaid_condition
+     * @return integer|boolean Time of cached record that matches this qubaid_condition or false is non found.
+     */
+    public function get_last_calculated_time($qubaids) {
+        global $DB;
+
+        $timemodified = time() - self::TIME_TO_CACHE;
+        return $DB->get_field_select('quiz_statistics', 'timemodified', 'hashcode = ? AND timemodified > ?',
+                                         array($qubaids->get_hash_code(), $timemodified));
+    }
+
+    /**
+     * Calculating count and mean of marks for first and ALL attempts by students.
+     *
+     * See : http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise
+     *                                      #Calculating_MEAN_of_grades_for_all_attempts_by_students
+     * @param int $quizid
+     * @param int $currentgroup
+     * @param bool $useallattempts
+     * @param array $groupstudents
+     * @return quiz_statistics_calculated containing calculated counts, totals and averages.
+     */
+    protected function attempt_counts_and_averages($quizid, $currentgroup, $useallattempts, $groupstudents) {
+        global $DB;
+
+        $quizstats = new quiz_statistics_calculated($useallattempts);
+
+        list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents, true);
+
+        $attempttotals = $DB->get_records_sql("
+                SELECT
+                    CASE WHEN attempt = 1 THEN 1 ELSE 0 END AS isfirst,
+                    COUNT(1) AS countrecs,
+                    SUM(sumgrades) AS total
+                FROM $fromqa
+                WHERE $whereqa
+                GROUP BY CASE WHEN attempt = 1 THEN 1 ELSE 0 END", $qaparams);
+
+        // Above query that returns sums and counts for first attempt and other non first attempts.
+        // We want to work out stats for first attempt or ALL attempts.
+
+        if (isset($attempttotals[1])) {
+            $quizstats->firstattemptscount = $attempttotals[1]->countrecs;
+            $firstattemptstotal = $attempttotals[1]->total;
+        } else {
+            $quizstats->firstattemptscount = 0;
+            $firstattemptstotal = 0;
+        }
+
+        if (isset($attempttotals[0])) {
+            $quizstats->allattemptscount = $quizstats->firstattemptscount + $attempttotals[0]->countrecs;
+            $allattemptstotal = $firstattemptstotal + $attempttotals[0]->total;
+        } else {
+            $quizstats->allattemptscount = $quizstats->firstattemptscount;
+            $allattemptstotal = $firstattemptstotal;
+        }
+
+        if ($quizstats->allattemptscount !== 0) {
+            $quizstats->allattemptsavg = $allattemptstotal / $quizstats->allattemptscount;
+        }
+
+        if ($quizstats->firstattemptscount !== 0) {
+            $quizstats->firstattemptsavg = $firstattemptstotal / $quizstats->firstattemptscount;
+        }
+
+        return $quizstats;
+    }
+
+    /**
+     * Median mark.
+     *
+     * http://docs.moodle.org/dev/Quiz_statistics_calculations#Median_Score
+     *
+     * @param $s integer count of attempts
+     * @param $fromqa string
+     * @param $whereqa string
+     * @param $qaparams string
+     * @return float
+     */
+    protected function median($s, $fromqa, $whereqa, $qaparams) {
+        global $DB;
+
+        if ($s % 2 == 0) {
+            // An even number of attempts.
+            $limitoffset = $s / 2 - 1;
+            $limit = 2;
+        } else {
+            $limitoffset = floor($s / 2);
+            $limit = 1;
+        }
+        $sql = "SELECT id, sumgrades
+                FROM $fromqa
+                WHERE $whereqa
+                ORDER BY sumgrades";
+
+        $medianmarks = $DB->get_records_sql_menu($sql, $qaparams, $limitoffset, $limit);
+
+        return array_sum($medianmarks) / count($medianmarks);
+    }
+
+    /**
+     * Fetch the sum of squared, cubed and to the power 4 differences between sumgrade and it's mean.
+     *
+     * Explanation here : http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise
+     *              #Calculating_Standard_Deviation.2C_Skewness_and_Kurtosis_of_grades_for_all_attempts_by_students
+     *
+     * @param $mean
+     * @param $fromqa
+     * @param $whereqa
+     * @param $qaparams
+     * @return object with properties power2, power3, power4
+     */
+    protected function sum_of_powers_of_difference_to_mean($mean, $fromqa, $whereqa, $qaparams) {
+        global $DB;
+
+        $sql = "SELECT
+                    SUM(POWER((quiza.sumgrades - $mean), 2)) AS power2,
+                    SUM(POWER((quiza.sumgrades - $mean), 3)) AS power3,
+                    SUM(POWER((quiza.sumgrades - $mean), 4)) AS power4
+                    FROM $fromqa
+                    WHERE $whereqa";
+        $params = array('mean1' => $mean, 'mean2' => $mean, 'mean3' => $mean) + $qaparams;
+
+        return $DB->get_record_sql($sql, $params, MUST_EXIST);
+    }
+
+}
index 4a15c7d..e148f9e 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-
 defined('MOODLE_INTERNAL') || die();
 
-
 /**
  * Post-install script
  */
index 4beb458..3581dfe 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-
 defined('MOODLE_INTERNAL') || die();
 
-
 /**
  * Quiz statistics report upgrade code.
  */
index 2dc0751..416981b 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-
 defined('MOODLE_INTERNAL') || die();
 
-
 /**
  * Serve questiontext files in the question text when they are displayed in this report.
  *
index 9db615c..5179ee8 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-
 defined('MOODLE_INTERNAL') || die();
 
 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_form.php');
 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_table.php');
 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_question_table.php');
-require_once($CFG->dirroot . '/question/engine/statistics.php');
-require_once($CFG->dirroot . '/question/engine/responseanalysis.php');
 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
-
 /**
  * The quiz statistics report provides summary information about each question in
  * a quiz, compared to the whole quiz. It also provides a drill-down to more
@@ -41,10 +37,13 @@ require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class quiz_statistics_report extends quiz_default_report {
-    /** @var integer Time after which statistics are automatically recomputed. */
-    const TIME_TO_CACHE_STATS = 900; // 15 minutes.
 
-    /** @var object instance of table class used for main questions stats table. */
+    /**
+     * @var context_module
+     */
+    protected $context;
+
+    /** @var quiz_statistics_table instance of table class used for main questions stats table. */
     protected $table;
 
     /**
@@ -106,7 +105,6 @@ class quiz_statistics_report extends quiz_default_report {
 
         $qubaids = quiz_statistics_qubaids_condition($quiz->id, $currentgroup, $groupstudents, $useallattempts);
 
-
         // If recalculate was requested, handle that.
         if ($recalculate && confirm_sesskey()) {
             $this->clear_cached_data($qubaids);
@@ -127,15 +125,21 @@ class quiz_statistics_report extends quiz_default_report {
                 get_string('quizstructureanalysis', 'quiz_statistics'));
         $questions = $this->load_and_initialise_questions_for_calculations($quiz);
 
-        // Get the data to be displayed.
-        list($quizstats, $questions, $subquestions, $s) =
-                $this->get_quiz_and_questions_stats($quiz, $currentgroup,
-                        $nostudentsingroup, $useallattempts, $groupstudents, $questions);
-        $quizinfo = $this->get_formatted_quiz_info_data($course, $cm, $quiz, $quizstats);
+        if (!$nostudentsingroup) {
+            // Get the data to be displayed.
+            list($quizstats, $questionstats, $subquestionstats) =
+                $this->get_quiz_and_questions_stats($quiz, $currentgroup, $useallattempts, $groupstudents, $questions);
+        } else {
+            // Or create empty stats containers.
+            $quizstats = new quiz_statistics_calculated($useallattempts);
+            $questionstats = array();
+            $subquestionstats = array();
+        }
+        $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
 
         // Set up the table, if there is data.
-        if ($s) {
-            $this->table->statistics_setup($quiz, $cm->id, $reporturl, $s);
+        if ($quizstats->s()) {
+            $this->table->statistics_setup($quiz, $cm->id, $reporturl, $quizstats->s());
         }
 
         // Print the page header stuff (if not downloading.
@@ -151,7 +155,7 @@ class quiz_statistics_report extends quiz_default_report {
 
             if (!quiz_questions_in_quiz($quiz->questions)) {
                 echo quiz_no_questions_message($quiz, $cm, $this->context);
-            } else if (!$this->table->is_downloading() && $s == 0) {
+            } else if (!$this->table->is_downloading() && $quizstats->s() == 0) {
                 echo $OUTPUT->notification(get_string('noattempts', 'quiz'));
             }
 
@@ -164,24 +168,24 @@ class quiz_statistics_report extends quiz_default_report {
             // Overall report, then the analysis of each question.
             $this->download_quiz_info_table($quizinfo);
 
-            if ($s) {
-                $this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
+            if ($quizstats->s()) {
+                $this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats);
 
-                if ($this->table->is_downloading() == 'xhtml' && $s != 0) {
+                if ($this->table->is_downloading() == 'xhtml' && $quizstats->s() != 0) {
                     $this->output_statistics_graph($quiz->id, $currentgroup, $useallattempts);
                 }
 
-                foreach ($questions as $question) {
+                foreach ($questions as $slot => $question) {
                     if (question_bank::get_qtype(
                             $question->qtype, false)->can_analyse_responses()) {
                         $this->output_individual_question_response_analysis(
-                                $question, $reporturl, $qubaids);
+                                $question, $questionstats[$slot]->s, $reporturl, $qubaids);
 
-                    } else if (!empty($question->_stats->subquestions)) {
-                        $subitemstodisplay = explode(',', $question->_stats->subquestions);
+                    } else if (!empty($questionstats[$slot]->subquestions)) {
+                        $subitemstodisplay = explode(',', $questionstats[$slot]->subquestions);
                         foreach ($subitemstodisplay as $subitemid) {
                             $this->output_individual_question_response_analysis(
-                                    $subquestions[$subitemid], $reporturl, $qubaids);
+                                $subquestionstats[$subitemid]->question, $subquestionstats[$subitemid]->s, $reporturl, $qubaids);
                         }
                     }
                 }
@@ -195,9 +199,8 @@ class quiz_statistics_report extends quiz_default_report {
                 print_error('questiondoesnotexist', 'question');
             }
 
-            $this->output_individual_question_data($quiz, $questions[$slot]);
-            $this->output_individual_question_response_analysis(
-                    $questions[$slot], $reporturl, $qubaids);
+            $this->output_individual_question_data($quiz, $questionstats[$slot]);
+            $this->output_individual_question_response_analysis($questions[$slot], $questionstats[$slot]->s, $reporturl, $qubaids);
 
             // Back to overview link.
             echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
@@ -210,9 +213,9 @@ class quiz_statistics_report extends quiz_default_report {
                 print_error('questiondoesnotexist', 'question');
             }
 
-            $this->output_individual_question_data($quiz, $subquestions[$qid]);
-            $this->output_individual_question_response_analysis(
-                    $subquestions[$qid], $reporturl, $qubaids);
+            $this->output_individual_question_data($quiz, $subquestionstats[$qid]);
+            $this->output_individual_question_response_analysis($subquestionstats[$qid]->question,
+                                                                $subquestionstats[$qid]->s, $reporturl, $qubaids);
 
             // Back to overview link.
             echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
@@ -222,7 +225,7 @@ class quiz_statistics_report extends quiz_default_report {
         } else if ($this->table->is_downloading()) {
             // Downloading overview report.
             $this->download_quiz_info_table($quizinfo);
-            $this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
+            $this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats);
             $this->table->finish_output();
 
         } else {
@@ -232,9 +235,9 @@ class quiz_statistics_report extends quiz_default_report {
                     $groupstudents, $useallattempts, $reporturl);
             echo $this->everything_download_options();
             echo $this->output_quiz_info_table($quizinfo);
-            if ($s) {
+            if ($quizstats->s()) {
                 echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'));
-                $this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
+                $this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats);
                 $this->output_statistics_graph($quiz->id, $currentgroup, $useallattempts);
             }
         }
@@ -246,16 +249,16 @@ class quiz_statistics_report extends quiz_default_report {
      * Display the statistical and introductory information about a question.
      * Only called when not downloading.
      * @param object $quiz the quiz settings.
-     * @param object $question the question to report on.
+     * @param \core_question\statistics\questions\calculated $questionstat the question to report on.
      * @param moodle_url $reporturl the URL to resisplay this report.
      * @param object $quizstats Holds the quiz statistics.
      */
-    protected function output_individual_question_data($quiz, $question) {
+    protected function output_individual_question_data($quiz, $questionstat) {
         global $OUTPUT;
 
         // On-screen display. Show a summary of the question's place in the quiz,
         // and the question statistics.
-        $datumfromtable = $this->table->format_row($question);
+        $datumfromtable = $this->table->format_row($questionstat);
 
         // Set up the question info table.
         $questioninfotable = new html_table();
@@ -266,13 +269,13 @@ class quiz_statistics_report extends quiz_default_report {
         $questioninfotable->data = array();
         $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name);
         $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'),
-                $question->name.'&nbsp;'.$datumfromtable['actions']);
+                $questionstat->question->name.'&nbsp;'.$datumfromtable['actions']);
         $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'),
                 $datumfromtable['icon'] . '&nbsp;' .
-                question_bank::get_qtype($question->qtype, false)->menu_name() . '&nbsp;' .
+                question_bank::get_qtype($questionstat->question->qtype, false)->menu_name() . '&nbsp;' .
                 $datumfromtable['icon']);
         $questioninfotable->data[] = array(get_string('positions', 'quiz_statistics'),
-                $question->_stats->positions);
+                $questionstat->positions);
 
         // Set up the question statistics table.
         $questionstatstable = new html_table();
@@ -303,7 +306,7 @@ class quiz_statistics_report extends quiz_default_report {
         // Display the various bits.
         echo $OUTPUT->heading(get_string('questioninformation', 'quiz_statistics'));
         echo html_writer::table($questioninfotable);
-        echo $this->render_question_text($question);
+        echo $this->render_question_text($questionstat->question);
         echo $OUTPUT->heading(get_string('questionstatistics', 'quiz_statistics'));
         echo html_writer::table($questionstatstable);
     }
@@ -330,8 +333,7 @@ class quiz_statistics_report extends quiz_default_report {
      * @param moodle_url $reporturl the URL to resisplay this report.
      * @param qubaid_condition $qubaids
      */
-    protected function output_individual_question_response_analysis($question,
-            $reporturl, $qubaids) {
+    protected function output_individual_question_response_analysis($question, $s, $reporturl, $qubaids) {
         global $OUTPUT;
 
         if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
@@ -364,10 +366,10 @@ class quiz_statistics_report extends quiz_default_report {
             }
         }
 
-        $responesstats = new question_response_analyser($question);
+        $responesstats = new \core_question\statistics\responses\analyser($question);
         $responesstats->load_cached($qubaids);
 
-        $qtable->question_setup($reporturl, $question, $responesstats);
+        $qtable->question_setup($reporturl, $question, $s, $responesstats);
         if ($this->table->is_downloading()) {
             $exportclass->output_headers($qtable->headers);
         }
@@ -406,100 +408,33 @@ class quiz_statistics_report extends quiz_default_report {
     /**
      * Output the table that lists all the questions in the quiz with their statistics.
      * @param int $s number of attempts.
-     * @param array $questions the questions in the quiz.
-     * @param array $subquestions the subquestions of any random questions.
+     * @param \core_question\statistics\questions\calculated[] $questionstats the stats for the main questions in the quiz.
+     * @param \core_question\statistics\questions\calculated_for_subquestion[] $subquestionstats the stats of any random questions.
      */
-    protected function output_quiz_structure_analysis_table($s, $questions, $subquestions) {
+    protected function output_quiz_structure_analysis_table($s, $questionstats, $subquestionstats) {
         if (!$s) {
             return;
         }
 
-        foreach ($questions as $question) {
-            // Output the data for this questions.
-            $this->table->add_data_keyed($this->table->format_row($question));
+        foreach ($questionstats as $questionstat) {
+            // Output the data for these question statistics.
+            $this->table->add_data_keyed($this->table->format_row($questionstat));
 
-            if (empty($question->_stats->subquestions)) {
+            if (empty($questionstat->subquestions)) {
                 continue;
             }
 
             // And its subquestions, if it has any.
-            $subitemstodisplay = explode(',', $question->_stats->subquestions);
+            $subitemstodisplay = explode(',', $questionstat->subquestions);
             foreach ($subitemstodisplay as $subitemid) {
-                $subquestions[$subitemid]->maxmark = $question->maxmark;
-                $this->table->add_data_keyed($this->table->format_row($subquestions[$subitemid]));
+                $subquestionstats[$subitemid]->maxmark = $questionstat->maxmark;
+                $this->table->add_data_keyed($this->table->format_row($subquestionstats[$subitemid]));
             }
         }
 
         $this->table->finish_output(!$this->table->is_downloading());
     }
 
-    protected function get_formatted_quiz_info_data($course, $cm, $quiz, $quizstats) {
-
-        // You can edit this array to control which statistics are displayed.
-        $todisplay = array('firstattemptscount' => 'number',
-                    'allattemptscount' => 'number',
-                    'firstattemptsavg' => 'summarks_as_percentage',
-                    'allattemptsavg' => 'summarks_as_percentage',
-                    'median' => 'summarks_as_percentage',
-                    'standarddeviation' => 'summarks_as_percentage',
-                    'skewness' => 'number_format',
-                    'kurtosis' => 'number_format',
-                    'cic' => 'number_format_percent',
-                    'errorratio' => 'number_format_percent',
-                    'standarderror' => 'summarks_as_percentage');
-
-        // General information about the quiz.
-        $quizinfo = array();
-        $quizinfo[get_string('quizname', 'quiz_statistics')] = format_string($quiz->name);
-        $quizinfo[get_string('coursename', 'quiz_statistics')] = format_string($course->fullname);
-        if ($cm->idnumber) {
-            $quizinfo[get_string('idnumbermod')] = $cm->idnumber;
-        }
-        if ($quiz->timeopen) {
-            $quizinfo[get_string('quizopen', 'quiz')] = userdate($quiz->timeopen);
-        }
-        if ($quiz->timeclose) {
-            $quizinfo[get_string('quizclose', 'quiz')] = userdate($quiz->timeclose);
-        }
-        if ($quiz->timeopen && $quiz->timeclose) {
-            $quizinfo[get_string('duration', 'quiz_statistics')] =
-                    format_time($quiz->timeclose - $quiz->timeopen);
-        }
-
-        // The statistics.
-        foreach ($todisplay as $property => $format) {
-            if (!isset($quizstats->$property) || !$format) {
-                continue;
-            }
-            $value = $quizstats->$property;
-
-            switch ($format) {
-                case 'summarks_as_percentage':
-                    $formattedvalue = quiz_report_scale_summarks_as_percentage($value, $quiz);
-                    break;
-                case 'number_format_percent':
-                    $formattedvalue = quiz_format_grade($quiz, $value) . '%';
-                    break;
-                case 'number_format':
-                    // 2 extra decimal places, since not a percentage,
-                    // and we want the same number of sig figs.
-                    $formattedvalue = format_float($value, $quiz->decimalpoints + 2);
-                    break;
-                case 'number':
-                    $formattedvalue = $value + 0;
-                    break;
-                default:
-                    $formattedvalue = $value;
-            }
-
-            $quizinfo[get_string($property, 'quiz_statistics',
-                    $this->using_attempts_string(!empty($quizstats->allattempts)))] =
-                    $formattedvalue;
-        }
-
-        return $quizinfo;
-    }
-
     /**
      * Output the table of overall quiz statistics.
      * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
@@ -566,273 +501,47 @@ class quiz_statistics_report extends quiz_default_report {
         echo $output->graph($imageurl, $graphname);
     }
 
-    /**
-     * Return the stats data for when there are no stats to show.
-     *
-     * @param int $firstattemptscount number of first attempts (optional).
-     * @param int $allattemptscount total number of attempts (optional).
-     * @return array with two elements:
-     *      - integer $s Number of attempts included in the stats (0).
-     *      - object $quizstats The statistics for overall attempt scores.
-     */
-    protected function get_empty_stats($firstattemptscount = 0, $allattemptscount = 0) {
-        $quizstats = new stdClass();
-        $quizstats->firstattemptscount = $firstattemptscount;
-        $quizstats->allattemptscount = $allattemptscount;
-
-        return array(0, $quizstats);
-    }
-
-    /**
-     * Compute the quiz statistics.
-     *
-     * @param int   $quizid            the quiz id.
-     * @param int   $currentgroup      the current group. 0 for none.
-     * @param bool  $useallattempts    use all attempts, or just first attempts.
-     * @param array $groupstudents     students in this group.
-     * @param int   $p                 number of positions (slots).
-     * @param float $sumofmarkvariance sum of mark variance, calculated as part of question statistics
-     * @return array with two elements:
-     *      - integer $s Number of attempts included in the stats.
-     *      - object $quizstats The statistics for overall attempt scores.
-     */
-    protected function calculate_quiz_stats($quizid, $currentgroup, $useallattempts, $groupstudents, $p, $sumofmarkvariance) {
-        global $DB;
-
-        // Calculating MEAN of marks for all attempts by students
-        // http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise
-        //     #Calculating_MEAN_of_grades_for_all_attempts_by_students.
-        list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
-                $quizid, $currentgroup, $groupstudents, true);
-
-        $attempttotals = $DB->get_records_sql("
-                SELECT
-                    CASE WHEN attempt = 1 THEN 1 ELSE 0 END AS isfirst,
-                    COUNT(1) AS countrecs,
-                    SUM(sumgrades) AS total
-                FROM $fromqa
-                WHERE $whereqa
-                GROUP BY CASE WHEN attempt = 1 THEN 1 ELSE 0 END", $qaparams);
-
-        if (!$attempttotals) {
-            return $this->get_empty_stats();
-        }
-
-        if (isset($attempttotals[1])) {
-            $firstattempts = $attempttotals[1];
-            $firstattempts->average = $firstattempts->total / $firstattempts->countrecs;
-        } else {
-            $firstattempts = new stdClass();
-            $firstattempts->countrecs = 0;
-            $firstattempts->total = 0;
-            $firstattempts->average = null;
-        }
-
-        $allattempts = new stdClass();
-        if (isset($attempttotals[0])) {
-            $allattempts->countrecs = $firstattempts->countrecs + $attempttotals[0]->countrecs;
-            $allattempts->total = $firstattempts->total + $attempttotals[0]->total;
-        } else {
-            $allattempts->countrecs = $firstattempts->countrecs;
-            $allattempts->total = $firstattempts->total;
-        }
-
-        if ($useallattempts) {
-            $usingattempts = $allattempts;
-            $usingattempts->sql = '';
-        } else {
-            $usingattempts = $firstattempts;
-            $usingattempts->sql = 'AND quiza.attempt = 1 ';
-        }
-
-        $s = $usingattempts->countrecs;
-        if ($s == 0) {
-            return $this->get_empty_stats($firstattempts->countrecs, $allattempts->countrecs);
-        }
-
-        $quizstats = new stdClass();
-        $quizstats->allattempts = $useallattempts;
-        $quizstats->firstattemptscount = $firstattempts->countrecs;
-        $quizstats->allattemptscount = $allattempts->countrecs;
-        $quizstats->firstattemptsavg = $firstattempts->average;
-        $quizstats->allattemptsavg = $allattempts->total / $allattempts->countrecs;
-
-        // Recalculate sql again this time possibly including test for first attempt.
-        list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
-                $quizid, $currentgroup, $groupstudents, $useallattempts);
-
-        // Median ...
-        if ($s % 2 == 0) {
-            // An even number of attempts.
-            $limitoffset = $s/2 - 1;
-            $limit = 2;
-        } else {
-            $limitoffset = floor($s/2);
-            $limit = 1;
-        }
-        $sql = "SELECT id, sumgrades
-                FROM $fromqa
-                WHERE $whereqa
-                ORDER BY sumgrades";
-
-        $medianmarks = $DB->get_records_sql_menu($sql, $qaparams, $limitoffset, $limit);
-
-        $quizstats->median = array_sum($medianmarks) / count($medianmarks);
-        if ($s > 1) {
-            // Fetch the sum of squared, cubed and power 4d
-            // differences between marks and mean mark.
-            $mean = $usingattempts->total / $s;
-            $sql = "SELECT
-                    SUM(POWER((quiza.sumgrades - $mean), 2)) AS power2,
-                    SUM(POWER((quiza.sumgrades - $mean), 3)) AS power3,
-                    SUM(POWER((quiza.sumgrades - $mean), 4)) AS power4
-                    FROM $fromqa
-                    WHERE $whereqa";
-            $params = array('mean1' => $mean, 'mean2' => $mean, 'mean3' => $mean)+$qaparams;
-
-            $powers = $DB->get_record_sql($sql, $params, MUST_EXIST);
-
-            // Standard_Deviation:
-            // see http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise
-            //         #Standard_Deviation.
-
-            $quizstats->standarddeviation = sqrt($powers->power2 / ($s - 1));
-
-            // Skewness.
-            if ($s > 2) {
-                // See http://docs.moodle.org/dev/
-                //      Quiz_item_analysis_calculations_in_practise#Skewness_and_Kurtosis.
-                $m2= $powers->power2 / $s;
-                $m3= $powers->power3 / $s;
-                $m4= $powers->power4 / $s;
-
-                $k2= $s*$m2/($s-1);
-                $k3= $s*$s*$m3/(($s-1)*($s-2));
-                if ($k2) {
-                    $quizstats->skewness = $k3 / (pow($k2, 3/2));
-                }
-
-                // Kurtosis.
-                if ($s > 3) {
-                    $k4= $s*$s*((($s+1)*$m4)-(3*($s-1)*$m2*$m2))/(($s-1)*($s-2)*($s-3));
-                    if ($k2) {
-                        $quizstats->kurtosis = $k4 / ($k2*$k2);
-                    }
-                }
-            }
-        }
-
-        if ($s > 1) {
-            if ($p > 1 && isset($k2)) {
-                $quizstats->cic = (100 * $p / ($p -1)) *
-                        (1 - ($sumofmarkvariance / $k2));
-                $quizstats->errorratio = 100 * sqrt(1 - ($quizstats->cic / 100));
-                $quizstats->standarderror = $quizstats->errorratio *
-                        $quizstats->standarddeviation / 100;
-            }
-        }
-
-        $this->cache_stats(quiz_statistics_qubaids_condition($quizid, $currentgroup, $groupstudents, $useallattempts), $quizstats);
-
-        return array($s, $quizstats);
-    }
-
-    /**
-     * Load the cached statistics from the database.
-     *
-     * @param $qubaids qubaid_condition
-     * @return The statistics for overall attempt scores or false if not cached.
-     */
-    protected function get_cached_quiz_stats($qubaids) {
-        global $DB;
-
-        $timemodified = time() - self::TIME_TO_CACHE_STATS;
-        return  $DB->get_record_select('quiz_statistics', 'hashcode = ? AND timemodified > ?',
-                                       array($qubaids->get_hash_code(), $timemodified));
-    }
-
-    /**
-     * @param $qubaids    qubaid_condition
-     * @param $quizstats  object            the quiz stats to cache
-     */
-    protected function cache_stats($qubaids, $quizstats) {
-        global $DB;
-
-        $toinsert = clone($quizstats);
-        $toinsert->hashcode = $qubaids->get_hash_code();
-        $toinsert->timemodified = time();
-
-        // Fix up some dodgy data.
-        if (isset($toinsert->errorratio) && is_nan($toinsert->errorratio)) {
-            $toinsert->errorratio = null;
-        }
-        if (isset($toinsert->standarderror) && is_nan($toinsert->standarderror)) {
-            $toinsert->standarderror = null;
-        }
-
-        // Store the data.
-        $DB->insert_record('quiz_statistics', $toinsert);
-
-    }
-
     /**
      * Get the quiz and question statistics, either by loading the cached results,
      * or by recomputing them.
      *
      * @param object $quiz the quiz settings.
      * @param int $currentgroup the current group. 0 for none.
-     * @param bool $nostudentsingroup true if there a no students.
      * @param bool $useallattempts use all attempts, or just first attempts.
      * @param array $groupstudents students in this group.
      * @param array $questions question definitions.
      * @return array with 4 elements:
      *     - $quizstats The statistics for overall attempt scores.
-     *     - $questions The questions, with an additional _stats field.
-     *     - $subquestions The subquestions, if any, with an additional _stats field.
-     *     - $s Number of attempts included in the stats.
+     *     - $questionstats array of \core_question\statistics\questions\calculated objects keyed by slot.
+     *     - $subquestionstats array of \core_question\statistics\questions\calculated_for_subquestion objects keyed by question id.
      */
-    protected function get_quiz_and_questions_stats($quiz, $currentgroup,
-            $nostudentsingroup, $useallattempts, $groupstudents, $questions) {
+    protected function get_quiz_and_questions_stats($quiz, $currentgroup, $useallattempts, $groupstudents, $questions) {
 
         $qubaids = quiz_statistics_qubaids_condition($quiz->id, $currentgroup, $groupstudents, $useallattempts);
 
-        $quizstats = $this->get_cached_quiz_stats($qubaids);
+        $qcalc = new \core_question\statistics\questions\calculator($questions);
 
-        $qstats = new question_statistics($questions);
+        $quizcalc = new quiz_statistics_calculator();
 
-        if (empty($quizstats)) {
+        if ($quizcalc->get_last_calculated_time($qubaids) === false) {
             // Recalculate now.
-            $qstats->calculate($qubaids);
+            list($questionstats, $subquestionstats) = $qcalc->calculate($qubaids);
 
-            if ($nostudentsingroup) {
-                list($s, $quizstats) = $this->get_empty_stats();
-            } else {
-                list($s, $quizstats) = $this->calculate_quiz_stats($quiz->id, $currentgroup, $useallattempts,
-                                                           $groupstudents, count($questions), $qstats->get_sum_of_mark_variance());
-            }
-
-            $questions = $qstats->questions;
-            $subquestions = $qstats->subquestions;
+            $quizstats = $quizcalc->calculate($quiz->id, $currentgroup, $useallattempts,
+                                               $groupstudents, count($questions), $qcalc->get_sum_of_mark_variance());
 
-            if ($s) {
-                $this->calculate_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestions);
+            if ($quizstats->s()) {
+                $this->calculate_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats);
             }
         } else {
-            if ($useallattempts) {
-                $s = $quizstats->allattemptscount;
-            } else {
-                $s = $quizstats->firstattemptscount;
-            }
-            $qstats->get_cached($qubaids);
-            $questions = $qstats->questions;
-            $subquestions = $qstats->subquestions;
-
+            $quizstats = $quizcalc->get_cached($qubaids);
+            list($questionstats, $subquestionstats) = $qcalc->get_cached($qubaids);
         }
 
-        return array($quizstats, $questions, $subquestions, $s);
+        return array($quizstats, $questionstats, $subquestionstats);
     }
 
-    protected function calculate_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestions) {
+    protected function calculate_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats) {
 
         $done = array();
         foreach ($questions as $question) {
@@ -841,18 +550,18 @@ class quiz_statistics_report extends quiz_default_report {
             }
             $done[$question->id] = 1;
 
-            $responesstats = new question_response_analyser($question);
+            $responesstats = new \core_question\statistics\responses\analyser($question);
             $responesstats->calculate($qubaids);
         }
 
-        foreach ($subquestions as $question) {
-            if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses() ||
-                    isset($done[$question->id])) {
+        foreach ($subquestionstats as $subquestionstat) {
+            if (!question_bank::get_qtype($subquestionstat->question->qtype, false)->can_analyse_responses() ||
+                    isset($done[$subquestionstat->question->id])) {
                 continue;
             }
-            $done[$question->id] = 1;
+            $done[$subquestionstat->question->id] = 1;
 
-            $responesstats = new question_response_analyser($question);
+            $responesstats = new \core_question\statistics\responses\analyser($subquestionstat->question);
             $responesstats->calculate($qubaids);
         }
     }
@@ -942,18 +651,6 @@ class quiz_statistics_report extends quiz_default_report {
         $DB->delete_records('question_response_analysis', array('hashcode' => $qubaids->get_hash_code()));
     }
 
-    /**
-     * @param bool $useallattempts whether we are using all attempts.
-     * @return the appropriate lang string to describe this option.
-     */
-    protected function using_attempts_string($useallattempts) {
-        if ($useallattempts) {
-            return get_string('allattempts', 'quiz_statistics');
-        } else {
-            return get_string('firstattempts', 'quiz_statistics');
-        }
-    }
-
     /**
      * @param object $quiz the quiz.
      * @return array of questions for this quiz.
index a583ea9..02fb2f4 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-
 defined('MOODLE_INTERNAL') || die();
 
 require_once($CFG->libdir . '/formslib.php');
 
-
 /**
  * This is the settings form for the quiz statistics report.
  *
index be6c7a9..bb21b0f 100644 (file)
@@ -28,7 +28,6 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-
 require_once(dirname(__FILE__) . '/../../../../config.php');
 require_once($CFG->libdir . '/graphlib.php');
 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
@@ -64,8 +63,7 @@ $qubaids = quiz_statistics_qubaids_condition($quizid, $currentgroup, $groupstude
 // Load the rest of the required data.
 $questions = quiz_report_get_significant_questions($quiz);
 
-// Load enough data to check permissions.
-$quizstatistics = $DB->get_record('quiz_statistics', array('hashcode' => $qubaids->get_hash_code()));
+// Only load main question not sub questions.
 $questionstatistics = $DB->get_records_select('question_statistics', 'hashcode = ? AND slot IS NOT NULL',
                                               array($qubaids->get_hash_code()));
 
index 508775b..4bfc411 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-
 defined('MOODLE_INTERNAL') || die();
 
 require_once($CFG->libdir . '/tablelib.php');
 
-
 /**
  * This table shows statistics about a particular question.
  *
@@ -40,12 +38,12 @@ require_once($CFG->libdir . '/tablelib.php');
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class quiz_statistics_question_table extends flexible_table {
-    /** @var object this question with a _stats field. */
+    /** @var object this question. */
     protected $questiondata;
 
     /**
      * Constructor.
-     * @param $qid the id of the particular question whose statistics are being
+     * @param int $qid the id of the particular question whose statistics are being
      * displayed.
      */
     public function __construct($qid) {
@@ -53,16 +51,14 @@ class quiz_statistics_question_table extends flexible_table {
     }
 
     /**
-     * Set up the columns and headers and other properties of the table and then
-     * call flexible_table::setup() method.
-     *
-     * @param moodle_url $reporturl the URL to redisplay this report.
-     * @param object $question a question with a _stats field
-     * @param bool $hassubqs
+     * @param moodle_url                                   $reporturl
+     * @param object                                       $questiondata
+     * @param integer                                      $s               number of attempts on this question.
+     * @param \core_question\statistics\responses\analyser $responesstats
      */
-    public function question_setup($reporturl, $questiondata,
-            question_response_analyser $responesstats) {
+    public function question_setup($reporturl, $questiondata, $s, \core_question\statistics\responses\analyser $responesstats) {
         $this->questiondata = $questiondata;
+        $this->s = $s;
 
         $this->define_baseurl($reporturl->out());
         $this->collapsible(false);
@@ -137,10 +133,10 @@ class quiz_statistics_question_table extends flexible_table {
      * @return string contents of this table cell.
      */
     protected function col_frequency($response) {
-        if (!$this->questiondata->_stats->s) {
+        if (!$this->s) {
             return '';
         }
 
-        return $this->format_percentage($response->count / $this->questiondata->_stats->s);
+        return $this->format_percentage($response->count / $this->s);
     }
 }
index 393cede..73217a5 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-
 defined('MOODLE_INTERNAL') || die();
 
 require_once($CFG->libdir.'/tablelib.php');
 
-
 /**
  * This table has one row for each question in the quiz, with sub-rows when
  * random questions appear. There are columns for the various statistics.
@@ -134,61 +132,63 @@ class quiz_statistics_table extends flexible_table {
 
     /**
      * The question number.
-     * @param object $question containst the data to display.
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_number($question) {
-        if ($question->_stats->subquestion) {
+    protected function col_number($questionstat) {
+        if ($questionstat->subquestion) {
             return '';
         }
 
-        return $question->number;
+        return $questionstat->question->number;
     }
 
     /**
      * The question type icon.
-     * @param object $question containst the data to display.
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_icon($question) {
-        return print_question_icon($question, true);
+    protected function col_icon($questionstat) {
+        return print_question_icon($questionstat->question, true);
     }
 
     /**
      * Actions that can be performed on the question by this user (e.g. edit or preview).
-     * @param object $question containst the data to display.
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_actions($question) {
-        return quiz_question_action_icons($this->quiz, $this->cmid, $question, $this->baseurl);
+    protected function col_actions($questionstat) {
+        return quiz_question_action_icons($this->quiz, $this->cmid, $questionstat->question, $this->baseurl);
     }
 
     /**
      * The question type name.
-     * @param object $question containst the data to display.
+     *
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_qtype($question) {
-        return question_bank::get_qtype_name($question->qtype);
+    protected function col_qtype($questionstat) {
+        return question_bank::get_qtype_name($questionstat->question->qtype);
     }
 
     /**
      * The question name.
-     * @param object $question containst the data to display.
+     *
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_name($question) {
-        $name = $question->name;
+    protected function col_name($questionstat) {
+        $name = $questionstat->question->name;
 
         if ($this->is_downloading()) {
             return $name;
         }
 
         $url = null;
-        if ($question->_stats->subquestion) {
-            $url = new moodle_url($this->baseurl, array('qid' => $question->id));
-        } else if ($question->_stats->slot && $question->qtype != 'random') {
-            $url = new moodle_url($this->baseurl, array('slot' => $question->_stats->slot));
+        if ($questionstat->subquestion) {
+            $url = new moodle_url($this->baseurl, array('qid' => $questionstat->questionid));
+        } else if ($questionstat->slot && $questionstat->question->qtype != 'random') {
+            $url = new moodle_url($this->baseurl, array('slot' => $questionstat->slot));
         }
 
         if ($url) {
@@ -196,7 +196,7 @@ class quiz_statistics_table extends flexible_table {
                     array('title' => get_string('detailedanalysis', 'quiz_statistics')));
         }
 
-        if ($this->is_dubious_question($question)) {
+        if ($this->is_dubious_question($questionstat)) {
             $name = html_writer::tag('div', $name, array('class' => 'dubious'));
         }
 
@@ -205,82 +205,82 @@ class quiz_statistics_table extends flexible_table {
 
     /**
      * The number of attempts at this question.
-     * @param object $question containst the data to display.
+     *
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_s($question) {
-        if (!isset($question->_stats->s)) {
+    protected function col_s($questionstat) {
+        if (!isset($questionstat->s)) {
             return 0;
         }
 
-        return $question->_stats->s;
+        return $questionstat->s;
     }
 
     /**
      * The facility index (average fraction).
-     * @param object $question containst the data to display.
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_facility($question) {
-        if (is_null($question->_stats->facility)) {
+    protected function col_facility($questionstat) {
+        if (is_null($questionstat->facility)) {
             return '';
         }
 
-        return number_format($question->_stats->facility*100, 2) . '%';
+        return number_format($questionstat->facility*100, 2) . '%';
     }
 
     /**
      * The standard deviation of the fractions.
-     * @param object $question containst the data to display.
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_sd($question) {
-        if (is_null($question->_stats->sd) || $question->_stats->maxmark == 0) {
+    protected function col_sd($questionstat) {
+        if (is_null($questionstat->sd) || $questionstat->maxmark == 0) {
             return '';
         }
 
-        return number_format($question->_stats->sd*100 / $question->_stats->maxmark, 2) . '%';
+        return number_format($questionstat->sd*100 / $questionstat->maxmark, 2) . '%';
     }
 
     /**
      * An estimate of the fraction a student would get by guessing randomly.
-     * @param object $question containst the data to display.
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_random_guess_score($question) {
-        if (is_null($question->_stats->randomguessscore)) {
+    protected function col_random_guess_score($questionstat) {
+        if (is_null($questionstat->randomguessscore)) {
             return '';
         }
 
-        return number_format($question->_stats->randomguessscore * 100, 2).'%';
+        return number_format($questionstat->randomguessscore * 100, 2).'%';
     }
 
     /**
      * The intended question weight. Maximum mark for the question as a percentage
      * of maximum mark for the quiz. That is, the indended influence this question
      * on the student's overall mark.
-     * @param object $question containst the data to display.
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_intended_weight($question) {
-        return quiz_report_scale_summarks_as_percentage(
-                $question->_stats->maxmark, $this->quiz);
+    protected function col_intended_weight($questionstat) {
+        return quiz_report_scale_summarks_as_percentage($questionstat->maxmark, $this->quiz);
     }
 
     /**
      * The effective question weight. That is, an estimate of the actual
      * influence this question has on the student's overall mark.
-     * @param object $question containst the data to display.
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_effective_weight($question) {
+    protected function col_effective_weight($questionstat) {
         global $OUTPUT;
 
-        if ($question->_stats->subquestion) {
+        if ($questionstat->subquestion) {
             return '';
         }
 
-        if ($question->_stats->negcovar) {
+        if ($questionstat->negcovar) {
             $negcovar = get_string('negcovar', 'quiz_statistics');
 
             if (!$this->is_downloading()) {
@@ -292,49 +292,49 @@ class quiz_statistics_table extends flexible_table {
             return $negcovar;
         }
 
-        return number_format($question->_stats->effectiveweight, 2) . '%';
+        return number_format($questionstat->effectiveweight, 2) . '%';
     }
 
     /**
      * Discrimination index. This is the product moment correlation coefficient
-     * between the fraction for this qestion, and the average fraction for the
+     * between the fraction for this question, and the average fraction for the
      * other questions in this quiz.
-     * @param object $question containst the data to display.
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_discrimination_index($question) {
-        if (!is_numeric($question->_stats->discriminationindex)) {
-            return $question->_stats->discriminationindex;
+    protected function col_discrimination_index($questionstat) {
+        if (!is_numeric($questionstat->discriminationindex)) {
+            return $questionstat->discriminationindex;
         }
 
-        return number_format($question->_stats->discriminationindex, 2) . '%';
+        return number_format($questionstat->discriminationindex, 2) . '%';
     }
 
     /**
      * Discrimination efficiency, similar to, but different from, the Discrimination index.
-     * @param object $question containst the data to display.
+     *
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_discriminative_efficiency($question) {
-        if (!is_numeric($question->_stats->discriminativeefficiency)) {
+    protected function col_discriminative_efficiency($questionstat) {
+        if (!is_numeric($questionstat->discriminativeefficiency)) {
             return '';
         }
 
-        return number_format($question->_stats->discriminativeefficiency, 2) . '%';
+        return number_format($questionstat->discriminativeefficiency, 2) . '%';
     }
 
     /**
      * This method encapsulates the test for wheter a question should be considered dubious.
-     * @param object question the question object with a property _stats which
-     * includes all the stats for the question.
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      *&n