diff --git a/admin/generator.php b/admin/generator.php index 0e0c11e4b35..5d642af08ea 100644 --- a/admin/generator.php +++ b/admin/generator.php @@ -622,7 +622,7 @@ class generator { require_once($CFG->libdir .'/questionlib.php'); require_once($CFG->dirroot .'/mod/quiz/editlib.php'); $questions = array(); - $questionsmenu = question_type_menu(); + $questionsmenu = question_bank::get_creatable_qtypes(); $questiontypes = array(); foreach ($questionsmenu as $qtype => $qname) { $questiontypes[] = $qtype; diff --git a/admin/index.php b/admin/index.php index 47ce50c19ab..50eb368c962 100644 --- a/admin/index.php +++ b/admin/index.php @@ -355,7 +355,6 @@ if (during_initial_install()) { } // login user and let him set password and admin details $adminuser->newadminuser = 1; - message_set_default_message_preferences($adminuser); complete_user_login($adminuser, false); redirect("$CFG->wwwroot/user/editadvanced.php?id=$adminuser->id"); // Edit thyself diff --git a/admin/message.php b/admin/message.php new file mode 100644 index 00000000000..8daf613defa --- /dev/null +++ b/admin/message.php @@ -0,0 +1,70 @@ +. + +/** + * Message outputs configuration page + * + * @package message + * @copyright 2011 Lancaster University Network Services Limited + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +require_once(dirname(__FILE__) . '/../config.php'); +require_once($CFG->dirroot . '/message/lib.php'); +require_once($CFG->libdir.'/adminlib.php'); + +// This is an admin page +admin_externalpage_setup('managemessageoutputs'); + +// Require site configuration capability +require_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM)); + +// Get the submitted params +$disable = optional_param('disable', 0, PARAM_INT); +$enable = optional_param('enable', 0, PARAM_INT); + +if (!empty($disable) && confirm_sesskey()) { + if (!$processor = $DB->get_record('message_processors', array('id'=>$disable))) { + print_error('outputdoesnotexist', 'message'); + } + $DB->set_field('message_processors', 'enabled', '0', array('id'=>$processor->id)); // Disable output +} + +if (!empty($enable) && confirm_sesskey() ) { + if (!$processor = $DB->get_record('message_processors', array('id'=>$enable))) { + print_error('outputdoesnotexist', 'message'); + } + $DB->set_field('message_processors', 'enabled', '1', array('id'=>$processor->id)); // Enable output +} + +if ($disable || $enable) { + $url = new moodle_url('message.php'); + redirect($url); +} +// Page settings +$PAGE->set_context(get_context_instance(CONTEXT_SYSTEM)); + +// Grab the renderer +$renderer = $PAGE->get_renderer('core', 'message'); + +// Display the manage message outputs interface +$processors = get_message_processors(); +$messageoutputs = $renderer->manage_messageoutputs($processors); + +// Display the page +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('managemessageoutputs', 'message')); +echo $messageoutputs; +echo $OUTPUT->footer(); \ No newline at end of file diff --git a/admin/qtypes.php b/admin/qtypes.php index 33a70d87a4f..bb0d1775afd 100644 --- a/admin/qtypes.php +++ b/admin/qtypes.php @@ -1,275 +1,297 @@ libdir . '/questionlib.php'); - require_once($CFG->libdir . '/adminlib.php'); - require_once($CFG->libdir . '/tablelib.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 . -/// Check permissions. - require_login(); - $systemcontext = get_context_instance(CONTEXT_SYSTEM); - require_capability('moodle/question:config', $systemcontext); - $canviewreports = has_capability('report/questioninstances:view', $systemcontext); +/** + * Allows the admin to manage question types. + * + * @package moodlecore + * @subpackage questionbank + * @copyright 2008 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ - admin_externalpage_setup('manageqtypes'); -/// Get some data we will need - question counts and which types are needed. - $counts = $DB->get_records_sql(" - SELECT qtype, COUNT(1) as numquestions, SUM(hidden) as numhidden - FROM {question} GROUP BY qtype", array()); - $needed = array(); - foreach ($QTYPES as $qtypename => $qtype) { - if (!isset($counts[$qtypename])) { - $counts[$qtypename] = new stdClass; - $counts[$qtypename]->numquestions = 0; - $counts[$qtypename]->numhidden = 0; - } - $needed[$qtypename] = $counts[$qtypename]->numquestions > 0; - $counts[$qtypename]->numquestions -= $counts[$qtypename]->numhidden; +require_once(dirname(__FILE__) . '/../config.php'); +require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->libdir . '/adminlib.php'); +require_once($CFG->libdir . '/tablelib.php'); + +// Check permissions. +require_login(); +$systemcontext = get_context_instance(CONTEXT_SYSTEM); +require_capability('moodle/question:config', $systemcontext); +$canviewreports = has_capability('report/questioninstances:view', $systemcontext); + +admin_externalpage_setup('manageqtypes'); + +$qtypes = question_bank::get_all_qtypes(); + +// Get some data we will need - question counts and which types are needed. +$counts = $DB->get_records_sql(" + SELECT qtype, COUNT(1) as numquestions, SUM(hidden) as numhidden + FROM {question} GROUP BY qtype", array()); +$needed = array(); +foreach ($qtypes as $qtypename => $qtype) { + if (!isset($counts[$qtypename])) { + $counts[$qtypename] = new stdClass; + $counts[$qtypename]->numquestions = 0; + $counts[$qtypename]->numhidden = 0; } - $needed['missingtype'] = true; // The system needs the missing question type. - foreach ($QTYPES as $qtypename => $qtype) { - foreach ($qtype->requires_qtypes() as $reqtype) { - $needed[$reqtype] = true; - } + $needed[$qtypename] = $counts[$qtypename]->numquestions > 0; + $counts[$qtypename]->numquestions -= $counts[$qtypename]->numhidden; +} +$needed['missingtype'] = true; // The system needs the missing question type. +foreach ($qtypes as $qtypename => $qtype) { + foreach ($qtype->requires_qtypes() as $reqtype) { + $needed[$reqtype] = true; } - foreach ($counts as $qtypename => $count) { - if (!isset($QTYPES[$qtypename])) { - $counts['missingtype']->numquestions += $count->numquestions - $count->numhidden; - $counts['missingtype']->numhidden += $count->numhidden; - } +} +foreach ($counts as $qtypename => $count) { + if (!isset($qtypes[$qtypename])) { + $counts['missingtype']->numquestions += $count->numquestions - $count->numhidden; + $counts['missingtype']->numhidden += $count->numhidden; + } +} + +// Work of the correct sort order. +$config = get_config('question'); +$sortedqtypes = array(); +foreach ($qtypes as $qtypename => $qtype) { + $sortedqtypes[$qtypename] = $qtype->local_name(); +} +$sortedqtypes = question_bank::sort_qtype_array($sortedqtypes, $config); + +// Process actions ============================================================ + +// Disable. +if (($disable = optional_param('disable', '', PARAM_SAFEDIR)) && confirm_sesskey()) { + if (!isset($qtypes[$disable])) { + print_error('unknownquestiontype', 'question', new moodle_url('/admin/qtypes.php'), $disable); } -/// Work of the correct sort order. - $config = get_config('question'); - $sortedqtypes = array(); - foreach ($QTYPES as $qtypename => $qtype) { - $sortedqtypes[$qtypename] = $qtype->local_name(); - } - $sortedqtypes = question_sort_qtype_array($sortedqtypes, $config); + set_config($disable . '_disabled', 1, 'question'); + redirect(admin_url('qtypes.php')); +} -/// Process actions ============================================================ - - // Disable. - if (($disable = optional_param('disable', '', PARAM_SAFEDIR)) && confirm_sesskey()) { - if (!isset($QTYPES[$disable])) { - print_error('unknownquestiontype', 'question', admin_url('qtypes.php'), $disable); - } - - set_config($disable . '_disabled', 1, 'question'); - redirect(admin_url('qtypes.php')); +// Enable. +if (($enable = optional_param('enable', '', PARAM_SAFEDIR)) && confirm_sesskey()) { + if (!isset($qtypes[$enable])) { + print_error('unknownquestiontype', 'question', new moodle_url('/admin/qtypes.php'), $enable); } - // Enable. - if (($enable = optional_param('enable', '', PARAM_SAFEDIR)) && confirm_sesskey()) { - if (!isset($QTYPES[$enable])) { - print_error('unknownquestiontype', 'question', admin_url('qtypes.php'), $enable); - } - - if (!$QTYPES[$enable]->menu_name()) { - print_error('cannotenable', 'question', admin_url('qtypes.php'), $enable); - } - - unset_config($enable . '_disabled', 'question'); - redirect(admin_url('qtypes.php')); + if (!$qtypes[$enable]->menu_name()) { + print_error('cannotenable', 'question', new moodle_url('/admin/qtypes.php'), $enable); } - // Move up in order. - if (($up = optional_param('up', '', PARAM_SAFEDIR)) && confirm_sesskey()) { - if (!isset($QTYPES[$up])) { - print_error('unknownquestiontype', 'question', admin_url('qtypes.php'), $up); - } + unset_config($enable . '_disabled', 'question'); + redirect(new moodle_url('/admin/qtypes.php')); +} - $neworder = question_reorder_qtypes($sortedqtypes, $up, -1); - question_save_qtype_order($neworder, $config); - redirect(admin_url('qtypes.php')); +// Move up in order. +if (($up = optional_param('up', '', PARAM_SAFEDIR)) && confirm_sesskey()) { + if (!isset($qtypes[$up])) { + print_error('unknownquestiontype', 'question', new moodle_url('/admin/qtypes.php'), $up); } - // Move down in order. - if (($down = optional_param('down', '', PARAM_SAFEDIR)) && confirm_sesskey()) { - if (!isset($QTYPES[$down])) { - print_error('unknownquestiontype', 'question', admin_url('qtypes.php'), $down); - } + $neworder = question_reorder_qtypes($sortedqtypes, $up, -1); + question_save_qtype_order($neworder, $config); + redirect(new moodle_url('/admin/qtypes.php')); +} - $neworder = question_reorder_qtypes($sortedqtypes, $down, +1); - question_save_qtype_order($neworder, $config); - redirect(admin_url('qtypes.php')); +// Move down in order. +if (($down = optional_param('down', '', PARAM_SAFEDIR)) && confirm_sesskey()) { + if (!isset($qtypes[$down])) { + print_error('unknownquestiontype', 'question', admin_url('qtypes.php'), $down); } - // Delete. - if (($delete = optional_param('delete', '', PARAM_SAFEDIR)) && confirm_sesskey()) { - // Check it is OK to delete this question type. - if ($delete == 'missingtype') { - print_error('cannotdeletemissingqtype', 'admin', admin_url('qtypes.php')); - } + $neworder = question_reorder_qtypes($sortedqtypes, $down, +1); + question_save_qtype_order($neworder, $config); + redirect(new moodle_url('/admin/qtypes.php')); +} - if (!isset($QTYPES[$delete])) { - print_error('unknownquestiontype', 'question', admin_url('qtypes.php'), $delete); - } +// Delete. +if (($delete = optional_param('delete', '', PARAM_SAFEDIR)) && confirm_sesskey()) { + // Check it is OK to delete this question type. + if ($delete == 'missingtype') { + print_error('cannotdeletemissingqtype', 'admin', new moodle_url('/admin/qtypes.php')); + } - $qtypename = $QTYPES[$delete]->local_name(); - if ($counts[$delete]->numquestions + $counts[$delete]->numhidden > 0) { - print_error('cannotdeleteqtypeinuse', 'admin', admin_url('qtypes.php'), $qtypename); - } + if (!isset($qtypes[$delete])) { + print_error('unknownquestiontype', 'question', new moodle_url('/admin/qtypes.php'), $delete); + } - if ($needed[$delete] > 0) { - print_error('cannotdeleteqtypeneeded', 'admin', admin_url('qtypes.php'), $qtypename); - } + $qtypename = $qtypes[$delete]->local_name(); + if ($counts[$delete]->numquestions + $counts[$delete]->numhidden > 0) { + print_error('cannotdeleteqtypeinuse', 'admin', new moodle_url('/admin/qtypes.php'), $qtypename); + } - // If not yet confirmed, display a confirmation message. - if (!optional_param('confirm', '', PARAM_BOOL)) { - $qtypename = $QTYPES[$delete]->local_name(); - echo $OUTPUT->header(); - echo $OUTPUT->heading(get_string('deleteqtypeareyousure', 'admin', $qtypename)); - echo $OUTPUT->confirm(get_string('deleteqtypeareyousuremessage', 'admin', $qtypename), - admin_url('qtypes.php?delete=' . $delete . '&confirm=1'), - admin_url('qtypes.php')); - echo $OUTPUT->footer(); - exit; - } + if ($needed[$delete] > 0) { + print_error('cannotdeleteqtypeneeded', 'admin', new moodle_url('/admin/qtypes.php'), $qtypename); + } - // Do the deletion. + // If not yet confirmed, display a confirmation message. + if (!optional_param('confirm', '', PARAM_BOOL)) { + $qtypename = $qtypes[$delete]->local_name(); echo $OUTPUT->header(); - echo $OUTPUT->heading(get_string('deletingqtype', 'admin', $qtypename)); - - // Delete any configuration records. - if (!unset_all_config_for_plugin('qtype_' . $delete)) { - echo $OUTPUT->notification(get_string('errordeletingconfig', 'admin', 'qtype_' . $delete)); - } - unset_config($delete . '_disabled', 'question'); - unset_config($delete . '_sortorder', 'question'); - - // Then the tables themselves - drop_plugin_tables($delete, $QTYPES[$delete]->plugin_dir() . '/db/install.xml', false); - - // Remove event handlers and dequeue pending events - events_uninstall('qtype/' . $delete); - - $a->qtype = $qtypename; - $a->directory = $QTYPES[$delete]->plugin_dir(); - echo $OUTPUT->box(get_string('qtypedeletefiles', 'admin', $a), 'generalbox', 'notice'); - echo $OUTPUT->continue_button(admin_url('qtypes.php')); + echo $OUTPUT->heading(get_string('deleteqtypeareyousure', 'admin', $qtypename)); + echo $OUTPUT->confirm(get_string('deleteqtypeareyousuremessage', 'admin', $qtypename), + new moodle_url('/admin/qtypes.php', array('delete' => $delete, 'confirm' => 1)), + new moodle_url('/admin/qtypes.php')); echo $OUTPUT->footer(); exit; } - // End of process actions ================================================== - -/// Print the page heading. + // Do the deletion. echo $OUTPUT->header(); - echo $OUTPUT->heading(get_string('manageqtypes', 'admin')); + echo $OUTPUT->heading(get_string('deletingqtype', 'admin', $qtypename)); -/// Set up the table. - $table = new flexible_table('qtypeadmintable'); - $table->define_columns(array('questiontype', 'numquestions', 'version', 'requires', - 'availableto', 'delete', 'settings')); - $table->define_headers(array(get_string('questiontype', 'admin'), get_string('numquestions', 'admin'), - get_string('version'), get_string('requires', 'admin'), get_string('availableq', 'question'), - get_string('delete'), get_string('settings'))); - $table->set_attribute('id', 'qtypes'); - $table->set_attribute('class', 'generaltable generalbox boxaligncenter boxwidthwide'); - $table->setup(); + // Delete any configuration records. + if (!unset_all_config_for_plugin('qtype_' . $delete)) { + echo $OUTPUT->notification(get_string('errordeletingconfig', 'admin', 'qtype_' . $delete)); + } + unset_config($delete . '_disabled', 'question'); + unset_config($delete . '_sortorder', 'question'); -/// Add a row for each question type. - $createabletypes = question_type_menu(); - foreach ($sortedqtypes as $qtypename => $localname) { - $qtype = $QTYPES[$qtypename]; - $row = array(); + // Then the tables themselves + drop_plugin_tables($delete, $qtypes[$delete]->plugin_dir() . '/db/install.xml', false); - // Question icon and name. - $fakequestion = new stdClass; - $fakequestion->qtype = $qtypename; - $icon = print_question_icon($fakequestion, true); - $row[] = $icon . ' ' . $localname; + // Remove event handlers and dequeue pending events + events_uninstall('qtype/' . $delete); - // Number of questions of this type. - if ($counts[$qtypename]->numquestions + $counts[$qtypename]->numhidden > 0) { - if ($counts[$qtypename]->numhidden > 0) { - $strcount = get_string('numquestionsandhidden', 'admin', $counts[$qtypename]); - } else { - $strcount = $counts[$qtypename]->numquestions; - } - if ($canviewreports) { - $row[] = '' . $strcount . ''; - } else { - $strcount; - } + $a->qtype = $qtypename; + $a->directory = $qtypes[$delete]->plugin_dir(); + echo $OUTPUT->box(get_string('qtypedeletefiles', 'admin', $a), 'generalbox', 'notice'); + echo $OUTPUT->continue_button(new moodle_url('/admin/qtypes.php')); + echo $OUTPUT->footer(); + exit; +} + +// End of process actions ================================================== + +// Print the page heading. +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('manageqtypes', 'admin')); + +// Set up the table. +$table = new flexible_table('qtypeadmintable'); +$table->define_baseurl(new moodle_url('/admin/qtypes.php')); +$table->define_columns(array('questiontype', 'numquestions', 'version', 'requires', + 'availableto', 'delete', 'settings')); +$table->define_headers(array(get_string('questiontype', 'admin'), get_string('numquestions', 'admin'), + get_string('version'), get_string('requires', 'admin'), get_string('availableq', 'question'), + get_string('delete'), get_string('settings'))); +$table->set_attribute('id', 'qtypes'); +$table->set_attribute('class', 'generaltable generalbox boxaligncenter boxwidthwide'); +$table->setup(); + +// Add a row for each question type. +$createabletypes = question_bank::get_creatable_qtypes(); +foreach ($sortedqtypes as $qtypename => $localname) { + $qtype = $qtypes[$qtypename]; + $row = array(); + + // Question icon and name. + $fakequestion = new stdClass; + $fakequestion->qtype = $qtypename; + $icon = print_question_icon($fakequestion, true); + $row[] = $icon . ' ' . $localname; + + // Number of questions of this type. + if ($counts[$qtypename]->numquestions + $counts[$qtypename]->numhidden > 0) { + if ($counts[$qtypename]->numhidden > 0) { + $strcount = get_string('numquestionsandhidden', 'admin', $counts[$qtypename]); } else { - $row[] = 0; + $strcount = $counts[$qtypename]->numquestions; } - - // Question version number. - $version = get_config('qtype_' . $qtypename, 'version'); - if ($version) { - $row[] = $version; + if ($canviewreports) { + $row[] = '' . $strcount . ''; } else { - $row[] = '' . get_string('nodatabase', 'admin') . ''; + $strcount; } - - // Other question types required by this one. - $requiredtypes = $qtype->requires_qtypes(); - $strtypes = array(); - if (!empty($requiredtypes)) { - foreach ($requiredtypes as $required) { - $strtypes[] = $QTYPES[$required]->local_name(); - } - $row[] = implode(', ', $strtypes); - } else { - $row[] = ''; - } - - // Are people allowed to create new questions of this type? - $rowclass = ''; - if ($qtype->menu_name()) { - $createable = isset($createabletypes[$qtypename]); - $icons = enable_disable_button($qtypename, $createable); - if (!$createable) { - $rowclass = 'dimmed_text'; - } - } else { - $icons = ''; - } - - // Move icons. - $icons .= icon_html('up', $qtypename, 't/up', get_string('up'), ''); - $icons .= icon_html('down', $qtypename, 't/down', get_string('down'), ''); - $row[] = $icons; - - // Delete link, if available. - if ($needed[$qtypename]) { - $row[] = ''; - } else { - $row[] = '' . get_string('delete') . ''; - } - - // Settings link, if available. - $settings = admin_get_root()->locate('qtypesetting' . $qtypename); - if ($settings instanceof admin_externalpage) { - $row[] = '' . get_string('settings') . ''; - } else if ($settings instanceof admin_settingpage) { - $row[] = '' . get_string('settings') . ''; - } else { - $row[] = ''; - } - - $table->add_data($row, $rowclass); + } else { + $row[] = 0; } - $table->finish_output(); + // Question version number. + $version = get_config('qtype_' . $qtypename, 'version'); + if ($version) { + $row[] = $version; + } else { + $row[] = '' . get_string('nodatabase', 'admin') . ''; + } - echo $OUTPUT->footer(); + // Other question types required by this one. + $requiredtypes = $qtype->requires_qtypes(); + $strtypes = array(); + if (!empty($requiredtypes)) { + foreach ($requiredtypes as $required) { + $strtypes[] = $qtypes[$required]->local_name(); + } + $row[] = implode(', ', $strtypes); + } else { + $row[] = ''; + } -function admin_url($endbit) { - global $CFG; - return $CFG->wwwroot . '/' . $CFG->admin . '/' . $endbit; + // Are people allowed to create new questions of this type? + $rowclass = ''; + if ($qtype->menu_name()) { + $createable = isset($createabletypes[$qtypename]); + $icons = enable_disable_button($qtypename, $createable); + if (!$createable) { + $rowclass = 'dimmed_text'; + } + } else { + $icons = ''; + } + + // Move icons. + $icons .= icon_html('up', $qtypename, 't/up', get_string('up'), ''); + $icons .= icon_html('down', $qtypename, 't/down', get_string('down'), ''); + $row[] = $icons; + + // Delete link, if available. + if ($needed[$qtypename]) { + $row[] = ''; + } else { + $row[] = '' . get_string('delete') . ''; + } + + // Settings link, if available. + $settings = admin_get_root()->locate('qtypesetting' . $qtypename); + if ($settings instanceof admin_externalpage) { + $row[] = '' . get_string('settings') . ''; + } else if ($settings instanceof admin_settingpage) { + $row[] = '' . get_string('settings') . ''; + } else { + $row[] = ''; + } + + $table->add_data($row, $rowclass); } +$table->finish_output(); + +echo $OUTPUT->footer(); + function enable_disable_button($qtypename, $createable) { if ($createable) { return icon_html('disable', $qtypename, 'i/hide', get_string('enabled', 'question'), get_string('disable')); @@ -283,7 +305,7 @@ function icon_html($action, $qtypename, $icon, $alt, $tip) { if ($tip) { $tip = 'title="' . $tip . '" '; } - $html = '
'; + $html = '
'; $html .= ''; $html .= ''; diff --git a/admin/registration/confirmregistration.php b/admin/registration/confirmregistration.php index 517f4e156c8..12afb50dbd8 100644 --- a/admin/registration/confirmregistration.php +++ b/admin/registration/confirmregistration.php @@ -47,7 +47,7 @@ $error = optional_param('error', '', PARAM_ALPHANUM); admin_externalpage_setup('registrationindex'); if (!empty($error) and $error == 'urlalreadyexist') { - throw new moodle_exception('urlalreadyregistered', 'hub', + throw new moodle_exception('urlalreadyregistered', 'hub', $CFG->wwwroot . '/' . $CFG->admin . '/registration/index.php'); } @@ -85,7 +85,7 @@ if (!empty($registeredhub) and $registeredhub->token == $token) { echo $OUTPUT->footer(); } else { - throw new moodle_exception('wrongtoken', 'hub', + throw new moodle_exception('wrongtoken', 'hub', $CFG->wwwroot . '/' . $CFG->admin . '/registration/index.php'); } diff --git a/admin/registration/index.php b/admin/registration/index.php index b1a2d91224d..02e5cdebac3 100644 --- a/admin/registration/index.php +++ b/admin/registration/index.php @@ -148,12 +148,12 @@ if (empty($cancel) and $unregistration and !$confirm) { $moodleorghub = $registrationmanager->get_registeredhub(HUB_MOODLEORGHUBURL); if (!empty($moodleorghub)) { $registeredonmoodleorg = true; - } + } echo $OUTPUT->heading(get_string('registeron', 'hub'), 3, 'main'); echo $renderer->registrationselector($registeredonmoodleorg); - if (extension_loaded('xmlrpc')) { + if (extension_loaded('xmlrpc')) { $hubs = $registrationmanager->get_registered_on_hubs(); if (!empty($hubs)) { echo $OUTPUT->heading(get_string('registeredon', 'hub'), 3, 'main'); diff --git a/admin/report/questioninstances/index.php b/admin/report/questioninstances/index.php index d69098280ff..24299d32a5d 100644 --- a/admin/report/questioninstances/index.php +++ b/admin/report/questioninstances/index.php @@ -22,8 +22,9 @@ echo $OUTPUT->header(); add_to_log(SITEID, "admin", "report questioninstances", "report/questioninstances/index.php?qtype=$requestedqtype", $requestedqtype); // Prepare the list of capabilities to choose from +$qtypes = question_bank::get_all_qtypes(); $qtypechoices = array(); -foreach ($QTYPES as $qtype) { +foreach ($qtypes as $qtype) { $qtypechoices[$qtype->name()] = $qtype->local_name(); } @@ -45,7 +46,7 @@ if ($requestedqtype) { // Work out the bits needed for the SQL WHERE clauses. if ($requestedqtype == 'missingtype') { - $othertypes = array_keys($QTYPES); + $othertypes = array_keys($qtypes); $key = array_search('missingtype', $othertypes); unset($othertypes[$key]); list($sqlqtypetest, $params) = $DB->get_in_or_equal($othertypes, SQL_PARAMS_QM, '', false); @@ -58,7 +59,8 @@ if ($requestedqtype) { } else { $sqlqtypetest = 'WHERE qtype = ?'; $params = array($requestedqtype); - $title = get_string('reportforqtype', 'report_questioninstances', $QTYPES[$requestedqtype]->local_name()); + $title = get_string('reportforqtype', 'report_questioninstances', + question_bank::get_qtype($requestedqtype)->local_name()); } // Get the question counts, and all the context information, for each diff --git a/admin/report/spamcleaner/module.js b/admin/report/spamcleaner/module.js index dd0565c7099..56b13ef154d 100644 --- a/admin/report/spamcleaner/module.js +++ b/admin/report/spamcleaner/module.js @@ -108,7 +108,7 @@ M.report_spamcleaner = { context.Y = Y; context.me = me; if (Y.one("#removeall_btn")) { - Y.on("click", context.del_all, "#removeall_btn"); + Y.on("click", context.del_all, "#removeall_btn"); } }); } diff --git a/admin/roles/module.js b/admin/roles/module.js index 92b47e9fa09..7326cf34252 100644 --- a/admin/roles/module.js +++ b/admin/roles/module.js @@ -138,7 +138,7 @@ M.core_role.init_cap_table_filter = function(Y, tableid, contextid) { lastheading = null; this.setFilterCookieValue(filtertext); - + this.button.set('disabled', (filtertext == '')); this.table.all('tr').each(function(row){ diff --git a/admin/settings/appearance.php b/admin/settings/appearance.php index 28081638b3f..dc2ba27ca43 100644 --- a/admin/settings/appearance.php +++ b/admin/settings/appearance.php @@ -18,6 +18,8 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page $temp->add(new admin_setting_configcheckbox('allowuserblockhiding', get_string('allowuserblockhiding', 'admin'), get_string('configallowuserblockhiding', 'admin'), 1)); $temp->add(new admin_setting_configcheckbox('allowblockstodock', get_string('allowblockstodock', 'admin'), get_string('configallowblockstodock', 'admin'), 1)); $temp->add(new admin_setting_configtextarea('custommenuitems', get_string('custommenuitems', 'admin'), get_string('configcustommenuitems', 'admin'), '', PARAM_TEXT, '50', '10')); + $temp->add(new admin_setting_configcheckbox('enabledevicedetection', get_string('enabledevicedetection', 'admin'), get_string('configenabledevicedetection', 'admin'), 1)); + $temp->add(new admin_setting_devicedetectregex('devicedetectregex', get_string('devicedetectregex', 'admin'), get_string('devicedetectregex_desc', 'admin'), '')); $ADMIN->add('themes', $temp); $ADMIN->add('themes', new admin_externalpage('themeselector', get_string('themeselector','admin'), $CFG->wwwroot . '/theme/index.php')); diff --git a/admin/settings/courses.php b/admin/settings/courses.php index d436a315699..13291c78093 100644 --- a/admin/settings/courses.php +++ b/admin/settings/courses.php @@ -139,7 +139,7 @@ if ($hassiteconfig 400 => '400', 500 => '500'); $temp->add(new admin_setting_configselect('backup/backup_auto_keep', get_string('keep'), get_string('backupkeephelp'), 1, $keepoptoins)); - + $temp->add(new admin_setting_heading('automatedsettings', get_string('automatedsettings','backup'), '')); $temp->add(new admin_setting_configcheckbox('backup/backup_auto_users', get_string('users'), get_string('backupusershelp'), 1)); @@ -152,8 +152,8 @@ if ($hassiteconfig $temp->add(new admin_setting_configcheckbox('backup/backup_auto_userscompletion', get_string('generaluserscompletion','backup'), get_string('configgeneraluserscompletion','backup'), 1)); $temp->add(new admin_setting_configcheckbox('backup/backup_auto_logs', get_string('logs'), get_string('backuplogshelp'), 0)); $temp->add(new admin_setting_configcheckbox('backup/backup_auto_histories', get_string('generalhistories','backup'), get_string('configgeneralhistories','backup'), 0)); - - + + //$temp->add(new admin_setting_configcheckbox('backup/backup_auto_messages', get_string('messages', 'message'), get_string('backupmessageshelp','message'), 0)); //$temp->add(new admin_setting_configcheckbox('backup/backup_auto_blogs', get_string('blogs', 'blog'), get_string('backupblogshelp','blog'), 0)); diff --git a/admin/settings/plugins.php b/admin/settings/plugins.php index bf8060500e5..3555039c9bb 100644 --- a/admin/settings/plugins.php +++ b/admin/settings/plugins.php @@ -46,6 +46,27 @@ if ($hassiteconfig) { } } + // message outputs + $ADMIN->add('modules', new admin_category('messageoutputs', get_string('messageoutputs', 'message'))); + $ADMIN->add('messageoutputs', new admin_page_managemessageoutputs()); + $ADMIN->add('messageoutputs', new admin_page_defaultmessageoutputs()); + require_once($CFG->dirroot.'/message/lib.php'); + $processors = get_message_processors(); + foreach ($processors as $processor) { + $processorname = $processor->name; + if (!$processor->available) { + continue; + } + if ($processor->hassettings) { + $strprocessorname = get_string('pluginname', 'message_'.$processorname); + $settings = new admin_settingpage('messagesetting'.$processorname, $strprocessorname, 'moodle/site:config', !$processor->enabled); + include($CFG->dirroot.'/message/output/'.$processor->name.'/settings.php'); + if ($settings) { + $ADMIN->add('messageoutputs', $settings); + } + } + } + // authentication plugins $ADMIN->add('modules', new admin_category('authsettings', get_string('authentication', 'admin'))); @@ -324,6 +345,9 @@ if ($hassiteconfig) { $ADMIN->add('webservicesettings', $temp); /// manage service $temp = new admin_settingpage('externalservices', get_string('externalservices', 'webservice')); + $enablemobiledocurl = new moodle_url(get_docs_url('Enable_mobile_web_services')); + $enablemobiledoclink = html_writer::link($enablemobiledocurl, get_string('documentation')); + $temp->add(new admin_setting_enablemobileservice('enablemobilewebservice', get_string('enablemobilewebservice', 'admin'), get_string('configenablemobilewebservice', 'admin', $enablemobiledoclink), 0)); $temp->add(new admin_setting_heading('manageserviceshelpexplaination', get_string('information', 'webservice'), get_string('servicehelpexplanation', 'webservice'))); $temp->add(new admin_setting_manageexternalservices()); $ADMIN->add('webservicesettings', $temp); diff --git a/admin/settings/server.php b/admin/settings/server.php index 7334fec0529..049663df3e0 100644 --- a/admin/settings/server.php +++ b/admin/settings/server.php @@ -17,46 +17,8 @@ $ADMIN->add('server', $temp); -// "email" settingpage -$temp = new admin_settingpage('mail', get_string('mail','admin')); -$temp->add(new admin_setting_configtext('smtphosts', get_string('smtphosts', 'admin'), get_string('configsmtphosts', 'admin'), '', PARAM_RAW)); -$temp->add(new admin_setting_configtext('smtpuser', get_string('smtpuser', 'admin'), get_string('configsmtpuser', 'admin'), '', PARAM_NOTAGS)); -$temp->add(new admin_setting_configpasswordunmask('smtppass', get_string('smtppass', 'admin'), get_string('configsmtpuser', 'admin'), '')); -$temp->add(new admin_setting_configtext('smtpmaxbulk', get_string('smtpmaxbulk', 'admin'), get_string('configsmtpmaxbulk', 'admin'), 1, PARAM_INT)); -$temp->add(new admin_setting_configtext('noreplyaddress', get_string('noreplyaddress', 'admin'), get_string('confignoreplyaddress', 'admin'), 'noreply@' . get_host_from_url($CFG->wwwroot), PARAM_NOTAGS)); -$temp->add(new admin_setting_configselect('digestmailtime', get_string('digestmailtime', 'admin'), get_string('configdigestmailtime', 'admin'), 17, array('00' => '00', - '01' => '01', - '02' => '02', - '03' => '03', - '04' => '04', - '05' => '05', - '06' => '06', - '07' => '07', - '08' => '08', - '09' => '09', - '10' => '10', - '11' => '11', - '12' => '12', - '13' => '13', - '14' => '14', - '15' => '15', - '16' => '16', - '17' => '17', - '18' => '18', - '19' => '19', - '20' => '20', - '21' => '21', - '22' => '22', - '23' => '23'))); -$charsets = get_list_of_charsets(); -unset($charsets['UTF-8']); // not needed here -$options = array(); -$options['0'] = 'UTF-8'; -$options = array_merge($options, $charsets); -$temp->add(new admin_setting_configselect('sitemailcharset', get_string('sitemailcharset', 'admin'), get_string('configsitemailcharset','admin'), '0', $options)); -$temp->add(new admin_setting_configcheckbox('allowusermailcharset', get_string('allowusermailcharset', 'admin'), get_string('configallowusermailcharset', 'admin'), 0)); -$options = array('LF'=>'LF', 'CRLF'=>'CRLF'); -$temp->add(new admin_setting_configselect('mailnewline', get_string('mailnewline', 'admin'), get_string('configmailnewline','admin'), 'LF', $options)); +// "supportcontact" settingpage +$temp = new admin_settingpage('supportcontact', get_string('supportcontact','admin')); if (isloggedin()) { global $USER; $primaryadminemail = $USER->email; @@ -73,17 +35,6 @@ $temp->add(new admin_setting_configtext('supportpage', get_string('supportpage', $ADMIN->add('server', $temp); -// Jabber settingpage -$temp = new admin_settingpage('jabber', get_string('jabber', 'admin')); -$temp->add(new admin_setting_configtext('jabberhost', get_string('jabberhost', 'admin'), get_string('configjabberhost', 'admin'), '', PARAM_RAW)); -$temp->add(new admin_setting_configtext('jabberserver', get_string('jabberserver', 'admin'), get_string('configjabberserver', 'admin'), '', PARAM_RAW)); -$temp->add(new admin_setting_configtext('jabberusername', get_string('jabberusername', 'admin'), get_string('configjabberusername', 'admin'), '', PARAM_RAW)); -$temp->add(new admin_setting_configpasswordunmask('jabberpassword', get_string('jabberpassword', 'admin'), get_string('configjabberpassword', 'admin'), '')); -$temp->add(new admin_setting_configtext('jabberport', get_string('jabberport', 'admin'), get_string('configjabberport', 'admin'), 5222, PARAM_INT)); -$ADMIN->add('server', $temp); - - - // "sessionhandling" settingpage $temp = new admin_settingpage('sessionhandling', get_string('sessionhandling', 'admin')); $temp->add(new admin_setting_configcheckbox('dbsessions', get_string('dbsessions', 'admin'), get_string('configdbsessions', 'admin'), 1)); diff --git a/admin/webservice/service_user_settings.php b/admin/webservice/service_user_settings.php index 40b165e4589..18d0ef56abc 100644 --- a/admin/webservice/service_user_settings.php +++ b/admin/webservice/service_user_settings.php @@ -33,7 +33,7 @@ $userid = required_param('userid', PARAM_INT); admin_externalpage_setup('externalserviceusersettings'); //define nav bar -$PAGE->set_url('/' . $CFG->admin . '/webservice/service_user_settings.php', +$PAGE->set_url('/' . $CFG->admin . '/webservice/service_user_settings.php', array('id' => $serviceid, 'userid' => $userid)); $node = $PAGE->settingsnav->find('externalservices', navigation_node::TYPE_SETTING); if ($node) { @@ -62,7 +62,7 @@ if ($usersettingsform->is_cancelled()) { $serviceuserinfo->id = $serviceuser->serviceuserid; $serviceuserinfo->iprestriction = $settingsformdata->iprestriction; $serviceuserinfo->validuntil = $settingsformdata->validuntil; - + $webservicemanager->update_ws_authorised_user($serviceuserinfo); //TODO: assign capability diff --git a/admin/webservice/testclient.php b/admin/webservice/testclient.php index 3888b01ff07..228c7b7bf21 100644 --- a/admin/webservice/testclient.php +++ b/admin/webservice/testclient.php @@ -37,7 +37,7 @@ $PAGE->set_url('/' . $CFG->admin . '/webservice/testclient.php'); $PAGE->navbar->ignore_active(true); $PAGE->navbar->add(get_string('administrationsite')); $PAGE->navbar->add(get_string('development', 'admin')); -$PAGE->navbar->add(get_string('testclient', 'webservice'), +$PAGE->navbar->add(get_string('testclient', 'webservice'), new moodle_url('/' . $CFG->admin . '/webservice/testclient.php')); if (!empty($function)) { $PAGE->navbar->add($function); @@ -169,4 +169,4 @@ if ($mform->is_cancelled()) { $mform->display(); echo $OUTPUT->footer(); die; -} \ No newline at end of file +} diff --git a/admin/webservice/tokens.php b/admin/webservice/tokens.php index 2d72943f554..ec06e23185f 100644 --- a/admin/webservice/tokens.php +++ b/admin/webservice/tokens.php @@ -94,7 +94,7 @@ switch ($action) { die; break; - case 'delete': + case 'delete': $token = $webservicemanager->get_created_by_user_ws_token($USER->id, $tokenid); //Delete the token diff --git a/backup/moodle2/backup_qtype_plugin.class.php b/backup/moodle2/backup_qtype_plugin.class.php index 603ac9d8918..5d940c55f87 100644 --- a/backup/moodle2/backup_qtype_plugin.class.php +++ b/backup/moodle2/backup_qtype_plugin.class.php @@ -111,8 +111,7 @@ abstract class backup_qtype_plugin extends backup_plugin { // Define the elements $options = new backup_nested_element('numerical_options'); $option = new backup_nested_element('numerical_option', array('id'), array( - 'instructions', 'instructionsformat', 'showunits', 'unitsleft', - 'unitgradingtype', 'unitpenalty')); + 'showunits', 'unitsleft', 'unitgradingtype', 'unitpenalty')); // Build the tree $element->add_child($options); diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index 4ba91207a42..a0acb1900fb 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -174,78 +174,76 @@ abstract class backup_questions_activity_structure_step extends backup_activity_ /** * Attach to $element (usually attempts) the needed backup structures - * for question_states for a given question_attempt + * for question_usages and all the associated data. */ - protected function add_question_attempts_states($element, $questionattemptname) { + protected function add_question_usages($element, $usageidname) { + global $CFG; + require_once($CFG->dirroot . '/question/engine/lib.php'); + // Check $element is one nested_backup_element if (! $element instanceof backup_nested_element) { throw new backup_step_exception('question_states_bad_parent_element', $element); } - // Check that the $questionattemptname is final element in $element - if (! $element->get_final_element($questionattemptname)) { - throw new backup_step_exception('question_states_bad_question_attempt_element', $questionattemptname); + if (! $element->get_final_element($usageidname)) { + throw new backup_step_exception('question_states_bad_question_attempt_element', $usageidname); } - // TODO: Some day we should stop these "encrypted" state->answers and - // TODO: delegate to qtypes plugin to proper XML writting the needed info on each question + $quba = new backup_nested_element('question_usage', array('id'), + array('component', 'preferredbehaviour')); - // TODO: Should be doing here some introspection in the "answer" element, based on qtype, - // TODO: to know which real questions are being used (for randoms and other qtypes...) - // TODO: Not needed if consistency is guaranteed, but it isn't right now :-( + $qas = new backup_nested_element('question_attempts'); + $qa = new backup_nested_element('question_attempt', array('id'), array( + 'slot', 'behaviour', 'questionid', 'maxmark', 'minfraction', + 'flagged', 'questionsummary', 'rightanswer', 'responsesummary', + 'timemodified')); - // Define the elements - $states = new backup_nested_element('states'); - $state = new backup_nested_element('state', array('id'), array( - 'question', 'seq_number', 'answer', 'timestamp', - 'event', 'grade', 'raw_grade', 'penalty')); + $steps = new backup_nested_element('steps'); + $step = new backup_nested_element('step', array('id'), array( + 'sequencenumber', 'state', 'fraction', 'timecreated', 'userid')); + + $response = new backup_nested_element('response'); + $variable = new backup_nested_element('variable', null, array('name', 'value')); // Build the tree - $element->add_child($states); - $states->add_child($state); + $element->add_child($quba); + $quba->add_child($qas); + $qas->add_child($qa); + $qa->add_child($steps); + $steps->add_child($step); + $step->add_child($response); + $response->add_child($variable); // Set the sources - $state->set_source_table('question_states', array('attempt' => '../../' . $questionattemptname)); + $quba->set_source_table('question_usages', + array('id' => '../' . $usageidname)); + $qa->set_source_sql(' + SELECT * + FROM {question_attempts} + WHERE questionusageid = :questionusageid + ORDER BY slot', + array('questionusageid' => backup::VAR_PARENTID)); + $step->set_source_sql(' + SELECT * + FROM {question_attempt_steps} + WHERE questionattemptid = :questionattemptid + ORDER BY sequencenumber', + array('questionattemptid' => backup::VAR_PARENTID)); + $variable->set_source_table('question_attempt_step_data', + array('attemptstepid' => backup::VAR_PARENTID)); // Annotate ids - $state->annotate_ids('question', 'question'); - } - - /** - * Attach to $element (usually attempts) the needed backup structures - * for question_sessions for a given question_attempt - */ - protected function add_question_attempts_sessions($element, $questionattemptname) { - // Check $element is one nested_backup_element - if (! $element instanceof backup_nested_element) { - throw new backup_step_exception('question_sessions_bad_parent_element', $element); - } - // Check that the $questionattemptname is final element in $element - if (! $element->get_final_element($questionattemptname)) { - throw new backup_step_exception('question_sessions_bad_question_attempt_element', $questionattemptname); - } - - // Define the elements - $sessions = new backup_nested_element('sessions'); - $session = new backup_nested_element('session', array('id'), array( - 'questionid', 'newest', 'newgraded', 'sumpenalty', - 'manualcomment', 'manualcommentformat', 'flagged')); - - // Build the tree - $element->add_child($sessions); - $sessions->add_child($session); - - // Set the sources - $session->set_source_table('question_sessions', array('attemptid' => '../../' . $questionattemptname)); - - // Annotate ids - $session->annotate_ids('question', 'questionid'); + $qa->annotate_ids('question', 'questionid'); + $step->annotate_ids('user', 'userid'); // Annotate files - // Note: question_sessions haven't files associated. On purpose manualcomment is lacking - // support for them, so we don't need to annotated them here. + $fileareas = question_engine::get_all_response_file_areas(); + foreach ($fileareas as $filearea) { + $step->annotate_files('question', $filearea, 'id'); + } } } + /** * backup structure step in charge of calculating the categories to be * included in backup, based in the context being backuped (module/course) @@ -1670,19 +1668,25 @@ class backup_questions_structure_step extends backup_structure_step { $question = new backup_nested_element('question', array('id'), array( 'parent', 'name', 'questiontext', 'questiontextformat', - 'generalfeedback', 'generalfeedbackformat', 'defaultgrade', 'penalty', + 'generalfeedback', 'generalfeedbackformat', 'defaultmark', 'penalty', 'qtype', 'length', 'stamp', 'version', 'hidden', 'timecreated', 'timemodified', 'createdby', 'modifiedby')); // attach qtype plugin structure to $question element, only one allowed $this->add_plugin_structure('qtype', $question, false); + $qhints = new backup_nested_element('question_hints'); + + $qhint = new backup_nested_element('question_hint', array('id'), array( + 'hint', 'hintformat', 'shownumcorrect', 'clearwrong', 'options')); + // Build the tree $qcategories->add_child($qcategory); $qcategory->add_child($questions); - $questions->add_child($question); + $question->add_child($qhints); + $qhints->add_child($qhint); // Define the sources @@ -1696,6 +1700,13 @@ class backup_questions_structure_step extends backup_structure_step { $question->set_source_table('question', array('category' => backup::VAR_PARENTID)); + $qhint->set_source_sql(' + SELECT * + FROM {question_hints} + WHERE questionid = :questionid + ORDER BY id', + array('questionid' => backup::VAR_PARENTID)); + // don't need to annotate ids nor files // (already done by {@link backup_annotate_all_question_files} diff --git a/backup/moodle2/restore_qtype_plugin.class.php b/backup/moodle2/restore_qtype_plugin.class.php index fb97bebbc95..97d8b85b6b6 100644 --- a/backup/moodle2/restore_qtype_plugin.class.php +++ b/backup/moodle2/restore_qtype_plugin.class.php @@ -115,6 +115,23 @@ abstract class restore_qtype_plugin extends restore_plugin { $newquestionid = $this->get_new_parentid('question'); $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false; + // In the past, there were some sloppily rounded fractions around. Fix them up. + $changes = array( + '-0.66666' => '-0.6666667', + '-0.33333' => '-0.3333333', + '-0.16666' => '-0.1666667', + '-0.142857' => '-0.1428571', + '0.11111' => '0.1111111', + '0.142857' => '0.1428571', + '0.16666' => '0.1666667', + '0.33333' => '0.3333333', + '0.333333' => '0.3333333', + '0.66666' => '0.6666667', + ); + if (array_key_exists($data->fraction, $changes)) { + $data->fraction = $changes[$data->fraction]; + } + // If the question has been created by restore, we need to create its question_answers too if ($questioncreated) { // Adjust some columns @@ -298,11 +315,14 @@ abstract class restore_qtype_plugin extends restore_plugin { } /** - * Decode one question_states for this qtype (default impl) + * Do any re-coding necessary in the student response. + * @param int $questionid the new id of the question + * @param int $sequencenumber of the step within the qusetion attempt. + * @param array the response data from the backup. + * @return array the recoded response. */ - public function recode_state_answer($state) { - // By default, return answer unmodified, qtypes needing recode will override this - return $state->answer; + public function recode_response($questionid, $sequencenumber, array $response) { + return $response; } /** diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index 7c66910ee5f..b1d3ad52535 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -2310,6 +2310,17 @@ class restore_create_categories_and_questions extends restore_structure_step { // we have loaded qcatids there for all parsed questions $data->category = $this->get_mappingid('question_category', $questionmapping->parentitemid); + // In the past, there were some very sloppy values of penalty. Fix them. + if ($data->penalty >= 0.33 && $data->penalty <= 0.34) { + $data->penalty = 0.3333333; + } + if ($data->penalty >= 0.66 && $data->penalty <= 0.67) { + $data->penalty = 0.6666667; + } + if ($data->penalty >= 1) { + $data->penalty = 1; + } + $data->timecreated = $this->apply_date_offset($data->timecreated); $data->timemodified = $this->apply_date_offset($data->timemodified); @@ -2339,6 +2350,47 @@ class restore_create_categories_and_questions extends restore_structure_step { // step will be in charge of restoring all the question files } + protected function process_question_hint($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + + // Detect if the question is created or mapped + $oldquestionid = $this->get_old_parentid('question'); + $newquestionid = $this->get_new_parentid('question'); + $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false; + + // If the question has been created by restore, we need to create its question_answers too + if ($questioncreated) { + // Adjust some columns + $data->questionid = $newquestionid; + // Insert record + $newitemid = $DB->insert_record('question_answers', $data); + + // The question existed, we need to map the existing question_answers + } else { + // Look in question_answers by answertext matching + $sql = 'SELECT id + FROM {question_hints} + WHERE questionid = ? + AND ' . $DB->sql_compare_text('hint', 255) . ' = ' . $DB->sql_compare_text('?', 255); + $params = array($newquestionid, $data->hint); + $newitemid = $DB->get_field_sql($sql, $params); + // If we haven't found the newitemid, something has gone really wrong, question in DB + // is missing answers, exception + if (!$newitemid) { + $info = new stdClass(); + $info->filequestionid = $oldquestionid; + $info->dbquestionid = $newquestionid; + $info->hint = $data->hint; + throw new restore_step_exception('error_question_hint_missing_in_db', $info); + } + } + // Create mapping (we'll use this intensively when restoring question_states. And also answerfeedback files) + $this->set_mapping('question_hint', $oldid, $newitemid); + } + protected function after_execute() { global $DB; @@ -2461,6 +2513,8 @@ class restore_create_question_files extends restore_execution_step { $oldctxid, $this->task->get_userid(), 'question_created', $question->itemid, $newctxid, true); restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'answerfeedback', $oldctxid, $this->task->get_userid(), 'question_answer', null, $newctxid, true); + restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'hint', + $oldctxid, $this->task->get_userid(), 'question_hint', null, $newctxid, true); // Add qtype dependent files $components = backup_qtype_plugin::get_components_and_fileareas($question->qtype); foreach ($components as $component => $fileareas) { @@ -2481,12 +2535,16 @@ class restore_create_question_files extends restore_execution_step { * (like the quiz module), to support qtype plugins, states and sessions */ abstract class restore_questions_activity_structure_step extends restore_activity_structure_step { + /** @var array question_attempt->id to qtype. */ + protected $qtypes = array(); + /** @var array question_attempt->id to questionid. */ + protected $newquestionids = array(); /** * Attach below $element (usually attempts) the needed restore_path_elements - * to restore question_states + * to restore question_usages and all they contain. */ - protected function add_question_attempts_states($element, &$paths) { + protected function add_question_usages($element, &$paths) { // Check $element is restore_path_element if (! $element instanceof restore_path_element) { throw new restore_step_exception('element_must_be_restore_path_element', $element); @@ -2495,70 +2553,114 @@ abstract class restore_questions_activity_structure_step extends restore_activit if (!is_array($paths)) { throw new restore_step_exception('paths_must_be_array', $paths); } - $paths[] = new restore_path_element('question_state', $element->get_path() . '/states/state'); + $paths[] = new restore_path_element('question_usage', + $element->get_path() . '/question_usage'); + $paths[] = new restore_path_element('question_attempt', + $element->get_path() . '/question_usage/question_attempts/question_attempt'); + $paths[] = new restore_path_element('question_attempt_step', + $element->get_path() . '/question_usage/question_attempts/question_attempt/steps/step', + true); + $paths[] = new restore_path_element('question_attempt_step_data', + $element->get_path() . '/question_usage/question_attempts/question_attempt/steps/step/response/variable'); + + // TODO Put back code for restoring legacy 2.0 backups. + // $paths[] = new restore_path_element('question_state', $element->get_path() . '/states/state'); + // $paths[] = new restore_path_element('question_session', $element->get_path() . '/sessions/session'); } /** - * Attach below $element (usually attempts) the needed restore_path_elements - * to restore question_sessions + * Process question_usages */ - protected function add_question_attempts_sessions($element, &$paths) { - // Check $element is restore_path_element - if (! $element instanceof restore_path_element) { - throw new restore_step_exception('element_must_be_restore_path_element', $element); - } - // Check $paths is one array - if (!is_array($paths)) { - throw new restore_step_exception('paths_must_be_array', $paths); - } - $paths[] = new restore_path_element('question_session', $element->get_path() . '/sessions/session'); - } - - /** - * Process question_states - */ - protected function process_question_state($data) { + protected function process_question_usage($data) { global $DB; + // Clear our caches. + $this->qtypes = array(); + $this->newquestionids = array(); + $data = (object)$data; $oldid = $data->id; - // Get complete question mapping, we'll need info - $question = $this->get_mapping('question', $data->question); - - // In the quiz_attempt mapping we are storing uniqueid - // and not id, so this gets the correct question_attempt to point to - $data->attempt = $this->get_new_parentid('quiz_attempt'); - $data->question = $question->newitemid; - $data->answer = $this->restore_recode_answer($data, $question->info->qtype); // Delegate recoding of answer - $data->timestamp= $this->apply_date_offset($data->timestamp); - - // Everything ready, insert and create mapping (needed by question_sessions) - $newitemid = $DB->insert_record('question_states', $data); - $this->set_mapping('question_state', $oldid, $newitemid); - } - - /** - * Process question_sessions - */ - protected function process_question_session($data) { - global $DB; - - $data = (object)$data; - $oldid = $data->id; - - // In the quiz_attempt mapping we are storing uniqueid - // and not id, so this gets the correct question_attempt to point to - $data->attemptid = $this->get_new_parentid('quiz_attempt'); - $data->questionid = $this->get_mappingid('question', $data->questionid); - $data->newest = $this->get_mappingid('question_state', $data->newest); - $data->newgraded = $this->get_mappingid('question_state', $data->newgraded); + $oldcontextid = $this->get_task()->get_old_contextid(); + $data->contextid = $this->get_mappingid('context', $this->task->get_old_contextid()); // Everything ready, insert (no mapping needed) - $newitemid = $DB->insert_record('question_sessions', $data); + $newitemid = $DB->insert_record('question_usages', $data); - // Note: question_sessions haven't files associated. On purpose manualcomment is lacking - // support for them, so we don't need to handle them here. + $this->inform_new_usage_id($newitemid); + + $this->set_mapping('question_usage', $oldid, $newitemid, false); + } + + /** + * When process_question_usage creates the new usage, it calls this method + * to let the activity link to the new usage. For example, the quiz uses + * this method to set quiz_attempts.uniqueid to the new usage id. + * @param integer $newusageid + */ + abstract protected function inform_new_usage_id($newusageid); + + /** + * Process question_attempts + */ + protected function process_question_attempt($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + $question = $this->get_mapping('question', $data->questionid); + + $data->questionusageid = $this->get_new_parentid('question_usage'); + $data->questionid = $question->newitemid; + $data->timemodified = $this->apply_date_offset($data->timemodified); + + $newitemid = $DB->insert_record('question_attempts', $data); + + $this->set_mapping('question_attempt', $oldid, $newitemid); + $this->qtypes[$newitemid] = $question->info->qtype; + $this->newquestionids[$newitemid] = $data->questionid; + } + + /** + * Process question_attempt_steps + */ + protected function process_question_attempt_step($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + + // Pull out the response data. + $response = array(); + if (!empty($data->response['variable'])) { + foreach ($data->response['variable'] as $variable) { + $response[$variable['name']] = $variable['value']; + } + } + unset($data->response); + + $data->questionattemptid = $this->get_new_parentid('question_attempt'); + $data->timecreated = $this->apply_date_offset($data->timecreated); + $data->userid = $this->get_mappingid('user', $data->userid); + + // Everything ready, insert and create mapping (needed by question_sessions) + $newitemid = $DB->insert_record('question_attempt_steps', $data); + $this->set_mapping('question_attempt_step', $oldid, $newitemid, true); + + // Now process the response data. + $qtyperestorer = $this->get_qtype_restorer($this->qtypes[$data->questionattemptid]); + if ($qtyperestorer) { + $response = $qtyperestorer->recode_response( + $this->newquestionids[$data->questionattemptid], + $data->sequencenumber, $response); + } + foreach ($response as $name => $value) { + $row = new stdClass(); + $row->attemptstepid = $newitemid; + $row->name = $name; + $row->value = $value; + $DB->insert_record('question_attempt_step_data', $row, false); + } } /** @@ -2581,25 +2683,33 @@ abstract class restore_questions_activity_structure_step extends restore_activit } /** - * Given one question_states record, return the answer - * recoded pointing to all the restored stuff + * Get the restore_qtype_plugin subclass for a specific question type. + * @param string $qtype e.g. multichoice. + * @return restore_qtype_plugin instance. */ - public function restore_recode_answer($state, $qtype) { + protected function get_qtype_restorer($qtype) { // Build one static cache to store {@link restore_qtype_plugin} // while we are needing them, just to save zillions of instantiations // or using static stuff that will break our nice API static $qtypeplugins = array(); - // If we haven't the corresponding restore_qtype_plugin for current qtype - // instantiate it and add to cache if (!isset($qtypeplugins[$qtype])) { $classname = 'restore_qtype_' . $qtype . '_plugin'; if (class_exists($classname)) { $qtypeplugins[$qtype] = new $classname('qtype', $qtype, $this); } else { - $qtypeplugins[$qtype] = false; + $qtypeplugins[$qtype] = null; } } - return !empty($qtypeplugins[$qtype]) ? $qtypeplugins[$qtype]->recode_state_answer($state) : $state->answer; + return $qtypeplugins[$qtype]; + } + + protected function after_execute() { + parent::after_execute(); + + // Restore any files belonging to responses. + foreach (question_engine::get_all_response_file_areas() as $filearea) { + $this->add_related_files('question', $filearea, 'question_attempt_step'); + } } } diff --git a/enrol/externallib.php b/enrol/externallib.php index 84cac5389d6..9813ef51788 100644 --- a/enrol/externallib.php +++ b/enrol/externallib.php @@ -43,9 +43,9 @@ class moodle_enrol_external extends external_api { return new external_function_parameters( array( 'courseid' => new external_value(PARAM_INT, 'Course id'), - 'withcapability' => new external_value(PARAM_CAPABILITY, 'User should have this capability'), - 'groupid' => new external_value(PARAM_INT, 'Group id, null means all groups'), - 'onlyactive' => new external_value(PARAM_INT, 'True means only active, false means all participants'), + 'withcapability' => new external_value(PARAM_CAPABILITY, 'User should have this capability', VALUE_DEFAULT, null), + 'groupid' => new external_value(PARAM_INT, 'Group id, null means all groups', VALUE_DEFAULT, null), + 'onlyactive' => new external_value(PARAM_INT, 'True means only active, false means all participants', VALUE_DEFAULT, 0), ) ); } @@ -59,12 +59,17 @@ class moodle_enrol_external extends external_api { * @param bool $onlyactive * @return array of course participants */ - public static function get_enrolled_users($courseid, $withcapability, $groupid, $onlyactive) { - global $DB; + public static function get_enrolled_users($courseid, $withcapability = null, $groupid = null, $onlyactive = false) { + global $DB, $CFG, $USER; // Do basic automatic PARAM checks on incoming data, using params description // If any problems are found then exceptions are thrown with helpful error messages - $params = self::validate_parameters(self::get_enrolled_users_parameters(), array('courseid'=>$courseid, 'withcapability'=>$withcapability, 'groupid'=>$groupid, 'onlyactive'=>$onlyactive)); + $params = self::validate_parameters(self::get_enrolled_users_parameters(), array( + 'courseid'=>$courseid, + 'withcapability'=>$withcapability, + 'groupid'=>$groupid, + 'onlyactive'=>$onlyactive) + ); $coursecontext = get_context_instance(CONTEXT_COURSE, $params['courseid']); if ($courseid == SITEID) { @@ -76,11 +81,10 @@ class moodle_enrol_external extends external_api { try { self::validate_context($context); } catch (Exception $e) { - $exceptionparam = new stdClass(); - $exceptionparam->message = $e->getMessage(); - $exceptionparam->courseid = $params['courseid']; - throw new moodle_exception( - get_string('errorcoursecontextnotvalid' , 'webservice', $exceptionparam)); + $exceptionparam = new stdClass(); + $exceptionparam->message = $e->getMessage(); + $exceptionparam->courseid = $params['courseid']; + throw new moodle_exception(get_string('errorcoursecontextnotvalid' , 'webservice', $exceptionparam)); } if ($courseid == SITEID) { @@ -92,28 +96,46 @@ class moodle_enrol_external extends external_api { if ($withcapability) { require_capability('moodle/role:review', $coursecontext); } - if ($groupid) { - if (groups_is_member($groupid)) { - require_capability('moodle/site:accessallgroups', $coursecontext); - } + if ($groupid && groups_is_member($groupid)) { + require_capability('moodle/site:accessallgroups', $coursecontext); } if ($onlyactive) { require_capability('moodle/course:enrolreview', $coursecontext); } - list($sql, $params) = get_enrolled_sql($coursecontext, $withcapability, $groupid, $onlyactive); - $sql = "SELECT DISTINCT ue.userid, e.courseid + list($sqlparams, $params) = get_enrolled_sql($coursecontext, $withcapability, $groupid, $onlyactive); + $sql = "SELECT ue.userid, e.courseid, u.firstname, u.lastname, u.username, c.id as usercontextid FROM {user_enrolments} ue JOIN {enrol} e ON (e.id = ue.enrolid) - WHERE e.courseid = :courseid AND ue.userid IN ($sql)"; + JOIN {user} u ON (ue.userid = u.id) + JOIN {context} c ON (u.id = c.instanceid AND contextlevel = " . CONTEXT_USER . ") + WHERE e.courseid = :courseid AND ue.userid IN ($sqlparams) + GROUP BY ue.userid, e.courseid, u.firstname, u.lastname, u.username, c.id"; $params['courseid'] = $courseid; - $enrolledusers = $DB->get_records_sql($sql, $params); - $result = array(); + $isadmin = is_siteadmin($USER); + $canviewfullnames = has_capability('moodle/site:viewfullnames', $context); foreach ($enrolledusers as $enrolleduser) { - $result[] = array('courseid' => $enrolleduser->courseid, - 'userid' => $enrolleduser->userid); + $profilimgurl = moodle_url::make_pluginfile_url($enrolleduser->usercontextid, 'user', 'icon', NULL, '/', 'f1'); + $profilimgurlsmall = moodle_url::make_pluginfile_url($enrolleduser->usercontextid, 'user', 'icon', NULL, '/', 'f2'); + $resultuser = array( + 'courseid' => $enrolleduser->courseid, + 'userid' => $enrolleduser->userid, + 'fullname' => fullname($enrolleduser), + 'profileimgurl' => $profilimgurl->out(false), + 'profileimgurlsmall' => $profilimgurlsmall->out(false) + ); + // check if we can return username + if ($isadmin) { + $resultuser['username'] = $enrolleduser->username; + } + // check if we can return first and last name + if ($isadmin or $canviewfullnames) { + $resultuser['firstname'] = $enrolleduser->firstname; + $resultuser['lastname'] = $enrolleduser->lastname; + } + $result[] = $resultuser; } return $result; @@ -129,12 +151,17 @@ class moodle_enrol_external extends external_api { array( 'courseid' => new external_value(PARAM_INT, 'id of course'), 'userid' => new external_value(PARAM_INT, 'id of user'), + 'firstname' => new external_value(PARAM_RAW, 'first name of user', VALUE_OPTIONAL), + 'lastname' => new external_value(PARAM_RAW, 'last name of user', VALUE_OPTIONAL), + 'fullname' => new external_value(PARAM_RAW, 'fullname of user'), + 'username' => new external_value(PARAM_RAW, 'username of user', VALUE_OPTIONAL), + 'profileimgurl' => new external_value(PARAM_URL, 'url of the profile image'), + 'profileimgurlsmall' => new external_value(PARAM_URL, 'url of the profile image (small version)') ) ) ); } - /** * Returns description of method parameters * @return external_function_parameters diff --git a/lang/en/admin.php b/lang/en/admin.php index 09817efec35..a653b4d701d 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -51,7 +51,6 @@ $string['allowobjectembed'] = 'Allow EMBED and OBJECT tags'; $string['allowrenames'] = 'Allow renames'; $string['allowthemechangeonurl'] = 'Allow theme changes in the URL'; $string['allowuserblockhiding'] = 'Allow users to hide blocks'; -$string['allowusermailcharset'] = 'Allow user to select character set'; $string['allowuserswitchrolestheycantassign'] = 'Allow users without the assign roles capability to switch roles'; $string['allowuserthemes'] = 'Allow user themes'; $string['antivirus'] = 'Anti-Virus'; @@ -133,7 +132,6 @@ $string['configallowoverride2'] = 'Select which role(s) can be overridden by eac $string['configallowswitch'] = 'Select which roles a user may switch to, based on which roles they already have. In addition to an entry in this table, a user must also have the moodle/role:switchroles capability to be able to switch.
Note that it is only possible to switch to roles that have the moodle/course:view capability, and that do not have the moodle/site:doanything capability, so some columns in this table are disabled.'; $string['configallowthemechangeonurl'] = 'If enabled, the theme can be changed by adding theme={themename} to any Moodle URL.'; $string['configallowuserblockhiding'] = 'Do you want to allow users to hide/show side blocks throughout this site? This feature uses Javascript and cookies to remember the state of each collapsible block, and only affects the user\'s own view.'; -$string['configallowusermailcharset'] = 'Enabling this, every user in the site will be able to specify his own charset for email.'; $string['configallowuserswitchrolestheycantassign'] = 'By default, moodle/role:assign is required for users to switch roles. Enabling this setting removes this requirement, and results in the roles available in the "Switch role to" dropdown menu being determined by settings in the "Allow role assignments" table only. It is recommended that the settings in the "Allow role assignments" table do not allow users to switch to a role with more capabilities than their existing role.'; $string['configallowuserthemes'] = 'If you enable this, then users will be allowed to set their own themes. User themes override site themes (but not course themes)'; @@ -187,6 +185,7 @@ $string['configdefaultuserroleid'] = 'All logged in users will be given the capa $string['configdeleteincompleteusers'] = 'After this period, old not fully setup accounts are deleted.'; $string['configdeleteunconfirmed'] = 'If you are using email authentication, this is the period within which a response will be accepted from users. After this period, old unconfirmed accounts are deleted.'; $string['configdenyemailaddresses'] = 'To deny email addresses from particular domains list them here in the same way. All other domains will be accepted. To deny subdomains add the domain with a preceding \'.\'. eg hotmail.com yahoo.co.uk .live.com'; +$string['configenabledevicedetection'] = 'Enables detection of mobiles, smartphones, tablets or default devices (desktop PCs, laptops, etc) for the application of themes and other features.'; $string['configdigestmailtime'] = 'People who choose to have emails sent to them in digest form will be emailed the digest daily. This setting controls which time of day the daily mail will be sent (the next cron that runs after this hour will send it).'; $string['configdisableuserimages'] = 'Disable the ability for users to change user profile images.'; $string['configdisplayloginfailures'] = 'This will display information to selected users about previous failed logins.'; @@ -202,6 +201,7 @@ $string['configenablecourserequests'] = 'This will allow any user to request a c $string['configenableglobalsearch'] = 'This setting enables global text searching in resources and activities, it is not compatible with PHP 4.'; $string['configenablegroupmembersonly'] = 'If enabled, access to activities can be restricted to group members only. This may result in an increased server load. In addition, gradebook categories must be set up in a certain way to ensure that activities are hidden from non-group members.'; $string['configenablehtmlpurifier'] = 'Use HTML Purifier instead of KSES for cleaning of untrusted text. HTML Purifier is actively developed and is believed to be more secure, but it is more resource intensive. Expect minor visual differences in the resulting html code. Please note that embed and object tags can not be enabled, MathML tags and old lang tags are not supported.'; +$string['configenablemobilewebservice'] = 'Enable mobile service for the official Moodle app or other app requesting it. For more information, read the {$a}'; $string['configenablerssfeeds'] = 'This switch will enable RSS feeds from across the site. To actually see any change you will need to enable RSS feeds in the individual modules too - go to the Modules settings under Admin Configuration.'; $string['configenablerssfeedsdisabled'] = 'It is not available because RSS feeds are disabled in all the Site. To enable them, go to the Variables settings under Admin Configuration.'; $string['configenablerssfeedsdisabled2'] = 'RSS feeds are disabled at the server level. You need to enable them first in Server/RSS.'; @@ -239,11 +239,6 @@ $string['configintroadmin'] = 'On this page you should configure your main admin $string['configintrosite'] = 'This page allows you to configure the front page and name of this new site. You can come back here later to change these settings any time using the Administration menus.'; $string['configintrotimezones'] = 'This page will search for new information about world timezones (including daylight savings time rules) and update your local database with this information. These locations will be checked, in order: {$a} This procedure is generally very safe and can not break normal installations. Do you wish to update your timezones now?'; $string['configiplookup'] = 'When you click on an IP address (such as 34.12.222.93), such as in the logs, you are shown a map with a best guess of where that IP is located. There are different plugins for this that you can choose from, each has benefits and disadvantages.'; -$string['configjabberhost'] = 'The server to connect to to send jabber message notifications'; -$string['configjabberserver'] = 'XMPP host ID (can be left empty if the same as Jabber host)'; -$string['configjabberusername'] = 'The user name to use when connecting to the Jabber server'; -$string['configjabberpassword'] = 'The password to use when connecting to the Jabber server'; -$string['configjabberport'] = 'The port to use when connecting to the Jabber server'; $string['configkeeptagnamecase'] = 'Check this if you want tag names to keep the original casing as entered by users who created them'; $string['configlang'] = 'Choose a default language for the whole site. Users can override this setting using the language menu or the setting in their personal profile.'; $string['configlangstringcache'] = 'Caches all the language strings into compiled files in the data directory. If you are translating Moodle or changing strings in the Moodle source code then you may want to switch this off. Otherwise leave it on to see performance benefits.'; @@ -255,7 +250,6 @@ $string['configlocale'] = 'Choose a sitewide locale - this will override the for $string['configloginhttps'] = 'Turning this on will make Moodle use a secure https connection just for the login page (providing a secure login), and then afterwards revert back to the normal http URL for general speed. CAUTION: this setting REQUIRES https to be specifically enabled on the web server - if it is not then YOU COULD LOCK YOURSELF OUT OF YOUR SITE.'; $string['configloglifetime'] = 'This specifies the length of time you want to keep logs about user activity. Logs that are older than this age are automatically deleted. It is best to keep logs as long as possible, in case you need them, but if you have a very busy server and are experiencing performance problems, then you may want to lower the log lifetime. Values lower than 30 are not recommended because statistics may not work properly.'; $string['configlookahead'] = 'Days to look ahead'; -$string['configmailnewline'] = 'Newline characters used in mail messages. CRLF is required according to RFC 822bis, some mail servers do automatic conversion from LF to CRLF, other mail servers do incorrect conversion from CRLF to CRCRLF, yet others reject mails with bare LF (qmail for example). Try changing this setting if you are having problems with undelivered emails or double newlines.'; $string['configmaxbytes'] = 'This specifies a maximum size that uploaded files can be throughout the whole site. This setting is limited by the PHP settings post_max_size and upload_max_filesize, as well as the Apache setting LimitRequestBody. In turn, maxbytes limits the range of sizes that can be chosen at course level or module level. If \'Server Limit\' is chosen, the server maximum allowed by the server will be used.'; $string['configmaxconsecutiveidentchars'] = 'Passwords must not have more than this number of consecutive identical characters. Use 0 to disable this check.'; $string['configmaxeditingtime'] = 'This specifies the amount of time people have to re-edit forum postings, glossary comments etc. Usually 30 minutes is a good value.'; @@ -276,7 +270,6 @@ $string['configmypagelocked'] = 'This setting prevents the default page from bei $string['confignavcourselimit'] = 'Limits the number of courses shown to the user when they are either not logged in or are not enrolled in any courses.'; $string['confignavshowallcourses'] = 'Setting this ensures that all courses on the site are shown in the navigation at all times.'; $string['confignavshowcategories'] = 'Show course categories in the navigation bar and navigation blocks. This does not occur with courses the user is currently enrolled in, they will still be listed under mycourses without categories.'; -$string['confignoreplyaddress'] = 'Emails are sometimes sent out on behalf of a user (eg forum posts). The email address you specify here will be used as the "From" address in those cases when the recipients should not be able to reply directly to the user (eg when a user chooses to keep their address private).'; $string['confignotifyloginfailures'] = 'If login failures have been recorded, email notifications can be sent out. Who should see these notifications?'; $string['confignotifyloginthreshold'] = 'If notifications about failed logins are active, how many failed login attempts by one user or one IP address is it worth notifying about?'; $string['confignotloggedinroleid'] = 'Users who are not logged in to the site will be treated as if they have this role granted to them at the site context. Guest is almost always what you want here, but you might want to create roles that are less or more restrictive. Things like creating posts still require the user to log in properly.'; @@ -329,14 +322,10 @@ $string['configshowcommentscount'] = 'Show comments count, it will cost one more $string['configshowsiteparticipantslist'] = 'All of these site students and site teachers will be listed on the site participants list. Who shall be allowed to see this site participants list?'; $string['configsitedefaultlicense'] = 'Default site licence'; $string['configsitedefaultlicensehelp'] = 'The default licence for publishing content on this site'; -$string['configsitemailcharset'] = 'All the emails generated by your site will be sent in the charset specified here. Anyway, every individual user will be able to adjust it if the next setting is enabled.'; $string['configsitemaxcategorydepth'] = 'Maximum category depth'; $string['configsitemaxcategorydepthhelp'] = 'This specifies the maximum depth of child categories shown'; $string['configslasharguments'] = 'Files (images, uploads etc) are provided via a script using \'slash arguments\'. This method allows files to be more easily cached in web browsers, proxy servers etc. Unfortunately, some PHP servers don\'t allow this method, so if you have trouble viewing uploaded files or images (eg user pictures), disable this setting.'; $string['configsmartpix'] = 'With this on, icons are served through a PHP script that searches the current theme, then all parent themes, then the Moodle /pix folder. This reduces the need to duplicate image files within themes, but has a slight performance cost.'; -$string['configsmtphosts'] = 'Give the full name of one or more local SMTP servers that Moodle should use to send mail (eg \'mail.a.com\' or \'mail.a.com;mail.b.com\'). To specify a non-default port (i.e other than port 25), you can use the [server]:[port] syntax (eg \'mail.a.com:587\'. If you leave it blank, Moodle will use the PHP default method of sending mail.'; -$string['configsmtpmaxbulk'] = 'Maximum number of messages sent per SMTP session. Grouping messages may speed up the sending of emails. Values lower than 2 force creation of new SMTP session for each email.'; -$string['configsmtpuser'] = 'If you have specified an SMTP server above, and the server requires authentication, then enter the username and password here.'; $string['configstartwday'] = 'Start of week'; $string['configstatsfirstrun'] = 'This specifies how far back the logs should be processed the first time the cronjob wants to process statistics. If you have a lot of traffic and are on shared hosting, it\'s probably not a good idea to go too far back, as it could take a long time to run and be quite resource intensive. (Note that for this setting, 1 month = 28 days. In the graphs and reports generated, 1 month = 1 calendar month.)'; $string['configstatsmaxruntime'] = 'Stats processing can be quite intensive, so use a combination of this field and the next one to specify when it will run and how long for.'; @@ -450,6 +439,12 @@ $string['deletingqtype'] = 'Deleting question type \'{$a}\''; $string['density'] = 'Density'; $string['denyemailaddresses'] = 'Denied email domains'; $string['development'] = 'Development'; +$string['devicedetectregex'] = 'Device detection regular expressions'; +$string['devicedetectregex_desc'] = '

By default, Moodle can detect devices of the type default (desktop PCs, laptops, etc), mobile (phones and small hand held devices), tablet (iPads, Android tablets) and legacy (Internet Explorer 6 users). The theme selector can be used to apply separate themes to all of these. This setting allows regular expressions that allow the detection of extra device types (these take precedence over the default types).

+

For example, you could enter the regular expression \'/(MIDP-1.0|Maemo|Windows CE)/\' to detect some commonly used feasture phones add the return value \'featurephone\'. This adds \'featurephone\' on the theme selector that would allow you to add a theme that would be used on these devices. Other phones would still use the theme selected for the mobile device type.

'; +$string['devicedetectregexexpression'] = 'Regular expression'; +$string['devicedetectregexvalue'] = 'Return value'; +$string['devicetype'] = 'Device type'; $string['digestmailtime'] = 'Hour to send digest emails'; $string['disableuserimages'] = 'Disable user profile images'; $string['displayerrorswarning'] = 'Enabling the PHP setting display_errors is not recommended on production sites because some error messages may reveal sensitive information about your server.'; @@ -491,9 +486,11 @@ $string['enablecomments'] = 'Enable comments'; $string['enablecourseajax'] = 'Enable AJAX course editing'; $string['enablecourseajax_desc'] = 'Allow AJAX when editing main course pages. Note that the course format and the theme must support AJAX editing and the user has to enable AJAX in their profiles, too.'; $string['enablecourserequests'] = 'Enable course requests'; +$string['enabledevicedetection'] = 'Enable device detection'; $string['enableglobalsearch'] = 'Enable global search'; $string['enablegroupmembersonly'] = 'Enable group members only'; $string['enablehtmlpurifier'] = 'Enable HTML Purifier'; +$string['enablemobilewebservice'] = 'Enable mobile web service'; $string['enablerecordcache'] = 'Enable record cache'; $string['enablerssfeeds'] = 'Enable RSS feeds'; $string['enablesafebrowserintegration'] = 'Enable Safe Exam Browser integration'; @@ -623,12 +620,6 @@ It is recommended to install local copy of free GeoLite City database from MaxMi IP address location is displayed on simple map or using Google Maps. Please note that you need to have a Google account and apply for free Google Maps API key to enable interactive maps.'; $string['iplookupmaxmindnote'] = 'This product includes GeoLite data created by MaxMind, available from http://www.maxmind.com/.'; $string['iplookupnetgeonote'] = 'The NetGeo server is currently being used to look up geographical information. For more accurate results we recommend installing a local copy of the MaxMind GeoLite database.'; -$string['jabber'] = 'Jabber'; -$string['jabberhost'] = 'Jabber host'; -$string['jabberserver'] = 'Jabber server'; -$string['jabberusername'] = 'Jabber user name'; -$string['jabberpassword'] = 'Jabber password'; -$string['jabberport'] = 'Jabber port'; $string['keeptagnamecase'] = 'Keep tag name casing'; $string['lang'] = 'Default language'; $string['langcache'] = 'Cache language menu'; @@ -674,8 +665,6 @@ $string['logguests_help'] = 'This setting enables logging of actions by guest ac $string['loginhttps'] = 'Use HTTPS for logins'; $string['loglifetime'] = 'Keep logs for'; $string['longtimewarning'] = 'Please note that this process can take a long time.'; -$string['mail'] = 'Email'; -$string['mailnewline'] = 'Newline characters in mail'; $string['maintenancemode'] = 'In maintenance mode'; $string['maintfileopenerror'] = 'Error opening maintenance files!'; $string['maintinprogress'] = 'Maintenance is in progress...'; @@ -752,11 +741,11 @@ $string['neverdeleteruns'] = 'Never delete runs'; $string['nobookmarksforuser'] = 'You do not have any bookmarks.'; $string['nodatabase'] = 'No database'; $string['nochanges'] = 'No changes'; +$string['nohttpsformobilewarning'] = 'It is recommended to enable HTTPS with a valid certificate. The Moodle app will always try to use a secured connection first.'; $string['nolangupdateneeded'] = 'All your language packs are up to date, no update is needed'; $string['nomissingstrings'] = 'No missing strings'; $string['nonewsettings'] = 'No new settings were added during this upgrade.'; $string['nonexistentbookmark'] = 'The bookmark you requested does not exist.'; -$string['noreplyaddress'] = 'No-reply address'; $string['noresults'] = 'No results found.'; $string['noroles'] = 'No roles'; $string['notifications'] = 'Notifications'; @@ -948,7 +937,6 @@ $string['showdetails'] = 'Show details'; $string['simpletest'] = 'Unit tests'; $string['simplexmlrequired'] = 'The SimpleXML PHP extension is now required by Moodle.'; $string['sitelangchanged'] = 'Site language setting changed successfully'; -$string['sitemailcharset'] = 'Character set'; $string['sitemaintenance'] = 'The site is undergoing maintenance and is currently not available'; $string['sitemaintenancemode'] = 'Maintenance mode'; $string['sitemaintenanceoff'] = 'Maintenance mode has been disabled and the site is running normally again'; @@ -963,10 +951,6 @@ $string['sitepolicyguest_help'] = 'If you have a site policy that all guests mus $string['sitesectionhelp'] = 'If selected, a topic section will be displayed on the site\'s front page.'; $string['slasharguments'] = 'Use slash arguments'; $string['smartpix'] = 'Smart pix search'; -$string['smtphosts'] = 'SMTP hosts'; -$string['smtpmaxbulk'] = 'SMTP session limit'; -$string['smtppass'] = 'SMTP password'; -$string['smtpuser'] = 'SMTP username'; $string['soaprecommended'] = 'Installing the optional soap extension is useful for web services and some contrib modules.'; $string['spellengine'] = 'Spell engine'; $string['splrequired'] = 'The SPL PHP extension is now required by Moodle.'; @@ -982,6 +966,7 @@ $string['stickyblocksduplicatenotice'] = 'If any block you add here is already p $string['stickyblocksmymoodle'] = 'My Moodle'; $string['stickyblockspagetype'] = 'Page type to configure'; $string['stripalltitletags'] = 'Remove HTML tags from all activity names'; +$string['supportcontact'] = 'Support contact'; $string['supportemail'] = 'Support email'; $string['supportname'] = 'Support name'; $string['supportpage'] = 'Support page'; @@ -993,7 +978,9 @@ $string['tabselectedtofront'] = 'On tables with tabs, should the row with the cu $string['tabselectedtofronttext'] = 'Bring selected tab row to front'; $string['themedesignermode'] = 'Theme designer mode'; $string['themelist'] = 'Theme list'; +$string['themenoselected'] = 'No theme selected'; $string['themeresetcaches'] = 'Clear theme caches'; +$string['themeselect'] = 'Select theme'; $string['themeselector'] = 'Theme selector'; $string['themesettings'] = 'Theme settings'; $string['therewereerrors'] = 'There were errors in your data'; diff --git a/lang/en/error.php b/lang/en/error.php index 20004e0a6fe..2bdeea6b818 100644 --- a/lang/en/error.php +++ b/lang/en/error.php @@ -277,6 +277,7 @@ $string['invalidcoursemodule'] = 'Invalid course module ID'; $string['invalidcoursenameshort'] = 'Invalid short course name'; $string['invaliddata'] = 'Data submitted is invalid'; $string['invaliddatarootpermissions'] = 'Invalid permissions detected in $CFG->dataroot directory, administrator has to fix permissions.'; +$string['invaliddevicetype'] = 'Invalid device type'; $string['invalidelementid'] = 'Incorrect element id!'; $string['invalidentry'] = 'This is not valid entry!'; $string['invalidevent'] = 'Invalid event'; diff --git a/lang/en/message.php b/lang/en/message.php index fa805a3bbf3..9b68be54f36 100644 --- a/lang/en/message.php +++ b/lang/en/message.php @@ -37,19 +37,23 @@ $string['blockcontact'] = 'Block contact'; $string['blockedmessages'] = '{$a} message(s) to/from blocked users'; $string['blockedusers'] = 'Blocked users ({$a})'; $string['blocknoncontacts'] = 'Prevent non-contacts from messaging me'; -$string['cannotsavemessageprefs'] = 'Could not save user messaging preferences'; $string['contactlistempty'] = 'Your contact list is empty'; $string['contacts'] = 'Contacts'; $string['context'] = 'context'; $string['couldnotfindpreference'] = 'Could not load preference {$a}. Does the component and name you supplied to message_send() match a row in message_providers? Message providers must appear in the database so users can configure how they will be notified when they receive messages.'; +$string['defaultmessageoutputs'] = 'Default message outputs'; +$string['defaults'] = 'Defaults'; $string['deletemessagesdays'] = 'Number of days before old messages are automatically deleted'; $string['disabled'] = 'Messaging is disabled on this site'; +$string['disallowed'] = 'Disallowed'; $string['discussion'] = 'Discussion'; $string['editmymessage'] = 'Messaging'; $string['emailmessages'] = 'Email messages when I am offline'; $string['emailtagline'] = 'This is a copy of a message sent to you at "{$a->sitename}". Go to {$a->url} to reply.'; $string['emptysearchstring'] = 'You must search for something'; $string['errorcallingprocessor'] = 'Error calling defined processor'; +$string['errortranslatingdefault'] = 'Error translating default setting provided by plugin, using system defaults instead.'; +$string['forced'] = 'Forced'; $string['formorethan'] = 'For more than'; $string['guestnoeditmessage'] = 'Guest user can not edit messaging options'; $string['guestnoeditmessageother'] = 'Guest user can not edit other user messaging options'; @@ -64,6 +68,8 @@ $string['loggedindescription'] = 'When I\'m logged in'; $string['loggedoff'] = 'Not online'; $string['loggedoffdescription'] = 'When I\'m offline'; $string['managecontacts'] = 'Manage my contacts'; +$string['managemessageoutputs'] = 'Manage message outputs'; +$string['messageoutputs'] = 'Message outputs'; $string['mostrecent'] = 'Recent messages'; $string['mostrecentconversations'] = 'Recent conversations'; $string['mostrecentnotifications'] = 'Recent notifications'; @@ -83,6 +89,7 @@ $string['nomessages'] = 'No messages waiting'; $string['nomessagesfound'] = 'No messages were found'; $string['noreply'] = 'Do not reply to this message'; $string['nosearchresults'] = 'There were no results from your search'; +$string['notpermitted'] = 'Not permitted'; $string['offline'] = 'Offline'; $string['offlinecontacts'] = 'Offline contacts ({$a})'; $string['online'] = 'Online'; @@ -90,7 +97,13 @@ $string['onlinecontacts'] = 'Online contacts ({$a})'; $string['onlyfromme'] = 'Only messages from me'; $string['onlymycourses'] = 'Only in my courses'; $string['onlytome'] = 'Only messages to me'; +$string['outputdisabled'] = 'Output disabled'; +$string['outputdoesnotexist'] = 'Message output does not exists'; +$string['outputenabled'] = 'Output enabled'; +$string['outputnotavailable'] = 'Not available'; +$string['outputnotconfigured'] = 'Not configured'; $string['pagerefreshes'] = 'This page refreshes automatically every {$a} seconds'; +$string['permitted'] = 'Permitted'; $string['private_config'] = 'Popup message window'; $string['processortag'] = 'Destination'; $string['providers_config'] = 'Configure notification methods for incoming messages'; @@ -103,6 +116,8 @@ $string['search'] = 'Search'; $string['searchforperson'] = 'Search for a person'; $string['searchmessages'] = 'Search messages'; $string['searchcombined'] = 'Search people and messages'; +$string['sendingvia'] = 'Sending "{$a->provider}" via "{$a->processor}"'; +$string['sendingviawhen'] = 'Sending "{$a->provider}" via "{$a->processor}" when {$a->state}'; $string['sendmessage'] = 'Send message'; $string['sendmessageto'] = 'Send message to {$a}'; $string['sendmessagetopopup'] = 'Send message to {$a} - new window'; @@ -112,6 +127,7 @@ $string['showmessagewindow'] = 'Popup window on new message'; $string['strftimedaydatetime'] = '%A, %d %B %Y, %I:%M %p'; $string['timenosee'] = 'Minutes since I was last seen online'; $string['timesent'] = 'Time sent'; +$string['touserdoesntexist'] = 'You can not send a message to a user id ({$a}) that doesn\'t exist'; $string['unblockcontact'] = 'Unblock contact'; $string['unreadmessages'] = 'Unread messages ({$a})'; $string['unreadnewmessages'] = 'New messages ({$a})'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 0d6215b3841..8e3fbc93f3c 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -906,7 +906,6 @@ $string['latestlanguagepack'] = 'Check for latest language pack on moodle.org'; $string['layouttable'] = 'Layout table'; $string['leavetokeep'] = 'Leave blank to keep current password'; $string['legacythemeinuse'] = 'This site is being displayed to you in compatibility mode because your browser is too old.'; -$string['legacythemesaved'] = 'New legacy theme saved'; $string['license'] = 'Licence'; $string['licenses'] = 'Licences'; $string['liketologin'] = 'Would you like to log in now with a full user account?'; @@ -1603,6 +1602,8 @@ $string['summary'] = 'Summary'; $string['summary_help'] = 'The idea of a summary is a short text to prepare students for the activities within the topic or week. The text is shown on the course page under the section name.'; $string['summaryof'] = 'Summary of {$a}'; $string['supplyinfo'] = 'More details'; +$string['switchdevicedefault'] = 'Switch to the standard theme'; +$string['switchdevicerecommended'] = 'Switch to the recommended theme for your device'; $string['switchrolereturn'] = 'Return to my normal role'; $string['switchroleto'] = 'Switch role to...'; $string['tag'] = 'Tag'; @@ -1692,8 +1693,6 @@ $string['uploadthisfile'] = 'Upload this file'; $string['url'] = 'URL'; $string['used'] = 'Used'; $string['usedinnplaces'] = 'Used in {$a} places'; -$string['useformaintheme'] = 'Use for modern browsers'; -$string['useforlegacytheme'] = 'Use for old browsers'; $string['usemessageform'] = 'or use the form below to send a message to the selected students'; $string['user'] = 'User'; $string['userconfirmed'] = 'Confirmed {$a}'; @@ -1723,6 +1722,7 @@ $string['userswithfiles'] = 'Users with files'; $string['useruploadtype'] = 'User upload type: {$a}'; $string['userviewingsettings'] = 'Profile settings for {$a}'; $string['userzones'] = 'User zones'; +$string['usetheme'] = 'Use theme'; $string['usingexistingcourse'] = 'Using existing course'; $string['valuealreadyused'] = 'This value has already been used.'; $string['version'] = 'Version'; diff --git a/lang/en/notes.php b/lang/en/notes.php index f8520faf9c5..7dc95e2150f 100644 --- a/lang/en/notes.php +++ b/lang/en/notes.php @@ -37,7 +37,9 @@ $string['deletenotes'] = 'Delete all notes'; $string['editnote'] = 'Edit note'; $string['enablenotes'] = 'Enable notes'; $string['groupaddnewnote'] = 'Add a common note'; +$string['invalidcourseid'] = 'Invalid course id: {$a}'; $string['invalidid'] = 'Invalid note ID specified'; +$string['invaliduserid'] = 'Invalid user id: {$a}'; $string['nocontent'] = 'Note content can not be empty'; $string['nonotes'] = 'There are no notes of this type yet'; $string['nopermissiontodelete'] = 'You may not delete this note'; diff --git a/lang/en/question.php b/lang/en/question.php index e262ae3cc50..71c3460f916 100644 --- a/lang/en/question.php +++ b/lang/en/question.php @@ -23,6 +23,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['addcategory'] = 'Add category'; $string['adminreport'] = 'Report on possible problems in your question database.'; $string['availableq'] = 'Available?'; $string['badbase'] = 'Bad base before **: {$a}**'; @@ -52,7 +53,9 @@ $string['cannotwriteto'] = 'Cannot write exported questions to {$a}'; $string['categorycurrent'] = 'Current category'; $string['categorycurrentuse'] = 'Use this category'; $string['categorydoesnotexist'] = 'This category does not exist'; +$string['categoryinfo'] = 'Category info'; $string['categorymoveto'] = 'Save in category'; +$string['categorynamecantbeblank'] = 'The category name cannot be blank.'; $string['clicktoflag'] = 'Click to flag this question'; $string['clicktounflag'] = 'Click to un-flag this question'; $string['contexterror'] = 'You shouldn\'t have got here if you\'re not moving a category to another context.'; @@ -76,6 +79,8 @@ $string['cwrqpfsnoprob'] = 'No question categories in your site are affected by $string['defaultfor'] = 'Default for {$a}'; $string['defaultinfofor'] = 'The default category for questions shared in context \'{$a}\'.'; $string['deletecoursecategorywithquestions'] = 'There are questions in the question bank associated with this course category. If you proceed, they will be deleted. You may wish to move them first, using the question bank interface.'; +$string['deletequestioncheck'] = 'Are you absolutely sure you want to delete \'{$a}\'?'; +$string['deletequestionscheck'] = 'Are you absolutely sure you want to delete the following questions?

{$a}'; $string['disabled'] = 'Disabled'; $string['disterror'] = 'The distribution {$a} caused problems'; $string['donothing'] = 'Don\'t copy or move files or change links.'; @@ -91,6 +96,7 @@ Each category has a context which determines where the questions in the category Categories are also used for random questions, as questions are selected from a particular category.'; $string['editcategories_link'] = 'question/category'; +$string['editcategory'] = 'Edit category'; $string['editingcategory'] = 'Editing a category'; $string['editingquestion'] = 'Editing a question'; $string['editthiscategory'] = 'Edit this category'; @@ -125,6 +131,7 @@ $string['exportquestions_help'] = 'This function enables the export of a complet $string['exportquestions_link'] = 'question/export'; $string['filecantmovefrom'] = 'The questions files cannot be moved because you do not have permission to remove files from the place you are trying to move questions from.'; $string['filecantmoveto'] = 'The question files cannot be moved or copied becuase you do not have permission to add files to the place you are trying to move the questions to.'; +$string['fileformat'] = 'File format'; $string['filesareacourse'] = 'the course files area'; $string['filesareasite'] = 'the site files area'; $string['filestomove'] = 'Move / copy files to {$a}?'; @@ -136,26 +143,36 @@ $string['getcategoryfromfile'] = 'Get category from file'; $string['getcontextfromfile'] = 'Get context from file'; $string['changepublishstatuscat'] = 'Category "{$a->name}" in course "{$a->coursename}" will have it\'s sharing status changed from {$a->changefrom} to {$a->changeto}.'; $string['chooseqtypetoadd'] = 'Choose a question type to add'; +$string['editquestions'] = 'Edit questions'; $string['ignorebroken'] = 'Ignore broken links'; $string['impossiblechar'] = 'Impossible character {$a} detected as parenthesis character'; $string['importcategory'] = 'Import category'; $string['importcategory_help'] = 'This setting determines the category into which the imported questions will go. Certain import formats, such as GIFT and Moodle XML, may include category and context data in the import file. To make use of this data, rather than the selected category, the appropriate checkboxes should be ticked. If categories specified in the import file do not exist, they will be created.'; +$string['importerror'] = 'An error occurred during import processing'; +$string['importerrorquestion'] = 'Error importing question'; +$string['importingquestions'] = 'Importing {$a} questions from file'; +$string['importparseerror'] = 'Error(s) found parsing the import file. No questions have been imported. To import any good questions try again setting \'Stop on error\' to \'No\''; $string['importquestions'] = 'Import questions from file'; $string['importquestions_help'] = 'This function enables questions in a variety of formats to be imported via text file. Note that the file must use UTF-8 encoding.'; $string['importquestions_link'] = 'question/import'; +$string['importwrongfiletype'] = 'The type of the file you selected ({$a->actualtype}) does not match the type expected by this import format ({$a->expectedtype}).'; $string['invalidarg'] = 'No valid arguments supplied or incorrect server configuration'; $string['invalidcategoryidforparent'] = 'Invalid category id for parent!'; $string['invalidcategoryidtomove'] = 'Invalid category id to move!'; $string['invalidconfirm'] = 'Confirmation string was incorrect'; $string['invalidcontextinhasanyquestions'] = 'Invalid context passed to question_context_has_any_questions.'; +$string['invalidpenalty'] = 'Invalid penalty'; $string['invalidwizardpage'] = 'Incorrect or no wizard page specified!'; $string['lastmodifiedby'] = 'Last modified by'; $string['linkedfiledoesntexist'] = 'Linked file {$a} doesn\'t exist'; $string['makechildof'] = 'Make child of \'{$a}\''; $string['maketoplevelitem'] = 'Move to top level'; +$string['matcherror'] = 'Grades do not match grade options - question skipped'; $string['matchgrades'] = 'Match grades'; +$string['matchgradeserror'] = 'Error if grade not listed'; +$string['matchgradesnearest'] = 'Nearest grade if not listed'; $string['matchgrades_help'] = 'Imported grades must match one of the fixed list of valid grades - 100, 90, 80, 75, 70, 66.666, 60, 50, 40, 33.333, 30, 25, 20, 16.666, 14.2857, 12.5, 11.111, 10, 5, 0 (also negative values). If not, there are two options: * Error if grade not listed - If a question contains any grades not found in the list an error is displayed and that question will not be imported @@ -171,6 +188,7 @@ $string['movedquestionsandcategories'] = 'Moved questions and question categorie $string['movelinksonly'] = 'Just change where links point to, do not move or copy files.'; $string['moveq'] = 'Move question(s)'; $string['moveqtoanothercontext'] = 'Move question to another context.'; +$string['moveto'] = 'Move to >>'; $string['movingcategory'] = 'Moving category'; $string['movingcategoryandfiles'] = 'Are you sure you want to move category {$a->name} and all child categories to context for "{$a->contextto}"?
We have detected {$a->urlcount} files linked from questions in {$a->fromareaname}, would you like to copy or move these to {$a->toareaname}?'; $string['movingcategorynofiles'] = 'Are you sure you want to move category "{$a->name}" and all child categories to context for "{$a->contextto}"?'; @@ -182,6 +200,7 @@ $string['nocate'] = 'No such category {$a}!'; $string['nopermissionadd'] = 'You don\'t have permission to add questions here.'; $string['nopermissionmove'] = 'You don\'t have permission to move questions from here. You must save the question in this category or save it as a new question.'; $string['noprobs'] = 'No problems found in your question database.'; +$string['notenoughanswers'] = 'This type of question requires at least {$a} answers'; $string['notenoughdatatoeditaquestion'] = 'Neither a question id, nor a category id and question type, was specified.'; $string['notenoughdatatomovequestions'] = 'You need to provide the question ids of questions you want to move.'; $string['notflagged'] = 'Not flagged'; @@ -191,6 +210,7 @@ $string['parentcategory_help'] = 'The parent category is the one in which the ne $string['parentcategory_link'] = 'question/category'; $string['parenthesisinproperclose'] = 'Parenthesis before ** is not properly closed in {$a}**'; $string['parenthesisinproperstart'] = 'Parenthesis before ** is not properly started in {$a}**'; +$string['parsingquestions'] = 'Parsing questions from import file.'; $string['penaltyfactor'] = 'Penalty factor'; $string['penaltyfactor_help'] = 'This setting determines what fraction of the achieved score is subtracted for each wrong response. It is only applicable if the quiz is run in adaptive mode. @@ -207,12 +227,16 @@ $string['questioncategory'] = 'Question category'; $string['questioncatsfor'] = 'Question categories for \'{$a}\''; $string['questiondoesnotexist'] = 'This question does not exist'; $string['questionname'] = 'Question name'; +$string['questionno'] = 'Question {$a}'; $string['questionsaveerror'] = 'Errors occur during saving question - ({$a})'; +$string['questionsinuse'] = '(* Questions marked by an asterisk are already in use in some quizzes. These question will not be deleted from these quizzes but only from the category list.)'; $string['questionsmovedto'] = 'Questions still in use moved to "{$a}" in the parent course category.'; $string['questionsrescuedfrom'] = 'Questions saved from context {$a}.'; $string['questionsrescuedfrominfo'] = 'These questions (some of which may be hidden) were saved when context {$a} was deleted because they are still used by some quizzes or other activities.'; $string['questiontype'] = 'Question type'; $string['questionuse'] = 'Use question in this activity'; +$string['questionvariant'] = 'Question variant'; +$string['reviewresponse'] = 'Review response'; $string['saveflags'] = 'Save the state of the flags'; $string['selectacategory'] = 'Select a category:'; $string['selectaqtypefordescription'] = 'Select a question type to see its description.'; @@ -232,3 +256,116 @@ $string['upgradeproblemunknowncategory'] = 'Problem detected when upgrading ques $string['wrongprefix'] = 'Wrongly formatted nameprefix {$a}'; $string['youmustselectaqtype'] = 'You must select a question type.'; $string['yourfileshoulddownload'] = 'Your export file should start to download shortly. If not, please click here.'; + +$string['action'] = 'Action'; +$string['addanotherhint'] = 'Add another hint'; +$string['answer'] = 'Answer'; +$string['answersaved'] = 'Answer saved'; +$string['attemptfinished'] = 'Attempt finished'; +$string['attemptfinishedsubmitting'] = 'Attempt finished submitting: '; +$string['behaviourbeingused'] = 'behaviour being used: {$a}'; +$string['cannotloadquestion'] = 'Could not load question'; +$string['cannotpreview'] = 'You can\'t preview these questions!'; +$string['category'] = 'Category'; +$string['changeoptions'] = 'Change options'; +$string['check'] = 'Check'; +$string['clearwrongparts'] = 'Clear incorrect responses'; +$string['clicktoflag'] = 'Click to flag this question'; +$string['clicktounflag'] = 'Click to un-flag this question'; +$string['closepreview'] = 'Close preview'; +$string['combinedfeedback'] = 'Combined feedback'; +$string['commented'] = 'Commented: {$a}'; +$string['comment'] = 'Comment'; +$string['commentormark'] = 'Make comment or override mark'; +$string['comments'] = 'Comments'; +$string['commentx'] = 'Comment: {$a}'; +$string['complete'] = 'Complete'; +$string['contexterror'] = 'You shouldn\'t have got here if you\'re not moving a category to another context.'; +$string['correct'] = 'Correct'; +$string['correctfeedback'] = 'For any correct response'; +$string['decimalplacesingrades'] = 'Decimal places in grades'; +$string['defaultmark'] = 'Default mark'; +$string['errorsavingflags'] = 'Error saving the flag state.'; +$string['feedback'] = 'Feedback'; +$string['fillincorrect'] = 'Fill in correct responses'; +$string['flagged'] = 'Flagged'; +$string['flagthisquestion'] = 'Flag this question'; +$string['generalfeedback'] = 'General feedback'; +$string['generalfeedback_help'] = 'General feedback is shown to the student after they have attempted the question. Unlike feedback, which depends on the question type and what response the student gave, the same general feedback text is shown to all students. + +You can use the general feedback to give students some background to what knowledge the question was testing, or give them a link to more information they can use if they did not understand the questions.'; +$string['hidden'] = 'Hidden'; +$string['hintn'] = 'Hint {no}'; +$string['hinttext'] = 'Hint text'; +$string['howquestionsbehave'] = 'How questions behave'; +$string['howquestionsbehave_help'] = 'Students can interact with the questions in the quiz in various different ways. For example, you may wish the students to enter an answer to each question and then submit the entire quiz, before anything is graded or they get any feedback. That would be \'Deferred feedback\' mode. Alternatively, you may wish for students to submit each question as they go along to get immediate feedback, and if they do not get it right immediately, have another try for fewer marks. That would be \'Interactive with multiple tries\' mode.'; +$string['importfromcoursefiles'] = '... or choose a course file to import.'; +$string['importfromupload'] = 'Select a file to upload ...'; +$string['includesubcategories'] = 'Also show questions from sub-categories'; +$string['incorrect'] = 'Incorrect'; +$string['incorrectfeedback'] = 'For any incorrect response'; +$string['information'] = 'Information'; +$string['invalidanswer'] = 'Incomplete answer'; +$string['makecopy'] = 'Make copy'; +$string['manualgradeoutofrange'] = 'This grade is outside the valid range.'; +$string['manuallygraded'] = 'Manually graded {$a->mark} with comment: {$a->comment}'; +$string['mark'] = 'Mark'; +$string['markedoutof'] = 'Marked out of'; +$string['markedoutofmax'] = 'Marked out of {$a}'; +$string['markoutofmax'] = 'Mark {$a->mark} out of {$a->max}'; +$string['marks'] = 'Marks'; +$string['noresponse'] = '[No response]'; +$string['notanswered'] = 'Not answered'; +$string['notflagged'] = 'Not flagged'; +$string['notgraded'] = 'Not graded'; +$string['notshown'] = 'Not shown'; +$string['notyetanswered'] = 'Not yet answered'; +$string['notyourpreview'] = 'This preview does not belong to you'; +$string['options'] = 'Options'; +$string['parent'] = 'Parent'; +$string['partiallycorrect'] = 'Partially correct'; +$string['partiallycorrectfeedback'] = 'For any partially correct response'; +$string['penaltyforeachincorrecttry'] = 'Penalty for each incorrect try'; +$string['penaltyforeachincorrecttry_help'] = 'When you run your questions using the \'Interactive with multiple tries\' or \'Adaptive mode\' behaviour, so that the the student will have several tries to get the question right, then this option controls how much they are penalised for each incorrect try. + +The penalty is a proportion of the total question grade, so if the question is worth three marks, and the penalty is 0.3333333, then the student will score 3 if they get the question right first time, 2 if they get it right second try, and 1 of they get it right on the third try.'; +$string['previewquestion'] = 'Preview question: {$a}'; +$string['questionbehaviouradminsetting'] = 'Question behaviour settings'; +$string['questionbehavioursdisabled'] = 'Question behaviours to disable'; +$string['questionbehavioursdisabledexplained'] = 'Enter a comma separated list of behaviours you do not want to appear in dropdown menu'; +$string['questionbehavioursorder'] = 'Question behaviours order'; +$string['questionbehavioursorderexplained'] = 'Enter a comma separated list of behaviours in the order you want them to appear in dropdown menu'; +$string['questionidmismatch'] = 'Question ids mismatch'; +$string['questionname'] = 'Question name'; +$string['questions'] = 'Questions'; +$string['questionx'] = 'Question {$a}'; +$string['questiontext'] = 'Question text'; +$string['requiresgrading'] = 'Requires grading'; +$string['responsehistory'] = 'Response history'; +$string['restart'] = 'Start again'; +$string['restartwiththeseoptions'] = 'Start again with these options'; +$string['rightanswer'] = 'Right answer'; +$string['saved'] = 'Saved: {$a}'; +$string['saveflags'] = 'Save the state of the flags'; +$string['settingsformultipletries'] = 'Settings for multiple tries'; +$string['showhidden'] = 'Also show old questions'; +$string['showmarkandmax'] = 'Show mark and max'; +$string['showmaxmarkonly'] = 'Show max mark only'; +$string['showquestiontext'] = 'Show question text in the question list'; +$string['shown'] = 'Shown'; +$string['shownumpartscorrect'] = 'Show the number of correct responses'; +$string['specificfeedback'] = 'Specific feedback'; +$string['started'] = 'Started'; +$string['state'] = 'State'; +$string['step'] = 'Step'; +$string['submissionoutofsequence'] = 'Access out of sequence. Please do not click the back button when working on quiz questions.'; +$string['submissionoutofsequencefriendlymessage'] = "You have entered data outside the normal sequence. This can occur if you use your browser's Back or Forward buttons; please don't use these during the test. It can also happen if you click on something while a page is loading. Click Continue to resume."; +$string['submit'] = 'Submit'; +$string['submitandfinish'] = 'Submit and finish'; +$string['submitted'] = 'Submit: {$a}'; +$string['unknownquestion'] = 'Unknown question: {$a}.'; +$string['unknownquestioncatregory'] = 'Unknown question category: {$a}.'; +$string['whethercorrect'] = 'Whether correct'; +$string['withselected'] = 'With selected'; +$string['xoutofmax'] = '{$a->mark} out of {$a->max}'; +$string['yougotnright'] = 'You have correctly selected {$a->num}.'; diff --git a/lang/en/webservice.php b/lang/en/webservice.php index f184bc6b046..fc15adf2724 100644 --- a/lang/en/webservice.php +++ b/lang/en/webservice.php @@ -99,6 +99,8 @@ $string['generalstructure'] = 'General structure'; $string['checkusercapability'] = 'Check user capability'; $string['checkusercapabilitydescription'] = 'The user should have appropriate capabilities according to the protocols used, for example webservice/rest:use, webservice/soap:use. To achieve this, create a web services role with protocol capabilities allowed and assign it to the web services user as a system role.'; $string['information'] = 'Information'; +$string['installserviceshortnameerror'] = 'Coding error: the service shortname "{$a}" should have contains numbers, letters and _-.. only.'; +$string['installexistingserviceshortnameerror'] = 'A web service with the shortname "{$a}" already exists. Can not install/update a different web service with this shortname.'; $string['invalidextparam'] = 'Invalid external api parameter: {$a}'; $string['invalidextresponse'] = 'Invalid external api response: {$a}'; $string['invalidiptoken'] = 'Invalid token - your IP is not supported'; @@ -115,6 +117,7 @@ $string['missingcaps'] = 'Missing capabilities'; $string['missingcaps_help'] = 'List of required capabilities for the service which the selected user does not have. Missing capabilities must be added to the user\'s role in order to use the service.'; $string['missingpassword'] = 'Missing password'; $string['missingusername'] = 'Missing username'; +$string['missingversionfile'] = 'Coding error: version.php file is missing for the component {$a}'; $string['nofunctions'] = 'This service has no functions.'; $string['norequiredcapability'] = 'No required capability'; $string['notoken'] = 'The token list is empty.'; diff --git a/lib/adminlib.php b/lib/adminlib.php index 49289ddc1ff..afe282db832 100644 --- a/lib/adminlib.php +++ b/lib/adminlib.php @@ -107,6 +107,7 @@ defined('MOODLE_INTERNAL') || die(); /// Add libraries require_once($CFG->libdir.'/ddllib.php'); require_once($CFG->libdir.'/xmlize.php'); +require_once($CFG->libdir.'/messagelib.php'); define('INSECURE_DATAROOT_WARNING', 1); define('INSECURE_DATAROOT_ERROR', 2); @@ -260,6 +261,14 @@ function uninstall_plugin($type, $name) { // delete the module configuration records unset_all_config_for_plugin($pluginname); + // delete message provider + message_provider_uninstall($component); + + // delete message processor + if ($type === 'message') { + message_processor_uninstall($name); + } + // delete the plugin tables $xmldbfilepath = $plugindirectory . '/db/install.xml'; drop_plugin_tables($pluginname, $xmldbfilepath, false); @@ -3762,6 +3771,35 @@ class admin_setting_special_calendar_weekend extends admin_setting { } +/** + * Admin setting that allows a user to pick a behaviour. + * + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class admin_setting_question_behaviour extends admin_setting_configselect { + /** + * @param string $name name of config variable + * @param string $visiblename display name + * @param string $description description + * @param string $default default. + */ + public function __construct($name, $visiblename, $description, $default) { + parent::__construct($name, $visiblename, $description, $default, NULL); + } + + /** + * Load list of behaviours as choices + * @return bool true => success, false => error. + */ + public function load_choices() { + global $CFG; + require_once($CFG->dirroot . '/question/engine/lib.php'); + $this->choices = question_engine::get_archetypal_behaviours(); + return true; + } +} + + /** * Admin setting that allows a user to pick appropriate roles for something. * @@ -4909,6 +4947,75 @@ class admin_page_manageblocks extends admin_externalpage { } } +/** + * Message outputs configuration + * + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class admin_page_managemessageoutputs extends admin_externalpage { + /** + * Calls parent::__construct with specific arguments + */ + public function __construct() { + global $CFG; + parent::__construct('managemessageoutputs', get_string('managemessageoutputs', 'message'), new moodle_url('/admin/message.php')); + } + + /** + * Search for a specific message processor + * + * @param string $query The string to search for + * @return array + */ + public function search($query) { + global $CFG, $DB; + if ($result = parent::search($query)) { + return $result; + } + + $found = false; + if ($processors = get_message_processors()) { + $textlib = textlib_get_instance(); + foreach ($processors as $processor) { + if (!$processor->available) { + continue; + } + if (strpos($processor->name, $query) !== false) { + $found = true; + break; + } + $strprocessorname = get_string('pluginname', 'message_'.$processor->name); + if (strpos($textlib->strtolower($strprocessorname), $query) !== false) { + $found = true; + break; + } + } + } + if ($found) { + $result = new stdClass(); + $result->page = $this; + $result->settings = array(); + return array($this->name => $result); + } else { + return array(); + } + } +} + +/** + * Default message outputs configuration + * + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class admin_page_defaultmessageoutputs extends admin_page_managemessageoutputs { + /** + * Calls parent::__construct with specific arguments + */ + public function __construct() { + global $CFG; + admin_externalpage::__construct('defaultmessageoutputs', get_string('defaultmessageoutputs', 'message'), new moodle_url('/message/defaultoutputs.php')); + } +} /** * Question type manage page @@ -4925,9 +5032,9 @@ class admin_page_manageqtypes extends admin_externalpage { } /** - * Search QTYPES for the specified string + * Search question types for the specified string * - * @param string $query The string to search for in QTYPES + * @param string $query The string to search for in question types * @return array */ public function search($query) { @@ -4938,9 +5045,8 @@ class admin_page_manageqtypes extends admin_externalpage { $found = false; $textlib = textlib_get_instance(); - require_once($CFG->libdir . '/questionlib.php'); - global $QTYPES; - foreach ($QTYPES as $qtype) { + require_once($CFG->dirroot . '/question/engine/bank.php'); + foreach (question_bank::get_all_qtypes() as $qtype) { if (strpos($textlib->strtolower($qtype->local_name()), $query) !== false) { $found = true; break; @@ -6381,6 +6487,162 @@ class admin_setting_managerepository extends admin_setting { } } +/** + * Special checkbox for enable mobile web service + * If enable then we store the service id of the mobile service into config table + * If disable then we unstore the service id from the config table + */ +class admin_setting_enablemobileservice extends admin_setting_configcheckbox { + + private $xmlrpcuse; //boolean: true => capability 'webservice/xmlrpc:use' is set for authenticated user role + + /** + * Return true if Authenticated user role has the capability 'webservice/xmlrpc:use', otherwise false + * @return boolean + */ + private function is_xmlrpc_cap_allowed() { + global $DB, $CFG; + + //if the $this->xmlrpcuse variable is not set, it needs to be set + if (empty($this->xmlrpcuse) and $this->xmlrpcuse!==false) { + $params = array(); + $params['permission'] = CAP_ALLOW; + $params['roleid'] = $CFG->defaultuserroleid; + $params['capability'] = 'webservice/xmlrpc:use'; + $this->xmlrpcuse = $DB->record_exists('role_capabilities', $params); + } + + return $this->xmlrpcuse; + } + + /** + * Set the 'webservice/xmlrpc:use' to the Authenticated user role (allow or not) + * @param type $status true to allow, false to not set + */ + private function set_xmlrpc_cap($status) { + global $CFG; + if ($status and !$this->is_xmlrpc_cap_allowed()) { + //need to allow the cap + $permission = CAP_ALLOW; + $assign = true; + } else if (!$status and $this->is_xmlrpc_cap_allowed()){ + //need to disallow the cap + $permission = CAP_INHERIT; + $assign = true; + } + if (!empty($assign)) { + $systemcontext = get_system_context(); + assign_capability('webservice/xmlrpc:use', $permission, $CFG->defaultuserroleid, $systemcontext->id, true); + } + } + + /** + * Builds XHTML to display the control. + * The main purpose of this overloading is to display a warning when https + * is not supported by the server + * @param string $data Unused + * @param string $query + * @return string XHTML + */ + public function output_html($data, $query='') { + global $CFG, $OUTPUT; + $html = parent::output_html($data, $query); + + if ((string)$data === $this->yes) { + require_once($CFG->dirroot . "/lib/filelib.php"); + $curl = new curl(); + $httpswwwroot = str_replace('http:', 'https:', $CFG->wwwroot); //force https url + $curl->head($httpswwwroot . "/login/index.php"); + $info = $curl->get_info(); + if (empty($info['http_code']) or ($info['http_code'] >= 400)) { + $html .= $OUTPUT->notification(get_string('nohttpsformobilewarning', 'admin')); + } + } + + return $html; + } + + /** + * Retrieves the current setting using the objects name + * + * @return string + */ + public function get_setting() { + global $CFG; + $webservicesystem = $CFG->enablewebservices; + require_once($CFG->dirroot . '/webservice/lib.php'); + $webservicemanager = new webservice(); + $mobileservice = $webservicemanager->get_external_service_by_shortname(MOODLE_OFFICIAL_MOBILE_SERVICE); + if ($mobileservice->enabled and !empty($webservicesystem) and $this->is_xmlrpc_cap_allowed()) { + return $this->config_read($this->name); //same as returning 1 + } else { + return 0; + } + } + + /** + * Save the selected setting + * + * @param string $data The selected site + * @return string empty string or error message + */ + public function write_setting($data) { + global $DB, $CFG; + $servicename = MOODLE_OFFICIAL_MOBILE_SERVICE; + + require_once($CFG->dirroot . '/webservice/lib.php'); + $webservicemanager = new webservice(); + + if ((string)$data === $this->yes) { + //code run when enable mobile web service + //enable web service systeme if necessary + set_config('enablewebservices', true); + + //enable mobile service + $mobileservice = $webservicemanager->get_external_service_by_shortname(MOODLE_OFFICIAL_MOBILE_SERVICE); + $mobileservice->enabled = 1; + $webservicemanager->update_external_service($mobileservice); + + //enable xml-rpc server + $activeprotocols = empty($CFG->webserviceprotocols) ? array() : explode(',', $CFG->webserviceprotocols); + + if (!in_array('xmlrpc', $activeprotocols)) { + $activeprotocols[] = 'xmlrpc'; + set_config('webserviceprotocols', implode(',', $activeprotocols)); + } + + //allow xml-rpc:use capability for authenticated user + $this->set_xmlrpc_cap(true); + + } else { + //disable web service system if no other services are enabled + $otherenabledservices = $DB->get_records_select('external_services', + 'enabled = :enabled AND (shortname != :shortname OR shortname IS NULL)', array('enabled' => 1, + 'shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE)); + if (empty($otherenabledservices)) { + set_config('enablewebservices', false); + + //also disable xml-rpc server + $activeprotocols = empty($CFG->webserviceprotocols) ? array() : explode(',', $CFG->webserviceprotocols); + $protocolkey = array_search('xmlrpc', $activeprotocols); + if ($protocolkey !== false) { + unset($activeprotocols[$protocolkey]); + set_config('webserviceprotocols', implode(',', $activeprotocols)); + } + + //disallow xml-rpc:use capability for authenticated user + $this->set_xmlrpc_cap(false); + } + + //disable the mobile service + $mobileservice = $webservicemanager->get_external_service_by_shortname(MOODLE_OFFICIAL_MOBILE_SERVICE); + $mobileservice->enabled = 0; + $webservicemanager->update_external_service($mobileservice); + } + + return (parent::write_setting($data)); + } +} /** * Special class for management of external services @@ -7307,9 +7569,199 @@ class admin_setting_configcolourpicker extends admin_setting { $content .= html_writer::end_tag('div'); return format_admin_setting($this, $this->visiblename, $content, $this->description, false, '', $this->get_defaultsetting(), $query); } - } +/** + * Administration interface for user specified regular expressions for device detection. + * + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class admin_setting_devicedetectregex extends admin_setting { + + /** + * Calls parent::__construct with specific args + * + * @param string $name + * @param string $visiblename + * @param string $description + * @param mixed $defaultsetting + */ + public function __construct($name, $visiblename, $description, $defaultsetting = '') { + global $CFG; + parent::__construct($name, $visiblename, $description, $defaultsetting); + } + + /** + * Return the current setting(s) + * + * @return array Current settings array + */ + public function get_setting() { + global $CFG; + + $config = $this->config_read($this->name); + if (is_null($config)) { + return null; + } + + return $this->prepare_form_data($config); + } + + /** + * Save selected settings + * + * @param array $data Array of settings to save + * @return bool + */ + public function write_setting($data) { + if (empty($data)) { + $data = array(); + } + + if ($this->config_write($this->name, $this->process_form_data($data))) { + return ''; // success + } else { + return get_string('errorsetting', 'admin') . $this->visiblename . html_writer::empty_tag('br'); + } + } + + /** + * Return XHTML field(s) for regexes + * + * @param array $data Array of options to set in HTML + * @return string XHTML string for the fields and wrapping div(s) + */ + public function output_html($data, $query='') { + global $OUTPUT; + + $out = html_writer::start_tag('table', array('border' => 1, 'class' => 'generaltable')); + $out .= html_writer::start_tag('thead'); + $out .= html_writer::start_tag('tr'); + $out .= html_writer::tag('th', get_string('devicedetectregexexpression', 'admin')); + $out .= html_writer::tag('th', get_string('devicedetectregexvalue', 'admin')); + $out .= html_writer::end_tag('tr'); + $out .= html_writer::end_tag('thead'); + $out .= html_writer::start_tag('tbody'); + + if (empty($data)) { + $looplimit = 1; + } else { + $looplimit = (count($data)/2)+1; + } + + for ($i=0; $i<$looplimit; $i++) { + $out .= html_writer::start_tag('tr'); + + $expressionname = 'expression'.$i; + + if (!empty($data[$expressionname])){ + $expression = $data[$expressionname]; + } else { + $expression = ''; + } + + $out .= html_writer::tag('td', + html_writer::empty_tag('input', + array( + 'type' => 'text', + 'class' => 'form-text', + 'name' => $this->get_full_name().'[expression'.$i.']', + 'value' => $expression, + ) + ), array('class' => 'c'.$i) + ); + + $valuename = 'value'.$i; + + if (!empty($data[$valuename])){ + $value = $data[$valuename]; + } else { + $value= ''; + } + + $out .= html_writer::tag('td', + html_writer::empty_tag('input', + array( + 'type' => 'text', + 'class' => 'form-text', + 'name' => $this->get_full_name().'[value'.$i.']', + 'value' => $value, + ) + ), array('class' => 'c'.$i) + ); + + $out .= html_writer::end_tag('tr'); + } + + $out .= html_writer::end_tag('tbody'); + $out .= html_writer::end_tag('table'); + + return format_admin_setting($this, $this->visiblename, $out, $this->description, false, '', null, $query); + } + + /** + * Converts the string of regexes + * + * @see self::process_form_data() + * @param $regexes string of regexes + * @return array of form fields and their values + */ + protected function prepare_form_data($regexes) { + + $regexes = json_decode($regexes); + + $form = array(); + + $i = 0; + + foreach ($regexes as $value => $regex) { + $expressionname = 'expression'.$i; + $valuename = 'value'.$i; + + $form[$expressionname] = $regex; + $form[$valuename] = $value; + $i++; + } + + return $form; + } + + /** + * Converts the data from admin settings form into a string of regexes + * + * @see self::prepare_form_data() + * @param array $data array of admin form fields and values + * @return false|string of regexes + */ + protected function process_form_data(array $form) { + + $count = count($form); // number of form field values + + if ($count % 2) { + // we must get five fields per expression + return false; + } + + $regexes = array(); + for ($i = 0; $i < $count / 2; $i++) { + $expressionname = "expression".$i; + $valuename = "value".$i; + + $expression = trim($form['expression'.$i]); + $value = trim($form['value'.$i]); + + if (empty($expression)){ + continue; + } + + $regexes[$value] = $expression; + } + + $regexes = json_encode($regexes); + + return $regexes; + } +} /** * Multiselect for current modules diff --git a/lib/cronlib.php b/lib/cronlib.php index ea7c11507d5..9ccfc3004bd 100644 --- a/lib/cronlib.php +++ b/lib/cronlib.php @@ -131,7 +131,7 @@ function cron_run() { plagiarism_cron(); mtrace("Starting quiz reports"); - if ($reports = $DB->get_records_select('quiz_report', "cron > 0 AND ((? - lastcron) > cron)", array($timenow))) { + if ($reports = $DB->get_records_select('quiz_reports', "cron > 0 AND ((? - lastcron) > cron)", array($timenow))) { foreach ($reports as $report) { $cronfile = "$CFG->dirroot/mod/quiz/report/$report->name/cron.php"; if (file_exists($cronfile)) { @@ -143,7 +143,7 @@ function cron_run() { $pre_dbqueries = $DB->perf_get_queries(); $pre_time = microtime(1); if ($cron_function()) { - $DB->set_field('quiz_report', "lastcron", $timenow, array("id"=>$report->id)); + $DB->set_field('quiz_reports', "lastcron", $timenow, array("id"=>$report->id)); } if (isset($pre_dbqueries)) { mtrace("... used " . ($DB->perf_get_queries() - $pre_dbqueries) . " dbqueries"); diff --git a/lib/db/install.xml b/lib/db/install.xml index 5c1397d2a94..98800d60752 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -1279,9 +1279,9 @@ - - - + + + @@ -1300,7 +1300,7 @@ - +
@@ -1315,16 +1315,93 @@
- +
- - + + + + + + + - + +
- +
+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +
+ @@ -2220,8 +2297,9 @@
- - + + + @@ -2484,7 +2562,8 @@ - + + @@ -2755,4 +2834,4 @@
-
\ No newline at end of file + diff --git a/lib/db/messages.php b/lib/db/messages.php index 11e2d803486..db087fb5f3d 100644 --- a/lib/db/messages.php +++ b/lib/db/messages.php @@ -39,6 +39,10 @@ $messageproviders = array ( ), 'instantmessage' => array ( + 'defaults' => array( + 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF, + 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDOFF, + ), ), 'backup' => array ( diff --git a/lib/db/services.php b/lib/db/services.php index 51cf05bae67..5086f9c1c1f 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -206,4 +206,48 @@ $functions = array( 'capabilities'=> 'moodle/course:create,moodle/course:visibility', ), + // === message related functions === + + 'moodle_message_send_messages' => array( + 'classname' => 'moodle_message_external', + 'methodname' => 'send_messages', + 'classpath' => 'message/externallib.php', + 'description' => 'Send messages', + 'type' => 'write', + 'capabilities'=> 'moodle/site:sendmessage', + ), + + // === notes related functions === + + 'moodle_notes_create_notes' => array( + 'classname' => 'moodle_notes_external', + 'methodname' => 'create_notes', + 'classpath' => 'notes/externallib.php', + 'description' => 'Create notes', + 'type' => 'write', + 'capabilities'=> 'moodle/notes:manage', + ), + + // === webservice related functions === + + 'moodle_webservice_get_siteinfo' => array( + 'classname' => 'moodle_webservice_external', + 'methodname' => 'get_siteinfo', + 'classpath' => 'webservice/externallib.php', + 'description' => 'Return some site info / user info / list web service functions', + 'type' => 'read', + ), + +); + +$services = array( + 'Moodle mobile web service' => array( + 'functions' => array ( + 'moodle_enrol_get_users_courses', + 'moodle_enrol_get_enrolled_users', + 'moodle_user_get_users_by_id'), + 'enabled' => 0, + 'restrictedusers' => 0, + 'shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE + ), ); diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 2918eb1bde4..9f68acdf39b 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -6112,6 +6112,438 @@ WHERE gradeitemid IS NOT NULL AND grademax IS NOT NULL"); upgrade_main_savepoint(true, 2011052300.02); } + // Question engine 2 changes (14) start here + if ($oldversion < 2011060300) { + // Changing the default of field penalty on table question to 0.3333333 + $table = new xmldb_table('question'); + $field = new xmldb_field('penalty'); + $field->set_attributes(XMLDB_TYPE_NUMBER, '12, 7', null, + XMLDB_NOTNULL, null, '0.3333333'); + + // Launch change of default for field penalty + $dbman->change_field_default($table, $field); + + // quiz savepoint reached + upgrade_main_savepoint(true, 2011060300); + } + + if ($oldversion < 2011060301) { + + // Rename field defaultgrade on table question to defaultmark + $table = new xmldb_table('question'); + $field = new xmldb_field('defaultgrade'); + $field->set_attributes(XMLDB_TYPE_NUMBER, '12, 7', null, + XMLDB_NOTNULL, null, '1'); + + // Launch rename field defaultmark + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'defaultmark'); + } + + // quiz savepoint reached + upgrade_main_savepoint(true, 2011060301); + } + + if ($oldversion < 2011060302) { + + // Rename the question_attempts table to question_usages. + $table = new xmldb_table('question_attempts'); + if (!$dbman->table_exists('question_usages')) { + $dbman->rename_table($table, 'question_usages'); + } + + // quiz savepoint reached + upgrade_main_savepoint(true, 2011060302); + } + + if ($oldversion < 2011060303) { + + // Rename the modulename field to component ... + $table = new xmldb_table('question_usages'); + $field = new xmldb_field('modulename'); + $field->set_attributes(XMLDB_TYPE_CHAR, '255', null, + XMLDB_NOTNULL, null, null, 'contextid'); + + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, 'component'); + } + + // ... and update its contents. + $DB->set_field('question_usages', 'component', 'mod_quiz', array('component' => 'quiz')); + + // Add the contextid field. + $field = new xmldb_field('contextid'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + null, null, null, 'id'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + + // And populate it. + $quizmoduleid = $DB->get_field('modules', 'id', array('name' => 'quiz')); + $DB->execute(" + UPDATE {question_usages} SET contextid = ( + SELECT ctx.id + FROM {context} ctx + JOIN {course_modules} cm ON cm.id = ctx.instanceid AND cm.module = $quizmoduleid + JOIN {quiz_attempts} quiza ON quiza.quiz = cm.instance + WHERE ctx.contextlevel = " . CONTEXT_MODULE . " + AND quiza.uniqueid = {question_usages}.id + ) + "); + + // Then make it NOT NULL. + $field = new xmldb_field('contextid'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, null, 'id'); + $dbman->change_field_notnull($table, $field); + } + + // Add the preferredbehaviour column. Populate it with a dummy value + // for now. We will fill in the appropriate behaviour name when + // updating all the rest of the attempt data. + $field = new xmldb_field('preferredbehaviour'); + if (!$dbman->field_exists($table, $field)) { + $field->set_attributes(XMLDB_TYPE_CHAR, '32', null, + XMLDB_NOTNULL, null, 'to_be_set_later', 'component'); + $dbman->add_field($table, $field); + + // Then remove the default value, now the column is populated. + $field = new xmldb_field('preferredbehaviour'); + $field->set_attributes(XMLDB_TYPE_CHAR, '32', null, + XMLDB_NOTNULL, null, null, 'component'); + $dbman->change_field_default($table, $field); + } + + // quiz savepoint reached + upgrade_main_savepoint(true, 2011060303); + } + + if ($oldversion < 2011060304) { + + // Define key contextid (foreign) to be added to question_usages + $table = new xmldb_table('question_usages'); + $key = new XMLDBKey('contextid'); + $key->set_attributes(XMLDB_KEY_FOREIGN, array('contextid'), 'context', array('id')); + + // Launch add key contextid + $dbman->add_key($table, $key); + + // quiz savepoint reached + upgrade_main_savepoint(true, 2011060304); + } + + if ($oldversion < 2011060305) { + + // Changing precision of field component on table question_usages to (255) + // This was missed during the upgrade from old versions. + $table = new xmldb_table('question_usages'); + $field = new xmldb_field('component'); + $field->set_attributes(XMLDB_TYPE_CHAR, '255', null, + XMLDB_NOTNULL, null, null, 'contextid'); + + // Launch change of precision for field component + $dbman->change_field_precision($table, $field); + + // quiz savepoint reached + upgrade_main_savepoint(true, 2011060305); + } + + if ($oldversion < 2011060306) { + + // Define table question_attempts to be created + $table = new xmldb_table('question_attempts'); + if (!$dbman->table_exists($table)) { + + // Adding fields to table question_attempts + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('questionusageid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, null); + $table->add_field('slot', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, null); + $table->add_field('behaviour', XMLDB_TYPE_CHAR, '32', null, + XMLDB_NOTNULL, null, null); + $table->add_field('questionid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, null); + $table->add_field('maxmark', XMLDB_TYPE_NUMBER, '12, 7', null, + XMLDB_NOTNULL, null, null); + $table->add_field('minfraction', XMLDB_TYPE_NUMBER, '12, 7', null, + XMLDB_NOTNULL, null, null); + $table->add_field('flagged', XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0'); + $table->add_field('questionsummary', XMLDB_TYPE_TEXT, 'small', null, + null, null, null); + $table->add_field('rightanswer', XMLDB_TYPE_TEXT, 'small', null, + null, null, null); + $table->add_field('responsesummary', XMLDB_TYPE_TEXT, 'small', null, + null, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, null); + + // Adding keys to table question_attempts + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('questionid', XMLDB_KEY_FOREIGN, array('questionid'), + 'question', array('id')); + $table->add_key('questionusageid', XMLDB_KEY_FOREIGN, array('questionusageid'), + 'question_usages', array('id')); + + // Adding indexes to table question_attempts + $table->add_index('questionusageid-slot', XMLDB_INDEX_UNIQUE, + array('questionusageid', 'slot')); + + // Launch create table for question_attempts + $dbman->create_table($table); + } + + // quiz savepoint reached + upgrade_main_savepoint(true, 2011060306); + } + + if ($oldversion < 2011060307) { + + // Define table question_attempt_steps to be created + $table = new xmldb_table('question_attempt_steps'); + if (!$dbman->table_exists($table)) { + + // Adding fields to table question_attempt_steps + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('questionattemptid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, null); + $table->add_field('sequencenumber', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, null); + $table->add_field('state', XMLDB_TYPE_CHAR, '13', null, + XMLDB_NOTNULL, null, null); + $table->add_field('fraction', XMLDB_TYPE_NUMBER, '12, 7', null, + null, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, null); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + null, null, null); + + // Adding keys to table question_attempt_steps + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('questionattemptid', XMLDB_KEY_FOREIGN, + array('questionattemptid'), 'question_attempts_new', array('id')); + $table->add_key('userid', XMLDB_KEY_FOREIGN, array('userid'), + 'user', array('id')); + + // Adding indexes to table question_attempt_steps + $table->add_index('questionattemptid-sequencenumber', XMLDB_INDEX_UNIQUE, + array('questionattemptid', 'sequencenumber')); + + // Launch create table for question_attempt_steps + $dbman->create_table($table); + } + + // quiz savepoint reached + upgrade_main_savepoint(true, 2011060307); + } + + if ($oldversion < 2011060308) { + + // Define table question_attempt_step_data to be created + $table = new xmldb_table('question_attempt_step_data'); + if (!$dbman->table_exists($table)) { + + // Adding fields to table question_attempt_step_data + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('attemptstepid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, null); + $table->add_field('name', XMLDB_TYPE_CHAR, '32', null, + XMLDB_NOTNULL, null, null); + $table->add_field('value', XMLDB_TYPE_TEXT, 'small', null, + null, null, null); + + // Adding keys to table question_attempt_step_data + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('attemptstepid', XMLDB_KEY_FOREIGN, array('attemptstepid'), + 'question_attempt_steps', array('id')); + + // Adding indexes to table question_attempt_step_data + $table->add_index('attemptstepid-name', XMLDB_INDEX_UNIQUE, + array('attemptstepid', 'name')); + + // Launch create table for question_attempt_step_data + $dbman->create_table($table); + } + + // quiz savepoint reached + upgrade_main_savepoint(true, 2011060308); + } + + if ($oldversion < 2011060309) { + + // Define table question_hints to be created + $table = new xmldb_table('question_hints'); + + // Adding fields to table question_hints + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('questionid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, null); + $table->add_field('hint', XMLDB_TYPE_TEXT, 'small', null, + XMLDB_NOTNULL, null, null); + $table->add_field('hintformat', XMLDB_TYPE_INTEGER, '4', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0'); + $table->add_field('shownumcorrect', XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, + null, null, null); + $table->add_field('clearwrong', XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, + null, null, null); + $table->add_field('options', XMLDB_TYPE_CHAR, '255', null, + null, null, null); + + // Adding keys to table question_hints + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('questionid', XMLDB_KEY_FOREIGN, array('questionid'), + 'question', array('id')); + + // Conditionally launch create table for question_hints + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // quiz savepoint reached + upgrade_main_savepoint(true, 2011060309); + } + + if ($oldversion < 2011060310) { + + // In the past, question_answer fractions were stored with rather + // sloppy rounding. Now update them to the new standard of 7 d.p. + $changes = array( + '-0.66666' => '-0.6666667', + '-0.33333' => '-0.3333333', + '-0.16666' => '-0.1666667', + '-0.142857' => '-0.1428571', + '0.11111' => '0.1111111', + '0.142857' => '0.1428571', + '0.16666' => '0.1666667', + '0.33333' => '0.3333333', + '0.333333' => '0.3333333', + '0.66666' => '0.6666667', + ); + foreach ($changes as $from => $to) { + $DB->set_field('question_answers', + 'fraction', $to, array('fraction' => $from)); + } + + // quiz savepoint reached + upgrade_main_savepoint(true, 2011060310); + } + + if ($oldversion < 2011060311) { + + // In the past, question penalties were stored with rather + // sloppy rounding. Now update them to the new standard of 7 d.p. + $DB->set_field('question', + 'penalty', 0.3333333, array('penalty' => 33.3)); + $DB->set_field_select('question', + 'penalty', 0.3333333, 'penalty >= 0.33 AND penalty <= 0.34'); + $DB->set_field_select('question', + 'penalty', 0.6666667, 'penalty >= 0.66 AND penalty <= 0.67'); + $DB->set_field_select('question', + 'penalty', 1, 'penalty > 1'); + + // quiz savepoint reached + upgrade_main_savepoint(true, 2011060311); + } + + if ($oldversion < 2011060312) { + + // Define field hintformat to be added to question_hints table. + $table = new xmldb_table('question_hints'); + $field = new xmldb_field('hintformat', XMLDB_TYPE_INTEGER, '4', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0'); + + // Conditionally launch add field partiallycorrectfeedbackformat + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + upgrade_main_savepoint(true, 2011060312); + } + + if ($oldversion < 2011060313) { + // Define field variant to be added to question_attempts + $table = new xmldb_table('question_attempts'); + $field = new xmldb_field('variant', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, 1, 'questionid'); + + // Launch add field component + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Main savepoint reached + upgrade_main_savepoint(true, 2011060313); + } + // Question engine 2 changes (14) end here + + if ($oldversion < 2011060500) { + + // Define index uniqueuserrating (not unique) to be dropped from rating + $table = new xmldb_table('rating'); + $index = new xmldb_index('uniqueuserrating', XMLDB_INDEX_NOTUNIQUE, + array('component', 'ratingarea', 'contextid', 'itemid')); + + // Drop dependent index before changing fields specs + if ($dbman->index_exists($table, $index)) { + $dbman->drop_index($table, $index); + } + + // Changing the default of field component on table rating to drop it + $field = new xmldb_field('component', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null, 'contextid'); + + // Launch change of default for field component + $dbman->change_field_default($table, $field); + + // Changing the default of field ratingarea on table rating to drop it + $field = new xmldb_field('ratingarea', XMLDB_TYPE_CHAR, '50', null, XMLDB_NOTNULL, null, null, 'component'); + + // Launch change of default for field ratingarea + $dbman->change_field_default($table, $field); + + // Add dependent index back + if (!$dbman->index_exists($table, $index)) { + $dbman->add_index($table, $index); + } + + // Main savepoint reached + upgrade_main_savepoint(true, 2011060500); + } + + if ($oldversion < 2011060800) { + // Add enabled field to message_processors + $table = new xmldb_table('message_processors'); + $field = new xmldb_field('enabled'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '1', 'name'); + + // Launch add field addition + if (!$dbman->field_exists($table,$field)) { + $dbman->add_field($table, $field); + } + + // Populate default messaging settings + upgrade_populate_default_messaging_prefs(); + + upgrade_main_savepoint(true, 2011060800); + } + + if ($oldversion < 2011060800.01) { //TODO: put the right latest version + // Define field shortname to be added to external_services + $table = new xmldb_table('external_services'); + $field = new xmldb_field('shortname', XMLDB_TYPE_CHAR, '255', null, null, null, null, 'timemodified'); + + // Conditionally launch add field shortname + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Main savepoint reached + upgrade_main_savepoint(true, 2011060800.01); + } return true; } diff --git a/lib/db/upgradelib.php b/lib/db/upgradelib.php index 27e797f683e..96ccc3d1907 100644 --- a/lib/db/upgradelib.php +++ b/lib/db/upgradelib.php @@ -645,3 +645,54 @@ function update_fix_automated_backup_config() { unset_config('backup_sche_gradebook_history'); unset_config('disablescheduleddbackups'); } + +/** + * This function is used to set default messaging preferences when the new + * admin-level messaging defaults settings have been introduced. + */ +function upgrade_populate_default_messaging_prefs() { + global $DB; + + $providers = $DB->get_records('message_providers'); + $processors = $DB->get_records('message_processors'); + $defaultpreferences = (object)$DB->get_records_menu('config_plugins', array('plugin'=>'message'), '', 'name,value'); + + $transaction = $DB->start_delegated_transaction(); + + $setting = new stdClass(); + $setting->plugin = 'message'; + + foreach ($providers as $provider) { + $componentproviderbase = $provider->component.'_'.$provider->name; + // set MESSAGE_PERMITTED to all combinations of message types + // (providers) and outputs (processors) + foreach ($processors as $processor) { + $preferencename = $processor->name.'_provider_'.$componentproviderbase.'_permitted'; + if (!isset($defaultpreferences->{$preferencename})) { + $setting->name = $preferencename; + $setting->value = 'permitted'; + $DB->insert_record('config_plugins', $setting); + } + } + // for email output we also have to set MESSAGE_DEFAULT_OFFLINE + MESSAGE_DEFAULT_ONLINE + foreach(array('loggedin', 'loggedoff') as $state) { + $preferencename = 'message_provider_'.$componentproviderbase.'_'.$state; + if (!isset($defaultpreferences->{$preferencename})) { + $setting->name = $preferencename; + $setting->value = 'email'; + // except instant message where default for popup should be + // MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF and for email + // MESSAGE_DEFAULT_LOGGEDOFF. + if ($componentproviderbase == 'moodle_instantmessage') { + if ($state == 'loggedoff') { + $setting->value = 'email,popup'; + } else { + $setting->value = 'popup'; + } + } + $DB->insert_record('config_plugins', $setting); + } + } + } + $transaction->allow_commit(); +} diff --git a/lib/html2text.php b/lib/html2text.php index 8d143bfb297..e2d0dffddbc 100644 --- a/lib/html2text.php +++ b/lib/html2text.php @@ -169,6 +169,7 @@ class html2text '/&(bull|#149|#8226);/i', // Bullet '/&(pound|#163);/i', // Pound sign '/&(euro|#8364);/i', // Euro sign + '/[ ]+([\n\t])/', // Trailing spaces before newline or tab '/[ ]{2,}/' // Runs of spaces, post-handling ); @@ -212,6 +213,7 @@ class html2text '*', 'ยฃ', 'EUR', // Euro sign. โ‚ฌ ? + '\\1', // Trailing spaces before newline or tab ' ' // Runs of spaces, post-handling ); diff --git a/lib/installlib.php b/lib/installlib.php index daaf16ccf5b..7b28f7c94e9 100644 --- a/lib/installlib.php +++ b/lib/installlib.php @@ -574,7 +574,6 @@ function install_cli_database(array $options, $interactive) { $admins = get_admins(); $admin = reset($admins); session_set_user($admin); - message_set_default_message_preferences($admin); // apply all default settings, do it twice to fill all defaults - some settings depend on other setting admin_apply_default_settings(NULL, true); diff --git a/lib/messagelib.php b/lib/messagelib.php index 2749c187931..f7a8c240f48 100644 --- a/lib/messagelib.php +++ b/lib/messagelib.php @@ -26,6 +26,8 @@ defined('MOODLE_INTERNAL') || die(); +require_once(dirname(dirname(__FILE__)) . '/message/lib.php'); + /** * Called when a message provider wants to send a message. * This functions checks the user's processor configuration to send the given type of message, @@ -34,8 +36,8 @@ defined('MOODLE_INTERNAL') || die(); * Required parameter $eventdata structure: * component string component name. must exist in message_providers * name string message type name. must exist in message_providers - * userfrom object the user sending the message - * userto object the message recipient + * userfrom object|int the user sending the message + * userto object|int the message recipient * subject string the message subject * fullmessage - the full message in a given format * fullmessageformat - the format if the full message (FORMAT_MOODLE, FORMAT_HTML, ..) @@ -57,12 +59,10 @@ function message_send($eventdata) { $DB->transactions_forbidden(); if (is_int($eventdata->userto)) { - mtrace('message_send() userto is a user ID when it should be a user object'); - $eventdata->userto = $DB->get_record('user', array('id' => $eventdata->useridto)); + $eventdata->userto = $DB->get_record('user', array('id' => $eventdata->userto)); } if (is_int($eventdata->userfrom)) { - mtrace('message_send() userfrom is a user ID when it should be a user object'); - $eventdata->userfrom = $DB->get_record('user', array('id' => $message->userfrom)); + $eventdata->userfrom = $DB->get_record('user', array('id' => $eventdata->userfrom)); } //after how long inactive should the user be considered logged off? @@ -109,25 +109,55 @@ function message_send($eventdata) { $savemessage->timecreated = time(); - // Find out what processors are defined currently - // When a user doesn't have settings none gets return, if he doesn't want contact "" gets returned - $preferencename = 'message_provider_'.$eventdata->component.'_'.$eventdata->name.'_'.$userstate; + // Fetch enabled processors + $processors = get_message_processors(true); + // Fetch default (site) preferences + $defaultpreferences = get_message_output_default_preferences(); - $processor = get_user_preferences($preferencename, null, $eventdata->userto->id); - if ($processor == NULL) { //this user never had a preference, save default - if (!message_set_default_message_preferences($eventdata->userto)) { - print_error('cannotsavemessageprefs', 'message'); - } - $processor = get_user_preferences($preferencename, NULL, $eventdata->userto->id); - if (empty($processor)) { + // Preset variables + $processorlist = array(); + $preferencebase = $eventdata->component.'_'.$eventdata->name; + // Fill in the array of processors to be used based on default and user preferences + foreach ($processors as $processor) { + // First find out permissions + $defaultpreference = $processor->name.'_provider_'.$preferencebase.'_permitted'; + if (isset($defaultpreferences->{$defaultpreference})) { + $permitted = $defaultpreferences->{$defaultpreference}; + } else { //MDL-25114 They supplied an $eventdata->component $eventdata->name combination which doesn't - //exist in the message_provider table + //exist in the message_provider table (thus there is no default settings for them) $preferrormsg = get_string('couldnotfindpreference', 'message', $preferencename); throw new coding_exception($preferrormsg,'blah'); } + + // Find out if user has configured this output + $userisconfigured = $processor->object->is_user_configured($eventdata->userto); + + // DEBUG: noify if we are forcing unconfigured output + if ($permitted == 'forced' && !$userisconfigured) { + debugging('Attempt to force message delivery to user who has "'.$processor->name.'" output unconfigured', DEBUG_NORMAL); + } + + // Populate the list of processors we will be using + if ($permitted == 'forced' && $userisconfigured) { + // We force messages for this processor, so use this processor unconditionally if user has configured it + $processorlist[] = $processor->name; + } else if ($permitted == 'permitted' && $userisconfigured) { + // User settings are permitted, see if user set any, otherwise use site default ones + $userpreferencename = 'message_provider_'.$preferencebase.'_'.$userstate; + if ($userpreference = get_user_preferences($userpreferencename, null, $eventdata->userto->id)) { + if (in_array($processor->name, explode(',', $userpreference))) { + $processorlist[] = $processor->name; + } + } else if (isset($defaultpreferences->{$userpreferencename})) { + if (in_array($processor->name, explode(',', $defaultpreferences->{$userpreferencename}))) { + $processorlist[] = $processor->name; + } + } + } } - if ($processor=='none' && $savemessage->notification) { + if (empty($processorlist) && $savemessage->notification) { //if they have deselected all processors and its a notification mark it read. The user doesnt want to be bothered $savemessage->timeread = time(); $messageid = $DB->insert_record('message_read', $savemessage); @@ -137,29 +167,14 @@ function message_send($eventdata) { $eventdata->savedmessageid = $savemessage->id; // Try to deliver the message to each processor - if ($processor!='none') { - $processorlist = explode(',', $processor); + if (!empty($processorlist)) { foreach ($processorlist as $procname) { - $processorfile = $CFG->dirroot. '/message/output/'.$procname.'/message_output_'.$procname.'.php'; - - if (is_readable($processorfile)) { - include_once($processorfile); // defines $module with version etc - $processclass = 'message_output_' . $procname; - - if (class_exists($processclass)) { - $pclass = new $processclass(); - - if (!$pclass->send_message($eventdata)) { - debugging('Error calling message processor '.$procname); - $messageid = false; - } - } - } else { - debugging('Error finding message processor '.$procname); + if (!$processors[$procname]->object->send_message($eventdata)) { + debugging('Error calling message processor '.$procname); $messageid = false; } } - + //if messaging is disabled and they previously had forum notifications handled by the popup processor //or any processor that puts a row in message_working then the notification will remain forever //unread. To prevent this mark the message read if messaging is disabled @@ -180,6 +195,7 @@ function message_send($eventdata) { /** * This code updates the message_providers table with the current set of providers + * * @param $component - examples: 'moodle', 'mod_forum', 'block_quiz_results' * @return boolean */ @@ -195,7 +211,7 @@ function message_update_providers($component='moodle') { foreach ($fileproviders as $messagename => $fileprovider) { if (!empty($dbproviders[$messagename])) { // Already exists in the database - + // check if capability has changed if ($dbproviders[$messagename]->capability == $fileprovider['capability']) { // Same, so ignore // exact same message provider already present in db, ignore this entry unset($dbproviders[$messagename]); @@ -217,19 +233,128 @@ function message_update_providers($component='moodle') { $provider->component = $component; $provider->capability = $fileprovider['capability']; + $transaction = $DB->start_delegated_transaction(); $DB->insert_record('message_providers', $provider); + message_set_default_message_preference($component, $messagename, $fileprovider); + $transaction->allow_commit(); } } foreach ($dbproviders as $dbprovider) { // Delete old ones $DB->delete_records('message_providers', array('id' => $dbprovider->id)); + $DB->delete_records_select('config_plugins', "plugin = 'message' AND ".$DB->sql_like('name', '?', false), array("%_provider_{$component}_{$dbprovider->name}_%")); + $DB->delete_records_select('user_preferences', $DB->sql_like('name', '?', false), array("message_provider_{$component}_{$dbprovider->name}_%")); } return true; } +/** + * This function populates default message preferences for all existing providers + * when the new message processor is added. + * + * @param string $processorname The name of message processor plugin (e.g. 'email', 'jabber') + * @return void + * @throws invalid_parameter_exception if $processorname does not exist + */ +function message_update_processors($processorname) { + global $DB; + + // validate if our processor exists + $processor = $DB->get_records('message_processors', array('name' => $processorname)); + if (empty($processor)) { + throw new invalid_parameter_exception(); + } + + $providers = $DB->get_records_sql('SELECT DISTINCT component FROM {message_providers}'); + + $transaction = $DB->start_delegated_transaction(); + foreach ($providers as $provider) { + // load message providers from files + $fileproviders = message_get_providers_from_file($provider->component); + foreach ($fileproviders as $messagename => $fileprovider) { + message_set_default_message_preference($provider->component, $messagename, $fileprovider, $processorname); + } + } + $transaction->allow_commit(); +} + +/** + * Setting default messaging preference for particular message provider + * + * @param string $component The name of component (e.g. moodle, mod_forum, etc.) + * @param string $messagename The name of message provider + * @param array $fileprovider The value of $messagename key in the array defined in plugin messages.php + * @param string $processorname The optinal name of message processor + * @return void + */ +function message_set_default_message_preference($component, $messagename, $fileprovider, $processorname='') { + global $DB; + + // Fetch message processors + $condition = null; + // If we need to process a particular processor, set the select condition + if (!empty($processorname)) { + $condition = array('name' => $processorname); + } + $processors = $DB->get_records('message_processors', $condition); + + // load default messaging preferences + $defaultpreferences = get_message_output_default_preferences(); + + // Setting default preference + $componentproviderbase = $component.'_'.$messagename; + $loggedinpref = array(); + $loggedoffpref = array(); + // set 'permitted' preference first for each messaging processor + foreach ($processors as $processor) { + $preferencename = $processor->name.'_provider_'.$componentproviderbase.'_permitted'; + // if we do not have this setting yet, set it + if (!isset($defaultpreferences->{$preferencename})) { + // determine plugin default settings + $plugindefault = 0; + if (isset($fileprovider['defaults'][$processor->name])) { + $plugindefault = $fileprovider['defaults'][$processor->name]; + } + // get string values of the settings + list($permitted, $loggedin, $loggedoff) = translate_message_default_setting($plugindefault, $processor->name); + // store default preferences for current processor + set_config($preferencename, $permitted, 'message'); + // save loggedin/loggedoff settings + if ($loggedin) { + $loggedinpref[] = $processor->name; + } + if ($loggedoff) { + $loggedoffpref[] = $processor->name; + } + } + } + // now set loggedin/loggedoff preferences + if (!empty($loggedinpref)) { + $preferencename = 'message_provider_'.$componentproviderbase.'_loggedin'; + if (isset($defaultpreferences->{$preferencename})) { + // We have the default preferences for this message provider, which + // likely means that we have been adding a new processor. Add defaults + // to exisitng preferences. + $loggedinpref = array_merge($loggedinpref, explode(',', $defaultpreferences->{$preferencename})); + } + set_config($preferencename, join(',', $loggedinpref), 'message'); + } + if (!empty($loggedoffpref)) { + $preferencename = 'message_provider_'.$componentproviderbase.'_loggedoff'; + if (isset($defaultpreferences->{$preferencename})) { + // We have the default preferences for this message provider, which + // likely means that we have been adding a new processor. Add defaults + // to exisitng preferences. + $loggedoffpref = array_merge($loggedoffpref, explode(',', $defaultpreferences->{$preferencename})); + } + set_config($preferencename, join(',', $loggedoffpref), 'message'); + } +} + /** * Returns the active providers for the current user, based on capability + * * @return array of message providers */ function message_get_my_providers() { @@ -253,6 +378,7 @@ function message_get_my_providers() { /** * Gets the message providers that are in the database for this component. + * * @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results' * @return array of message providers * @@ -267,6 +393,7 @@ function message_get_providers_from_db($component) { /** * Loads the messages definitions for the component (from file). If no * messages are defined for the component, we simply return an empty array. + * * @param $component - examples: 'moodle', 'mod_forum', 'block_quiz_results' * @return array of message providerss or empty array if not exists * @@ -285,64 +412,43 @@ function message_get_providers_from_file($component) { if (empty($messageprovider['capability'])) { $messageproviders[$name]['capability'] = NULL; } + if (empty($messageprovider['defaults'])) { + $messageproviders[$name]['defaults'] = array(); + } } return $messageproviders; } /** - * Remove all message providers - * @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results' + * Remove all message providers for particular plugin and corresponding settings + * + * @param string $component - examples: 'moodle', 'mod_forum', 'block_quiz_results' + * @return void */ -function message_uninstall($component) { +function message_provider_uninstall($component) { global $DB; - return $DB->delete_records('message_providers', array('component' => $component)); + + $transaction = $DB->start_delegated_transaction(); + $DB->delete_records('message_providers', array('component' => $component)); + $DB->delete_records_select('config_plugins', "plugin = 'message' AND ".$DB->sql_like('name', '?', false), array("%_provider_{$component}_%")); + $DB->delete_records_select('user_preferences', $DB->sql_like('name', '?', false), array("message_provider_{$component}_%")); + $transaction->allow_commit(); } /** - * Set default message preferences. - * @param $user - User to set message preferences + * Remove message processor + * + * @param string $name - examples: 'email', 'jabber' + * @return void */ -function message_set_default_message_preferences($user) { +function message_processor_uninstall($name) { global $DB; - //check for the pre 2.0 disable email setting - $useemail = empty($user->emailstop); - - //look for the pre-2.0 preference if it exists - $oldpreference = get_user_preferences('message_showmessagewindow', -1, $user->id); - //if they elected to see popups or the preference didnt exist - $usepopups = (intval($oldpreference)==1 || intval($oldpreference)==-1); - - $defaultonlineprocessor = 'none'; - $defaultofflineprocessor = 'none'; - - if ($useemail) { - $defaultonlineprocessor = 'email'; - $defaultofflineprocessor = 'email'; - } else if ($usepopups) { - $defaultonlineprocessor = 'popup'; - $defaultofflineprocessor = 'popup'; - } - - $offlineprocessortouse = $onlineprocessortouse = null; - - $providers = $DB->get_records('message_providers'); - $preferences = array(); - - foreach ($providers as $providerid => $provider) { - - //force some specific defaults for IMs - if ($provider->name=='instantmessage' && $usepopups && $useemail) { - $onlineprocessortouse = 'popup'; - $offlineprocessortouse = 'email,popup'; - } else { - $onlineprocessortouse = $defaultonlineprocessor; - $offlineprocessortouse = $defaultofflineprocessor; - } - - $preferences['message_provider_'.$provider->component.'_'.$provider->name.'_loggedin'] = $onlineprocessortouse; - $preferences['message_provider_'.$provider->component.'_'.$provider->name.'_loggedoff'] = $offlineprocessortouse; - } - return set_user_preferences($preferences, $user->id); + $transaction = $DB->start_delegated_transaction(); + $DB->delete_records('message_processors', array('name' => $name)); + // delete permission preferences only, we do not care about loggedin/loggedoff + // defaults, they will be removed on the next attempt to update the preferences + $DB->delete_records_select('config_plugins', "plugin = 'message' AND ".$DB->sql_like('name', '?', false), array("{$name}_provider_%")); + $transaction->allow_commit(); } diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 3dee9f948be..e159340b914 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -424,6 +424,10 @@ define('HUB_HUBDIRECTORYURL', "http://hubdirectory.moodle.org"); */ define('HUB_MOODLEORGHUBURL', "http://hub.moodle.org"); +/** + * Moodle mobile app service name + */ +define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app'); /// PARAMETER HANDLING //////////////////////////////////////////////////// @@ -1554,26 +1558,27 @@ function get_user_preferences($name = null, $default = null, $user = null) { * @param int $hour The hour part to create timestamp of * @param int $minute The minute part to create timestamp of * @param int $second The second part to create timestamp of - * @param float $timezone Timezone modifier - * @param bool $applydst Toggle Daylight Saving Time, default true + * @param mixed $timezone Timezone modifier, if 99 then use default user's timezone + * @param bool $applydst Toggle Daylight Saving Time, default true, will be + * applied only if timezone is 99 or string. * @return int timestamp */ function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) { - $strtimezone = NULL; - if (!is_numeric($timezone)) { - $strtimezone = $timezone; - } + //save input timezone, required for dst offset check. + $passedtimezone = $timezone; $timezone = get_user_timezone_offset($timezone); - if (abs($timezone) > 13) { + if (abs($timezone) > 13) { //server time $time = mktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year); } else { $time = gmmktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year); $time = usertime($time, $timezone); - if($applydst) { - $time -= dst_offset_on($time, $strtimezone); + + //Apply dst for string timezones or if 99 then try dst offset with user's default timezone + if ($applydst && ((99 == $passedtimezone) || !is_numeric($passedtimezone))) { + $time -= dst_offset_on($time, $passedtimezone); } } @@ -1664,7 +1669,8 @@ function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, * @param int $date the timestamp in UTC, as obtained from the database. * @param string $format strftime format. You should probably get this using * get_string('strftime...', 'langconfig'); - * @param float $timezone by default, uses the user's time zone. + * @param mixed $timezone by default, uses the user's time zone. if numeric and + * not 99 then daylight saving will not be added. * @param bool $fixday If true (default) then the leading zero from %d is removed. * If false then the leading zero is maintained. * @return string the formatted date/time. @@ -1673,11 +1679,6 @@ function userdate($date, $format = '', $timezone = 99, $fixday = true) { global $CFG; - $strtimezone = NULL; - if (!is_numeric($timezone)) { - $strtimezone = $timezone; - } - if (empty($format)) { $format = get_string('strftimedaydatetime', 'langconfig'); } @@ -1689,7 +1690,11 @@ function userdate($date, $format = '', $timezone = 99, $fixday = true) { $fixday = ($formatnoday != $format); } - $date += dst_offset_on($date, $strtimezone); + //add daylight saving offset for string timezones only, as we can't get dst for + //float values. if timezone is 99 (user default timezone), then try update dst. + if ((99 == $timezone) || !is_numeric($timezone)) { + $date += dst_offset_on($date, $timezone); + } $timezone = get_user_timezone_offset($timezone); @@ -1732,15 +1737,14 @@ function userdate($date, $format = '', $timezone = 99, $fixday = true) { * @todo Finish documenting this function * @uses HOURSECS * @param int $time Timestamp in GMT - * @param float $timezone ? + * @param mixed $timezone offset time with timezone, if float and not 99, then no + * dst offset is applyed * @return array An array that represents the date in user time */ function usergetdate($time, $timezone=99) { - $strtimezone = NULL; - if (!is_numeric($timezone)) { - $strtimezone = $timezone; - } + //save input timezone, required for dst offset check. + $passedtimezone = $timezone; $timezone = get_user_timezone_offset($timezone); @@ -1748,8 +1752,12 @@ function usergetdate($time, $timezone=99) { return getdate($time); } - // There is no gmgetdate so we use gmdate instead - $time += dst_offset_on($time, $strtimezone); + //add daylight saving offset for string timezones only, as we can't get dst for + //float values. if timezone is 99 (user default timezone), then try update dst. + if ($passedtimezone == 99 || !is_numeric($passedtimezone)) { + $time += dst_offset_on($time, $passedtimezone); + } + $time += intval((float)$timezone * HOURSECS); $datestring = gmstrftime('%B_%A_%j_%Y_%m_%w_%d_%H_%M_%S', $time); @@ -1900,7 +1908,7 @@ function get_timezone_offset($tz) { * * @global object * @global object - * @param float $tz If this value is provided and not equal to 99, it will be returned as is and no other settings will be checked + * @param mixed $tz If this value is provided and not equal to 99, it will be returned as is and no other settings will be checked * @return mixed */ function get_user_timezone($tz = 99) { @@ -1954,7 +1962,7 @@ function get_timezone_record($timezonename) { * @global object * @param mixed $from_year Start year for the table, defaults to 1971 * @param mixed $to_year End year for the table, defaults to 2035 - * @param mixed $strtimezone + * @param mixed $strtimezone, if null or 99 then user's default timezone is used * @return bool */ function calculate_user_dst_table($from_year = NULL, $to_year = NULL, $strtimezone = NULL) { @@ -2115,9 +2123,12 @@ function dst_changes_for_year($year, $timezone) { /** * Calculates the Daylight Saving Offset for a given date/time (timestamp) + * - Note: Daylight saving only works for string timezones and not for float. * * @global object * @param int $time must NOT be compensated at all, it has to be a pure timestamp + * @param mixed $strtimezone timezone for which offset is expected, if 99 or null + * then user's default timezone is used. * @return int */ function dst_offset_on($time, $strtimezone = NULL) { @@ -7067,7 +7078,8 @@ function get_plugin_types($fullpaths=true) { static $fullinfo = null; if (!$info) { - $info = array('mod' => 'mod', + $info = array('qtype' => 'question/type', + 'mod' => 'mod', 'auth' => 'auth', 'enrol' => 'enrol', 'message' => 'message/output', @@ -7085,7 +7097,7 @@ function get_plugin_types($fullpaths=true) { 'webservice' => 'webservice', 'repository' => 'repository', 'portfolio' => 'portfolio', - 'qtype' => 'question/type', + 'qbehaviour' => 'question/behaviour', 'qformat' => 'question/format', 'plagiarism' => 'plagiarism', 'theme' => 'theme'); // this is a bit hacky, themes may be in $CFG->themedir too @@ -7603,6 +7615,143 @@ function check_php_version($version='5.2.4') { return false; } +/** + * Returns whether a device/browser combination is mobile, tablet, legacy, default or the result of + * an optional admin specified regular expression. If enabledevicedetection is set to no or not set + * it returns default + * + * @return string device type + */ +function get_device_type() { + global $CFG; + + if (empty($CFG->enabledevicedetection) || empty($_SERVER['HTTP_USER_AGENT'])) { + return 'default'; + } + + $useragent = $_SERVER['HTTP_USER_AGENT']; + + if (!empty($CFG->devicedetectregex)) { + $regexes = json_decode($CFG->devicedetectregex); + + foreach ($regexes as $value=>$regex) { + if (preg_match($regex, $useragent)) { + return $value; + } + } + } + + //mobile detection PHP direct copy from open source detectmobilebrowser.com + $phonesregex = '/android|avantgo|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i'; + $modelsregex = '/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|e\-|e\/|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(di|rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|xda(\-|2|g)|yas\-|your|zeto|zte\-/i'; + if (preg_match($phonesregex,$useragent) || preg_match($modelsregex,substr($useragent, 0, 4))){ + return 'mobile'; + } + + $tabletregex = '/Tablet browser|iPad|iProd|GT-P1000|GT-I9000|SHW-M180S|SGH-T849|SCH-I800|Build\/ERE27|sholest/i'; + if (preg_match($tabletregex, $useragent)) { + return 'tablet'; + } + + if (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE 6.') !== false) { + return 'legacy'; + } + + return 'default'; +} + +/** + * Returns a list of the device types supporting by Moodle + * + * @param boolean $incusertypes includes types specified using the devicedetectregex admin setting + * @return array $types + */ +function get_device_type_list($incusertypes = true) { + global $CFG; + + $types = array('default', 'legacy', 'mobile', 'tablet'); + + if ($incusertypes && !empty($CFG->devicedetectregex)) { + $regexes = json_decode($CFG->devicedetectregex); + + foreach ($regexes as $value => $regex) { + $types[] = $value; + } + } + + return $types; +} + +/** + * Returns the theme selected for a particular device or false if none selected. + * + * @param string $devicetype + * @return string|false The name of the theme to use for the device or the false if not set + */ +function get_selected_theme_for_device_type($devicetype = null) { + global $CFG; + + if (empty($devicetype)) { + $devicetype = get_user_device_type(); + } + + $themevarname = get_device_cfg_var_name($devicetype); + if (empty($CFG->$themevarname)) { + return false; + } + + return $CFG->$themevarname; +} + +/** + * Returns the name of the device type theme var in $CFG (because there is not a standard convention to allow backwards compatability + * + * @param string $devicetype + * @return string The config variable to use to determine the theme + */ +function get_device_cfg_var_name($devicetype = null) { + if ($devicetype == 'default' || empty($devicetype)) { + return 'theme'; + } + + return 'theme' . $devicetype; +} + +/** + * Allows the user to switch the device they are seeing the theme for. + * This allows mobile users to switch back to the default theme, or theme for any other device. + * + * @param string $newdevice The device the user is currently using. + * @return string The device the user has switched to + */ +function set_user_device_type($newdevice) { + global $USER; + + $devicetype = get_device_type(); + $devicetypes = get_device_type_list(); + + if ($newdevice == $devicetype) { + unset_user_preference('switchdevice'.$devicetype); + } else if (in_array($newdevice, $devicetypes)) { + set_user_preference('switchdevice'.$devicetype, $newdevice); + } +} + +/** + * Returns the device the user is currently using, or if the user has chosen to switch devices + * for the current device type the type they have switched to. + * + * @return string The device the user is currently using or wishes to use + */ +function get_user_device_type() { + $device = get_device_type(); + $switched = get_user_preferences('switchdevice'.$device, false); + if ($switched != false) { + return $switched; + } + return $device; +} + /** * Returns one or several CSS class names that match the user's browser. These can be put * in the body tag of the page to apply browser-specific rules without relying on CSS hacks diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php index 786444f9579..7f63cf31272 100644 --- a/lib/outputrenderers.php +++ b/lib/outputrenderers.php @@ -362,10 +362,14 @@ class core_renderer extends renderer_base { // but some of the content won't be known until later, so we return a placeholder // for now. This will be replaced with the real content in {@link footer()}. $output = self::PERFORMANCE_INFO_TOKEN; - if ($this->page->legacythemeinuse) { + if ($this->page->devicetypeinuse == 'legacy') { // The legacy theme is in use print the notification $output .= html_writer::tag('div', get_string('legacythemeinuse'), array('class'=>'legacythemeinuse')); } + + // Get links to switch device types (only shown for users not on a default device) + $output .= $this->theme_switch_links(); + if (!empty($CFG->debugpageinfo)) { $output .= '
This page is: ' . $this->page->debug_summary() . '
'; } @@ -2496,8 +2500,40 @@ EOD; // Return the sub menu return $content; } -} + /** + * Renders theme links for switching between default and other themes. + * + * @return string + */ + protected function theme_switch_links() { + + $actualdevice = get_device_type(); + $currentdevice = $this->page->devicetypeinuse; + $switched = ($actualdevice != $currentdevice); + + if (!$switched && $currentdevice == 'default' && $actualdevice == 'default') { + // The user is using the a default device and hasn't switched so don't shown the switch + // device links. + return ''; + } + + if ($switched) { + $linktext = get_string('switchdevicerecommended'); + $devicetype = $actualdevice; + } else { + $linktext = get_string('switchdevicedefault'); + $devicetype = 'default'; + } + $linkurl = new moodle_url('/theme/switchdevice.php', array('url' => $this->page->url, 'device' => $devicetype, 'sesskey' => sesskey())); + + $content = html_writer::start_tag('div', array('id' => 'theme_switch_link')); + $content .= html_writer::link($linkurl, $linktext); + $content .= html_writer::end_tag('div'); + + return $content; + } +} /// RENDERERS diff --git a/lib/outputrequirementslib.php b/lib/outputrequirementslib.php index 70f05e40df5..336f36e6a81 100644 --- a/lib/outputrequirementslib.php +++ b/lib/outputrequirementslib.php @@ -445,6 +445,7 @@ class page_requirements_manager { break; case 'core_message': $module = array('name' => 'core_message', + 'requires' => array('base', 'node', 'event', 'node-event-simulate'), 'fullpath' => '/message/module.js'); break; case 'core_flashdetect': diff --git a/lib/pagelib.php b/lib/pagelib.php index 2fef3af923e..b6c9cf0e33f 100644 --- a/lib/pagelib.php +++ b/lib/pagelib.php @@ -64,13 +64,13 @@ defined('MOODLE_INTERNAL') || die(); * @property-read object $course The current course that we are inside - a row from the * course table. (Also available as $COURSE global.) If we are not inside * an actual course, this will be the site course. + * @property-read string $devicetypeinuse The name of the device type in use * @property-read string $docspath The path to the Moodle docs for this page. * @property-read string $focuscontrol The id of the HTML element to be focused when the page has loaded. * @property-read bool $headerprinted * @property-read string $heading The main heading that should be displayed at the top of the . * @property-read string $headingmenu The menu (or actions) to display in the heading * @property-read array $layout_options Returns arrays with options for layout file. - * @property-read bool $legacythemeinuse Returns true if the legacy theme is being used. * @property-read navbar $navbar Returns the navbar object used to display the navbar * @property-read global_navigation $navigation Returns the global navigation structure * @property-read xml_container_stack $opencontainers Tracks XHTML tags on this page that have been opened but not closed. @@ -217,10 +217,12 @@ class moodle_page { protected $_legacybrowsers = array('MSIE' => 6.0); /** - * Is set to true if the chosen legacy theme is in use. False by default. - * @var bool + * Is set to the name of the device type in use. + * This will we worked out when it is first used. + * + * @var string */ - protected $_legacythemeinuse = false; + protected $_devicetypeinuse = null; protected $_https_login_required = false; @@ -522,12 +524,26 @@ class moodle_page { return $this->_theme; } + /** + * Please do not call this method directly, use the ->devicetypeinuse syntax. {@link __get()}. + * + * @return string The device type being used. + */ + protected function magic_get_devicetypeinuse() { + if (empty($this->_devicetypeinuse)) { + $this->_devicetypeinuse = get_user_device_type(); + } + return $this->_devicetypeinuse; + } + /** * Please do not call this method directly, use the ->legacythemeinuse syntax. {@link __get()}. + * @deprecated since 2.1 * @return bool */ protected function magic_get_legacythemeinuse() { - return ($this->_legacythemeinuse); + debugging('$PAGE->legacythemeinuse is a deprecated property - please use $PAGE->devicetypeinuse and check if it is equal to legacy.', DEVELOPER_DEBUG); + return ($this->devicetypeinuse == 'legacy'); } /** @@ -1280,16 +1296,15 @@ class moodle_page { } } - $theme = ''; foreach ($themeorder as $themetype) { switch ($themetype) { case 'course': - if (!empty($CFG->allowcoursethemes) and !empty($this->course->theme)) { - return $this->course->theme; + if (!empty($CFG->allowcoursethemes) && !empty($this->_course->theme) && $this->devicetypeinuse == 'default') { + return $this->_course->theme; } case 'category': - if (!empty($CFG->allowcategorythemes)) { + if (!empty($CFG->allowcategorythemes) && $this->devicetypeinuse == 'default') { $categories = $this->categories; foreach ($categories as $category) { if (!empty($category->theme)) { @@ -1304,7 +1319,7 @@ class moodle_page { } case 'user': - if (!empty($CFG->allowuserthemes) and !empty($USER->theme)) { + if (!empty($CFG->allowuserthemes) && !empty($USER->theme) && $this->devicetypeinuse == 'default') { if ($mnetpeertheme) { return $mnetpeertheme; } else { @@ -1315,33 +1330,23 @@ class moodle_page { case 'site': if ($mnetpeertheme) { return $mnetpeertheme; - } else if(!empty($CFG->themelegacy) && $this->browser_is_outdated()) { - $this->_legacythemeinuse = true; - return $CFG->themelegacy; - } else { - return $CFG->theme; } + // First try for the device the user is using. + $devicetheme = get_selected_theme_for_device_type($this->devicetypeinuse); + if (!empty($devicetheme)) { + return $devicetheme; + } + // Next try for the default device (as a fallback) + $devicetheme = get_selected_theme_for_device_type('default'); + if (!empty($devicetheme)) { + return $devicetheme; + } + // The default device theme isn't set up - use the overall default theme. + return theme_config::DEFAULT_THEME; } } } - /** - * Determines whether the current browser should - * default to the admin-selected legacy theme - * - * @return true if legacy theme should be used, otherwise false - * - */ - protected function browser_is_outdated() { - foreach($this->_legacybrowsers as $browser => $version) { - // Check the browser is valid first then that its version is suitable - if(check_browser_version($browser, '0') && - !check_browser_version($browser, $version)) { - return true; - } - } - return false; - } /** * Sets ->pagetype from the script name. For example, if the script that was @@ -1448,8 +1453,8 @@ class moodle_page { $this->add_body_class('drag'); } - if ($this->_legacythemeinuse) { - $this->add_body_class('legacytheme'); + if ($this->_devicetypeinuse != 'default') { + $this->add_body_class($this->_devicetypeinuse . 'theme'); } } diff --git a/lib/questionlib.php b/lib/questionlib.php index dc3805ff8f6..8371f3bc3e3 100644 --- a/lib/questionlib.php +++ b/lib/questionlib.php @@ -1,5 +1,4 @@ dirroot . '/question/engine/lib.php'); +require_once($CFG->dirroot . '/question/type/questiontypebase.php'); + + + /// CONSTANTS /////////////////////////////////// -/**#@+ - * The different types of events that can create question states - */ -define('QUESTION_EVENTOPEN', '0'); // The state was created by Moodle -define('QUESTION_EVENTNAVIGATE', '1'); // The responses were saved because the student navigated to another page (this is not currently used) -define('QUESTION_EVENTSAVE', '2'); // The student has requested that the responses should be saved but not submitted or validated -define('QUESTION_EVENTGRADE', '3'); // Moodle has graded the responses. A SUBMIT event can be changed to a GRADE event by Moodle. -define('QUESTION_EVENTDUPLICATE', '4'); // The responses submitted were the same as previously -define('QUESTION_EVENTVALIDATE', '5'); // The student has requested a validation. This causes the responses to be saved as well, but not graded. -define('QUESTION_EVENTCLOSEANDGRADE', '6'); // Moodle has graded the responses. A CLOSE event can be changed to a CLOSEANDGRADE event by Moodle. -define('QUESTION_EVENTSUBMIT', '7'); // The student response has been submitted but it has not yet been marked -define('QUESTION_EVENTCLOSE', '8'); // The response has been submitted and the session has been closed, either because the student requested it or because Moodle did it (e.g. because of a timelimit). The responses have not been graded. -define('QUESTION_EVENTMANUALGRADE', '9'); // Grade was entered by teacher - -define('QUESTION_EVENTS_GRADED', QUESTION_EVENTGRADE.','. - QUESTION_EVENTCLOSEANDGRADE.','. - QUESTION_EVENTMANUALGRADE); - - -define('QUESTION_EVENTS_CLOSED', QUESTION_EVENTCLOSE.','. - QUESTION_EVENTCLOSEANDGRADE.','. - QUESTION_EVENTMANUALGRADE); - -define('QUESTION_EVENTS_CLOSED_OR_GRADED', QUESTION_EVENTGRADE.','. - QUESTION_EVENTS_CLOSED); - -/**#@-*/ - /**#@+ * The core question types. */ @@ -87,7 +61,7 @@ define("ESSAY", "essay"); * Constant determines the number of answer boxes supplied in the editing * form for multiple choice and similar question types. */ -define("QUESTION_NUMANS", "10"); +define("QUESTION_NUMANS", 10); /** * Constant determines the number of answer boxes supplied in the editing @@ -103,134 +77,6 @@ define("QUESTION_NUMANS_START", 3); */ define("QUESTION_NUMANS_ADD", 3); -/** - * The options used when popping up a question preview window in Javascript. - */ -define('QUESTION_PREVIEW_POPUP_OPTIONS', 'scrollbars=yes&resizable=yes&width=700&height=540'); - -/**#@+ - * Option flags for ->optionflags - * The options are read out via bitwise operation using these constants - */ -/** - * Whether the questions is to be run in adaptive mode. If this is not set then - * a question closes immediately after the first submission of responses. This - * is how question is Moodle always worked before version 1.5 - */ -define('QUESTION_ADAPTIVE', 1); -/**#@-*/ - -/**#@+ - * Options for whether flags are shown/editable when rendering questions. - */ -define('QUESTION_FLAGSHIDDEN', 0); -define('QUESTION_FLAGSSHOWN', 1); -define('QUESTION_FLAGSEDITABLE', 2); -/**#@-*/ - -/** - * GLOBAL VARAIBLES - * @global array $QTYPES - * @name $QTYPES - */ -global $QTYPES; -/** - * Array holding question type objects. Initialised via calls to - * question_register_questiontype as the question type classes are included. - */ -$QTYPES = array(); - -/** - * Add a new question type to the various global arrays above. - * - * @global object - * @param object $qtype An instance of the new question type class. - */ -function question_register_questiontype($qtype) { - global $QTYPES; - - $name = $qtype->name(); - $QTYPES[$name] = $qtype; -} - -require_once("$CFG->dirroot/question/type/questiontype.php"); - -// Load the questiontype.php file for each question type -// These files in turn call question_register_questiontype() -// with a new instance of each qtype class. -$qtypenames = get_plugin_list('qtype'); -foreach($qtypenames as $qtypename => $qdir) { - // Instanciates all plug-in question types - $qtypefilepath= "$qdir/questiontype.php"; - - // echo "Loading $qtypename
"; // Uncomment for debugging - if (is_readable($qtypefilepath)) { - require_once($qtypefilepath); - } -} - -/** - * An array of question type names translated to the user's language, suitable for use when - * creating a drop-down menu of options. - * - * Long-time Moodle programmers will realise that this replaces the old $QTYPE_MENU array. - * The array returned will only hold the names of all the question types that the user should - * be able to create directly. Some internal question types like random questions are excluded. - * - * @global object - * @return array an array of question type names translated to the user's language. - */ -function question_type_menu() { - global $QTYPES; - static $menuoptions = null; - if (is_null($menuoptions)) { - $config = get_config('question'); - $menuoptions = array(); - foreach ($QTYPES as $name => $qtype) { - // Get the name if this qtype is enabled. - $menuname = $qtype->menu_name(); - $enabledvar = $name . '_disabled'; - if ($menuname && !isset($config->$enabledvar)) { - $menuoptions[$name] = $menuname; - } - } - - $menuoptions = question_sort_qtype_array($menuoptions, $config); - } - return $menuoptions; -} - -/** - * Sort an array of question type names according to the question type sort order stored in - * config_plugins. Entries for which there is no xxx_sortorder defined will go - * at the end, sorted according to textlib_get_instance()->asort($inarray). - * @param $inarray an array $qtype => $QTYPES[$qtype]->local_name(). - * @param $config get_config('question'), if you happen to have it around, to save one DB query. - * @return array the sorted version of $inarray. - */ -function question_sort_qtype_array($inarray, $config = null) { - if (is_null($config)) { - $config = get_config('question'); - } - - $sortorder = array(); - foreach ($inarray as $name => $notused) { - $sortvar = $name . '_sortorder'; - if (isset($config->$sortvar)) { - $sortorder[$config->$sortvar] = $name; - } - } - - ksort($sortorder); - $outarray = array(); - foreach ($sortorder as $name) { - $outarray[$name] = $inarray[$name]; - unset($inarray[$name]); - } - textlib_get_instance()->asort($inarray); - return array_merge($outarray, $inarray); -} - /** * Move one question type in a list of question types. If you try to move one element * off of the end, nothing will change. @@ -282,93 +128,59 @@ function question_save_qtype_order($neworder, $config = null) { } } -/// OTHER CLASSES ///////////////////////////////////////////////////////// - -/** - * This holds the options that are set by the course module - * - * @package moodlecore - * @subpackage question - * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class cmoptions { - /** - * Whether a new attempt should be based on the previous one. If true - * then a new attempt will start in a state where all responses are set - * to the last responses from the previous attempt. - */ - var $attemptonlast = false; - - /** - * Various option flags. The flags are accessed via bitwise operations - * using the constants defined in the CONSTANTS section above. - */ - var $optionflags = QUESTION_ADAPTIVE; - - /** - * Determines whether in the calculation of the score for a question - * penalties for earlier wrong responses within the same attempt will - * be subtracted. - */ - var $penaltyscheme = true; - - /** - * The maximum time the user is allowed to answer the questions withing - * an attempt. This is measured in minutes so needs to be multiplied by - * 60 before compared to timestamps. If set to 0 no timelimit will be applied - */ - var $timelimit = 0; - - /** - * Timestamp for the closing time. Responses submitted after this time will - * be saved but no credit will be given for them. - */ - var $timeclose = 9999999999; - - /** - * The id of the course from withing which the question is currently being used - */ - var $course = SITEID; - - /** - * Whether the answers in a multiple choice question should be randomly - * shuffled when a new attempt is started. - */ - var $shuffleanswers = true; - - /** - * The number of decimals to be shown when scores are printed - */ - var $decimalpoints = 2; -} - - /// FUNCTIONS ////////////////////////////////////////////////////// /** * Returns an array of names of activity modules that use this question * - * @global object - * @global object + * @deprecated since Moodle 2.1. Use {@link questions_in_use} instead. + * * @param object $questionid * @return array of strings */ function question_list_instances($questionid) { - global $CFG, $DB; - $instances = array(); - $modules = $DB->get_records('modules'); - foreach ($modules as $module) { - $fullmod = $CFG->dirroot . '/mod/' . $module->name; - if (file_exists($fullmod . '/lib.php')) { - include_once($fullmod . '/lib.php'); - $fn = $module->name.'_question_list_instances'; + throw new coding_exception('question_list_instances has been deprectated. ' . + 'Please use questions_in_use instead.'); +} + +/** + * @param array $questionids of question ids. + * @return boolean whether any of these questions are being used by any part of Moodle. + */ +function questions_in_use($questionids) { + global $CFG; + + if (question_engine::questions_in_use($questionids)) { + return true; + } + + foreach (get_plugin_list('mod') as $module => $path) { + $lib = $path . '/lib.php'; + if (is_readable($lib)) { + include_once($lib); + + $fn = $module . '_questions_in_use'; if (function_exists($fn)) { - $instances = $instances + $fn($questionid); + if ($fn($questionids)) { + return true; + } + } else { + + // Fallback for legacy modules. + $fn = $module . '_question_list_instances'; + if (function_exists($fn)) { + foreach ($questionids as $questionid) { + $instances = $fn($questionid); + if (!empty($instances)) { + return true; + } + } + } } } } - return $instances; + + return false; } /** @@ -376,7 +188,6 @@ function question_list_instances($questionid) { * question categories contain any questions. This will return true even if all the questions are * hidden. * - * @global object * @param mixed $context either a context object, or a context id. * @return boolean whether any of the question categories beloning to this context have * any questions in them. @@ -399,57 +210,16 @@ function question_context_has_any_questions($context) { /** * Returns list of 'allowed' grades for grade selection * formatted suitably for dropdown box function + * + * @deprecated since 2.1. Use {@link question_bank::fraction_options()} or + * {@link question_bank::fraction_options_full()} instead. + * * @return object ->gradeoptionsfull full array ->gradeoptions +ve only */ function get_grade_options() { - // define basic array of grades. This list comprises all fractions of the form: - // a. p/q for q <= 6, 0 <= p <= q - // b. p/10 for 0 <= p <= 10 - // c. 1/q for 1 <= q <= 10 - // d. 1/20 - $grades = array( - 1.0000000, - 0.9000000, - 0.8333333, - 0.8000000, - 0.7500000, - 0.7000000, - 0.6666667, - 0.6000000, - 0.5000000, - 0.4000000, - 0.3333333, - 0.3000000, - 0.2500000, - 0.2000000, - 0.1666667, - 0.1428571, - 0.1250000, - 0.1111111, - 0.1000000, - 0.0500000, - 0.0000000); - - // iterate through grades generating full range of options - $gradeoptionsfull = array(); - $gradeoptions = array(); - foreach ($grades as $grade) { - $percentage = 100 * $grade; - $neggrade = -$grade; - $gradeoptions["$grade"] = "$percentage %"; - $gradeoptionsfull["$grade"] = "$percentage %"; - $gradeoptionsfull["$neggrade"] = -$percentage." %"; - } - $gradeoptionsfull["0"] = $gradeoptions["0"] = get_string("none"); - - // sort lists - arsort($gradeoptions, SORT_NUMERIC); - arsort($gradeoptionsfull, SORT_NUMERIC); - - // construct return object - $grades = new stdClass; - $grades->gradeoptions = $gradeoptions; - $grades->gradeoptionsfull = $gradeoptionsfull; + $grades = new stdClass(); + $grades->gradeoptions = question_bank::fraction_options(); + $grades->gradeoptionsfull = question_bank::fraction_options_full(); return $grades; } @@ -463,21 +233,20 @@ function get_grade_options() { * @return mixed either 'fixed' value or false if erro */ function match_grade_options($gradeoptionsfull, $grade, $matchgrades='error') { - // if we just need an error... - if ($matchgrades=='error') { - foreach($gradeoptionsfull as $value => $option) { + if ($matchgrades == 'error') { + // if we just need an error... + foreach ($gradeoptionsfull as $value => $option) { // slightly fuzzy test, never check floats for equality :-) - if (abs($grade-$value)<0.00001) { + if (abs($grade - $value) < 0.00001) { return $grade; } } // didn't find a match so that's an error return false; - } - // work out nearest value - else if ($matchgrades=='nearest') { + } else if ($matchgrades == 'nearest') { + // work out nearest value $hownear = array(); - foreach($gradeoptionsfull as $value => $option) { + foreach ($gradeoptionsfull as $value => $option) { if ($grade==$value) { return $grade; } @@ -487,39 +256,49 @@ function match_grade_options($gradeoptionsfull, $grade, $matchgrades='error') { asort( $hownear, SORT_NUMERIC ); reset( $hownear ); return key( $hownear ); - } - else { + } else { return false; } } /** - * Tests whether a category is in use by any activity module - * - * @global object - * @return boolean - * @param integer $categoryid - * @param boolean $recursive Whether to examine category children recursively + * @deprecated Since Moodle 2.1. Use {@link question_category_in_use} instead. + * @param integer $categoryid a question category id. + * @param boolean $recursive whether to check child categories too. + * @return boolean whether any question in this category is in use. */ function question_category_isused($categoryid, $recursive = false) { + throw new coding_exception('question_category_isused has been deprectated. ' . + 'Please use question_category_in_use instead.'); +} + +/** + * Tests whether any question in a category is used by any part of Moodle. + * + * @param integer $categoryid a question category id. + * @param boolean $recursive whether to check child categories too. + * @return boolean whether any question in this category is in use. + */ +function question_category_in_use($categoryid, $recursive = false) { global $DB; //Look at each question in the category - if ($questions = $DB->get_records('question', array('category'=>$categoryid), '', 'id,qtype')) { - foreach ($questions as $question) { - if (count(question_list_instances($question->id))) { - return true; - } + if ($questions = $DB->get_records_menu('question', + array('category' => $categoryid), '', 'id, 1')) { + if (questions_in_use(array_keys($questions))) { + return true; } } + if (!$recursive) { + return false; + } //Look under child categories recursively - if ($recursive) { - if ($children = $DB->get_records('question_categories', array('parent'=>$categoryid))) { - foreach ($children as $child) { - if (question_category_isused($child->id, $recursive)) { - return true; - } + if ($children = $DB->get_records('question_categories', + array('parent' => $categoryid), '', 'id, 1')) { + foreach ($children as $child) { + if (question_category_in_use($child->id, $recursive)) { + return true; } } } @@ -527,44 +306,14 @@ function question_category_isused($categoryid, $recursive = false) { return false; } -/** - * Deletes all data associated to an attempt from the database - * - * @global object - * @global object - * @param integer $attemptid The id of the attempt being deleted - */ -function delete_attempt($attemptid) { - global $QTYPES, $DB; - - $states = $DB->get_records('question_states', array('attempt'=>$attemptid)); - if ($states) { - $stateslist = implode(',', array_keys($states)); - - // delete question-type specific data - foreach ($QTYPES as $qtype) { - $qtype->delete_states($stateslist); - } - } - - // delete entries from all other question tables - // It is important that this is done only after calling the questiontype functions - $DB->delete_records("question_states", array("attempt"=>$attemptid)); - $DB->delete_records("question_sessions", array("attemptid"=>$attemptid)); - $DB->delete_records("question_attempts", array("id"=>$attemptid)); -} - /** * Deletes question and all associated data from the database * * It will not delete a question if it is used by an activity module - * - * @global object - * @global object * @param object $question The question being deleted */ -function delete_question($questionid) { - global $QTYPES, $DB; +function question_delete_question($questionid) { + global $DB; $question = $DB->get_record_sql(' SELECT q.*, qc.contextid @@ -579,48 +328,37 @@ function delete_question($questionid) { } // Do not delete a question if it is used by an activity module - if (count(question_list_instances($questionid))) { + if (questions_in_use(array($questionid))) { return; } - // delete questiontype-specific data + // Check permissions. question_require_capability_on($question, 'edit'); - if (isset($QTYPES[$question->qtype])) { - $QTYPES[$question->qtype]->delete_question($questionid, $question->contextid); - } - if ($states = $DB->get_records('question_states', array('question'=>$questionid))) { - $stateslist = implode(',', array_keys($states)); + $dm = new question_engine_data_mapper(); + $dm->delete_previews($questionid); - // delete questiontype-specific data - foreach ($QTYPES as $qtype) { - $qtype->delete_states($stateslist); - } - } - - // Delete entries from all other question tables - // It is important that this is done only after calling the questiontype functions - $DB->delete_records('question_answers', array('question' => $questionid)); - $DB->delete_records('question_states', array('question' => $questionid)); - $DB->delete_records('question_sessions', array('questionid' => $questionid)); + // delete questiontype-specific data + question_bank::get_qtype($question->qtype, false)->delete_question( + $questionid, $question->contextid); // Now recursively delete all child questions - if ($children = $DB->get_records('question', array('parent' => $questionid), '', 'id,qtype')) { + if ($children = $DB->get_records('question', + array('parent' => $questionid), '', 'id, qtype')) { foreach ($children as $child) { if ($child->id != $questionid) { - delete_question($child->id); + question_delete_question($child->id); } } } // Finally delete the question record itself - $DB->delete_records('question', array('id'=>$questionid)); + $DB->delete_records('question', array('id' => $questionid)); } /** * All question categories and their questions are deleted for this course. * - * @global object * @param object $mod an object representing the activity * @param boolean $feedback to specify if the process must output a summary of its work * @return boolean @@ -634,7 +372,8 @@ function question_delete_course($course, $feedback=true) { //Cache some strings $strcatdeleted = get_string('unusedcategorydeleted', 'quiz'); $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id); - $categoriescourse = $DB->get_records('question_categories', array('contextid'=>$coursecontext->id), 'parent', 'id, parent, name, contextid'); + $categoriescourse = $DB->get_records('question_categories', + array('contextid' => $coursecontext->id), 'parent', 'id, parent, name, contextid'); if ($categoriescourse) { @@ -646,14 +385,15 @@ function question_delete_course($course, $feedback=true) { //Delete it completely (questions and category itself) //deleting questions - if ($questions = $DB->get_records('question', array('category' => $category->id), '', 'id,qtype')) { + if ($questions = $DB->get_records('question', + array('category' => $category->id), '', 'id,qtype')) { foreach ($questions as $question) { - delete_question($question->id); + question_delete_question($question->id); } - $DB->delete_records("question", array("category"=>$category->id)); + $DB->delete_records("question", array("category" => $category->id)); } //delete the category - $DB->delete_records('question_categories', array('id'=>$category->id)); + $DB->delete_records('question_categories', array('id' => $category->id)); //Fill feedback $feedbackdata[] = array($category->name, $strcatdeleted); @@ -661,7 +401,7 @@ function question_delete_course($course, $feedback=true) { //Inform about changes performed if feedback is enabled if ($feedback) { $table = new html_table(); - $table->head = array(get_string('category','quiz'), get_string('action')); + $table->head = array(get_string('category', 'quiz'), get_string('action')); $table->data = $feedbackdata; echo html_writer::table($table); } @@ -674,9 +414,9 @@ function question_delete_course($course, $feedback=true) { * 1/ All question categories and their questions are deleted for this course category. * 2/ All questions are moved to new category * - * @global object * @param object $category course category object - * @param object $newcategory empty means everything deleted, otherwise id of category where content moved + * @param object $newcategory empty means everything deleted, otherwise id of + * category where content moved * @param boolean $feedback to specify if the process must output a summary of its work * @return boolean */ @@ -690,28 +430,34 @@ function question_delete_course_category($category, $newcategory, $feedback=true $strcatdeleted = get_string('unusedcategorydeleted', 'quiz'); // Loop over question categories. - if ($categories = $DB->get_records('question_categories', array('contextid'=>$context->id), 'parent', 'id, parent, name')) { + if ($categories = $DB->get_records('question_categories', + array('contextid'=>$context->id), 'parent', 'id, parent, name')) { foreach ($categories as $category) { // Deal with any questions in the category. - if ($questions = $DB->get_records('question', array('category' => $category->id), '', 'id,qtype')) { + if ($questions = $DB->get_records('question', + array('category' => $category->id), '', 'id,qtype')) { // Try to delete each question. foreach ($questions as $question) { - delete_question($question->id); + question_delete_question($question->id); } - // Check to see if there were any questions that were kept because they are - // still in use somehow, even though quizzes in courses in this category will - // already have been deteted. This could happen, for example, if questions are - // added to a course, and then that course is moved to another category (MDL-14802). - $questionids = $DB->get_records_menu('question', array('category'=>$category->id), '', 'id,1'); + // Check to see if there were any questions that were kept because + // they are still in use somehow, even though quizzes in courses + // in this category will already have been deteted. This could + // happen, for example, if questions are added to a course, + // and then that course is moved to another category (MDL-14802). + $questionids = $DB->get_records_menu('question', + array('category'=>$category->id), '', 'id, 1'); if (!empty($questionids)) { - if (!$rescueqcategory = question_save_from_deletion(array_keys($questionids), - get_parent_contextid($context), print_context_name($context), $rescueqcategory)) { + if (!$rescueqcategory = question_save_from_deletion( + array_keys($questionids), get_parent_contextid($context), + print_context_name($context), $rescueqcategory)) { return false; - } - $feedbackdata[] = array($category->name, get_string('questionsmovedto', 'question', $rescueqcategory->name)); + } + $feedbackdata[] = array($category->name, + get_string('questionsmovedto', 'question', $rescueqcategory->name)); } } @@ -727,7 +473,7 @@ function question_delete_course_category($category, $newcategory, $feedback=true // Output feedback if requested. if ($feedback and $feedbackdata) { $table = new html_table(); - $table->head = array(get_string('questioncategory','question'), get_string('action')); + $table->head = array(get_string('questioncategory', 'question'), get_string('action')); $table->data = $feedbackdata; echo html_writer::table($table); } @@ -737,12 +483,14 @@ function question_delete_course_category($category, $newcategory, $feedback=true if (!$newcontext = get_context_instance(CONTEXT_COURSECAT, $newcategory->id)) { return false; } - $DB->set_field('question_categories', 'contextid', $newcontext->id, array('contextid'=>$context->id)); + $DB->set_field('question_categories', 'contextid', $newcontext->id, + array('contextid'=>$context->id)); if ($feedback) { - $a = new stdClass; + $a = new stdClass(); $a->oldplace = print_context_name($context); $a->newplace = print_context_name($newcontext); - echo $OUTPUT->notification(get_string('movedquestionsandcategories', 'question', $a), 'notifysuccess'); + echo $OUTPUT->notification( + get_string('movedquestionsandcategories', 'question', $a), 'notifysuccess'); } } @@ -752,14 +500,15 @@ function question_delete_course_category($category, $newcategory, $feedback=true /** * Enter description here... * - * @global object * @param string $questionids list of questionids * @param object $newcontext the context to create the saved category in. - * @param string $oldplace a textual description of the think being deleted, e.g. from get_context_name + * @param string $oldplace a textual description of the think being deleted, + * e.g. from get_context_name * @param object $newcategory * @return mixed false on */ -function question_save_from_deletion($questionids, $newcontextid, $oldplace, $newcategory = null) { +function question_save_from_deletion($questionids, $newcontextid, $oldplace, + $newcategory = null) { global $DB; // Make a category in the parent context to move the questions to. @@ -784,7 +533,6 @@ function question_save_from_deletion($questionids, $newcontextid, $oldplace, $ne /** * All question categories and their questions are deleted for this activity. * - * @global object * @param object $cm the course module object representing the activity * @param boolean $feedback to specify if the process must output a summary of its work * @return boolean @@ -798,7 +546,8 @@ function question_delete_activity($cm, $feedback=true) { //Cache some strings $strcatdeleted = get_string('unusedcategorydeleted', 'quiz'); $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id); - if ($categoriesmods = $DB->get_records('question_categories', array('contextid'=>$modcontext->id), 'parent', 'id, parent, name, contextid')){ + if ($categoriesmods = $DB->get_records('question_categories', + array('contextid' => $modcontext->id), 'parent', 'id, parent, name, contextid')) { //Sort categories following their tree (parent-child) relationships //this will make the feedback more readable $categoriesmods = sort_categories_by_tree($categoriesmods); @@ -807,9 +556,10 @@ function question_delete_activity($cm, $feedback=true) { //Delete it completely (questions and category itself) //deleting questions - if ($questions = $DB->get_records('question', array('category' => $category->id), '', 'id,qtype')) { + if ($questions = $DB->get_records('question', + array('category' => $category->id), '', 'id,qtype')) { foreach ($questions as $question) { - delete_question($question->id); + question_delete_question($question->id); } $DB->delete_records("question", array("category"=>$category->id)); } @@ -822,7 +572,7 @@ function question_delete_activity($cm, $feedback=true) { //Inform about changes performed if feedback is enabled if ($feedback) { $table = new html_table(); - $table->head = array(get_string('category','quiz'), get_string('action')); + $table->head = array(get_string('category', 'quiz'), get_string('action')); $table->data = $feedbackdata; echo html_writer::table($table); } @@ -832,16 +582,16 @@ function question_delete_activity($cm, $feedback=true) { /** * This function should be considered private to the question bank, it is called from - * question/editlib.php question/contextmoveq.php and a few similar places to to the work of - * acutally moving questions and associated data. However, callers of this function also have to - * do other work, which is why you should not call this method directly from outside the questionbank. + * question/editlib.php question/contextmoveq.php and a few similar places to to the + * work of acutally moving questions and associated data. However, callers of this + * function also have to do other work, which is why you should not call this method + * directly from outside the questionbank. * - * @global object * @param string $questionids a comma-separated list of question ids. * @param integer $newcategoryid the id of the category to move to. */ function question_move_questions_to_category($questionids, $newcategoryid) { - global $DB, $QTYPES; + global $DB; $newcontextid = $DB->get_field('question_categories', 'contextid', array('id' => $newcategoryid)); @@ -853,16 +603,18 @@ function question_move_questions_to_category($questionids, $newcategoryid) { WHERE q.id $questionidcondition", $params); foreach ($questions as $question) { if ($newcontextid != $question->contextid) { - $QTYPES[$question->qtype]->move_files($question->id, - $question->contextid, $newcontextid); + question_bank::get_qtype($question->qtype)->move_files( + $question->id, $question->contextid, $newcontextid); } } // Move the questions themselves. - $DB->set_field_select('question', 'category', $newcategoryid, "id $questionidcondition", $params); + $DB->set_field_select('question', 'category', $newcategoryid, + "id $questionidcondition", $params); // Move any subquestions belonging to them. - $DB->set_field_select('question', 'category', $newcategoryid, "parent $questionidcondition", $params); + $DB->set_field_select('question', 'category', $newcategoryid, + "parent $questionidcondition", $params); // TODO Deal with datasets. @@ -878,26 +630,78 @@ function question_move_questions_to_category($questionids, $newcategoryid) { * @param integer $newcontextid the new context id. */ function question_move_category_to_context($categoryid, $oldcontextid, $newcontextid) { - global $DB, $QTYPES; + global $DB; $questionids = $DB->get_records_menu('question', array('category' => $categoryid), '', 'id,qtype'); foreach ($questionids as $questionid => $qtype) { - $QTYPES[$qtype]->move_files($questionid, $oldcontextid, $newcontextid); + question_bank::get_qtype($qtype)->move_files( + $questionid, $oldcontextid, $newcontextid); } $subcatids = $DB->get_records_menu('question_categories', array('parent' => $categoryid), '', 'id,1'); foreach ($subcatids as $subcatid => $notused) { - $DB->set_field('question_categories', 'contextid', $newcontextid, array('id' => $subcatid)); + $DB->set_field('question_categories', 'contextid', $newcontextid, + array('id' => $subcatid)); question_move_category_to_context($subcatid, $oldcontextid, $newcontextid); } } /** - * Given a list of ids, load the basic information about a set of questions from the questions table. - * The $join and $extrafields arguments can be used together to pull in extra data. - * See, for example, the usage in mod/quiz/attemptlib.php, and + * Generate the URL for starting a new preview of a given question with the given options. + * @param integer $questionid the question to preview. + * @param string $preferredbehaviour the behaviour to use for the preview. + * @param float $maxmark the maximum to mark the question out of. + * @param question_display_options $displayoptions the display options to use. + * @param int $variant the variant of the question to preview. If null, one will + * be picked randomly. + * @return string the URL. + */ +function question_preview_url($questionid, $preferredbehaviour = null, + $maxmark = null, $displayoptions = null, $variant = null) { + + $params = array('id' => $questionid); + + if (!is_null($preferredbehaviour)) { + $params['behaviour'] = $preferredbehaviour; + } + + if (!is_null($maxmark)) { + $params['maxmark'] = $maxmark; + } + + if (!is_null($displayoptions)) { + $params['correctness'] = $displayoptions->correctness; + $params['marks'] = $displayoptions->marks; + $params['markdp'] = $displayoptions->markdp; + $params['feedback'] = (bool) $displayoptions->feedback; + $params['generalfeedback'] = (bool) $displayoptions->generalfeedback; + $params['rightanswer'] = (bool) $displayoptions->rightanswer; + $params['history'] = (bool) $displayoptions->history; + } + + if ($variant) { + $params['variant'] = $variant; + } + + return new moodle_url('/question/preview.php', $params); +} + +/** + * @return array that can be passed as $params to the {@link popup_action} constructor. + */ +function question_preview_popup_params() { + return array( + 'height' => 600, + 'width' => 800, + ); +} + +/** + * Given a list of ids, load the basic information about a set of questions from + * the questions table. The $join and $extrafields arguments can be used together + * to pull in extra data. See, for example, the usage in mod/quiz/attemptlib.php, and * read the code below to see how the SQL is assembled. Throws exceptions on error. * * @global object @@ -911,8 +715,9 @@ function question_move_category_to_context($categoryid, $oldcontextid, $newconte * @return array partially complete question objects. You need to call get_question_options * on them before they can be properly used. */ -function question_preload_questions($questionids, $extrafields = '', $join = '', $extraparams = array()) { - global $CFG, $DB; +function question_preload_questions($questionids, $extrafields = '', $join = '', + $extraparams = array()) { + global $DB; if (empty($questionids)) { return array(); } @@ -924,12 +729,14 @@ function question_preload_questions($questionids, $extrafields = '', $join = '', } list($questionidcondition, $params) = $DB->get_in_or_equal( $questionids, SQL_PARAMS_NAMED, 'qid0000'); - $sql = 'SELECT q.*' . $extrafields . ' FROM {question} q' . $join . - ' WHERE q.id ' . $questionidcondition; + $sql = 'SELECT q.*, qc.contextid' . $extrafields . ' FROM {question} q + JOIN {question_categories} qc ON q.category = qc.id' . + $join . + ' WHERE q.id ' . $questionidcondition; // Load the questions if (!$questions = $DB->get_records_sql($sql, $extraparams + $params)) { - return 'Could not load questions.'; + return array(); } foreach ($questions as $question) { @@ -972,29 +779,23 @@ function question_load_questions($questionids, $extrafields = '', $join = '') { /** * Private function to factor common code out of get_question_options(). * - * @global object - * @global object * @param object $question the question to tidy. * @param boolean $loadtags load the question tags from the tags table. Optional, default false. - * @return boolean true if successful, else false. */ -function _tidy_question(&$question, $loadtags = false) { - global $CFG, $QTYPES; - if (!array_key_exists($question->qtype, $QTYPES)) { - $question->qtype = 'missingtype'; - $question->questiontext = '

' . get_string('warningmissingtype', 'quiz') . '

' . $question->questiontext; +function _tidy_question($question, $loadtags = false) { + global $CFG; + if (!question_bank::is_qtype_installed($question->qtype)) { + $question->questiontext = html_writer::tag('p', get_string('warningmissingtype', + 'qtype_missingtype')) . $question->questiontext; } - $question->name_prefix = question_make_name_prefix($question->id); - if ($success = $QTYPES[$question->qtype]->get_question_options($question)) { - if (isset($question->_partiallyloaded)) { - unset($question->_partiallyloaded); - } + question_bank::get_qtype($question->qtype)->get_question_options($question); + if (isset($question->_partiallyloaded)) { + unset($question->_partiallyloaded); } if ($loadtags && !empty($CFG->usetags)) { require_once($CFG->dirroot . '/tag/lib.php'); $question->tags = tag_get_tags_array('question', $question->id); } - return $success; } /** @@ -1012,1033 +813,32 @@ function _tidy_question(&$question, $loadtags = false) { function get_question_options(&$questions, $loadtags = false) { if (is_array($questions)) { // deal with an array of questions foreach ($questions as $i => $notused) { - if (!_tidy_question($questions[$i], $loadtags)) { - return false; - } + _tidy_question($questions[$i], $loadtags); } - return true; } else { // deal with single question - return _tidy_question($questions, $loadtags); - } -} - -/** - * Load the basic state information for - * - * @global object - * @param integer $attemptid the attempt id to load the states for. - * @return array an array of state data from the database, you will subsequently - * need to call question_load_states to get fully loaded states that can be - * used by the question types. The states here should be sufficient for - * basic tasks like rendering navigation. - */ -function question_preload_states($attemptid) { - global $DB; - // Note, changes here probably also need to be reflected in - // regrade_question_in_attempt and question_load_specific_state. - - // The questionid field must be listed first so that it is used as the - // array index in the array returned by $DB->get_records_sql - $statefields = 'n.questionid as question, s.id, s.attempt, ' . - 's.seq_number, s.answer, s.timestamp, s.event, s.grade, s.raw_grade, ' . - 's.penalty, n.sumpenalty, n.manualcomment, n.manualcommentformat, ' . - 'n.flagged, n.id as questionsessionid'; - - // Load the newest states for the questions - $sql = "SELECT $statefields - FROM {question_states} s, {question_sessions} n - WHERE s.id = n.newest AND n.attemptid = ?"; - $states = $DB->get_records_sql($sql, array($attemptid)); - if (!$states) { - return false; - } - - // Load the newest graded states for the questions - $sql = "SELECT $statefields - FROM {question_states} s, {question_sessions} n - WHERE s.id = n.newgraded AND n.attemptid = ?"; - $gradedstates = $DB->get_records_sql($sql, array($attemptid)); - - // Hook the two together. - foreach ($states as $questionid => $state) { - $states[$questionid]->_partiallyloaded = true; - if ($gradedstates[$questionid]) { - $states[$questionid]->last_graded = $gradedstates[$questionid]; - $states[$questionid]->last_graded->_partiallyloaded = true; - } - } - - return $states; -} - -/** - * Finish loading the question states that were extracted from the database with - * question_preload_states, creating new states for any question where there - * is not a state in the database. - * - * @global object - * @global object - * @param array $questions the questions to load state for. - * @param array $states the partially loaded states this array is updated. - * @param object $cmoptions options from the module we are loading the states for. E.g. $quiz. - * @param object $attempt The attempt for which the question sessions are - * to be restored or created. - * @param mixed either the id of a previous attempt, if this attmpt is - * building on a previous one, or false for a clean attempt. - * @return true or false for success or failure. - */ -function question_load_states(&$questions, &$states, $cmoptions, $attempt, $lastattemptid = false) { - global $QTYPES, $DB; - - // loop through all questions and set the last_graded states - foreach (array_keys($questions) as $qid) { - if (isset($states[$qid])) { - restore_question_state($questions[$qid], $states[$qid]); - if (isset($states[$qid]->_partiallyloaded)) { - unset($states[$qid]->_partiallyloaded); - } - if (isset($states[$qid]->last_graded)) { - restore_question_state($questions[$qid], $states[$qid]->last_graded); - if (isset($states[$qid]->last_graded->_partiallyloaded)) { - unset($states[$qid]->last_graded->_partiallyloaded); - } - } else { - $states[$qid]->last_graded = clone($states[$qid]); - } - } else { - - if ($lastattemptid) { - // If the new attempt is to be based on this previous attempt. - // Find the responses from the previous attempt and save them to the new session - - // Load the last graded state for the question. Note, $statefields is - // the same as above, except that we don't want n.manualcomment. - $statefields = 'n.questionid as question, s.id, s.attempt, ' . - 's.seq_number, s.answer, s.timestamp, s.event, s.grade, s.raw_grade, ' . - 's.penalty, n.sumpenalty'; - $sql = "SELECT $statefields - FROM {question_states} s, {question_sessions} n - WHERE s.id = n.newest - AND n.attemptid = ? - AND n.questionid = ?"; - if (!$laststate = $DB->get_record_sql($sql, array($lastattemptid, $qid))) { - // Only restore previous responses that have been graded - continue; - } - // Restore the state so that the responses will be restored - restore_question_state($questions[$qid], $laststate); - $states[$qid] = clone($laststate); - unset($states[$qid]->id); - } else { - // create a new empty state - $states[$qid] = new stdClass(); - $states[$qid]->question = $qid; - $states[$qid]->responses = array('' => ''); - $states[$qid]->raw_grade = 0; - } - - // now fill/overide initial values - $states[$qid]->attempt = $attempt->uniqueid; - $states[$qid]->seq_number = 0; - $states[$qid]->timestamp = $attempt->timestart; - $states[$qid]->event = ($attempt->timefinish) ? QUESTION_EVENTCLOSE : QUESTION_EVENTOPEN; - $states[$qid]->grade = 0; - $states[$qid]->penalty = 0; - $states[$qid]->sumpenalty = 0; - $states[$qid]->manualcomment = ''; - $states[$qid]->manualcommentformat = FORMAT_HTML; - $states[$qid]->flagged = 0; - - // Prevent further changes to the session from incrementing the - // sequence number - $states[$qid]->changed = true; - - if ($lastattemptid) { - // prepare the previous responses for new processing - $action = new stdClass; - $action->responses = $laststate->responses; - $action->timestamp = $laststate->timestamp; - $action->event = QUESTION_EVENTSAVE; //emulate save of questions from all pages MDL-7631 - - // Process these responses ... - question_process_responses($questions[$qid], $states[$qid], $action, $cmoptions, $attempt); - - // Fix for Bug #5506: When each attempt is built on the last one, - // preserve the options from any previous attempt. - if ( isset($laststate->options) ) { - $states[$qid]->options = $laststate->options; - } - } else { - // Create the empty question type specific information - if (!$QTYPES[$questions[$qid]->qtype]->create_session_and_responses( - $questions[$qid], $states[$qid], $cmoptions, $attempt)) { - return false; - } - } - $states[$qid]->last_graded = clone($states[$qid]); - } + _tidy_question($questions, $loadtags); } return true; } /** -* Loads the most recent state of each question session from the database -* or create new one. -* -* For each question the most recent session state for the current attempt -* is loaded from the question_states table and the question type specific data and -* responses are added by calling {@link restore_question_state()} which in turn -* calls {@link restore_session_and_responses()} for each question. -* If no states exist for the question instance an empty state object is -* created representing the start of a session and empty question -* type specific information and responses are created by calling -* {@link create_session_and_responses()}. -* -* @return array An array of state objects representing the most recent -* states of the question sessions. -* @param array $questions The questions for which sessions are to be restored or -* created. -* @param object $cmoptions -* @param object $attempt The attempt for which the question sessions are -* to be restored or created. -* @param mixed either the id of a previous attempt, if this attmpt is -* building on a previous one, or false for a clean attempt. -*/ -function get_question_states(&$questions, $cmoptions, $attempt, $lastattemptid = false) { - // Preload the states. - $states = question_preload_states($attempt->uniqueid); - if (!$states) { - $states = array(); - } - - // Then finish the job. - if (!question_load_states($questions, $states, $cmoptions, $attempt, $lastattemptid)) { - return false; - } - - return $states; -} - -/** - * Load a particular previous state of a question. + * Print the icon for the question type * - * @global object - * @param array $question The question to load the state for. - * @param object $cmoptions Options from the specifica activity module, e.g. $quiz. - * @param integer $attemptid The question_attempts this is part of. - * @param integer $stateid The id of a specific state of this question. - * @return object the requested state. False on error. + * @param object $question The question object for which the icon is required. + * Only $question->qtype is used. + * @return string the HTML for the img tag. */ -function question_load_specific_state($question, $cmoptions, $attemptid, $stateid) { - global $DB; +function print_question_icon($question) { + global $OUTPUT; - // Load specified states for the question. - // sess.sumpenalty is probably wrong here shoul really be a sum of penalties from before the one we are asking for. - $sql = 'SELECT st.*, sess.sumpenalty, sess.manualcomment, sess.manualcommentformat, - sess.flagged, sess.id as questionsessionid - FROM {question_states} st, {question_sessions} sess - WHERE st.id = ? - AND st.attempt = ? - AND sess.attemptid = st.attempt - AND st.question = ? - AND sess.questionid = st.question'; - $state = $DB->get_record_sql($sql, array($stateid, $attemptid, $question->id)); - if (!$state) { - return false; - } - restore_question_state($question, $state); + $qtype = question_bank::get_qtype($question->qtype, false); + $namestr = $qtype->menu_name(); - // Load the most recent graded states for the questions before the specified one. - $sql = 'SELECT st.*, sess.sumpenalty, sess.manualcomment, sess.manualcommentformat, - sess.flagged, sess.id as questionsessionid - FROM {question_states} st, {question_sessions} sess - WHERE st.seq_number <= ? - AND st.attempt = ? - AND sess.attemptid = st.attempt - AND st.question = ? - AND sess.questionid = st.question - AND st.event IN ('.QUESTION_EVENTS_GRADED.') '. - 'ORDER BY st.seq_number DESC'; - $gradedstates = $DB->get_records_sql($sql, array($state->seq_number, $attemptid, $question->id), 0, 1); - if (empty($gradedstates)) { - $state->last_graded = clone($state); - } else { - $gradedstate = reset($gradedstates); - restore_question_state($question, $gradedstate); - $state->last_graded = $gradedstate; - } - return $state; -} - -/** -* Creates the run-time fields for the states -* -* Extends the state objects for a question by calling -* {@link restore_session_and_responses()} - * - * @global object -* @param object $question The question for which the state is needed -* @param object $state The state as loaded from the database -* @return boolean Represents success or failure -*/ -function restore_question_state(&$question, &$state) { - global $QTYPES; - - // initialise response to the value in the answer field - $state->responses = array('' => $state->answer); - - // Set the changed field to false; any code which changes the - // question session must set this to true and must increment - // ->seq_number. The save_question_session - // function will save the new state object to the database if the field is - // set to true. - $state->changed = false; - - // Load the question type specific data - return $QTYPES[$question->qtype]->restore_session_and_responses($question, $state); - -} - -/** -* Saves the current state of the question session to the database -* -* The state object representing the current state of the session for the -* question is saved to the question_states table with ->responses[''] saved -* to the answer field of the database table. The information in the -* question_sessions table is updated. -* The question type specific data is then saved. - * - * @global array - * @global object -* @return mixed The id of the saved or updated state or false -* @param object $question The question for which session is to be saved. -* @param object $state The state information to be saved. In particular the -* most recent responses are in ->responses. The object -* is updated to hold the new ->id. -*/ -function save_question_session($question, $state) { - global $QTYPES, $DB; - - // Check if the state has changed - if (!$state->changed && isset($state->id)) { - if (isset($state->newflaggedstate) && $state->flagged != $state->newflaggedstate) { - // If this fails, don't worry too much, it is not critical data. - question_update_flag($state->questionsessionid, $state->newflaggedstate); - } - return $state->id; - } - // Set the legacy answer field - $state->answer = isset($state->responses['']) ? $state->responses[''] : ''; - - // Save the state - if (!empty($state->update)) { // this forces the old state record to be overwritten - $DB->update_record('question_states', $state); - } else { - $state->id = $DB->insert_record('question_states', $state); - } - - // create or update the session - if (!$session = $DB->get_record('question_sessions', array('attemptid' => $state->attempt, 'questionid' => $question->id))) { - $session = new stdClass; - $session->attemptid = $state->attempt; - $session->questionid = $question->id; - $session->newest = $state->id; - // The following may seem weird, but the newgraded field needs to be set - // already even if there is no graded state yet. - $session->newgraded = $state->id; - $session->sumpenalty = $state->sumpenalty; - $session->manualcomment = $state->manualcomment; - $session->manualcommentformat = $state->manualcommentformat; - $session->flagged = !empty($state->newflaggedstate); - $DB->insert_record('question_sessions', $session); - } else { - $session->newest = $state->id; - if (question_state_is_graded($state) or $state->event == QUESTION_EVENTOPEN) { - // this state is graded or newly opened, so it goes into the lastgraded field as well - $session->newgraded = $state->id; - $session->sumpenalty = $state->sumpenalty; - $session->manualcomment = $state->manualcomment; - $session->manualcommentformat = $state->manualcommentformat; - } - $session->flagged = !empty($state->newflaggedstate); - $DB->update_record('question_sessions', $session); - } - - unset($state->answer); - - // Save the question type specific state information and responses - if (!$QTYPES[$question->qtype]->save_session_and_responses($question, $state)) { - return false; - } - - // Reset the changed flag - $state->changed = false; - return $state->id; -} - -/** -* Determines whether a state has been graded by looking at the event field -* -* @return boolean true if the state has been graded -* @param object $state -*/ -function question_state_is_graded($state) { - static $question_events_graded = array(); - if (!$question_events_graded){ - $question_events_graded = explode(',', QUESTION_EVENTS_GRADED); - } - return (in_array($state->event, $question_events_graded)); -} - -/** -* Determines whether a state has been closed by looking at the event field -* -* @return boolean true if the state has been closed -* @param object $state -*/ -function question_state_is_closed($state) { - static $question_events_closed = array(); - if (!$question_events_closed){ - $question_events_closed = explode(',', QUESTION_EVENTS_CLOSED); - } - return (in_array($state->event, $question_events_closed)); -} - - -/** - * Extracts responses from submitted form - * - * This can extract the responses given to one or several questions present on a page - * It returns an array with one entry for each question, indexed by question id - * Each entry is an object with the properties - * ->event The event that has triggered the submission. This is determined by which button - * the user has pressed. - * ->responses An array holding the responses to an individual question, indexed by the - * name of the corresponding form element. - * ->timestamp A unix timestamp - * @return array array of action objects, indexed by question ids. - * @param array $questions an array containing at least all questions that are used on the form - * @param array $formdata the data submitted by the form on the question page - * @param integer $defaultevent the event type used if no 'mark' or 'validate' is submitted - */ -function question_extract_responses($questions, $formdata, $defaultevent=QUESTION_EVENTSAVE) { - - $time = time(); - $actions = array(); - foreach ($formdata as $key => $response) { - // Get the question id from the response name - if (false !== ($quid = question_get_id_from_name_prefix($key))) { - // check if this is a valid id - if (!isset($questions[$quid])) { - print_error('formquestionnotinids', 'question'); - } - - // Remove the name prefix from the name - //decrypt trying - $key = substr($key, strlen($questions[$quid]->name_prefix)); - if (false === $key) { - $key = ''; - } - // Check for question validate and mark buttons & set events - if ($key === 'validate') { - $actions[$quid]->event = QUESTION_EVENTVALIDATE; - } else if ($key === 'submit') { - $actions[$quid]->event = QUESTION_EVENTSUBMIT; - } else { - $actions[$quid]->event = $defaultevent; - } - // Update the state with the new response - $actions[$quid]->responses[$key] = $response; - - // Set the timestamp - $actions[$quid]->timestamp = $time; - } - } - foreach ($actions as $quid => $notused) { - ksort($actions[$quid]->responses); - } - return $actions; -} - - -/** - * Returns the html for question feedback image. - * - * @global object - * @param float $fraction value representing the correctness of the user's - * response to a question. - * @param boolean $selected whether or not the answer is the one that the - * user picked. - * @return string - */ -function question_get_feedback_image($fraction, $selected=true) { - global $CFG, $OUTPUT; - static $icons = array('correct' => 'tick_green', 'partiallycorrect' => 'tick_amber', - 'incorrect' => 'cross_red'); - - if ($selected) { - $size = 'big'; - } else { - $size = 'small'; - } - $class = question_get_feedback_class($fraction); - return '' . get_string($class, 'quiz') . ''; -} - -/** - * Returns the class name for question feedback. - * @param float $fraction value representing the correctness of the user's - * response to a question. - * @return string - */ -function question_get_feedback_class($fraction) { - if ($fraction >= 1/1.01) { - return 'correct'; - } else if ($fraction > 0.0) { - return 'partiallycorrect'; - } else { - return 'incorrect'; - } -} - - -/** -* For a given question in an attempt we walk the complete history of states -* and recalculate the grades as we go along. -* -* This is used when a question is changed and old student -* responses need to be marked with the new version of a question. -* -* @todo Make sure this is not quiz-specific -* - * @global object -* @return boolean Indicates whether the grade has changed -* @param object $question A question object -* @param object $attempt The attempt, in which the question needs to be regraded. -* @param object $cmoptions -* @param boolean $verbose Optional. Whether to print progress information or not. -* @param boolean $dryrun Optional. Whether to make changes to grades records -* or record that changes need to be made for a later regrade. -*/ -function regrade_question_in_attempt($question, $attempt, $cmoptions, $verbose=false, $dryrun=false) { - global $DB, $OUTPUT; - - // load all states for this question in this attempt, ordered in sequence - if ($states = $DB->get_records('question_states', - array('attempt'=>$attempt->uniqueid, 'question'=>$question->id), - 'seq_number ASC')) { - $states = array_values($states); - - // Subtract the grade for the latest state from $attempt->sumgrades to get the - // sumgrades for the attempt without this question. - $attempt->sumgrades -= $states[count($states)-1]->grade; - - // Initialise the replaystate - $replaystate = question_load_specific_state($question, $cmoptions, $attempt->uniqueid, $states[0]->id); - $replaystate->sumpenalty = 0; - $replaystate->last_graded->sumpenalty = 0; - - $changed = false; - for($j = 1; $j < count($states); $j++) { - restore_question_state($question, $states[$j]); - $action = new stdClass; - $action->responses = $states[$j]->responses; - $action->timestamp = $states[$j]->timestamp; - - // Change event to submit so that it will be reprocessed - if (in_array($states[$j]->event, array(QUESTION_EVENTCLOSE, - QUESTION_EVENTGRADE, QUESTION_EVENTCLOSEANDGRADE))) { - $action->event = QUESTION_EVENTSUBMIT; - - // By default take the event that was saved in the database - } else { - $action->event = $states[$j]->event; - } - - if ($action->event == QUESTION_EVENTMANUALGRADE) { - // Ensure that the grade is in range - in the past this was not checked, - // but now it is (MDL-14835) - so we need to ensure the data is valid before - // proceeding. - if ($states[$j]->grade < 0) { - $states[$j]->grade = 0; - $changed = true; - } else if ($states[$j]->grade > $question->maxgrade) { - $states[$j]->grade = $question->maxgrade; - $changed = true; - - } - if (!$dryrun){ - $error = question_process_comment($question, $replaystate, $attempt, - $replaystate->manualcomment, $replaystate->manualcommentformat, $states[$j]->grade); - if (is_string($error)) { - echo $OUTPUT->notification($error); - } - } else { - $replaystate->grade = $states[$j]->grade; - } - } else { - // Reprocess (regrade) responses - if (!question_process_responses($question, $replaystate, - $action, $cmoptions, $attempt) && $verbose) { - $a = new stdClass; - $a->qid = $question->id; - $a->stateid = $states[$j]->id; - echo $OUTPUT->notification(get_string('errorduringregrade', 'question', $a)); - } - // We need rounding here because grades in the DB get truncated - // e.g. 0.33333 != 0.3333333, but we want them to be equal here - if ((round((float)$replaystate->raw_grade, 5) != round((float)$states[$j]->raw_grade, 5)) - or (round((float)$replaystate->penalty, 5) != round((float)$states[$j]->penalty, 5)) - or (round((float)$replaystate->grade, 5) != round((float)$states[$j]->grade, 5))) { - $changed = true; - } - // If this was previously a closed state, and it has been knoced back to - // graded, then fix up the state again. - if ($replaystate->event == QUESTION_EVENTGRADE && - ($states[$j]->event == QUESTION_EVENTCLOSE || - $states[$j]->event == QUESTION_EVENTCLOSEANDGRADE)) { - $replaystate->event = $states[$j]->event; - } - } - - $replaystate->id = $states[$j]->id; - $replaystate->changed = true; - $replaystate->update = true; // This will ensure that the existing database entry is updated rather than a new one created - if (!$dryrun){ - save_question_session($question, $replaystate); - } - } - if ($changed) { - if (!$dryrun){ - // TODO, call a method in quiz to do this, where 'quiz' comes from - // the question_attempts table. - $DB->update_record('quiz_attempts', $attempt); - } - } - if ($changed){ - $toinsert = new stdClass(); - $toinsert->oldgrade = round((float)$states[count($states)-1]->grade, 5); - $toinsert->newgrade = round((float)$replaystate->grade, 5); - $toinsert->attemptid = $attempt->uniqueid; - $toinsert->questionid = $question->id; - //the grade saved is the old grade if the new grade is saved - //it is the new grade if this is a dry run. - $toinsert->regraded = $dryrun?0:1; - $toinsert->timemodified = time(); - $DB->insert_record('quiz_question_regrade', $toinsert); - return true; - } else { - return false; - } - } - return false; -} - -/** -* Processes an array of student responses, grading and saving them as appropriate -* - * @global array -* @param object $question Full question object, passed by reference -* @param object $state Full state object, passed by reference -* @param object $action object with the fields ->responses which -* is an array holding the student responses, -* ->action which specifies the action, e.g., QUESTION_EVENTGRADE, -* and ->timestamp which is a timestamp from when the responses -* were submitted by the student. -* @param object $cmoptions -* @param object $attempt The attempt is passed by reference so that -* during grading its ->sumgrades field can be updated -* @return boolean Indicates success/failure -*/ -function question_process_responses($question, &$state, $action, $cmoptions, &$attempt) { - global $QTYPES; - - // if no responses are set initialise to empty response - if (!isset($action->responses)) { - $action->responses = array('' => ''); - } - - $state->newflaggedstate = !empty($action->responses['_flagged']); - - // make sure these are gone! - unset($action->responses['submit'], $action->responses['validate'], $action->responses['_flagged']); - - // Check the question session is still open - if (question_state_is_closed($state)) { - return true; - } - - // If $action->event is not set that implies saving - if (! isset($action->event)) { - debugging('Ambiguous action in question_process_responses.' , DEBUG_DEVELOPER); - $action->event = QUESTION_EVENTSAVE; - } - // If submitted then compare against last graded - // responses, not last given responses in this case - if (question_isgradingevent($action->event)) { - $state->responses = $state->last_graded->responses; - } - - // Check for unchanged responses (exactly unchanged, not equivalent). - // We also have to catch questions that the student has not yet attempted - $sameresponses = $QTYPES[$question->qtype]->compare_responses($question, $action, $state); - if (!empty($state->last_graded) && $state->last_graded->event == QUESTION_EVENTOPEN && - question_isgradingevent($action->event)) { - $sameresponses = false; - } - - // If the response has not been changed then we do not have to process it again - // unless the attempt is closing or validation is requested - if ($sameresponses and QUESTION_EVENTCLOSE != $action->event - and QUESTION_EVENTVALIDATE != $action->event) { - return true; - } - - // Roll back grading information to last graded state and set the new - // responses - $newstate = clone($state->last_graded); - $newstate->responses = $action->responses; - $newstate->seq_number = $state->seq_number + 1; - $newstate->changed = true; // will assure that it gets saved to the database - $newstate->last_graded = clone($state->last_graded); - $newstate->timestamp = $action->timestamp; - $newstate->newflaggedstate = $state->newflaggedstate; - $newstate->flagged = $state->flagged; - $newstate->questionsessionid = $state->questionsessionid; - $state = $newstate; - - // Set the event to the action we will perform. The question type specific - // grading code may override this by setting it to QUESTION_EVENTCLOSE if the - // attempt at the question causes the session to close - $state->event = $action->event; - - if (!question_isgradingevent($action->event)) { - // Grade the response but don't update the overall grade - if (!$QTYPES[$question->qtype]->grade_responses($question, $state, $cmoptions)) { - return false; - } - - // Temporary hack because question types are not given enough control over what is going - // on. Used by Opaque questions. - // TODO fix this code properly. - if (!empty($state->believeevent)) { - // If the state was graded we need to ... - if (question_state_is_graded($state)) { - question_apply_penalty_and_timelimit($question, $state, $attempt, $cmoptions); - - // update the attempt grade - $attempt->sumgrades -= (float)$state->last_graded->grade; - $attempt->sumgrades += (float)$state->grade; - - // and update the last_graded field. - unset($state->last_graded); - $state->last_graded = clone($state); - unset($state->last_graded->changed); - } - } else { - // Don't allow the processing to change the event type - $state->event = $action->event; - } - - } else { // grading event - - // Unless the attempt is closing, we want to work out if the current responses - // (or equivalent responses) were already given in the last graded attempt. - if(QUESTION_EVENTCLOSE != $action->event && QUESTION_EVENTOPEN != $state->last_graded->event && - $QTYPES[$question->qtype]->compare_responses($question, $state, $state->last_graded)) { - $state->event = QUESTION_EVENTDUPLICATE; - } - - // If we did not find a duplicate or if the attempt is closing, perform grading - if ((!$sameresponses and QUESTION_EVENTDUPLICATE != $state->event) or - QUESTION_EVENTCLOSE == $action->event) { - if (!$QTYPES[$question->qtype]->grade_responses($question, $state, $cmoptions)) { - return false; - } - - // Calculate overall grade using correct penalty method - question_apply_penalty_and_timelimit($question, $state, $attempt, $cmoptions); - } - - // If the state was graded we need to ... - if (question_state_is_graded($state)) { - // update the attempt grade - $attempt->sumgrades -= (float)$state->last_graded->grade; - $attempt->sumgrades += (float)$state->grade; - - // and update the last_graded field. - unset($state->last_graded); - $state->last_graded = clone($state); - unset($state->last_graded->changed); - } - } - $attempt->timemodified = $action->timestamp; - - return true; -} - -/** -* Determine if event requires grading -*/ -function question_isgradingevent($event) { - return (QUESTION_EVENTSUBMIT == $event || QUESTION_EVENTCLOSE == $event); -} - -/** -* Applies the penalty from the previous graded responses to the raw grade -* for the current responses -* -* The grade for the question in the current state is computed by subtracting the -* penalty accumulated over the previous graded responses at the question from the -* raw grade. If the timestamp is more than 1 minute beyond the end of the attempt -* the grade is set to zero. The ->grade field of the state object is modified to -* reflect the new grade but is never allowed to decrease. -* @param object $question The question for which the penalty is to be applied. -* @param object $state The state for which the grade is to be set from the -* raw grade and the cumulative penalty from the last -* graded state. The ->grade field is updated by applying -* the penalty scheme determined in $cmoptions to the ->raw_grade and -* ->last_graded->penalty fields. -* @param object $cmoptions The options set by the course module. -* The ->penaltyscheme field determines whether penalties -* for incorrect earlier responses are subtracted. -*/ -function question_apply_penalty_and_timelimit(&$question, &$state, $attempt, $cmoptions) { - // TODO. Quiz dependancy. The fact that the attempt that is passed in here - // is from quiz_attempts, and we use things like $cmoptions->timelimit. - - // deal with penalty - if ($cmoptions->penaltyscheme) { - $state->grade = $state->raw_grade - $state->sumpenalty; - $state->sumpenalty += (float) $state->penalty; - } else { - $state->grade = $state->raw_grade; - } - - // deal with timelimit - if ($cmoptions->timelimit) { - // We allow for 5% uncertainty in the following test - if ($state->timestamp - $attempt->timestart > $cmoptions->timelimit * 1.05) { - $cm = get_coursemodule_from_instance('quiz', $cmoptions->id); - if (!has_capability('mod/quiz:ignoretimelimits', get_context_instance(CONTEXT_MODULE, $cm->id), - $attempt->userid, false)) { - $state->grade = 0; - } - } - } - - // deal with closing time - if ($cmoptions->timeclose and $state->timestamp > ($cmoptions->timeclose + 60) // allowing 1 minute lateness - and !$attempt->preview) { // ignore closing time for previews - $state->grade = 0; - } - - // Ensure that the grade does not go down - $state->grade = max($state->grade, $state->last_graded->grade); -} - -/** -* Print the icon for the question type -* - * @global array - * @global object -* @param object $question The question object for which the icon is required -* only $question->qtype is used. -* @param boolean $return If true the functions returns the link as a string -*/ -function print_question_icon($question, $return = false) { - global $QTYPES, $CFG, $OUTPUT; - - if (array_key_exists($question->qtype, $QTYPES)) { - $namestr = $QTYPES[$question->qtype]->local_name(); - } else { - $namestr = 'missingtype'; - } - $html = '' .
+    // TODO convert to return a moodle_icon object, or whatever the class is.
+    $html = '<img src=pix_url('icon', $qtype->plugin_name()) . '" alt="' . $namestr . '" title="' . $namestr . '" />'; - if ($return) { - return $html; - } else { - echo $html; - } -} -/** - * @param $question - * @param $state - * @param $prefix - * @param $cmoptions - * @param $caption - */ -function question_print_comment_fields($question, $state, $prefix, $cmoptions, $caption = '') { - global $QTYPES; - $idprefix = preg_replace('/[^-_a-zA-Z0-9]/', '', $prefix); - $otherquestionsinuse = ''; - if (!empty($cmoptions->questions)) { - $otherquestionsinuse = $cmoptions->questions; - } - if (!question_state_is_graded($state) && $QTYPES[$question->qtype]->is_question_manual_graded($question, $otherquestionsinuse)) { - $grade = ''; - } else { - $grade = question_format_grade($cmoptions, $state->last_graded->grade); - } - $maxgrade = question_format_grade($cmoptions, $question->maxgrade); - $fieldsize = strlen($maxgrade) - 1; - if (empty($caption)) { - $caption = format_string($question->name); - } - ?> -
- - -
- $question->maxgrade) { - $a = new stdClass; - $a->grade = $grade; - $a->maxgrade = $question->maxgrade; - $a->name = $question->name; - return get_string('errormanualgradeoutofrange', 'question', $a); - } - - // Update the comment and save it in the database - $comment = trim($comment); - $state->manualcomment = $comment; - $state->manualcommentformat = $commentformat; - $state->newflaggedstate = $state->flagged; - $DB->set_field('question_sessions', 'manualcomment', $comment, array('attemptid'=>$attempt->uniqueid, 'questionid'=>$question->id)); - - // Update the attempt if the score has changed. - if ($grade !== '' && (abs($state->last_graded->grade - $grade) > 0.002 || $state->last_graded->event != QUESTION_EVENTMANUALGRADE)) { - $attempt->sumgrades = $attempt->sumgrades - $state->last_graded->grade + $grade; - $attempt->timemodified = time(); - $DB->update_record('quiz_attempts', $attempt); - - // We want to update existing state (rather than creating new one) if it - // was itself created by a manual grading event. - $state->update = $state->event == QUESTION_EVENTMANUALGRADE; - - // Update the other parts of the state object. - $state->raw_grade = $grade; - $state->grade = $grade; - $state->penalty = 0; - $state->timestamp = time(); - $state->seq_number++; - $state->event = QUESTION_EVENTMANUALGRADE; - - // Update the last graded state (don't simplify!) - unset($state->last_graded); - $state->last_graded = clone($state); - - // We need to indicate that the state has changed in order for it to be saved. - $state->changed = 1; - } - - return true; -} - -/** -* Construct name prefixes for question form element names -* -* Construct the name prefix that should be used for example in the -* names of form elements created by questions. -* This is called by {@link get_question_options()} -* to set $question->name_prefix. -* This name prefix includes the question id which can be -* extracted from it with {@link question_get_id_from_name_prefix()}. -* -* @return string -* @param integer $id The question id -*/ -function question_make_name_prefix($id) { - return 'resp' . $id . '_'; -} - -/** - * Extract question id from the prefix of form element names - * - * @return integer The question id - * @param string $name The name that contains a prefix that was - * constructed with {@link question_make_name_prefix()} - */ -function question_get_id_from_name_prefix($name) { - if (!preg_match('/^resp([0-9]+)_/', $name, $matches)) { - return false; - } - return (integer) $matches[1]; -} - -/** - * Extract question id from the prefix of form element names - * - * @return integer The question id - * @param string $name The name that contains a prefix that was - * constructed with {@link question_make_name_prefix()} - */ -function question_id_and_key_from_post_name($name) { - if (!preg_match('/^resp([0-9]+)_(.*)$/', $name, $matches)) { - return array(false, false); - } - return array((integer) $matches[1], $matches[2]); -} - -/** - * Returns the unique id for a new attempt - * - * Every module can keep their own attempts table with their own sequential ids but - * the question code needs to also have a unique id by which to identify all these - * attempts. Hence a module, when creating a new attempt, calls this function and - * stores the return value in the 'uniqueid' field of its attempts table. - * - * @global object - */ -function question_new_attempt_uniqueid($modulename='quiz') { - global $DB; - - $attempt = new stdClass; - $attempt->modulename = $modulename; - $id = $DB->insert_record('question_attempts', $attempt); - return $id; + return $html; } /** @@ -2054,191 +854,33 @@ function question_hash($question) { return make_unique_id_code(); } -/** - * Round a grade to to the correct number of decimal places, and format it for display. - * If $cmoptions->questiondecimalpoints is set, that is used, otherwise - * else if $cmoptions->decimalpoints is used, - * otherwise a default of 2 is used, but this should not be relied upon, and generated a developer debug warning. - * However, if $cmoptions->questiondecimalpoints is -1, the means use $cmoptions->decimalpoints. - * - * @param object $cmoptions The modules settings. - * @param float $grade The grade to round. - */ -function question_format_grade($cmoptions, $grade) { - if (isset($cmoptions->questiondecimalpoints) && $cmoptions->questiondecimalpoints != -1) { - $decimalplaces = $cmoptions->questiondecimalpoints; - } else if (isset($cmoptions->decimalpoints)) { - $decimalplaces = $cmoptions->decimalpoints; - } else { - $decimalplaces = 2; - debugging('Code that leads to question_format_grade being called should set ' . - '$cmoptions->questiondecimalpoints or $cmoptions->decimalpoints', DEBUG_DEVELOPER); - } - return format_float($grade, $decimalplaces); -} - -/** - * @return string An inline script that creates a JavaScript object storing - * various strings and bits of configuration that the scripts in qengine.js need - * to get from PHP. - */ -function question_init_qengine_js() { - global $CFG, $PAGE, $OUTPUT; - static $done = false; - if ($done) { - return; - } - $module = array( - 'name' => 'core_question_flags', - 'fullpath' => '/question/flags.js', - 'requires' => array('base', 'dom', 'event-delegate', 'io-base'), - ); - $actionurl = $CFG->wwwroot . '/question/toggleflag.php'; - $flagattributes = array( - 0 => array( - 'src' => $OUTPUT->pix_url('i/unflagged') . '', - 'title' => get_string('clicktoflag', 'question'), - 'alt' => get_string('notflagged', 'question'), - ), - 1 => array( - 'src' => $OUTPUT->pix_url('i/flagged') . '', - 'title' => get_string('clicktounflag', 'question'), - 'alt' => get_string('flagged', 'question'), - ), - ); - $PAGE->requires->js_init_call('M.core_question_flags.init', - array($actionurl, $flagattributes), false, $module); - $done = true; -} - /// FUNCTIONS THAT SIMPLY WRAP QUESTIONTYPE METHODS ////////////////////////////////// /** - * Give the questions in $questionlist a chance to request the CSS or JavaScript - * they need, before the header is printed. - * - * If your code is going to call the print_question function, it must call this - * funciton before print_header. - * - * @param array $questionlist a list of questionids of the questions what will appear on this page. - * @param array $questions an array of question objects, whose keys are question ids. - * Must contain all the questions in $questionlist - * @param array $states an array of question state objects, whose keys are question ids. - * Must contain the state of all the questions in $questionlist - */ -function question_get_html_head_contributions($questionlist, &$questions, &$states) { - global $CFG, $PAGE, $QTYPES; - - // The question engine's own JavaScript. - question_init_qengine_js(); - - // Anything that questions on this page need. - foreach ($questionlist as $questionid) { - $question = $questions[$questionid]; - $QTYPES[$question->qtype]->get_html_head_contributions($question, $states[$questionid]); - } -} - -/** - * Like {@link get_html_head_contributions()} but for the editing page - * question/question.php. + * Get anything that needs to be included in the head of the question editing page + * for a particular question type. This function is called by question/question.php. * * @param $question A question object. Only $question->qtype is used. * @return string Deprecated. Some HTML code that can go inside the head tag. */ function question_get_editing_head_contributions($question) { - global $QTYPES; - $QTYPES[$question->qtype]->get_editing_head_contributions(); + question_bank::get_qtype($question->qtype, false)->get_editing_head_contributions(); } -/** - * Prints a question - * - * Simply calls the question type specific print_question() method. - * - * @global array - * @param object $question The question to be rendered. - * @param object $state The state to render the question in. - * @param integer $number The number for this question. - * @param object $cmoptions The options specified by the course module - * @param object $options An object specifying the rendering options. - */ -function print_question(&$question, &$state, $number, $cmoptions, $options=null, $context=null) { - global $QTYPES; - $QTYPES[$question->qtype]->print_question($question, $state, $number, $cmoptions, $options, $context); -} /** * Saves question options * * Simply calls the question type specific save_question_options() method. - * - * @global array */ function save_question_options($question) { - global $QTYPES; - - $QTYPES[$question->qtype]->save_question_options($question); + question_bank::get_qtype($question->qtype)->save_question_options($question); } -/** -* Gets all teacher stored answers for a given question -* -* Simply calls the question type specific get_all_responses() method. - * - * @global array -*/ -// ULPGC ecastro -function get_question_responses($question, $state) { - global $QTYPES; - $r = $QTYPES[$question->qtype]->get_all_responses($question, $state); - return $r; -} - -/** -* Gets the response given by the user in a particular state -* -* Simply calls the question type specific get_actual_response() method. - * - * @global array -*/ -// ULPGC ecastro -function get_question_actual_response($question, $state) { - global $QTYPES; - - $r = $QTYPES[$question->qtype]->get_actual_response($question, $state); - return $r; -} - -/** -* TODO: document this - * - * @global array -*/ -// ULPGc ecastro -function get_question_fraction_grade($question, $state) { - global $QTYPES; - - $r = $QTYPES[$question->qtype]->get_fractional_grade($question, $state); - return $r; -} -/** - * @global array -* @return integer grade out of 1 that a random guess by a student might score. -*/ -// ULPGc ecastro -function question_get_random_guess_score($question) { - global $QTYPES; - - $r = $QTYPES[$question->qtype]->get_random_guess_score($question); - return $r; -} /// CATEGORY FUNCTIONS ///////////////////////////////////////////////////////////////// /** * returns the categories with their names ordered following parent-child relationships * finally it tries to return pending categories (those being orphaned, whose parent is * incorrect) to avoid missing any category from original array. - * - * @global object */ function sort_categories_by_tree(&$categories, $id = 0, $level = 1) { global $DB; @@ -2250,18 +892,23 @@ function sort_categories_by_tree(&$categories, $id = 0, $level = 1) { if (!isset($categories[$key]->processed) && $categories[$key]->parent == $id) { $children[$key] = $categories[$key]; $categories[$key]->processed = true; - $children = $children + sort_categories_by_tree($categories, $children[$key]->id, $level+1); + $children = $children + sort_categories_by_tree( + $categories, $children[$key]->id, $level+1); } } - //If level = 1, we have finished, try to look for non processed categories (bad parent) and sort them too + //If level = 1, we have finished, try to look for non processed categories + // (bad parent) and sort them too if ($level == 1) { foreach ($keys as $key) { - // If not processed and it's a good candidate to start (because its parent doesn't exist in the course) - if (!isset($categories[$key]->processed) && !$DB->record_exists( - 'question_categories', array('contextid'=>$categories[$key]->contextid, 'id'=>$categories[$key]->parent))) { + // If not processed and it's a good candidate to start (because its + // parent doesn't exist in the course) + if (!isset($categories[$key]->processed) && !$DB->record_exists('question_categories', + array('contextid' => $categories[$key]->contextid, + 'id' => $categories[$key]->parent))) { $children[$key] = $categories[$key]; $categories[$key]->processed = true; - $children = $children + sort_categories_by_tree($categories, $children[$key]->id, $level+1); + $children = $children + sort_categories_by_tree( + $categories, $children[$key]->id, $level + 1); } } } @@ -2287,12 +934,14 @@ function flatten_category_tree(&$categories, $id, $depth = 0, $nochildrenof = -1 // Indent the name of this category. $newcategories = array(); $newcategories[$id] = $categories[$id]; - $newcategories[$id]->indentedname = str_repeat('   ', $depth) . $categories[$id]->name; + $newcategories[$id]->indentedname = str_repeat('   ', $depth) . + $categories[$id]->name; // Recursively indent the children. foreach ($categories[$id]->childids as $childid) { - if ($childid != $nochildrenof){ - $newcategories = $newcategories + flatten_category_tree($categories, $childid, $depth + 1, $nochildrenof); + if ($childid != $nochildrenof) { + $newcategories = $newcategories + flatten_category_tree( + $categories, $childid, $depth + 1, $nochildrenof); } } @@ -2310,8 +959,9 @@ function flatten_category_tree(&$categories, $id, $depth = 0, $nochildrenof = -1 */ function add_indented_names($categories, $nochildrenof = -1) { - // Add an array to each category to hold the child category ids. This array will be removed - // again by flatten_category_tree(). It should not be used outside these two functions. + // Add an array to each category to hold the child category ids. This array + // will be removed again by flatten_category_tree(). It should not be used + // outside these two functions. foreach (array_keys($categories) as $id) { $categories[$id]->childids = array(); } @@ -2321,7 +971,8 @@ function add_indented_names($categories, $nochildrenof = -1) { // categories from other courses, but not their parents. $toplevelcategoryids = array(); foreach (array_keys($categories) as $id) { - if (!empty($categories[$id]->parent) && array_key_exists($categories[$id]->parent, $categories)) { + if (!empty($categories[$id]->parent) && + array_key_exists($categories[$id]->parent, $categories)) { $categories[$categories[$id]->parent]->childids[] = $id; } else { $toplevelcategoryids[] = $id; @@ -2331,7 +982,8 @@ function add_indented_names($categories, $nochildrenof = -1) { // Flatten the tree to and add the indents. $newcategories = array(); foreach ($toplevelcategoryids as $id) { - $newcategories = $newcategories + flatten_category_tree($categories, $id, 0, $nochildrenof); + $newcategories = $newcategories + flatten_category_tree( + $categories, $id, 0, $nochildrenof); } return $newcategories; @@ -2346,32 +998,35 @@ function add_indented_names($categories, $nochildrenof = -1) { * @param integer $courseid the id of the course to get the categories for. * @param integer $published if true, include publised categories from other courses. * @param integer $only_editable if true, exclude categories this user is not allowed to edit. - * @param integer $selected optionally, the id of a category to be selected by default in the dropdown. + * @param integer $selected optionally, the id of a category to be selected by + * default in the dropdown. */ -function question_category_select_menu($contexts, $top = false, $currentcat = 0, $selected = "", $nochildrenof = -1) { +function question_category_select_menu($contexts, $top = false, $currentcat = 0, + $selected = "", $nochildrenof = -1) { global $OUTPUT; - $categoriesarray = question_category_options($contexts, $top, $currentcat, false, $nochildrenof); + $categoriesarray = question_category_options($contexts, $top, $currentcat, + false, $nochildrenof); if ($selected) { $choose = ''; } else { $choose = 'choosedots'; } $options = array(); - foreach($categoriesarray as $group=>$opts) { - $options[] = array($group=>$opts); + foreach ($categoriesarray as $group => $opts) { + $options[] = array($group => $opts); } echo html_writer::select($options, 'category', $selected, $choose); } /** - * @global object * @param integer $contextid a context id. * @return object the default question category for that context, or false if none. */ function question_get_default_category($contextid) { global $DB; - $category = $DB->get_records('question_categories', array('contextid' => $contextid),'id','*',0,1); + $category = $DB->get_records('question_categories', + array('contextid' => $contextid), 'id', '*', 0, 1); if (!empty($category)) { return reset($category); } else { @@ -2380,8 +1035,375 @@ function question_get_default_category($contextid) { } /** - * @global object - * @global object + * Gets the default category in the most specific context. + * If no categories exist yet then default ones are created in all contexts. + * + * @param array $contexts The context objects for this context and all parent contexts. + * @return object The default category - the category in the course context + */ +function question_make_default_categories($contexts) { + global $DB; + static $preferredlevels = array( + CONTEXT_COURSE => 4, + CONTEXT_MODULE => 3, + CONTEXT_COURSECAT => 2, + CONTEXT_SYSTEM => 1, + ); + + $toreturn = null; + $preferredness = 0; + // If it already exists, just return it. + foreach ($contexts as $key => $context) { + if (!$exists = $DB->record_exists("question_categories", + array('contextid' => $context->id))) { + // Otherwise, we need to make one + $category = new stdClass(); + $contextname = print_context_name($context, false, true); + $category->name = get_string('defaultfor', 'question', $contextname); + $category->info = get_string('defaultinfofor', 'question', $contextname); + $category->contextid = $context->id; + $category->parent = 0; + // By default, all categories get this number, and are sorted alphabetically. + $category->sortorder = 999; + $category->stamp = make_unique_id_code(); + $category->id = $DB->insert_record('question_categories', $category); + } else { + $category = question_get_default_category($context->id); + } + if ($preferredlevels[$context->contextlevel] > $preferredness && has_any_capability( + array('moodle/question:usemine', 'moodle/question:useall'), $context)) { + $toreturn = $category; + $preferredness = $preferredlevels[$context->contextlevel]; + } + } + + if (!is_null($toreturn)) { + $toreturn = clone($toreturn); + } + return $toreturn; +} + +/** + * Get all the category objects, including a count of the number of questions in that category, + * for all the categories in the lists $contexts. + * + * @param mixed $contexts either a single contextid, or a comma-separated list of context ids. + * @param string $sortorder used as the ORDER BY clause in the select statement. + * @return array of category objects. + */ +function get_categories_for_contexts($contexts, $sortorder = 'parent, sortorder, name ASC') { + global $DB; + return $DB->get_records_sql(" + SELECT c.*, (SELECT count(1) FROM {question} q + WHERE c.id = q.category AND q.hidden='0' AND q.parent='0') AS questioncount + FROM {question_categories} c + WHERE c.contextid IN ($contexts) + ORDER BY $sortorder"); +} + +/** + * Output an array of question categories. + */ +function question_category_options($contexts, $top = false, $currentcat = 0, + $popupform = false, $nochildrenof = -1) { + global $CFG; + $pcontexts = array(); + foreach ($contexts as $context) { + $pcontexts[] = $context->id; + } + $contextslist = join($pcontexts, ', '); + + $categories = get_categories_for_contexts($contextslist); + + $categories = question_add_context_in_key($categories); + + if ($top) { + $categories = question_add_tops($categories, $pcontexts); + } + $categories = add_indented_names($categories, $nochildrenof); + + // sort cats out into different contexts + $categoriesarray = array(); + foreach ($pcontexts as $pcontext) { + $contextstring = print_context_name( + get_context_instance_by_id($pcontext), true, true); + foreach ($categories as $category) { + if ($category->contextid == $pcontext) { + $cid = $category->id; + if ($currentcat != $cid || $currentcat == 0) { + $countstring = !empty($category->questioncount) ? + " ($category->questioncount)" : ''; + $categoriesarray[$contextstring][$cid] = $category->indentedname.$countstring; + } + } + } + } + if ($popupform) { + $popupcats = array(); + foreach ($categoriesarray as $contextstring => $optgroup) { + $group = array(); + foreach ($optgroup as $key => $value) { + $key = str_replace($CFG->wwwroot, '', $key); + $group[$key] = $value; + } + $popupcats[] = array($contextstring => $group); + } + return $popupcats; + } else { + return $categoriesarray; + } +} + +function question_add_context_in_key($categories) { + $newcatarray = array(); + foreach ($categories as $id => $category) { + $category->parent = "$category->parent,$category->contextid"; + $category->id = "$category->id,$category->contextid"; + $newcatarray["$id,$category->contextid"] = $category; + } + return $newcatarray; +} + +function question_add_tops($categories, $pcontexts) { + $topcats = array(); + foreach ($pcontexts as $context) { + $newcat = new stdClass(); + $newcat->id = "0,$context"; + $newcat->name = get_string('top'); + $newcat->parent = -1; + $newcat->contextid = $context; + $topcats["0,$context"] = $newcat; + } + //put topcats in at beginning of array - they'll be sorted into different contexts later. + return array_merge($topcats, $categories); +} + +/** + * @return array of question category ids of the category and all subcategories. + */ +function question_categorylist($categoryid) { + global $DB; + + $subcategories = $DB->get_records('question_categories', + array('parent' => $categoryid), 'sortorder ASC', 'id, 1'); + + $categorylist = array($categoryid); + foreach ($subcategories as $subcategory) { + $categorylist = array_merge($categorylist, question_categorylist($subcategory->id)); + } + + return $categorylist; +} + +//=========================== +// Import/Export Functions +//=========================== + +/** + * Get list of available import or export formats + * @param string $type 'import' if import list, otherwise export list assumed + * @return array sorted list of import/export formats available + */ +function get_import_export_formats($type) { + global $CFG; + + $fileformats = get_plugin_list('qformat'); + + $fileformatname = array(); + require_once($CFG->dirroot . '/question/format.php'); + foreach ($fileformats as $fileformat => $fdir) { + $formatfile = $fdir . '/format.php'; + if (is_readable($formatfile)) { + include_once($formatfile); + } else { + continue; + } + + $classname = 'qformat_' . $fileformat; + $formatclass = new $classname(); + if ($type == 'import') { + $provided = $formatclass->provide_import(); + } else { + $provided = $formatclass->provide_export(); + } + + if ($provided) { + $fileformatnames[$fileformat] = get_string($fileformat, 'qformat_' . $fileformat); + } + } + + textlib_get_instance()->asort($fileformatnames); + return $fileformatnames; +} + + +/** + * Create a reasonable default file name for exporting questions from a particular + * category. + * @param object $course the course the questions are in. + * @param object $category the question category. + * @return string the filename. + */ +function question_default_export_filename($course, $category) { + // We build a string that is an appropriate name (questions) from the lang pack, + // then the corse shortname, then the question category name, then a timestamp. + + $base = clean_filename(get_string('exportfilename', 'question')); + + $dateformat = str_replace(' ', '_', get_string('exportnameformat', 'question')); + $timestamp = clean_filename(userdate(time(), $dateformat, 99, false)); + + $shortname = clean_filename($course->shortname); + if ($shortname == '' || $shortname == '_' ) { + $shortname = $course->id; + } + + $categoryname = clean_filename(format_string($category->name)); + + return "{$base}-{$shortname}-{$categoryname}-{$timestamp}"; + + return $export_name; +} + +/** + * Converts contextlevels to strings and back to help with reading/writing contexts + * to/from import/export files. + * + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class context_to_string_translator{ + /** + * @var array used to translate between contextids and strings for this context. + */ + protected $contexttostringarray = array(); + + public function __construct($contexts) { + $this->generate_context_to_string_array($contexts); + } + + public function context_to_string($contextid) { + return $this->contexttostringarray[$contextid]; + } + + public function string_to_context($contextname) { + $contextid = array_search($contextname, $this->contexttostringarray); + return $contextid; + } + + protected function generate_context_to_string_array($contexts) { + if (!$this->contexttostringarray) { + $catno = 1; + foreach ($contexts as $context) { + switch ($context->contextlevel) { + case CONTEXT_MODULE : + $contextstring = 'module'; + break; + case CONTEXT_COURSE : + $contextstring = 'course'; + break; + case CONTEXT_COURSECAT : + $contextstring = "cat$catno"; + $catno++; + break; + case CONTEXT_SYSTEM : + $contextstring = 'system'; + break; + } + $this->contexttostringarray[$context->id] = $contextstring; + } + } + } + +} + +/** + * Check capability on category + * + * @param mixed $question object or id + * @param string $cap 'add', 'edit', 'view', 'use', 'move' + * @param integer $cachecat useful to cache all question records in a category + * @return boolean this user has the capability $cap for this question $question? + */ +function question_has_capability_on($question, $cap, $cachecat = -1) { + global $USER, $DB; + + // these are capabilities on existing questions capabilties are + //set per category. Each of these has a mine and all version. Append 'mine' and 'all' + $question_questioncaps = array('edit', 'view', 'use', 'move'); + static $questions = array(); + static $categories = array(); + static $cachedcat = array(); + if ($cachecat != -1 && array_search($cachecat, $cachedcat) === false) { + $questions += $DB->get_records('question', array('category' => $cachecat)); + $cachedcat[] = $cachecat; + } + if (!is_object($question)) { + if (!isset($questions[$question])) { + if (!$questions[$question] = $DB->get_record('question', + array('id' => $question), 'id,category,createdby')) { + print_error('questiondoesnotexist', 'question'); + } + } + $question = $questions[$question]; + } + if (!isset($categories[$question->category])) { + if (!$categories[$question->category] = $DB->get_record('question_categories', + array('id'=>$question->category))) { + print_error('invalidcategory', 'quiz'); + } + } + $category = $categories[$question->category]; + $context = get_context_instance_by_id($category->contextid); + + if (array_search($cap, $question_questioncaps)!== false) { + if (!has_capability('moodle/question:' . $cap . 'all', $context)) { + if ($question->createdby == $USER->id) { + return has_capability('moodle/question:' . $cap . 'mine', $context); + } else { + return false; + } + } else { + return true; + } + } else { + return has_capability('moodle/question:' . $cap, $context); + } + +} + +/** + * Require capability on question. + */ +function question_require_capability_on($question, $cap) { + if (!question_has_capability_on($question, $cap)) { + print_error('nopermissions', '', '', $cap); + } + return true; +} + +/** + * Get the real state - the correct question id and answer - for a random + * question. + * @param object $state with property answer. + * @return mixed return integer real question id or false if there was an + * error.. + */ +function question_get_real_state($state) { + global $OUTPUT; + $realstate = clone($state); + $matches = array(); + if (!preg_match('|^random([0-9]+)-(.*)|', $state->answer, $matches)) { + echo $OUTPUT->notification(get_string('errorrandom', 'quiz_statistics')); + return false; + } else { + $realstate->question = $matches[1]; + $realstate->answer = $matches[2]; + return $realstate; + } +} + +/** * @param object $context a context * @return string A URL for editing questions in this context. */ @@ -2411,294 +1433,45 @@ function question_edit_url($context) { } /** -* Gets the default category in the most specific context. -* If no categories exist yet then default ones are created in all contexts. -* - * @global object -* @param array $contexts The context objects for this context and all parent contexts. -* @return object The default category - the category in the course context -*/ -function question_make_default_categories($contexts) { - global $DB; - static $preferredlevels = array( - CONTEXT_COURSE => 4, - CONTEXT_MODULE => 3, - CONTEXT_COURSECAT => 2, - CONTEXT_SYSTEM => 1, - ); - - $toreturn = null; - $preferredness = 0; - // If it already exists, just return it. - foreach ($contexts as $key => $context) { - if (!$exists = $DB->record_exists("question_categories", array('contextid'=>$context->id))) { - // Otherwise, we need to make one - $category = new stdClass; - $contextname = print_context_name($context, false, true); - $category->name = get_string('defaultfor', 'question', $contextname); - $category->info = get_string('defaultinfofor', 'question', $contextname); - $category->contextid = $context->id; - $category->parent = 0; - $category->sortorder = 999; // By default, all categories get this number, and are sorted alphabetically. - $category->stamp = make_unique_id_code(); - $category->id = $DB->insert_record('question_categories', $category); - } else { - $category = question_get_default_category($context->id); - } - if ($preferredlevels[$context->contextlevel] > $preferredness && - has_any_capability(array('moodle/question:usemine', 'moodle/question:useall'), $context)) { - $toreturn = $category; - $preferredness = $preferredlevels[$context->contextlevel]; - } - } - - if (!is_null($toreturn)) { - $toreturn = clone($toreturn); - } - return $toreturn; -} - -/** - * Get all the category objects, including a count of the number of questions in that category, - * for all the categories in the lists $contexts. + * Adds question bank setting links to the given navigation node if caps are met. * - * @global object - * @param mixed $contexts either a single contextid, or a comma-separated list of context ids. - * @param string $sortorder used as the ORDER BY clause in the select statement. - * @return array of category objects. + * @param navigation_node $navigationnode The navigation node to add the question branch to + * @param object $context + * @return navigation_node Returns the question branch that was added */ -function get_categories_for_contexts($contexts, $sortorder = 'parent, sortorder, name ASC') { - global $DB; - return $DB->get_records_sql(" - SELECT c.*, (SELECT count(1) FROM {question} q - WHERE c.id = q.category AND q.hidden='0' AND q.parent='0') AS questioncount - FROM {question_categories} c - WHERE c.contextid IN ($contexts) - ORDER BY $sortorder"); -} +function question_extend_settings_navigation(navigation_node $navigationnode, $context) { + global $PAGE; -/** - * Output an array of question categories. - * @global object - */ -function question_category_options($contexts, $top = false, $currentcat = 0, $popupform = false, $nochildrenof = -1) { - global $CFG; - $pcontexts = array(); - foreach($contexts as $context){ - $pcontexts[] = $context->id; - } - $contextslist = join($pcontexts, ', '); - - $categories = get_categories_for_contexts($contextslist); - - $categories = question_add_context_in_key($categories); - - if ($top){ - $categories = question_add_tops($categories, $pcontexts); - } - $categories = add_indented_names($categories, $nochildrenof); - - //sort cats out into different contexts - $categoriesarray = array(); - foreach ($pcontexts as $pcontext){ - $contextstring = print_context_name(get_context_instance_by_id($pcontext), true, true); - foreach ($categories as $category) { - if ($category->contextid == $pcontext){ - $cid = $category->id; - if ($currentcat!= $cid || $currentcat==0) { - $countstring = (!empty($category->questioncount))?" ($category->questioncount)":''; - $categoriesarray[$contextstring][$cid] = $category->indentedname.$countstring; - } - } - } - } - if ($popupform){ - $popupcats = array(); - foreach ($categoriesarray as $contextstring => $optgroup){ - $group = array(); - foreach ($optgroup as $key=>$value) { - $key = str_replace($CFG->wwwroot, '', $key); - $group[$key] = $value; - } - $popupcats[] = array($contextstring=>$group); - } - return $popupcats; + if ($context->contextlevel == CONTEXT_COURSE) { + $params = array('courseid'=>$context->instanceid); + } else if ($context->contextlevel == CONTEXT_MODULE) { + $params = array('cmid'=>$context->instanceid); } else { - return $categoriesarray; - } -} - -function question_add_context_in_key($categories){ - $newcatarray = array(); - foreach ($categories as $id => $category) { - $category->parent = "$category->parent,$category->contextid"; - $category->id = "$category->id,$category->contextid"; - $newcatarray["$id,$category->contextid"] = $category; - } - return $newcatarray; -} -function question_add_tops($categories, $pcontexts){ - $topcats = array(); - foreach ($pcontexts as $context){ - $newcat = new stdClass(); - $newcat->id = "0,$context"; - $newcat->name = get_string('top'); - $newcat->parent = -1; - $newcat->contextid = $context; - $topcats["0,$context"] = $newcat; - } - //put topcats in at beginning of array - they'll be sorted into different contexts later. - return array_merge($topcats, $categories); -} - -/** - * Returns a comma separated list of ids of the category and all subcategories - * @global object - */ -function question_categorylist($categoryid) { - global $DB; - - // returns a comma separated list of ids of the category and all subcategories - $categorylist = $categoryid; - if ($subcategories = $DB->get_records('question_categories', array('parent'=>$categoryid), 'sortorder ASC', 'id, 1')) { - foreach ($subcategories as $subcategory) { - $categorylist .= ','. question_categorylist($subcategory->id); - } - } - return $categorylist; -} - - - - -//=========================== -// Import/Export Functions -//=========================== - -/** - * Get list of available import or export formats - * - * @global object - * @param string $type 'import' if import list, otherwise export list assumed - * @return array sorted list of import/export formats available - */ -function get_import_export_formats( $type ) { - - global $CFG; - $fileformats = get_plugin_list("qformat"); - - $fileformatname=array(); - require_once( "{$CFG->dirroot}/question/format.php" ); - foreach ($fileformats as $fileformat=>$fdir) { - $format_file = "$fdir/format.php"; - if (file_exists($format_file) ) { - require_once($format_file); - } - else { - continue; - } - $classname = "qformat_$fileformat"; - $format_class = new $classname(); - if ($type=='import') { - $provided = $format_class->provide_import(); - } - else { - $provided = $format_class->provide_export(); - } - if ($provided) { - $formatname = get_string($fileformat, 'quiz'); - if ($formatname == "[[$fileformat]]") { - $formatname = get_string($fileformat, 'qformat_'.$fileformat); - if ($formatname == "[[$fileformat]]") { - $formatname = $fileformat; // Just use the raw folder name - } - } - $fileformatnames[$fileformat] = $formatname; - } - } - natcasesort($fileformatnames); - - return $fileformatnames; -} - - -/** -* Create a reasonable default file name for exporting questions from a particular -* category. -* @param object $course the course the questions are in. -* @param object $category the question category. -* @return string the filename. -*/ -function question_default_export_filename($course, $category) { - // We build a string that is an appropriate name (questions) from the lang pack, - // then the corse shortname, then the question category name, then a timestamp. - - $base = clean_filename(get_string('exportfilename', 'question')); - - $dateformat = str_replace(' ', '_', get_string('exportnameformat', 'question')); - $timestamp = clean_filename(userdate(time(), $dateformat, 99, false)); - - $shortname = clean_filename($course->shortname); - if ($shortname == '' || $shortname == '_' ) { - $shortname = $course->id; + return; } - $categoryname = clean_filename(format_string($category->name)); + $questionnode = $navigationnode->add(get_string('questionbank', 'question'), + new moodle_url('/question/edit.php', $params), navigation_node::TYPE_CONTAINER); - return "{$base}-{$shortname}-{$categoryname}-{$timestamp}"; - - return $export_name; -} - -/** - * @package moodlecore - * @subpackage question - * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class context_to_string_translator{ - /** - * @var array used to translate between contextids and strings for this context. - */ - var $contexttostringarray = array(); - - function context_to_string_translator($contexts){ - $this->generate_context_to_string_array($contexts); - } - - function context_to_string($contextid){ - return $this->contexttostringarray[$contextid]; - } - - function string_to_context($contextname){ - $contextid = array_search($contextname, $this->contexttostringarray); - return $contextid; - } - - function generate_context_to_string_array($contexts){ - if (!$this->contexttostringarray){ - $catno = 1; - foreach ($contexts as $context){ - switch ($context->contextlevel){ - case CONTEXT_MODULE : - $contextstring = 'module'; - break; - case CONTEXT_COURSE : - $contextstring = 'course'; - break; - case CONTEXT_COURSECAT : - $contextstring = "cat$catno"; - $catno++; - break; - case CONTEXT_SYSTEM : - $contextstring = 'system'; - break; - } - $this->contexttostringarray[$context->id] = $contextstring; - } - } + $contexts = new question_edit_contexts($context); + if ($contexts->have_one_edit_tab_cap('questions')) { + $questionnode->add(get_string('questions', 'quiz'), new moodle_url( + '/question/edit.php', $params), navigation_node::TYPE_SETTING); + } + if ($contexts->have_one_edit_tab_cap('categories')) { + $questionnode->add(get_string('categories', 'quiz'), new moodle_url( + '/question/category.php', $params), navigation_node::TYPE_SETTING); + } + if ($contexts->have_one_edit_tab_cap('import')) { + $questionnode->add(get_string('import', 'quiz'), new moodle_url( + '/question/import.php', $params), navigation_node::TYPE_SETTING); + } + if ($contexts->have_one_edit_tab_cap('export')) { + $questionnode->add(get_string('export', 'quiz'), new moodle_url( + '/question/export.php', $params), navigation_node::TYPE_SETTING); } + return $questionnode; } /** @@ -2728,196 +1501,9 @@ function question_get_all_capabilities() { return $caps; } -/** - * Check capability on category - * - * @global object - * @global object - * @param mixed $question object or id - * @param string $cap 'add', 'edit', 'view', 'use', 'move' - * @param integer $cachecat useful to cache all question records in a category - * @return boolean this user has the capability $cap for this question $question? - */ -function question_has_capability_on($question, $cap, $cachecat = -1){ - global $USER, $DB; - - // nicolasconnault@gmail.com In some cases I get $question === false. Since no such object exists, it can't be deleted, we can safely return true - if ($question === false) { - return true; - } - - // these are capabilities on existing questions capabilties are - //set per category. Each of these has a mine and all version. Append 'mine' and 'all' - $question_questioncaps = array('edit', 'view', 'use', 'move'); - static $questions = array(); - static $categories = array(); - static $cachedcat = array(); - if ($cachecat != -1 && array_search($cachecat, $cachedcat) === false) { - $questions += $DB->get_records('question', array('category' => $cachecat)); - $cachedcat[] = $cachecat; - } - if (!is_object($question)){ - if (!isset($questions[$question])){ - if (!$questions[$question] = $DB->get_record('question', array('id' => $question), 'id,category,createdby')) { - print_error('questiondoesnotexist', 'question'); - } - } - $question = $questions[$question]; - } - if (!isset($categories[$question->category])){ - if (!$categories[$question->category] = $DB->get_record('question_categories', array('id'=>$question->category))) { - print_error('invalidcategory', 'quiz'); - } - } - $category = $categories[$question->category]; - $context = get_context_instance_by_id($category->contextid); - - if (array_search($cap, $question_questioncaps)!== FALSE){ - if (!has_capability('moodle/question:'.$cap.'all', $context)){ - if ($question->createdby == $USER->id){ - return has_capability('moodle/question:'.$cap.'mine', $context); - } else { - return false; - } - } else { - return true; - } - } else { - return has_capability('moodle/question:'.$cap, $context); - } - -} - -/** - * Require capability on question. - */ -function question_require_capability_on($question, $cap){ - if (!question_has_capability_on($question, $cap)){ - print_error('nopermissions', '', '', $cap); - } - return true; -} - -/** - * Get the real state - the correct question id and answer - for a random - * question. - * @param object $state with property answer. - * @return mixed return integer real question id or false if there was an - * error.. - */ -function question_get_real_state($state) { - global $OUTPUT; - $realstate = clone($state); - $matches = array(); - if (!preg_match('|^random([0-9]+)-(.*)|', $state->answer, $matches)){ - echo $OUTPUT->notification(get_string('errorrandom', 'quiz_statistics')); - return false; - } else { - $realstate->question = $matches[1]; - $realstate->answer = $matches[2]; - return $realstate; - } -} - -/** - * Update the flagged state of a particular question session. - * - * @global object - * @param integer $sessionid question_session id. - * @param boolean $newstate the new state for the flag. - * @return boolean success or failure. - */ -function question_update_flag($sessionid, $newstate) { - global $DB; - return $DB->set_field('question_sessions', 'flagged', $newstate, array('id' => $sessionid)); -} - -/** - * Update the flagged state of all the questions in an attempt, where a new . - * - * @global object - * @param integer $sessionid question_session id. - * @param boolean $newstate the new state for the flag. - * @return boolean success or failure. - */ -function question_save_flags($formdata, $attemptid, $questionids) { - global $DB; - $donequestionids = array(); - foreach ($formdata as $postvariable => $value) { - list($qid, $key) = question_id_and_key_from_post_name($postvariable); - if ($qid !== false && in_array($qid, $questionids)) { - if ($key == '_flagged') { - $DB->set_field('question_sessions', 'flagged', !empty($value), - array('attemptid' => $attemptid, 'questionid' => $qid)); - $donequestionids[$qid] = 1; - } - } - } - foreach ($questionids as $qid) { - if (!isset($donequestionids[$qid])) { - $DB->set_field('question_sessions', 'flagged', 0, - array('attemptid' => $attemptid, 'questionid' => $qid)); - } - } -} - -/** - * - * @global object - * @param integer $attemptid the question_attempt id. - * @param integer $questionid the question id. - * @param integer $sessionid the question_session id. - * @param object $user a user, or null to use $USER. - * @return string that needs to be sent to question/toggleflag.php for it to work. - */ -function question_get_toggleflag_checksum($attemptid, $questionid, $sessionid, $user = null) { - if (is_null($user)) { - global $USER; - $user = $USER; - } - return md5($attemptid . "_" . $user->secret . "_" . $questionid . "_" . $sessionid); -} - -/** - * Adds question bank setting links to the given navigation node if caps are met. - * - * @param navigation_node $navigationnode The navigation node to add the question branch to - * @param stdClass $context - * @return navigation_node Returns the question branch that was added - */ -function question_extend_settings_navigation(navigation_node $navigationnode, $context) { - global $PAGE; - - if ($context->contextlevel == CONTEXT_COURSE) { - $params = array('courseid'=>$context->instanceid); - } else if ($context->contextlevel == CONTEXT_MODULE) { - $params = array('cmid'=>$context->instanceid); - } else { - return; - } - - $questionnode = $navigationnode->add(get_string('questionbank','question'), new moodle_url('/question/edit.php', $params), navigation_node::TYPE_CONTAINER); - - $contexts = new question_edit_contexts($context); - if ($contexts->have_one_edit_tab_cap('questions')) { - $questionnode->add(get_string('questions', 'quiz'), new moodle_url('/question/edit.php', $params), navigation_node::TYPE_SETTING); - } - if ($contexts->have_one_edit_tab_cap('categories')) { - $questionnode->add(get_string('categories', 'quiz'), new moodle_url('/question/category.php', $params), navigation_node::TYPE_SETTING); - } - if ($contexts->have_one_edit_tab_cap('import')) { - $questionnode->add(get_string('import', 'quiz'), new moodle_url('/question/import.php', $params), navigation_node::TYPE_SETTING); - } - if ($contexts->have_one_edit_tab_cap('export')) { - $questionnode->add(get_string('export', 'quiz'), new moodle_url('/question/export.php', $params), navigation_node::TYPE_SETTING); - } - - return $questionnode; -} - class question_edit_contexts { - public static $CAPS = array( + public static $caps = array( 'editq' => array('moodle/question:add', 'moodle/question:editmine', 'moodle/question:editall', @@ -2943,10 +1529,10 @@ class question_edit_contexts { /** * @param current context */ - public function question_edit_contexts($thiscontext){ + public function __construct($thiscontext) { $pcontextids = get_parent_contexts($thiscontext); $contexts = array($thiscontext); - foreach ($pcontextids as $pcontextid){ + foreach ($pcontextids as $pcontextid) { $contexts[] = get_context_instance_by_id($pcontextid); } $this->allcontexts = $contexts; @@ -2954,23 +1540,23 @@ class question_edit_contexts { /** * @return array all parent contexts */ - public function all(){ + public function all() { return $this->allcontexts; } /** * @return object lowest context which must be either the module or course context */ - public function lowest(){ + public function lowest() { return $this->allcontexts[0]; } /** * @param string $cap capability * @return array parent contexts having capability, zero based index */ - public function having_cap($cap){ + public function having_cap($cap) { $contextswithcap = array(); - foreach ($this->allcontexts as $context){ - if (has_capability($cap, $context)){ + foreach ($this->allcontexts as $context) { + if (has_capability($cap, $context)) { $contextswithcap[] = $context; } } @@ -2980,11 +1566,11 @@ class question_edit_contexts { * @param array $caps capabilities * @return array parent contexts having at least one of $caps, zero based index */ - public function having_one_cap($caps){ + public function having_one_cap($caps) { $contextswithacap = array(); - foreach ($this->allcontexts as $context){ - foreach ($caps as $cap){ - if (has_capability($cap, $context)){ + foreach ($this->allcontexts as $context) { + foreach ($caps as $cap) { + if (has_capability($cap, $context)) { $contextswithacap[] = $context; break; //done with caps loop } @@ -2996,8 +1582,8 @@ class question_edit_contexts { * @param string $tabname edit tab name * @return array parent contexts having at least one of $caps, zero based index */ - public function having_one_edit_tab_cap($tabname){ - return $this->having_one_cap(self::$CAPS[$tabname]); + public function having_one_edit_tab_cap($tabname) { + return $this->having_one_cap(self::$caps[$tabname]); } /** * Has at least one parent context got the cap $cap? @@ -3005,7 +1591,7 @@ class question_edit_contexts { * @param string $cap capability * @return boolean */ - public function have_cap($cap){ + public function have_cap($cap) { return (count($this->having_cap($cap))); } @@ -3015,7 +1601,7 @@ class question_edit_contexts { * @param array $caps capability * @return boolean */ - public function have_one_cap($caps){ + public function have_one_cap($caps) { foreach ($caps as $cap) { if ($this->have_cap($cap)) { return true; @@ -3023,31 +1609,34 @@ class question_edit_contexts { } return false; } + /** * Has at least one parent context got one of the caps for actions on $tabname * * @param string $tabname edit tab name * @return boolean */ - public function have_one_edit_tab_cap($tabname){ - return $this->have_one_cap(self::$CAPS[$tabname]); + public function have_one_edit_tab_cap($tabname) { + return $this->have_one_cap(self::$caps[$tabname]); } + /** * Throw error if at least one parent context hasn't got the cap $cap * * @param string $cap capability */ - public function require_cap($cap){ - if (!$this->have_cap($cap)){ + public function require_cap($cap) { + if (!$this->have_cap($cap)) { print_error('nopermissions', '', '', $cap); } } + /** * Throw error if at least one parent context hasn't got one of the caps $caps * * @param array $cap capabilities */ - public function require_one_cap($caps) { + public function require_one_cap($caps) { if (!$this->have_one_cap($caps)) { $capsstring = join($caps, ', '); print_error('nopermissions', '', '', $capsstring); @@ -3059,7 +1648,7 @@ class question_edit_contexts { * * @param string $tabname edit tab name */ - public function require_one_edit_tab_cap($tabname){ + public function require_one_edit_tab_cap($tabname) { if (!$this->have_one_edit_tab_cap($tabname)) { print_error('nopermissions', '', '', 'access question edit tab '.$tabname); } @@ -3082,7 +1671,8 @@ class question_edit_contexts { * @param array $options * @return string */ -function quiz_rewrite_question_urls($text, $file, $contextid, $component, $filearea, array $ids, $itemid, array $options=null) { +function question_rewrite_question_urls($text, $file, $contextid, $component, + $filearea, array $ids, $itemid, array $options=null) { global $CFG; $options = (array)$options; @@ -3190,28 +1780,21 @@ function question_pluginfile($course, $context, $component, $filearea, $args, $f send_file_not_found(); } - //DEBUG - //echo ''; - //die; send_file($content, $filename, 0, 0, true, true, $qformat->mime_type()); } - $attemptid = (int)array_shift($args); - $questionid = (int)array_shift($args); + $qubaid = (int)array_shift($args); + $slot = (int)array_shift($args); + $module = $DB->get_field('question_usages', 'component', + array('id' => $qubaid)); - if ($attemptid === 0) { - // preview + if ($module === 'core_question_preview') { require_once($CFG->dirroot . '/question/previewlib.php'); return question_preview_question_pluginfile($course, $context, - $component, $filearea, $attemptid, $questionid, $args, $forcedownload); + $component, $filearea, $qubaid, $slot, $args, $forcedownload); } else { - $module = $DB->get_field('question_attempts', 'modulename', - array('id' => $attemptid)); - $dir = get_component_directory($module); if (!file_exists("$dir/lib.php")) { send_file_not_found(); @@ -3223,32 +1806,13 @@ function question_pluginfile($course, $context, $component, $filearea, $args, $f send_file_not_found(); } - $filefunction($course, $context, $component, $filearea, $attemptid, $questionid, + $filefunction($course, $context, $component, $filearea, $qubaid, $slot, $args, $forcedownload); send_file_not_found(); } } -/** - * Final test for whether a studnet should be allowed to see a particular file. - * This delegates the decision to the question type plugin. - * - * @param object $question The question to be rendered. - * @param object $state The state to render the question in. - * @param object $options An object specifying the rendering options. - * @param string $component the name of the component we are serving files for. - * @param string $filearea the name of the file area. - * @param array $args the remaining bits of the file path. - * @param bool $forcedownload whether the user must be forced to download the file. - */ -function question_check_file_access($question, $state, $options, $contextid, $component, - $filearea, $args, $forcedownload) { - global $QTYPES; - return $QTYPES[$question->qtype]->check_file_access($question, $state, $options, $contextid, $component, - $filearea, $args, $forcedownload); -} - /** * Create url for question export * @@ -3259,8 +1823,11 @@ function question_check_file_access($question, $state, $options, $contextid, $co * @param string $ithcontexts * @param moodle_url export file url */ -function question_make_export_url($contextid, $categoryid, $format, $withcategories, $withcontexts, $filename) { +function question_make_export_url($contextid, $categoryid, $format, $withcategories, + $withcontexts, $filename) { global $CFG; $urlbase = "$CFG->httpswwwroot/pluginfile.php"; - return moodle_url::make_file_url($urlbase, "/$contextid/question/export/{$categoryid}/{$format}/{$withcategories}/{$withcontexts}/{$filename}", true); + return moodle_url::make_file_url($urlbase, + "/$contextid/question/export/{$categoryid}/{$format}/{$withcategories}" . + "/{$withcontexts}/{$filename}", true); } diff --git a/lib/resourcelib.php b/lib/resourcelib.php index 9df4927823c..7aeed914059 100644 --- a/lib/resourcelib.php +++ b/lib/resourcelib.php @@ -487,10 +487,15 @@ function resourcelib_embed_general($fullurl, $title, $clicktoopen, $mimetype) { } $iframe = false; + + $param = ''; + // IE can not embed stuff properly if stored on different server // that is why we use iframe instead, unfortunately this tag does not validate // in xhtml strict mode if ($mimetype === 'text/html' and check_browser_version('MSIE', 5)) { + // The param tag needs to be removed to avoid trouble in IE. + $param = ''; if (preg_match('(^https?://[^/]*)', $fullurl, $matches)) { if (strpos($CFG->wwwroot, $matches[0]) !== 0) { $iframe = true; @@ -510,7 +515,7 @@ EOT; $code = << - + $param $clicktoopen
diff --git a/lib/simpletest/testmoodlelib.php b/lib/simpletest/testmoodlelib.php index 2f8ca704b1c..f8ca529996a 100644 --- a/lib/simpletest/testmoodlelib.php +++ b/lib/simpletest/testmoodlelib.php @@ -515,7 +515,14 @@ class moodlelib_test extends UnitTestCase { } function test_usergetdate() { - global $USER; + global $USER, $CFG; + + //Check if forcetimezone is set then save it and set it to use user timezone + $cfgforcetimezone = null; + if (isset($CFG->forcetimezone)) { + $cfgforcetimezone = $CFG->forcetimezone; + $CFG->forcetimezone = 99; //get user default timezone. + } $userstimezone = $USER->timezone; $USER->timezone = 2;//set the timezone to a known state @@ -559,6 +566,12 @@ class moodlelib_test extends UnitTestCase { //set the timezone back to what it was $USER->timezone = $userstimezone; + + //restore forcetimezone if changed. + if (!is_null($cfgforcetimezone)) { + $CFG->forcetimezone = $cfgforcetimezone; + } + setlocale(LC_TIME, $oldlocale); } @@ -757,4 +770,286 @@ class moodlelib_test extends UnitTestCase { $this->assertTrue(true); } } + + public function test_userdate() { + global $USER, $CFG; + + $testvalues = array( + array( + 'time' => '1309514400', + 'usertimezone' => 'America/Moncton', + 'timezone' => '0.0', //no dst offset + 'expectedoutput' => 'Friday, 1 July 2011, 10:00 AM' + ), + array( + 'time' => '1309514400', + 'usertimezone' => 'America/Moncton', + 'timezone' => '99', //dst offset and timezone offset. + 'expectedoutput' => 'Friday, 1 July 2011, 07:00 AM' + ), + array( + 'time' => '1309514400', + 'usertimezone' => 'America/Moncton', + 'timezone' => 'America/Moncton', //dst offset and timezone offset. + 'expectedoutput' => 'Friday, 1 July 2011, 07:00 AM' + ), + array( + 'time' => '1293876000 ', + 'usertimezone' => 'America/Moncton', + 'timezone' => '0.0', //no dst offset + 'expectedoutput' => 'Saturday, 1 January 2011, 10:00 AM' + ), + array( + 'time' => '1293876000 ', + 'usertimezone' => 'America/Moncton', + 'timezone' => '99', //no dst offset in jan, so just timezone offset. + 'expectedoutput' => 'Saturday, 1 January 2011, 06:00 AM' + ), + array( + 'time' => '1293876000 ', + 'usertimezone' => 'America/Moncton', + 'timezone' => 'America/Moncton', //no dst offset in jan + 'expectedoutput' => 'Saturday, 1 January 2011, 06:00 AM' + ), + array( + 'time' => '1293876000 ', + 'usertimezone' => '2', + 'timezone' => '99', //take user timezone + 'expectedoutput' => 'Saturday, 1 January 2011, 12:00 PM' + ), + array( + 'time' => '1293876000 ', + 'usertimezone' => '-2', + 'timezone' => '99', //take user timezone + 'expectedoutput' => 'Saturday, 1 January 2011, 08:00 AM' + ), + array( + 'time' => '1293876000 ', + 'usertimezone' => '-10', + 'timezone' => '2', //take this timezone + 'expectedoutput' => 'Saturday, 1 January 2011, 12:00 PM' + ), + array( + 'time' => '1293876000 ', + 'usertimezone' => '-10', + 'timezone' => '-2', //take this timezone + 'expectedoutput' => 'Saturday, 1 January 2011, 08:00 AM' + ), + array( + 'time' => '1293876000 ', + 'usertimezone' => '-10', + 'timezone' => 'random/time', //this should show server time + 'expectedoutput' => 'Saturday, 1 January 2011, 06:00 PM' + ), + array( + 'time' => '1293876000 ', + 'usertimezone' => '14', //server time zone + 'timezone' => '99', //this should show user time + 'expectedoutput' => 'Saturday, 1 January 2011, 06:00 PM' + ), + ); + + //Check if forcetimezone is set then save it and set it to use user timezone + $cfgforcetimezone = null; + if (isset($CFG->forcetimezone)) { + $cfgforcetimezone = $CFG->forcetimezone; + $CFG->forcetimezone = 99; //get user default timezone. + } + //store user default timezone to restore later + $userstimezone = $USER->timezone; + + // The string version of date comes from server locale setting and does + // not respect user language, so it is necessary to reset that. + $oldlocale = setlocale(LC_TIME, '0'); + setlocale(LC_TIME, 'en_AU.UTF-8'); + + foreach ($testvalues as $vals) { + $USER->timezone = $vals['usertimezone']; + $actualoutput = userdate($vals['time'], '%A, %d %B %Y, %I:%M %p', $vals['timezone']); + $this->assertEqual($vals['expectedoutput'], $actualoutput, + "Expected: {$vals['expectedoutput']} => Actual: {$actualoutput}, + Please check if timezones are updated (Site adminstration -> location -> update timezone)"); + } + + //restore user timezone back to what it was + $USER->timezone = $userstimezone; + + //restore forcetimezone + if (!is_null($cfgforcetimezone)) { + $CFG->forcetimezone = $cfgforcetimezone; + } + + setlocale(LC_TIME, $oldlocale); + } + + public function test_make_timestamp() { + global $USER, $CFG; + + $testvalues = array( + array( + 'usertimezone' => 'America/Moncton', + 'year' => '2011', + 'month' => '7', + 'day' => '1', + 'hour' => '10', + 'minutes' => '00', + 'seconds' => '00', + 'timezone' => '0.0', //no dst offset + 'applydst' => false, + 'expectedoutput' => '1309528800' + ), + array( + 'usertimezone' => 'America/Moncton', + 'year' => '2011', + 'month' => '7', + 'day' => '1', + 'hour' => '10', + 'minutes' => '00', + 'seconds' => '00', + 'timezone' => '99', //user default timezone + 'applydst' => false, //don't apply dst + 'expectedoutput' => '1309528800' + ), + array( + 'usertimezone' => 'America/Moncton', + 'year' => '2011', + 'month' => '7', + 'day' => '1', + 'hour' => '10', + 'minutes' => '00', + 'seconds' => '00', + 'timezone' => '99', //user default timezone + 'applydst' => true, //apply dst + 'expectedoutput' => '1309525200' + ), + array( + 'usertimezone' => 'America/Moncton', + 'year' => '2011', + 'month' => '7', + 'day' => '1', + 'hour' => '10', + 'minutes' => '00', + 'seconds' => '00', + 'timezone' => 'America/Moncton', //string timezone + 'applydst' => true, //apply dst + 'expectedoutput' => '1309525200' + ), + array( + 'usertimezone' => '2',//no dst applyed + 'year' => '2011', + 'month' => '7', + 'day' => '1', + 'hour' => '10', + 'minutes' => '00', + 'seconds' => '00', + 'timezone' => '99', //take user timezone + 'applydst' => true, //apply dst + 'expectedoutput' => '1309507200' + ), + array( + 'usertimezone' => '-2',//no dst applyed + 'year' => '2011', + 'month' => '7', + 'day' => '1', + 'hour' => '10', + 'minutes' => '00', + 'seconds' => '00', + 'timezone' => '99', //take usertimezone + 'applydst' => true, //apply dst + 'expectedoutput' => '1309521600' + ), + array( + 'usertimezone' => '-10',//no dst applyed + 'year' => '2011', + 'month' => '7', + 'day' => '1', + 'hour' => '10', + 'minutes' => '00', + 'seconds' => '00', + 'timezone' => '2', //take this timezone + 'applydst' => true, //apply dst + 'expectedoutput' => '1309507200' + ), + array( + 'usertimezone' => '-10',//no dst applyed + 'year' => '2011', + 'month' => '7', + 'day' => '1', + 'hour' => '10', + 'minutes' => '00', + 'seconds' => '00', + 'timezone' => '-2', //take this timezone + 'applydst' => true, //apply dst, + 'expectedoutput' => '1309521600' + ), + array( + 'usertimezone' => '-10',//no dst applyed + 'year' => '2011', + 'month' => '7', + 'day' => '1', + 'hour' => '10', + 'minutes' => '00', + 'seconds' => '00', + 'timezone' => 'random/time', //This should show server time + 'applydst' => true, //apply dst, + 'expectedoutput' => '1309485600' + ), + array( + 'usertimezone' => '14',//server time + 'year' => '2011', + 'month' => '7', + 'day' => '1', + 'hour' => '10', + 'minutes' => '00', + 'seconds' => '00', + 'timezone' => '99', //get user time + 'applydst' => true, //apply dst, + 'expectedoutput' => '1309485600' + ) + ); + + //Check if forcetimezone is set then save it and set it to use user timezone + $cfgforcetimezone = null; + if (isset($CFG->forcetimezone)) { + $cfgforcetimezone = $CFG->forcetimezone; + $CFG->forcetimezone = 99; //get user default timezone. + } + + //store user default timezone to restore later + $userstimezone = $USER->timezone; + + // The string version of date comes from server locale setting and does + // not respect user language, so it is necessary to reset that. + $oldlocale = setlocale(LC_TIME, '0'); + setlocale(LC_TIME, 'en_AU.UTF-8'); + + //Test make_timestamp with all testvals and assert if anything wrong. + foreach ($testvalues as $vals) { + $USER->timezone = $vals['usertimezone']; + $actualoutput = make_timestamp( + $vals['year'], + $vals['month'], + $vals['day'], + $vals['hour'], + $vals['minutes'], + $vals['seconds'], + $vals['timezone'], + $vals['applydst'] + ); + + $this->assertEqual($vals['expectedoutput'], $actualoutput, + "Expected: {$vals['expectedoutput']} => Actual: {$actualoutput}, + Please check if timezones are updated (Site adminstration -> location -> update timezone)"); + } + + //restore user timezone back to what it was + $USER->timezone = $userstimezone; + + //restore forcetimezone + if (!is_null($cfgforcetimezone)) { + $CFG->forcetimezone = $cfgforcetimezone; + } + + setlocale(LC_TIME, $oldlocale); + } } diff --git a/lib/simpletest/testquestionlib.php b/lib/simpletest/testquestionlib.php index 61e0f183985..2e1f205beb9 100644 --- a/lib/simpletest/testquestionlib.php +++ b/lib/simpletest/testquestionlib.php @@ -1,140 +1,60 @@ . /** * Unit tests for (some of) ../questionlib.php. * - * @copyright © 2006 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package question + * @package moodlecore + * @subpackage questionbank + * @copyright 2006 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -if (!defined('MOODLE_INTERNAL')) { - die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page -} + +defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/questionlib.php'); + +/** + * Unit tests for (some of) ../questionlib.php. + * + * @copyright 2006 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ class questionlib_test extends UnitTestCase { public static $includecoverage = array('lib/questionlib.php'); - function test_question_sort_qtype_array() { - $config = new stdClass(); - $config->multichoice_sortorder = '1'; - $config->calculated_sortorder = '2'; - $qtypes = array( - 'frog' => 'toad', - 'calculated' => 'newt', - 'multichoice' => 'eft', - ); - $this->assertEqual(question_sort_qtype_array($qtypes), array( - 'multichoice' => 'eft', - 'calculated' => 'newt', - 'frog' => 'toad', - )); - } - - function test_question_reorder_qtypes() { - $this->assertEqual(question_reorder_qtypes(array('t1' => '', 't2' => '', 't3' => ''), 't1', +1), + public function test_question_reorder_qtypes() { + $this->assertEqual(question_reorder_qtypes( + array('t1' => '', 't2' => '', 't3' => ''), 't1', +1), array(0 => 't2', 1 => 't1', 2 => 't3')); - $this->assertEqual(question_reorder_qtypes(array('t1' => '', 't2' => '', 't3' => ''), 't1', -1), + $this->assertEqual(question_reorder_qtypes( + array('t1' => '', 't2' => '', 't3' => ''), 't1', -1), array(0 => 't1', 1 => 't2', 2 => 't3')); - $this->assertEqual(question_reorder_qtypes(array('t1' => '', 't2' => '', 't3' => ''), 't2', -1), + $this->assertEqual(question_reorder_qtypes( + array('t1' => '', 't2' => '', 't3' => ''), 't2', -1), array(0 => 't2', 1 => 't1', 2 => 't3')); - $this->assertEqual(question_reorder_qtypes(array('t1' => '', 't2' => '', 't3' => ''), 't3', +1), + $this->assertEqual(question_reorder_qtypes( + array('t1' => '', 't2' => '', 't3' => ''), 't3', +1), array(0 => 't1', 1 => 't2', 2 => 't3')); - $this->assertEqual(question_reorder_qtypes(array('t1' => '', 't2' => '', 't3' => ''), 'missing', +1), + $this->assertEqual(question_reorder_qtypes( + array('t1' => '', 't2' => '', 't3' => ''), 'missing', +1), array(0 => 't1', 1 => 't2', 2 => 't3')); } - function test_question_state_is_closed() { - $state = new stdClass(); - $state->event = QUESTION_EVENTOPEN; - $this->assertFalse(question_state_is_closed($state)); - - $state->event = QUESTION_EVENTNAVIGATE; - $this->assertFalse(question_state_is_closed($state)); - - $state->event = QUESTION_EVENTSAVE; - $this->assertFalse(question_state_is_closed($state)); - - $state->event = QUESTION_EVENTGRADE; - $this->assertFalse(question_state_is_closed($state)); - - $state->event = QUESTION_EVENTDUPLICATE; - $this->assertFalse(question_state_is_closed($state)); - - $state->event = QUESTION_EVENTVALIDATE; - $this->assertFalse(question_state_is_closed($state)); - - $state->event = QUESTION_EVENTSUBMIT; - $this->assertFalse(question_state_is_closed($state)); - - $state->event = QUESTION_EVENTCLOSEANDGRADE; - $this->assertTrue(question_state_is_closed($state)); - - $state->event = QUESTION_EVENTCLOSE; - $this->assertTrue(question_state_is_closed($state)); - - $state->event = QUESTION_EVENTMANUALGRADE; - $this->assertTrue(question_state_is_closed($state)); - - } - function test_question_state_is_graded() { - $state = new stdClass(); - $state->event = QUESTION_EVENTOPEN; - $this->assertFalse(question_state_is_graded($state)); - - $state->event = QUESTION_EVENTNAVIGATE; - $this->assertFalse(question_state_is_graded($state)); - - $state->event = QUESTION_EVENTSAVE; - $this->assertFalse(question_state_is_graded($state)); - - $state->event = QUESTION_EVENTDUPLICATE; - $this->assertFalse(question_state_is_graded($state)); - - $state->event = QUESTION_EVENTVALIDATE; - $this->assertFalse(question_state_is_graded($state)); - - $state->event = QUESTION_EVENTSUBMIT; - $this->assertFalse(question_state_is_graded($state)); - - $state->event = QUESTION_EVENTCLOSE; - $this->assertFalse(question_state_is_graded($state)); - - $state->event = QUESTION_EVENTCLOSEANDGRADE; - $this->assertTrue(question_state_is_graded($state)); - - $state->event = QUESTION_EVENTMANUALGRADE; - $this->assertTrue(question_state_is_graded($state)); - - $state->event = QUESTION_EVENTGRADE; - $this->assertTrue(question_state_is_graded($state)); - } } - - diff --git a/lib/simpletest/testweblib.php b/lib/simpletest/testweblib.php index 359e2987219..20f6ebf73e1 100644 --- a/lib/simpletest/testweblib.php +++ b/lib/simpletest/testweblib.php @@ -136,6 +136,10 @@ class web_test extends UnitTestCase { $this->assertEqual("\n\nAll the WORLDโ€™S a stage.", html_to_text('

All the worldโ€™s a stage.

')); } + public function test_html_to_text_trailing_whitespace() { + $this->assertEqual('With trailing whitespace and some more text', html_to_text("With trailing whitespace \nand some more text", 0)); + } + public function test_clean_text() { $text = "lala xx"; $this->assertEqual($text, clean_text($text, FORMAT_PLAIN)); @@ -143,7 +147,4 @@ class web_test extends UnitTestCase { $this->assertEqual('lala xx', clean_text($text, FORMAT_MOODLE)); $this->assertEqual('lala xx', clean_text($text, FORMAT_HTML)); } - } - - diff --git a/lib/upgradelib.php b/lib/upgradelib.php index 976d59a59bb..a5f9ba952c6 100644 --- a/lib/upgradelib.php +++ b/lib/upgradelib.php @@ -324,6 +324,9 @@ function upgrade_plugins($type, $startcallback, $endcallback, $verbose) { external_update_descriptions($component); events_update_definition($component); message_update_providers($component); + if ($type === 'message') { + message_update_processors($plug); + } upgrade_plugin_mnet_functions($component); $endcallback($component, true, $verbose); } @@ -357,6 +360,9 @@ function upgrade_plugins($type, $startcallback, $endcallback, $verbose) { external_update_descriptions($component); events_update_definition($component); message_update_providers($component); + if ($type === 'message') { + message_update_processors($plug); + } upgrade_plugin_mnet_functions($component); purge_all_caches(); @@ -387,6 +393,9 @@ function upgrade_plugins($type, $startcallback, $endcallback, $verbose) { external_update_descriptions($component); events_update_definition($component); message_update_providers($component); + if ($type === 'message') { + message_update_processors($plug); + } upgrade_plugin_mnet_functions($component); purge_all_caches(); @@ -895,6 +904,7 @@ function external_update_descriptions($component) { $service['enabled'] = empty($service['enabled']) ? 0 : $service['enabled']; $service['requiredcapability'] = empty($service['requiredcapability']) ? null : $service['requiredcapability']; $service['restrictedusers'] = !isset($service['restrictedusers']) ? 1 : $service['restrictedusers']; + $service['shortname'] = !isset($service['shortname']) ? null : $service['shortname']; $update = false; if ($dbservice->enabled != $service['enabled']) { @@ -909,6 +919,23 @@ function external_update_descriptions($component) { $dbservice->restrictedusers = $service['restrictedusers']; $update = true; } + //if shortname is not a PARAM_ALPHANUMEXT, fail (tested here for service update and creation) + if (isset($service['shortname']) and + (clean_param($service['shortname'], PARAM_ALPHANUMEXT) != $service['shortname'])) { + throw new moodle_exception('installserviceshortnameerror', 'webservice', '', $service['shortname']); + } + if ($dbservice->shortname != $service['shortname']) { + //check that shortname is unique + if (isset($service['shortname'])) { //we currently accepts multiple shortname == null + $existingservice = $DB->get_record('external_services', + array('shortname' => $service['shortname'])); + if (!empty($existingservice)) { + throw new moodle_exception('installexistingserviceshortnameerror', 'webservice', '', $service['shortname']); + } + } + $dbservice->shortname = $service['shortname']; + $update = true; + } if ($update) { $DB->update_record('external_services', $dbservice); } @@ -931,11 +958,21 @@ function external_update_descriptions($component) { unset($functions); } foreach ($services as $name => $service) { + //check that shortname is unique + if (isset($service['shortname'])) { //we currently accepts multiple shortname == null + $existingservice = $DB->get_record('external_services', + array('shortname' => $service['shortname'])); + if (!empty($existingservice)) { + throw new moodle_exception('installserviceshortnameerror', 'webservice'); + } + } + $dbservice = new stdClass(); $dbservice->name = $name; $dbservice->enabled = empty($service['enabled']) ? 0 : $service['enabled']; $dbservice->requiredcapability = empty($service['requiredcapability']) ? null : $service['requiredcapability']; $dbservice->restrictedusers = !isset($service['restrictedusers']) ? 1 : $service['restrictedusers']; + $dbservice->shortname = !isset($service['shortname']) ? null : $service['shortname']; $dbservice->component = $component; $dbservice->timecreated = time(); $dbservice->id = $DB->insert_record('external_services', $dbservice); diff --git a/lib/weblib.php b/lib/weblib.php index 9e1b939943e..fb4b7ea18ad 100644 --- a/lib/weblib.php +++ b/lib/weblib.php @@ -2816,10 +2816,29 @@ function convert_tabrows_to_tree($tabrows, $selected, $inactive, $activated) { */ function get_docs_url($path) { global $CFG; - if (!empty($CFG->docroot)) { - return $CFG->docroot . '/' . current_language() . '/' . $path; + // Check that $CFG->release has been set up, during installation it won't be. + if (empty($CFG->release)) { + // It's not there yet so look at version.php + include($CFG->dirroot.'/version.php'); } else { - return 'http://docs.moodle.org/en/'.$path; + // We can use $CFG->release and avoid having to include version.php + $release = $CFG->release; + } + // Attempt to match the branch from the release + if (preg_match('/^(.)\.(.)/', $release, $matches)) { + // We should ALWAYS get here + $branch = $matches[1].$matches[2]; + } else { + // We should never get here but in case we do lets set $branch to . + // the smart one's will know that this is the current directory + // and the smarter ones will know that there is some smart matching + // that will ensure people end up at the latest version of the docs. + $branch = '.'; + } + if (!empty($CFG->docroot)) { + return $CFG->docroot . '/' . $branch . '/' . current_language() . '/' . $path; + } else { + return 'http://docs.moodle.org/'. $branch . '/en/' . $path; } } diff --git a/local/qeupgradehelper/README.txt b/local/qeupgradehelper/README.txt new file mode 100644 index 00000000000..6167e357612 --- /dev/null +++ b/local/qeupgradehelper/README.txt @@ -0,0 +1,48 @@ +This plugin can help upgrade site with a large number of question attempts from +Moodle 2.0 to 2.1. + +With a lot of question attempts, doing the whole conversion on upgrade is very +slow. The plugin can help with that in various ways. + + +To install using git, type this command in the root of your Moodle install + git clone git://github.com/timhunt/moodle-local_qeupgradehelper.git local/qeupgradehelper +Then add /local/qeupgradehelper to your git ignore. + +Alternatively, download the zip from + https://github.com/timhunt/moodle-local_qeupgradehelper/zipball/master +unzip it into the local folder, and then rename the new folder to qeupgradehelper. + + +When installed in a Moodle 2.0 site: + +1. It provies a report of how much data there is to upgrade. + +2. It can extract test-cases from the database. This can help you report bugs +in the upgrade process to the developers. + +3. You can set up cron to complete the conversion of quiz attempts, if you have +configured a partial upgrade. + + +If this plugin is present during upgrade: + +4. then only a subset of attempts are upgraded. Read the instructions in the +partialupgrade-example.php script. + + +If this plugin is present in a Moodle 2.1 site after upgrade: + +5. If not all attempts have been upgraded in a 2.1 site, then this plugin +displays a list of how many quizzes still need to be upgraded + +6. ... and can be used to complete the upgrade manually ... + +7. or this plugin has a cron script that can be used to finish the upgrade +automatically after the main upgrade has finished. + +8. It can also reset any attempts that were upgraded (provided they have not +subsequently been modified) so you can re-upgrade them. This may allow you to +recover from a buggy upgrade. + +9. Finally, you can still use the extract test-cases script to help report bugs. diff --git a/local/qeupgradehelper/afterupgradelib.php b/local/qeupgradehelper/afterupgradelib.php new file mode 100644 index 00000000000..5799b3972fa --- /dev/null +++ b/local/qeupgradehelper/afterupgradelib.php @@ -0,0 +1,162 @@ +. + + +/** + * Question engine upgrade helper library code that relies on other parts of the + * new question engine code. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once($CFG->dirroot . '/question/engine/upgrade/upgradelib.php'); + + +class local_qeupgradehelper_attempt_upgrader extends question_engine_attempt_upgrader { + public $quizid; + public $attemptsdone = 0; + public $attemptstodo; + + public function __construct($quizid, $attemptstodo) { + $this->quizid = $quizid; + $this->attemptstodo = $attemptstodo; + } + + protected function get_quiz_ids() { + return array($this->quizid => 1); + } + + protected function print_progress($done, $outof, $quizid) { + } + + protected function convert_quiz_attempt($quiz, $attempt, $questionsessionsrs, $questionsstatesrs) { + $this->attemptsdone += 1; + return parent::convert_quiz_attempt($quiz, $attempt, $questionsessionsrs, $questionsstatesrs); + } + + protected function reset_progress($done, $outof) { + if (is_null($this->progressbar)) { + $this->progressbar = new progress_bar('qe2reset'); + } + + gc_collect_cycles(); // This was really helpful in PHP 5.2. Perhaps remove. + $a = new stdClass(); + $a->done = $done; + $a->todo = $outof; + $this->progressbar->update($done, $outof, + get_string('resettingquizattemptsprogress', 'local_qeupgradehelper', $a)); + } + + protected function get_resettable_attempts($quiz) { + global $DB; + return $DB->get_records_sql(" + SELECT + quiza.* + + FROM {quiz_attempts} quiza + LEFT JOIN ( + SELECT attempt, MAX(timestamp) AS time + FROM {question_states} + GROUP BY attempt + ) AS oldtimemodified ON oldtimemodified.attempt = quiza.uniqueid + LEFT JOIN ( + SELECT qa.questionusageid, MAX(qas.timecreated) AS time + FROM {question_attempts} qa + JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id + GROUP BY qa.questionusageid + ) AS newtimemodified ON newtimemodified.questionusageid = quiza.uniqueid + + WHERE quiza.preview = 0 + AND quiza.needsupgradetonewqe = 0 + AND (newtimemodified.time IS NULL OR oldtimemodified.time >= newtimemodified.time) + AND quiza.quiz = :quizid", array('quizid' => $quiz->id)); + } + + public function reset_all_resettable_attempts() { + global $DB; + + $transaction = $DB->start_delegated_transaction(); + + $quiz = $DB->get_record('quiz', array('id' => $this->quizid)); + $attempts = $this->get_resettable_attempts($quiz); + foreach ($attempts as $attempt) { + $this->reset_attempt($quiz, $attempt); + } + + $transaction->allow_commit(); + } + + protected function reset_attempt($quiz, $attempt) { + global $DB; + + $this->attemptsdone += 1; + $this->reset_progress($this->attemptsdone, $this->attemptstodo); + + $questionids = explode(',', $quiz->questions); + $slottoquestionid = array(0 => 0); + foreach ($questionids as $questionid) { + if ($questionid) { + $slottoquestionid[] = $questionid; + } + } + + $slotlayout = explode(',', $attempt->layout); + $oldlayout = array(); + $ok = true; + foreach ($slotlayout as $slot) { + if (array_key_exists($slot, $slottoquestionid)) { + $oldlayout[] = $slottoquestionid[$slot]; + } else if (in_array($slot, $questionids)) { + // OK there was probably a problem during the original upgrade. + $oldlayout[] = $slot; + } else { + $ok = false; + break; + } + } + + if ($ok) { + $layout = implode(',', $oldlayout); + } else { + $layout = $attempt->layout; + } + + $DB->delete_records_select('question_attempt_step_data', "attemptstepid IN ( + SELECT qas.id + FROM {question_attempts} qa + JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id + WHERE questionusageid = :uniqueid)", + array('uniqueid' => $attempt->uniqueid)); + $DB->delete_records_select('question_attempt_steps', "questionattemptid IN ( + SELECT qa.id + FROM {question_attempts} qa + WHERE questionusageid = :uniqueid)", + array('uniqueid' => $attempt->uniqueid)); + $DB->delete_records('question_attempts', + array('questionusageid' => $attempt->uniqueid)); + + $DB->set_field('question_usages', 'preferredbehaviour', 'to_be_set_later', + array('id' => $attempt->uniqueid)); + $DB->set_field('quiz_attempts', 'layout', $layout, + array('uniqueid' => $attempt->uniqueid)); + $DB->set_field('quiz_attempts', 'needsupgradetonewqe', 1, + array('uniqueid' => $attempt->uniqueid)); + } +} diff --git a/local/qeupgradehelper/convertquiz.php b/local/qeupgradehelper/convertquiz.php new file mode 100644 index 00000000000..69636b726e0 --- /dev/null +++ b/local/qeupgradehelper/convertquiz.php @@ -0,0 +1,76 @@ +. + +/** + * Script to upgrade the attempts at a particular quiz, after confirmation. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(dirname(__FILE__) . '/../../config.php'); +require_once(dirname(__FILE__) . '/locallib.php'); +require_once(dirname(__FILE__) . '/afterupgradelib.php'); +require_once($CFG->libdir . '/adminlib.php'); + +$quizid = required_param('quizid', PARAM_INT); +$confirmed = optional_param('confirmed', false, PARAM_BOOL); + +require_login(); +require_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM)); +local_qeupgradehelper_require_upgraded(); + +admin_externalpage_setup('qeupgradehelper', '', array(), + local_qeupgradehelper_url('convertquiz', array('quizid' => $quizid))); +$PAGE->navbar->add(get_string('listtodo', 'local_qeupgradehelper'), + local_qeupgradehelper_url('listtodo')); +$PAGE->navbar->add(get_string('convertattempts', 'local_qeupgradehelper')); + +$renderer = $PAGE->get_renderer('local_qeupgradehelper'); + +$quizsummary = local_qeupgradehelper_get_quiz($quizid); +if (!$quizsummary) { + print_error('invalidquizid', 'local_qeupgradehelper', + local_qeupgradehelper_url('listtodo')); +} + +$quizsummary->name = format_string($quizsummary->name); + +if ($confirmed && data_submitted() && confirm_sesskey()) { + // Actually do the conversion. + echo $renderer->header(); + echo $renderer->heading(get_string( + 'upgradingquizattempts', 'local_qeupgradehelper', $quizsummary)); + + $upgrader = new local_qeupgradehelper_attempt_upgrader( + $quizsummary->id, $quizsummary->numtoconvert); + $upgrader->convert_all_quiz_attempts(); + + echo $renderer->heading(get_string('conversioncomplete', 'local_qeupgradehelper')); + echo $renderer->end_of_page_link( + new moodle_url('/mod/quiz/report.php', array('q' => $quizsummary->id)), + get_string('gotoquizreport', 'local_qeupgradehelper')); + echo $renderer->end_of_page_link(local_qeupgradehelper_url('listtodo'), + get_string('listtodo', 'local_qeupgradehelper')); + + echo $renderer->footer(); + exit; +} + +echo $renderer->convert_quiz_are_you_sure($quizsummary); diff --git a/local/qeupgradehelper/cronsetup.php b/local/qeupgradehelper/cronsetup.php new file mode 100644 index 00000000000..e89958a6d27 --- /dev/null +++ b/local/qeupgradehelper/cronsetup.php @@ -0,0 +1,69 @@ +. + +/** + * Script to set up cron to complete the upgrade automatically. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(dirname(__FILE__) . '/../../config.php'); +require_once(dirname(__FILE__) . '/locallib.php'); +require_once(dirname(__FILE__) . '/cronsetup_form.php'); +require_once($CFG->libdir . '/adminlib.php'); + +require_login(); +require_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM)); + +admin_externalpage_setup('qeupgradehelper', '', array(), + local_qeupgradehelper_url('cronsetup')); +$PAGE->navbar->add(get_string('cronsetup', 'local_qeupgradehelper')); + +$renderer = $PAGE->get_renderer('local_qeupgradehelper'); + +$form = new local_qeupgradehelper_cron_setup_form( + new moodle_url('/local/qeupgradehelper/cronsetup.php')); +$form->set_data(get_config('local_qeupgradehelper')); + +if ($form->is_cancelled()) { + redirect(local_qeupgradehelper_url('index')); + +} else if ($fromform = $form->get_data()) { + if ($fromform->cronenabled) { + set_config('cronenabled', $fromform->cronenabled, 'local_qeupgradehelper'); + set_config('starthour', $fromform->starthour, 'local_qeupgradehelper'); + set_config('stophour', $fromform->stophour, 'local_qeupgradehelper'); + set_config('procesingtime', $fromform->procesingtime, 'local_qeupgradehelper'); + + } else { + unset_config('cronenabled', 'local_qeupgradehelper'); + unset_config('starthour', 'local_qeupgradehelper'); + unset_config('stophour', 'local_qeupgradehelper'); + unset_config('procesingtime', 'local_qeupgradehelper'); + } + redirect(local_qeupgradehelper_url('index')); + +} + +echo $renderer->header(); +echo $renderer->heading(get_string('cronsetup', 'local_qeupgradehelper')); +echo $renderer->box(get_string('croninstructions', 'local_qeupgradehelper')); +$form->display(); +echo $renderer->footer(); diff --git a/local/qeupgradehelper/cronsetup_form.php b/local/qeupgradehelper/cronsetup_form.php new file mode 100644 index 00000000000..affd7d0ddbb --- /dev/null +++ b/local/qeupgradehelper/cronsetup_form.php @@ -0,0 +1,62 @@ +. + +/** + * Settings form for cronsetup.php. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/formslib.php'); + + +/** + * Cron setup form. + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class local_qeupgradehelper_cron_setup_form extends moodleform { + public function definition() { + $mform = $this->_form; + + $mform->addElement('selectyesno', 'cronenabled', + get_string('cronenabled', 'local_qeupgradehelper')); + + $mform->addElement('select', 'starthour', + get_string('cronstarthour', 'local_qeupgradehelper'), range(0, 23)); + + $mform->addElement('select', 'stophour', + get_string('cronstophour', 'local_qeupgradehelper'), + array_combine(range(1, 24), range(1, 24))); + $mform->setDefault('stophour', 24); + + $mform->addElement('duration', 'procesingtime', + get_string('cronprocesingtime', 'local_qeupgradehelper')); + $mform->setDefault('procesingtime', 60); + + $mform->disabledIf('starthour', 'cronenabled', 'eq', 0); + $mform->disabledIf('stophour', 'cronenabled', 'eq', 0); + $mform->disabledIf('procesingtime', 'cronenabled', 'eq', 0); + + $this->add_action_buttons(); + } +} diff --git a/local/qeupgradehelper/extracttestcase.php b/local/qeupgradehelper/extracttestcase.php new file mode 100644 index 00000000000..4cbc5cb87f0 --- /dev/null +++ b/local/qeupgradehelper/extracttestcase.php @@ -0,0 +1,74 @@ +. + +/** + * Script to help create unit tests for the upgrade using example data from the + * database. + * + * (The theory is that if the upgrade dies with an error, you can restore the + * database from backup, and then use this script to extract the problem case + * as a unit test. Then you can fix that unit tests. Then you can repeat the upgrade.) + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(dirname(__FILE__) . '/../../config.php'); +require_once(dirname(__FILE__) . '/locallib.php'); +require_once(dirname(__FILE__) . '/extracttestcase_form.php'); +require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->libdir . '/adminlib.php'); + + +require_login(); +require_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM)); + +admin_externalpage_setup('qeupgradehelper', '', array(), + local_qeupgradehelper_url('extracttestcase')); +$PAGE->navbar->add(get_string('extracttestcase', 'local_qeupgradehelper')); + +$renderer = $PAGE->get_renderer('local_qeupgradehelper'); + +$mform = new local_qeupgradehelper_extract_options_form( + new moodle_url('/local/qeupgradehelper/extracttestcase.php'), null, 'get'); + +echo $OUTPUT->header(); +if ($fromform = $mform->get_data()) { + $qsid = null; + if (!empty($fromform->attemptid) && !empty($fromform->questionid)) { + $qsid = local_qeupgradehelper_get_session_id($fromform->attemptid, $fromform->questionid); + $name = 'qsession' . $qsid; + + } else if (!empty($fromform->statehistory)) { + notify('Searching ...', 'notifysuccess'); + flush(); + $qsid = local_qeupgradehelper_find_test_case($fromform->behaviour, $fromform->statehistory, + $fromform->qtype, $fromform->extratests); + $name = 'history' . $fromform->statehistory; + } + + if ($qsid) { + local_qeupgradehelper_generate_unit_test($qsid, $name); + } else { + notify('No suitable attempts found.'); + } +} + +$mform->display(); +echo $OUTPUT->footer(); diff --git a/local/qeupgradehelper/extracttestcase_form.php b/local/qeupgradehelper/extracttestcase_form.php new file mode 100644 index 00000000000..5c946b25629 --- /dev/null +++ b/local/qeupgradehelper/extracttestcase_form.php @@ -0,0 +1,62 @@ +. + +/** + * Settings form for extracttestcase.php. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/formslib.php'); + + +/** + * Options form. + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class local_qeupgradehelper_extract_options_form extends moodleform { + public function definition() { + $mform = $this->_form; + + $behaviour = array( + 'deferredfeedback' => 'Deferred feedback', + 'adaptive' => 'Adaptive', + 'adaptivenopenalty' => 'Adaptive (no penalties)', + ); + + $qtypes = get_plugin_list('qtype'); + foreach ($qtypes as $qtype => $notused) { + $qtypes[$qtype] = get_string($qtype, 'qtype_' . $qtype); + } + + $mform->addElement('header', 'h1', 'Either extract a specific question_session'); + $mform->addElement('text', 'attemptid', 'Quiz attempt id', array('size' => '10')); + $mform->addElement('text', 'questionid', 'Question id', array('size' => '10')); + $mform->addElement('header', 'h2', 'Or find and extract an example by type'); + $mform->addElement('select', 'behaviour', 'Behaviour', $behaviour); + $mform->addElement('text', 'statehistory', 'State history', array('size' => '10')); + $mform->addElement('select', 'qtype', 'Question type', $qtypes); + $mform->addElement('text', 'extratests', 'Extra conditions', array('size' => '50')); + $this->add_action_buttons(false, 'Create test case'); + } +} diff --git a/local/qeupgradehelper/index.php b/local/qeupgradehelper/index.php new file mode 100644 index 00000000000..4973871c17e --- /dev/null +++ b/local/qeupgradehelper/index.php @@ -0,0 +1,55 @@ +. + +/** + * This plugin can help upgrade site with a large number of question attempts + * from Moodle 2.0 to 2.1. + * + * This screen is the main entry-point to the plugin, it gives the admin a list + * of options available to them. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(__FILE__) . '/../../config.php'); +require_once(dirname(__FILE__) . '/locallib.php'); +require_once($CFG->libdir . '/adminlib.php'); + +require_login(); +require_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM)); +admin_externalpage_setup('qeupgradehelper'); + +$renderer = $PAGE->get_renderer('local_qeupgradehelper'); + +$actions = array(); +if (local_qeupgradehelper_is_upgraded()) { + $detected = get_string('upgradedsitedetected', 'local_qeupgradehelper'); + $actions[] = local_qeupgradehelper_action::make('listtodo'); + $actions[] = local_qeupgradehelper_action::make('listupgraded'); + $actions[] = local_qeupgradehelper_action::make('extracttestcase'); + $actions[] = local_qeupgradehelper_action::make('cronsetup'); + +} else { + $detected = get_string('oldsitedetected', 'local_qeupgradehelper'); + $actions[] = local_qeupgradehelper_action::make('listpreupgrade'); + $actions[] = local_qeupgradehelper_action::make('extracttestcase'); + $actions[] = local_qeupgradehelper_action::make('cronsetup'); +} + +echo $renderer->index_page($detected, $actions); diff --git a/local/qeupgradehelper/lang/en/local_qeupgradehelper.php b/local/qeupgradehelper/lang/en/local_qeupgradehelper.php new file mode 100644 index 00000000000..7897c0e6592 --- /dev/null +++ b/local/qeupgradehelper/lang/en/local_qeupgradehelper.php @@ -0,0 +1,83 @@ +. + +/** + * Question engine upgrade helper langauge strings. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +$string['action'] = 'Action'; +$string['alreadydone'] = 'Everything has already been converted'; +$string['areyousure'] = 'Are you sure?'; +$string['areyousuremessage'] = 'Do you wish to proceed with upgrading all {$a->numtoconvert} attempts at quiz \'{$a->name}\' in course {$a->shortname}?'; +$string['areyousureresetmessage'] = 'Quiz \'{$a->name}\' in course {$a->shortname} has {$a->totalattempts} attempts, of which {$a->convertedattempts} were upgraded from the old system. Of those, {$a->resettableattempts} can be reset, for later re-conversion. Do you want to proceed with this?'; +$string['attemptstoconvert'] = 'Attempts needing conversion'; +$string['backtoindex'] = 'Back to the main page'; +$string['conversioncomplete'] = 'Conversion complete'; +$string['convertattempts'] = 'Convert attempts'; +$string['convertquiz'] = 'Convert attempts...'; +$string['convertedattempts'] = 'Converted attempts'; +$string['cronenabled'] = 'Cron enabled'; +$string['croninstructions'] = 'You can enable cron to automatically complete the upgrade following a partial upgrade. Cron will run between set hours on the day (according to server local time). Each time cron runs, it will process a number of attempts until Time limit amount of time has been used, then it will stop and wait for the next cron run. Even if you have set up cron, it will not do anything unless it detects that the main upgrade to 2.1 has been completed.'; +$string['cronprocesingtime'] = 'Processing time each cron run'; +$string['cronsetup'] = 'Configure cron'; +$string['cronsetup_desc'] = 'You can configure cron to complete the upgrade of quiz attempt data automatically.'; +$string['cronstarthour'] = 'Start hour'; +$string['cronstophour'] = 'Stop hour'; +$string['extracttestcase'] = 'Extract test case'; +$string['extracttestcase_desc'] = 'Use example data from the database to help create unit tests that can be used to test the upgrade.'; +$string['gotoindex'] = 'Back to the list of quizzes that can be upgraded'; +$string['gotoquizreport'] = 'Go to the reports for this quiz, to check the upgrade'; +$string['gotoresetlink'] = 'Go to the list of quizzes that can be reset'; +$string['includedintheupgrade'] = 'Included in the upgrade?'; +$string['invalidquizid'] = 'Invaid quiz id. Either the quiz does not exist, or it has no attempts to convert.'; +$string['listpreupgrade'] = 'List quizzes and attempts'; +$string['listpreupgrade_desc'] = 'This will show a report of all the quizzes on the system and how many attempts they have. This will give you an idea of the scope of the upgrade you have to do.'; +$string['listpreupgradeintro'] = 'These are the number of quiz attempts that will need to be processed when you upgrade your site. A few tens of thousands is no worry. Much beyond that and you need to think about how long the upgrade will take.'; +$string['listtodo'] = 'List quizzes still to upgrade'; +$string['listtodo_desc'] = 'This will show a report of all the quizzes on the system (if any) that have attempts that still need to be upgraded to the new question engine.'; +$string['listtodointro'] = 'These are all the quizzes with attempt data that still needs to be converted. You can convert the attempts by clicking the link.'; +$string['listupgraded'] = 'List already upgrade quizzes that can be reset'; +$string['listupgraded_desc'] = 'This will show a report of all the quizzes on the system whose attepmts have been upgraded, and where the old data is still present so the upgrade could be reset and redone.'; +$string['listupgradedintro'] = 'These are all the quizzes that have attempts that were upgraded, and where the old attempt data is so there, so they could be reset, and the upgrade re-done.'; +$string['noquizattempts'] = 'Your site does not have any quiz attempts at all!'; +$string['nothingupgradedyet'] = 'No upgraded attempts that can be reset'; +$string['notupgradedsiterequired'] = 'This script can only work before the site has been upgraded.'; +$string['numberofattempts'] = 'Number of quiz attempts'; +$string['oldsitedetected'] = 'This appears to be a site that has not yet been upgraded to include the new question engine.'; +$string['outof'] = '{$a->some} out of {$a->total}'; +$string['pluginname'] = 'Question engine upgrade helper'; +$string['pretendupgrade'] = 'Do a dry-run of the attempts upgrade'; +$string['pretendupgrade_desc'] = 'The upgrade does three things: Load the existing data from the database; transform it; then write the transformed data to the DB. This script will test the first two parts of the process.'; +$string['questionsessions'] = 'Question sessions'; +$string['quizid'] = 'Quiz id'; +$string['quizupgrade'] = 'Quiz upgrade status'; +$string['quizzesthatcanbereset'] = 'The following quizzes have converted attempts that you may be able to reset'; +$string['quizzestobeupgraded'] = 'All quizzes with attempts'; +$string['quizzeswithunconverted'] = 'The following quizzes have attempts that need to be converted'; +$string['resetquiz'] = 'Reset attempts...'; +$string['resetcomplete'] = 'Reset complete'; +$string['resettingquizattempts'] = 'Resetting quiz attempts'; +$string['resettingquizattemptsprogress'] = 'Resetting attempt {$a->done} / {$a->outof}'; +$string['upgradingquizattempts'] = 'Upgrading the attempts for quiz \'{$a->name}\' in course {$a->shortname}'; +$string['upgradedsitedetected'] = 'This appears to be a site that has been upgraded to include the new question engine.'; +$string['upgradedsiterequired'] = 'This script can only work after the site has been upgraded.'; +$string['veryoldattemtps'] = 'Your site has {$a} quiz attempts that were never completely updated during the upgrade from Moodle 1.4 to Moodle 1.5. These attempts will be dealt wiht before the main upgrade. You need to to consider the extra time required for this.'; diff --git a/local/qeupgradehelper/lib.php b/local/qeupgradehelper/lib.php new file mode 100644 index 00000000000..35b9e87ac81 --- /dev/null +++ b/local/qeupgradehelper/lib.php @@ -0,0 +1,74 @@ +. + +/** + * Lib functions (cron) to automatically complete the question engine upgrade + * if it was not done all at once during the main upgrade. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once (dirname(__FILE__) . '/locallib.php'); + + +/** + * Standard cron function + */ +function local_qeupgradehelper_cron() { + $settings = get_config('local_qeupgradehelper'); + if (empty($settings->cronenabled)) { + return; + } + + mtrace('qeupgradehelper: local_qeupgradehelper_cron() started at '. date('H:i:s')); + try { + local_qeupgradehelper_process($settings); + } catch (Exception $e) { + mtrace('qeupgradehelper: local_qeupgradehelper_cron() failed with an exception:'); + mtrace($e->getMessage()); + } + mtrace('qeupgradehelper: local_qeupgradehelper_cron() finished at ' . date('H:i:s')); +} + +/** + * This function does the cron process within the time range according to settings. + */ +function local_qeupgradehelper_process($settings) { + if (!local_qeupgradehelper_is_upgraded()) { + mtrace('qeupgradehelper: site not yet upgraded. Doing nothing.'); + return; + } + + $hour = (int) date('H'); + if ($hour < $settings->starthour || $hour >= $settings->stophour) { + mtrace('qeupgradehelper: not between starthour and stophour, so doing nothing (hour = ' . + $hour . ').'); + return; + } + + $stoptime = time() + $settings->procesingtime; + while (time() < $stoptime) { + mtrace('qeupgradehelper: processing ...'); + + // TODO + mtrace('qeupgradehelper: sorry, not implemented yet.'); + return; + } +} diff --git a/local/qeupgradehelper/listpreupgrade.php b/local/qeupgradehelper/listpreupgrade.php new file mode 100644 index 00000000000..e70f04b0c51 --- /dev/null +++ b/local/qeupgradehelper/listpreupgrade.php @@ -0,0 +1,61 @@ +. + +/** + * Script to show all the quizzes in the site with how many attempts they have + * that will need to be upgraded. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(dirname(__FILE__) . '/../../config.php'); +require_once(dirname(__FILE__) . '/locallib.php'); +require_once($CFG->libdir . '/adminlib.php'); + +require_login(); +require_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM)); +local_qeupgradehelper_require_not_upgraded(); + +admin_externalpage_setup('qeupgradehelper', '', array(), local_qeupgradehelper_url('')); +$PAGE->navbar->add(get_string('listpreupgrade', 'local_qeupgradehelper')); + +$renderer = $PAGE->get_renderer('local_qeupgradehelper'); + +$quizzes = new local_qeupgradehelper_pre_upgrade_quiz_list(); + +// Look to see if the admin has set things up to only upgrade certain attempts. +$partialupgradefile = $CFG->dirroot . '/local/qeupgradehelper/partialupgrade.php'; +$partialupgradefunction = 'local_qeupgradehelper_get_quizzes_to_upgrade'; +if (is_readable($partialupgradefile)) { + include_once($partialupgradefile); + if (function_exists($partialupgradefunction)) { + $quizzes = new local_qeupgradehelper_pre_upgrade_quiz_list_restricted( + $partialupgradefunction()); + } +} + +$numveryoldattemtps = local_qeupgradehelper_get_num_very_old_attempts(); + +if ($quizzes->is_empty()) { + echo $renderer->simple_message_page(get_string('noquizattempts', 'local_qeupgradehelper')); + +} else { + echo $renderer->quiz_list_page($quizzes, $numveryoldattemtps); +} diff --git a/local/qeupgradehelper/listtodo.php b/local/qeupgradehelper/listtodo.php new file mode 100644 index 00000000000..ae0a6ad082f --- /dev/null +++ b/local/qeupgradehelper/listtodo.php @@ -0,0 +1,49 @@ +. + +/** + * Script to show all the quizzes with attempts that still need to be upgraded + * after the main upgrade. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(dirname(__FILE__) . '/../../config.php'); +require_once(dirname(__FILE__) . '/locallib.php'); +require_once($CFG->libdir . '/adminlib.php'); + +require_login(); +require_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM)); +local_qeupgradehelper_require_upgraded(); + +admin_externalpage_setup('qeupgradehelper', '', array(), + local_qeupgradehelper_url('listtodo')); +$PAGE->navbar->add(get_string('listtodo', 'local_qeupgradehelper')); + +$renderer = $PAGE->get_renderer('local_qeupgradehelper'); + +$quizzes = new local_qeupgradehelper_upgradable_quiz_list(); + +if ($quizzes->is_empty()) { + echo $renderer->simple_message_page(get_string('alreadydone', 'local_qeupgradehelper')); + +} else { + echo $renderer->quiz_list_page($quizzes); +} diff --git a/local/qeupgradehelper/listupgraded.php b/local/qeupgradehelper/listupgraded.php new file mode 100644 index 00000000000..9355ff959e7 --- /dev/null +++ b/local/qeupgradehelper/listupgraded.php @@ -0,0 +1,50 @@ +. + +/** + * Script to show all the quizzes with attempts that have been upgraded + * after the main upgrade. With an option to reset the conversion, so it can be + * re-done if necessary. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(dirname(__FILE__) . '/../../config.php'); +require_once(dirname(__FILE__) . '/locallib.php'); +require_once($CFG->libdir . '/adminlib.php'); + +require_login(); +require_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM)); +local_qeupgradehelper_require_upgraded(); + +admin_externalpage_setup('qeupgradehelper', '', array(), + local_qeupgradehelper_url('listupgraded')); +$PAGE->navbar->add(get_string('listupgraded', 'local_qeupgradehelper')); + +$renderer = $PAGE->get_renderer('local_qeupgradehelper'); + +$quizzes = new local_qeupgradehelper_resettable_quiz_list(); + +if ($quizzes->is_empty()) { + echo $renderer->simple_message_page(get_string('nothingupgradedyet', 'local_qeupgradehelper')); + +} else { + echo $renderer->quiz_list_page($quizzes); +} diff --git a/local/qeupgradehelper/locallib.php b/local/qeupgradehelper/locallib.php new file mode 100644 index 00000000000..dfb7d1ae7ad --- /dev/null +++ b/local/qeupgradehelper/locallib.php @@ -0,0 +1,661 @@ +. + +/** + * Question engine upgrade helper library code. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Detect whether this site has been upgraded to the new question engine yet. + * @return bool whether the site has been upgraded. + */ +function local_qeupgradehelper_is_upgraded() { + global $CFG, $DB; + $dbman = $DB->get_manager(); + return is_readable($CFG->dirroot . '/question/engine/upgrade/upgradelib.php') && + $dbman->table_exists('question_usages'); +} + +/** + * If the site has not yet been upgraded, display an error. + */ +function local_qeupgradehelper_require_upgraded() { + if (!local_qeupgradehelper_is_upgraded()) { + throw new moodle_exception('upgradedsiterequired', 'local_qeupgradehelper', + local_qeupgradehelper_url('index')); + } +} + +/** + * If the site has been upgraded, display an error. + */ +function local_qeupgradehelper_require_not_upgraded() { + if (local_qeupgradehelper_is_upgraded()) { + throw new moodle_exception('notupgradedsiterequired', 'local_qeupgradehelper', + local_qeupgradehelper_url('index')); + } +} + +/** + * Get the URL of a script within this plugin. + * @param string $script the script name, without .php. E.g. 'index'. + * @param array $params URL parameters (optional). + */ +function local_qeupgradehelper_url($script, $params = array()) { + return new moodle_url('/local/qeupgradehelper/' . $script . '.php', $params); +} + + +/** + * Class to encapsulate one of the functionalities that this plugin offers. + * + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class local_qeupgradehelper_action { + /** @var string the name of this action. */ + public $name; + /** @var moodle_url the URL to launch this action. */ + public $url; + /** @var string a description of this aciton. */ + public $description; + + /** + * Constructor to set the fields. + */ + protected function __construct($name, moodle_url $url, $description) { + $this->name = $name; + $this->url = $url; + $this->description = $description; + } + + /** + * Make an action with standard values. + * @param string $shortname internal name of the action. Used to get strings + * and build a URL. + * @param array $params any URL params required. + */ + public static function make($shortname, $params = array()) { + return new self( + get_string($shortname, 'local_qeupgradehelper'), + local_qeupgradehelper_url($shortname, $params), + get_string($shortname . '_desc', 'local_qeupgradehelper')); + } +} + + +/** + * A class to represent a list of quizzes with various information about + * attempts that can be displayed as a table. + * + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class local_qeupgradehelper_quiz_list { + public $title; + public $intro; + public $quizacolheader; + public $sql; + public $quizlist = null; + public $totalquizas = 0; + public $totalqas = 0; + + protected function __construct($title, $intro, $quizacolheader) { + global $DB; + $this->title = get_string($title, 'local_qeupgradehelper'); + $this->intro = get_string($intro, 'local_qeupgradehelper'); + $this->quizacolheader = get_string($quizacolheader, 'local_qeupgradehelper'); + $this->build_sql(); + $this->quizlist = $DB->get_records_sql($this->sql); + } + + protected function build_sql() { + $this->sql = ' + SELECT + quiz.id, + quiz.name, + c.shortname, + c.id AS courseid, + COUNT(1) AS attemptcount, + SUM(qsesscounts.num) AS questionattempts + + FROM {quiz_attempts} quiza + JOIN {quiz} quiz ON quiz.id = quiza.quiz + JOIN {course} c ON c.id = quiz.course + LEFT JOIN ( + SELECT attemptid, COUNT(1) AS num + FROM {question_sessions} + GROUP BY attemptid + ) qsesscounts ON qsesscounts.attemptid = quiza.uniqueid + + WHERE quiza.preview = 0 + ' . $this->extra_where_clause() . ' + + GROUP BY quiz.id, quiz.name, c.shortname, c.id + + ORDER BY c.shortname, quiz.name, quiz.id'; + } + + abstract protected function extra_where_clause(); + + public function get_col_headings() { + return array( + get_string('quizid', 'local_qeupgradehelper'), + get_string('course'), + get_string('pluginname', 'quiz'), + $this->quizacolheader, + get_string('questionsessions', 'local_qeupgradehelper'), + ); + } + + public function get_row($quizinfo) { + $this->totalquizas += $quizinfo->attemptcount; + $this->totalqas += $quizinfo->questionattempts; + return array( + $quizinfo->id, + html_writer::link(new moodle_url('/course/view.php', + array('id' => $quizinfo->courseid)), format_string($quizinfo->shortname)), + html_writer::link(new moodle_url('/mod/quiz/view.php', + array('id' => $quizinfo->name)), format_string($quizinfo->name)), + $quizinfo->attemptcount, + $quizinfo->questionattempts ? $quizinfo->questionattempts : 0, + ); + } + + public function get_row_class($quizinfo) { + return null; + } + + public function get_total_row() { + return array( + '', + html_writer::tag('b', get_string('total')), + '', + html_writer::tag('b', $this->totalquizas), + html_writer::tag('b', $this->totalqas), + ); + } + + public function is_empty() { + return empty($this->quizlist); + } +} + + +/** + * A list of quizzes that still need to be upgraded after the main upgrade. + * + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class local_qeupgradehelper_upgradable_quiz_list extends local_qeupgradehelper_quiz_list { + public function __construct() { + parent::__construct('quizzeswithunconverted', 'listtodointro', 'attemptstoconvert'); + } + + protected function extra_where_clause() { + return 'AND quiza.needsupgradetonewqe = 1'; + } + + public function get_col_headings() { + $headings = parent::get_col_headings(); + $headings[] = get_string('action', 'local_qeupgradehelper'); + return $headings; + } + + public function get_row($quizinfo) { + $row = parent::get_row($quizinfo); + $row[] = html_writer::link(local_qeupgradehelper_url('convertquiz', array('quizid' => $quizinfo->id)), + get_string('convertquiz', 'local_qeupgradehelper')); + return $row; + } +} + + +/** + * A list of quizzes that still need to be upgraded after the main upgrade. + * + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class local_qeupgradehelper_resettable_quiz_list extends local_qeupgradehelper_quiz_list { + public function __construct() { + parent::__construct('quizzesthatcanbereset', 'listupgradedintro', 'convertedattempts'); + } + + protected function extra_where_clause() { + return 'AND quiza.needsupgradetonewqe = 0 + AND EXISTS(SELECT 1 FROM {question_states} + WHERE attempt = quiza.uniqueid)'; + } + + public function get_col_headings() { + $headings = parent::get_col_headings(); + $headings[] = get_string('action', 'local_qeupgradehelper'); + return $headings; + } + + public function get_row($quizinfo) { + $row = parent::get_row($quizinfo); + $row[] = html_writer::link(local_qeupgradehelper_url('resetquiz', array('quizid' => $quizinfo->id)), + get_string('resetquiz', 'local_qeupgradehelper')); + return $row; + } +} + + +/** + * A list of quizzes that will be upgraded during the main upgrade. + * + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class local_qeupgradehelper_pre_upgrade_quiz_list extends local_qeupgradehelper_quiz_list { + public function __construct() { + parent::__construct('quizzestobeupgraded', 'listpreupgradeintro', 'numberofattempts'); + } + + protected function extra_where_clause() { + return ''; + } +} + + +/** + * A list of quizzes that will be upgraded during the main upgrade, when the + * partialupgrade.php script is being used. + * + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class local_qeupgradehelper_pre_upgrade_quiz_list_restricted extends local_qeupgradehelper_pre_upgrade_quiz_list { + protected $quizids; + protected $restrictedtotalquizas = 0; + protected $restrictedtotalqas = 0; + + public function __construct($quizids) { + parent::__construct(); + $this->quizids = $quizids; + } + + public function get_row_class($quizinfo) { + if (!in_array($quizinfo->id, $this->quizids)) { + return 'dimmed'; + } else { + return parent::get_row_class($quizinfo); + } + } + + public function get_col_headings() { + $headings = parent::get_col_headings(); + $headings[] = get_string('includedintheupgrade', 'local_qeupgradehelper'); + return $headings; + } + + public function get_row($quizinfo) { + $row = parent::get_row($quizinfo); + if (in_array($quizinfo->id, $this->quizids)) { + $this->restrictedtotalquizas += $quizinfo->attemptcount; + $this->restrictedtotalqas += $quizinfo->questionattempts; + $row[] = get_string('yes'); + } else { + $row[] = get_string('no'); + } + return $row; + } + + protected function out_of($restrictedtotal, $fulltotal) { + $a = new stdClass(); + $a->some = $a->some = html_writer::tag('b', $restrictedtotal); + $a->total = $fulltotal; + return get_string('outof', 'local_qeupgradehelper', $a); + } + + public function get_total_row() { + return array( + '', + html_writer::tag('b', get_string('total')), + '', + $this->out_of($this->restrictedtotalquizas, $this->totalquizas), + $this->out_of($this->restrictedtotalqas, $this->totalqas), + ); + } +} + + +/** + * List the number of quiz attempts that were never upgraded from 1.4 -> 1.5. + * @return int the number of such attempts. + */ +function local_qeupgradehelper_get_num_very_old_attempts() { + global $DB; + return $DB->count_records_sql(' + SELECT COUNT(1) + FROM {quiz_attempts} quiza + WHERE uniqueid IN ( + SELECT DISTINCT qst.attempt + FROM {question_states} qst + LEFT JOIN {question_sessions} qsess ON + qst.question = qsess.questionid AND qst.attempt = qsess.attemptid + WHERE qsess.id IS NULL)'); +} + +/** + * Get the information about a quiz to be upgraded. + * @param integer $quizid the quiz id. + * @return object the information about that quiz, as for + * {@link local_qeupgradehelper_get_upgradable_quizzes()}. + */ +function local_qeupgradehelper_get_quiz($quizid) { + global $DB; + return $DB->get_record_sql(" + SELECT + quiz.id, + quiz.name, + c.shortname, + c.id AS courseid, + COUNT(1) AS numtoconvert + + FROM {quiz_attempts} quiza + JOIN {quiz} quiz ON quiz.id = quiza.quiz + JOIN {course} c ON c.id = quiz.course + + WHERE quiza.preview = 0 + AND quiza.needsupgradetonewqe = 1 + AND quiz.id = ? + + GROUP BY quiz.id, quiz.name, c.shortname, c.id + + ORDER BY c.shortname, quiz.name, quiz.id", array($quizid)); +} + +/** + * Get the information about a quiz to be upgraded. + * @param integer $quizid the quiz id. + * @return object the information about that quiz, as for + * {@link local_qeupgradehelper_get_resettable_quizzes()}, but with extra fields + * totalattempts and resettableattempts. + */ +function local_qeupgradehelper_get_resettable_quiz($quizid) { + global $DB; + return $DB->get_record_sql(" + SELECT + quiz.id, + quiz.name, + c.shortname, + c.id AS courseid, + COUNT(1) AS totalattempts, + SUM(CASE WHEN quiza.needsupgradetonewqe = 0 AND + oldtimemodified.time IS NOT NULL THEN 1 ELSE 0 END) AS convertedattempts, + SUM(CASE WHEN quiza.needsupgradetonewqe = 0 AND + newtimemodified.time IS NULL OR oldtimemodified.time >= newtimemodified.time + THEN 1 ELSE 0 END) AS resettableattempts + + FROM {quiz_attempts} quiza + JOIN {quiz} quiz ON quiz.id = quiza.quiz + JOIN {course} c ON c.id = quiz.course + LEFT JOIN ( + SELECT attempt, MAX(timestamp) AS time + FROM {question_states} + GROUP BY attempt + ) AS oldtimemodified ON oldtimemodified.attempt = quiza.uniqueid + LEFT JOIN ( + SELECT qa.questionusageid, MAX(qas.timecreated) AS time + FROM {question_attempts} qa + JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id + GROUP BY qa.questionusageid + ) AS newtimemodified ON newtimemodified.questionusageid = quiza.uniqueid + + WHERE quiza.preview = 0 + AND quiz.id = ? + + GROUP BY quiz.id, quiz.name, c.shortname, c.id", array($quizid)); +} + +/** + * Get a question session id form a quiz attempt id and a question id. + * @param int $attemptid a quiz attempt id. + * @param int $questionid a question id. + * @return int the question session id. + */ +function local_qeupgradehelper_get_session_id($attemptid, $questionid) { + global $DB; + $attempt = $DB->get_record('quiz_attempts', array('id' => $attemptid)); + if (!$attempt) { + return null; + } + return $DB->get_field('question_sessions', 'id', + array('attemptid' => $attempt->uniqueid, 'questionid' => $questionid)); +} + +/** + * Identify the question session id of a question attempt matching certain + * requirements. + * @param integer $behaviour 0 = deferred feedback, 1 = interactive. + * @param string $statehistory of states, last first. E.g. 620. + * @param string $qtype question type. + * @return integer question_session.id. + */ +function local_qeupgradehelper_find_test_case($behaviour, $statehistory, $qtype, $extratests) { + global $DB; + + $params = array( + 'qtype' => $qtype, + 'statehistory' => $statehistory + ); + + if ($behaviour == 'deferredfeedback') { + $extrawhere = ''; + $params['optionflags'] = 0; + + } else if ($behaviour == 'adaptive') { + $extrawhere = 'AND penaltyscheme = :penaltyscheme'; + $params['optionflags'] = 0; + $params['penaltyscheme'] = 0; + + } else { + $extrawhere = 'AND penaltyscheme = :penaltyscheme'; + $params['optionflags'] = 0; + $params['penaltyscheme'] = 1; + } + + $possibleids = $DB->get_records_sql_menu(' + SELECT + qsess.id, + 1 + + FROM {question_sessions} qsess + JOIN {question_states} qst ON qst.attempt = qsess.attemptid + AND qst.question = qsess.questionid + JOIN {quiz_attempts} quiza ON quiza.uniqueid = qsess.attemptid + JOIN {quiz} quiz ON quiz.id = quiza.quiz + JOIN {question} q ON q.id = qsess.questionid + + WHERE q.qtype = :qtype + AND quiz.optionflags = :optionflags + ' . $extrawhere . ' + + GROUP BY + qsess.id + + HAVING SUM( + (CASE WHEN qst.event = 10 THEN 1 ELSE qst.event END) * + POWER(10, CAST(qst.seq_number AS NUMERIC(110,0))) + ) = :statehistory' . $extratests, $params, 0, 100); + + if (!$possibleids) { + return null; + } + + return array_rand($possibleids); +} + +/** + * Grab all the data that upgrade will need for upgrading one + * attempt at one question from the old DB. + */ +function local_qeupgradehelper_generate_unit_test($questionsessionid, $namesuffix) { + global $DB; + + $qsession = $DB->get_record('question_sessions', array('id' => $questionsessionid)); + $attempt = $DB->get_record('quiz_attempts', array('uniqueid' => $qsession->attemptid)); + $quiz = $DB->get_record('quiz', array('id' => $attempt->quiz)); + $qstates = $DB->get_records('question_states', + array('attempt' => $qsession->attemptid, 'question' => $qsession->questionid), + 'seq_number, id'); + + $question = local_qeupgradehelper_load_question($qsession->questionid, $quiz->id); + + if (!local_qeupgradehelper_is_upgraded()) { + if (!$quiz->optionflags) { + $quiz->preferredbehaviour = 'deferredfeedback'; + } else if (!$quiz->penaltyscheme) { + $quiz->preferredbehaviour = 'adaptive'; + } else { + $quiz->preferredbehaviour = 'adaptivenopenalty'; + } + unset($quiz->optionflags); + unset($quiz->penaltyscheme); + + $question->defaultmark = $question->defaultgrade; + unset($question->defaultgrade); + } + + $attempt->needsupgradetonewqe = 1; + + echo ''; +} + +function local_qeupgradehelper_format_var($name, $var) { + $out = var_export($var, true); + $out = str_replace('<', '<', $out); + $out = str_replace('ADOFetchObj::__set_state(array(', '(object) array(', $out); + $out = str_replace('stdClass::__set_state(array(', '(object) array(', $out); + $out = str_replace('array (', 'array(', $out); + $out = preg_replace('/=> \n\s*/', '=> ', $out); + $out = str_replace(')),', '),', $out); + $out = str_replace('))', ')', $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n (?! )/', "\n ", $out); + $out = preg_replace('/\n(?! )/', "\n ", $out); + $out = preg_replace('/\bNULL\b/', 'null', $out); + return " $name = $out;\n"; +} + +function local_qeupgradehelper_display_convert_attempt_input($quiz, $attempt, + $question, $qsession, $qstates) { + echo local_qeupgradehelper_format_var('$quiz', $quiz); + echo local_qeupgradehelper_format_var('$attempt', $attempt); + echo local_qeupgradehelper_format_var('$question', $question); + echo local_qeupgradehelper_format_var('$qsession', $qsession); + echo local_qeupgradehelper_format_var('$qstates', $qstates); +} + +function local_qeupgradehelper_load_question($questionid, $quizid) { + global $CFG, $DB; + + $question = $DB->get_record_sql(' + SELECT q.*, qqi.grade AS maxmark + FROM {question} q + JOIN {quiz_question_instances} qqi ON qqi.question = q.id + WHERE q.id = :questionid AND qqi.quiz = :quizid', + array('questionid' => $questionid, 'quizid' => $quizid)); + + if (local_qeupgradehelper_is_upgraded()) { + require_once($CFG->dirroot . '/question/engine/bank.php'); + $qtype = question_bank::get_qtype($question->qtype, false); + } else { + global $QTYPES; + if (!array_key_exists($question->qtype, $QTYPES)) { + $question->qtype = 'missingtype'; + $question->questiontext = '

' . get_string('warningmissingtype', 'quiz') . '

' . $question->questiontext; + } + $qtype = $QTYPES[$question->qtype]; + } + + $qtype->get_question_options($question); + + return $question; +} diff --git a/local/qeupgradehelper/partialupgrade-example.php b/local/qeupgradehelper/partialupgrade-example.php new file mode 100644 index 00000000000..95d3eccfd28 --- /dev/null +++ b/local/qeupgradehelper/partialupgrade-example.php @@ -0,0 +1,115 @@ +. + +/** + * Example script, showing how it is possible to only do a part-upgrade of the + * attempt data during the main upgrade, and then finish the job off later. + * + * If you want to use this facility, then you need to: + * + * 1. Rename this script to partialupgrade.php. + * 2. Look at the various example functions below for controlling the upgrade, + * chooose one you like, and un-comment it. Alternatively, write your own + * custom function. + * 3. Use the List quizzes and attempts options in this plugin, which should now + * display updated information. + * 4. Once you are sure that works, you can proceed with the upgrade as usual. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +/** + * This is a very simple example that just uses a hard-coded array to control + * which attempts are upgraded. + * + * @return array of quiz ids that are the ones to upgrade during the main + * upgrade from 2.0 to 2.1. Attempts at other quizzes are left alone, you will + * have to take steps to upgrade them yourself using the facilities provided by + * this plugin. + */ +//function local_qeupgradehelper_get_quizzes_to_upgrade() { +// return array(1, 2, 3); +//} + + +/** + * This example function uses a list of quiz ids from a file. + * + * It is currently set to use the file quiz-ids-to-upgrade.txt in the same + * folder as this script, but you can change that if you like. + * + * That file should contain one quiz id per line, with no punctuation. Any line + * that does not look like an integer is ignored. + * + * @return array of quiz ids that are the ones to upgrade during the main + * upgrade from 2.0 to 2.1. Attempts at other quizzes are left alone, you will + * have to take steps to upgrade them yourself using the facilities provided by + * this plugin. + */ +//function local_qeupgradehelper_get_quizzes_to_upgrade() { +// global $CFG; +// $rawids = file($CFG->dirroot . '/local/qeupgradehelper/quiz-ids-to-upgrade.txt'); +// $cleanids = array(); +// foreach ($rawids as $id) { +// $id = clean_param($id, PARAM_INT); +// if ($id) { +// $cleanids[] = $id; +// } +// } +// return $cleanids; +//} + + +/** + * This example uses a complex SQL query to decide which attempts to upgrade. + * + * The particular example I have done here is to return the ids of all the quizzes + * in courses that started more recently than one year ago. Of coures, you can + * write any query you like to meet your needs. + * + * Remember that you can use the List quizzes and attempts options option provided + * by this plugin to verify that your query is selecting the quizzes you intend. + * + * @return array of quiz ids that are the ones to upgrade during the main + * upgrade from 2.0 to 2.1. Attempts at other quizzes are left alone, you will + * have to take steps to upgrade them yourself using the facilities provided by + * this plugin. + */ +//function local_qeupgradehelper_get_quizzes_to_upgrade() { +// global $DB; +// +// $quizmoduleid = $DB->get_field('modules', 'id', array('name' => 'quiz')); +// +// $oneyearago = strtotime('-1 year'); +// +// return $DB->get_fieldset_sql(' +// SELECT DISTINCT quiz.id +// +// FROM {quiz} quiz +// JOIN {course_modules} cm ON cm.module = :quizmoduleid +// AND cm.instance = quiz.id +// JOIN {course} c ON quiz.course = c.id +// +// WHERE c.startdate > :cutoffdate +// +// ORDER BY quiz.id +// ', array('quizmoduleid' => $quizmoduleid, 'cutoffdate' => $oneyearago)); +// "); +//} diff --git a/local/qeupgradehelper/renderer.php b/local/qeupgradehelper/renderer.php new file mode 100644 index 00000000000..340f939a0ad --- /dev/null +++ b/local/qeupgradehelper/renderer.php @@ -0,0 +1,168 @@ +. + +/** + * Defines the renderer for the question engine upgrade helper plugin. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Renderer for the question engine upgrade helper plugin. + * + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class local_qeupgradehelper_renderer extends plugin_renderer_base { + + /** + * Render the index page. + * @param string $detected information about what sort of site was detected. + * @param array $actions list of actions to show on this page. + * @return string html to output. + */ + public function index_page($detected, array $actions) { + $output = ''; + $output .= $this->header(); + $output .= $this->heading(get_string('pluginname', 'local_qeupgradehelper')); + $output .= $this->box($detected); + $output .= html_writer::start_tag('ul'); + foreach ($actions as $action) { + $output .= html_writer::tag('li', + html_writer::link($action->url, $action->name) . ' - ' . + $action->description); + } + $output .= html_writer::end_tag('ul'); + $output .= $this->footer(); + return $output; + } + + /** + * Render a page that is just a simple message. + * @param string $message the message to display. + * @return string html to output. + */ + public function simple_message_page($message) { + $output = ''; + $output .= $this->header(); + $output .= $this->heading($message); + $output .= $this->back_to_index(); + $output .= $this->footer(); + return $output; + } + + /** + * Render the list of quizzes that still need to be upgraded page. + * @param array $quizzes of data about quizzes. + * @param int $numveryoldattemtps only relevant before upgrade. + * @return string html to output. + */ + public function quiz_list_page(local_qeupgradehelper_quiz_list $quizzes, + $numveryoldattemtps = null) { + $output = ''; + $output .= $this->header(); + $output .= $this->heading($quizzes->title); + $output .= $this->box($quizzes->intro); + + $table = new html_table(); + $table->head = $quizzes->get_col_headings(); + + $rowcount = 0; + foreach ($quizzes->quizlist as $quizinfo) { + $table->data[$rowcount] = $quizzes->get_row($quizinfo); + if ($class = $quizzes->get_row_class($quizinfo)) { + $table->rowclasses[$rowcount] = $class; + } + $rowcount += 1; + } + $table->data[] = $quizzes->get_total_row(); + $output .= html_writer::table($table); + + if ($numveryoldattemtps) { + $output .= $this->box(get_string('veryoldattemtps', 'local_qeupgradehelper', + $numveryoldattemtps)); + } + + $output .= $this->back_to_index(); + $output .= $this->footer(); + return $output; + } + + /** + * Render the are-you-sure page to confirm a manual upgrade. + * @param object $quizsummary data about the quiz to upgrade. + * @return string html to output. + */ + public function convert_quiz_are_you_sure($quizsummary) { + $output = ''; + $output .= $this->header(); + $output .= $this->heading(get_string('areyousure', 'local_qeupgradehelper')); + + $params = array('quizid' => $quizsummary->id, 'confirmed' => 1, 'sesskey' => sesskey()); + $output .= $this->confirm(get_string('areyousuremessage', 'local_qeupgradehelper', $quizsummary), + new single_button(local_qeupgradehelper_url('convertquiz', $params), get_string('yes')), + local_qeupgradehelper_url('listtodo')); + + $output .= $this->footer(); + return $output; + } + + /** + * Render the are-you-sure page to confirm a manual reset. + * @param object $quizsummary data about the quiz to reset. + * @return string html to output. + */ + public function reset_quiz_are_you_sure($quizsummary) { + $output = ''; + $output .= $this->header(); + $output .= $this->heading(get_string('areyousure', 'local_qeupgradehelper')); + + $params = array('quizid' => $quizsummary->id, 'confirmed' => 1, 'sesskey' => sesskey()); + $output .= $this->confirm(get_string('areyousureresetmessage', 'local_qeupgradehelper', $quizsummary), + new single_button(local_qeupgradehelper_url('resetquiz', $params), get_string('yes')), + local_qeupgradehelper_url('listupgraded')); + + $output .= $this->footer(); + return $output; + } + + /** + * Render a link in a div, such as the 'Back to plugin main page' link. + * @param $url the link URL. + * @param $text the link text. + * @return string html to output. + */ + public function end_of_page_link($url, $text) { + return html_writer::tag('div', html_writer::link($url ,$text), + array('class' => 'mdl-align')); + } + + /** + * Output a link back to the plugin index page. + * @return string html to output. + */ + public function back_to_index() { + return $this->end_of_page_link(local_qeupgradehelper_url('index'), + get_string('backtoindex', 'local_qeupgradehelper')); + } +} diff --git a/local/qeupgradehelper/resetquiz.php b/local/qeupgradehelper/resetquiz.php new file mode 100644 index 00000000000..a4d15da570a --- /dev/null +++ b/local/qeupgradehelper/resetquiz.php @@ -0,0 +1,72 @@ +. + +/** + * Script to reset the upgrade of attempts at a particular quiz, after confirmation. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(__FILE__) . '/../../config.php'); +require_once(dirname(__FILE__) . '/locallib.php'); +require_once(dirname(__FILE__) . '/afterupgradelib.php'); +require_once($CFG->libdir . '/adminlib.php'); + +$quizid = required_param('quizid', PARAM_INT); +$confirmed = optional_param('confirmed', false, PARAM_BOOL); + +require_login(); +require_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM)); +local_qeupgradehelper_require_upgraded(); + +admin_externalpage_setup('qeupgradehelper', '', array(), + local_qeupgradehelper_url('resetquiz', array('quizid' => $quizid))); +$PAGE->navbar->add(get_string('listupgraded', 'local_qeupgradehelper'), + local_qeupgradehelper_url('listtodo')); +$PAGE->navbar->add(get_string('resetquiz', 'local_qeupgradehelper')); + +$renderer = $PAGE->get_renderer('local_qeupgradehelper'); + +$quizsummary = local_qeupgradehelper_get_resettable_quiz($quizid); +if (!$quizsummary) { + print_error('invalidquizid', 'local_qeupgradehelper', + local_qeupgradehelper_url('listupgraded')); +} + +$quizsummary->name = format_string($quizsummary->name); + +if ($confirmed && data_submitted() && confirm_sesskey()) { + // Actually do the conversion. + echo $renderer->header(); + echo $renderer->heading(get_string( + 'resettingquizattempts', 'local_qeupgradehelper', $quizsummary)); + + $upgrader = new local_qeupgradehelper_attempt_upgrader( + $quizsummary->id, $quizsummary->resettableattempts); + $upgrader->reset_all_resettable_attempts(); + + echo $renderer->heading(get_string('resetcomplete', 'local_qeupgradehelper')); + echo $renderer->end_of_page_link(local_qeupgradehelper_url('listupgraded'), + get_string('listupgraded', 'local_qeupgradehelper')); + + echo $renderer->footer(); + exit; +} + +echo $renderer->reset_quiz_are_you_sure($quizsummary); diff --git a/local/qeupgradehelper/settings.php b/local/qeupgradehelper/settings.php new file mode 100644 index 00000000000..2991f38d02c --- /dev/null +++ b/local/qeupgradehelper/settings.php @@ -0,0 +1,32 @@ +. + +/** + * Adds this plugin to the admin menu. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +if ($hassiteconfig) { // needs this condition or there is error on login page + $ADMIN->add('root', new admin_externalpage('qeupgradehelper', + get_string('pluginname', 'local_qeupgradehelper'), + new moodle_url('/local/qeupgradehelper/index.php'))); +} diff --git a/local/qeupgradehelper/styles.css b/local/qeupgradehelper/styles.css new file mode 100644 index 00000000000..b328ced177b --- /dev/null +++ b/local/qeupgradehelper/styles.css @@ -0,0 +1,6 @@ +#page-admin-local-qeupgradehelper-index .dimmed { + color: grey; +} +#page-admin-local-qeupgradehelper-index .dimmed a { + color: #88c; +} diff --git a/local/qeupgradehelper/version.php b/local/qeupgradehelper/version.php new file mode 100644 index 00000000000..606099e0741 --- /dev/null +++ b/local/qeupgradehelper/version.php @@ -0,0 +1,29 @@ +. + +/** + * Version details. + * + * @package local + * @subpackage qeupgradehelper + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +$plugin->version = 2011040400; +$plugin->requires = 2010080300; diff --git a/message/defaultoutputs.php b/message/defaultoutputs.php new file mode 100644 index 00000000000..4331ab5c9fe --- /dev/null +++ b/message/defaultoutputs.php @@ -0,0 +1,115 @@ +. + +/** + * Default message outputs configuration page + * + * @package message + * @copyright 2011 Lancaster University Network Services Limited + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +require_once(dirname(__FILE__) . '/../config.php'); +require_once($CFG->dirroot . '/message/lib.php'); +require_once($CFG->libdir.'/adminlib.php'); + +// This is an admin page +admin_externalpage_setup('defaultmessageoutputs'); + +// Require site configuration capability +require_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM)); + +// Fetch processors +$processors = get_message_processors(true); +// Fetch message providers +$providers = $DB->get_records('message_providers', null, 'name'); + +if (($form = data_submitted()) && confirm_sesskey()) { + $preferences = array(); + // Prepare default message outputs settings + foreach ( $providers as $provider) { + $componentproviderbase = $provider->component.'_'.$provider->name; + foreach (array('permitted', 'loggedin', 'loggedoff') as $setting){ + $value = null; + $componentprovidersetting = $componentproviderbase.'_'.$setting; + if ($setting == 'permitted') { + // if we deal with permitted select element, we need to create individual + // setting for each possible processor. Note that this block will + // always be processed first after entring parental foreach iteration + // so we can change form values on this stage. + foreach($processors as $processor) { + $value = ''; + if (isset($form->{$componentprovidersetting}[$processor->name])) { + $value = $form->{$componentprovidersetting}[$processor->name]; + } + // Ensure that loggedin loggedoff options are set correctly + // for this permission + if ($value == 'forced') { + $form->{$componentproviderbase.'_loggedin'}[$processor->name] = 1; + $form->{$componentproviderbase.'_loggedoff'}[$processor->name] = 1; + } else if ($value == 'disallowed') { + // It might be better to unset them, but I can't figure out why that cause error + $form->{$componentproviderbase.'_loggedin'}[$processor->name] = 0; + $form->{$componentproviderbase.'_loggedoff'}[$processor->name] = 0; + } + // record the site preference + $preferences[$processor->name.'_provider_'.$componentprovidersetting] = $value; + } + } else if (array_key_exists($componentprovidersetting, $form)) { + // we must be processing loggedin or loggedoff checkboxes. Store + // defained comma-separated processors as setting value. + // Using array_filter eliminates elements set to 0 above + $value = join(',', array_keys(array_filter($form->{$componentprovidersetting}))); + if (empty($value)) { + $value = null; + } + } + if ($setting != 'permitted') { + // we have already recoded site preferences for 'permitted' type + $preferences['message_provider_'.$componentprovidersetting] = $value; + } + } + } + + // Update database + $transaction = $DB->start_delegated_transaction(); + foreach ($preferences as $name => $value) { + set_config($name, $value, 'message'); + } + $transaction->allow_commit(); + + // Redirect + $url = new moodle_url('defaultoutputs.php'); + redirect($url); +} + + + +// Page settings +$PAGE->set_context(get_context_instance(CONTEXT_SYSTEM)); +$PAGE->requires->js_init_call('M.core_message.init_defaultoutputs'); + +// Grab the renderer +$renderer = $PAGE->get_renderer('core', 'message'); + +// Display the manage message outputs interface +$preferences = get_message_output_default_preferences(); +$messageoutputs = $renderer->manage_defaultmessageoutputs($processors, $providers, $preferences); + +// Display the page +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('defaultmessageoutputs', 'message')); +echo $messageoutputs; +echo $OUTPUT->footer(); \ No newline at end of file diff --git a/message/edit.php b/message/edit.php index ca66db93562..9af68c182be 100644 --- a/message/edit.php +++ b/message/edit.php @@ -23,7 +23,8 @@ * @package message */ -require_once('../config.php'); +require_once(dirname(__FILE__) . '/../config.php'); +require_once($CFG->dirroot . '/message/lib.php'); $userid = optional_param('id', $USER->id, PARAM_INT); // user id $course = optional_param('course', SITEID, PARAM_INT); // course id (defaults to Site) @@ -97,13 +98,13 @@ if (($form = data_submitted()) && confirm_sesskey()) { /// Set all the preferences for all the message providers $providers = message_get_my_providers(); - $possiblestates = array('loggedin', 'loggedoff'); - foreach ( $providers as $providerid => $provider){ - foreach ($possiblestates as $state){ + foreach ($providers as $provider) { + $componentproviderbase = $provider->component.'_'.$provider->name; + foreach (array('loggedin', 'loggedoff') as $state) { $linepref = ''; - $componentproviderstate = $provider->component.'_'.$provider->name.'_'.$state; + $componentproviderstate = $componentproviderbase.'_'.$state; if (array_key_exists($componentproviderstate, $form)) { - foreach ($form->{$componentproviderstate} as $process=>$one){ + foreach (array_keys($form->{$componentproviderstate}) as $process){ if ($linepref == ''){ $linepref = $process; } else { @@ -111,41 +112,25 @@ if (($form = data_submitted()) && confirm_sesskey()) { } } } - $preferences['message_provider_'.$provider->component.'_'.$provider->name.'_'.$state] = $linepref; - } - } - foreach ( $providers as $providerid => $provider){ - foreach ($possiblestates as $state){ - $preferencekey = 'message_provider_'.$provider->component.'_'.$provider->name.'_'.$state; - if (empty($preferences[$preferencekey])) { - $preferences[$preferencekey] = 'none'; + if (empty($linepref)) { + $linepref = 'none'; } + $preferences['message_provider_'.$provider->component.'_'.$provider->name.'_'.$state] = $linepref; } } /// Set all the processor options as well - $processors = $DB->get_records('message_processors'); - foreach ( $processors as $processorid => $processor){ - $processorfile = $CFG->dirroot. '/message/output/'.$processor->name.'/message_output_'.$processor->name.'.php'; - if ( is_readable($processorfile) ) { - include_once( $processorfile ); - - $processclass = 'message_output_' . $processor->name; - if ( class_exists($processclass) ){ - $pclass = new $processclass(); - $pclass->process_form($form, $preferences); - } else{ - print_error('errorcallingprocessor', 'message'); - } - } + $processors = get_message_processors(true); + foreach ($processors as $processor) { + $processor->object->process_form($form, $preferences); } //process general messaging preferences - $preferences['message_blocknoncontacts'] = !empty($form->blocknoncontacts)?1:0; + $preferences['message_blocknoncontacts'] = !empty($form->blocknoncontacts)?1:0; //$preferences['message_beepnewmessage'] = !empty($form->beepnewmessage)?1:0; // Save all the new preferences to the database - if (!set_user_preferences( $preferences, $user->id ) ){ + if (!set_user_preferences($preferences, $user->id)) { print_error('cannotupdateusermsgpref'); } @@ -158,34 +143,25 @@ $preferences->userdefaultemail = $user->email;//may be displayed by the email pr /// Get providers preferences $providers = message_get_my_providers(); -foreach ( $providers as $providerid => $provider){ - foreach (array('loggedin', 'loggedoff') as $state){ +foreach ($providers as $provider) { + foreach (array('loggedin', 'loggedoff') as $state) { $linepref = get_user_preferences('message_provider_'.$provider->component.'_'.$provider->name.'_'.$state, '', $user->id); if ($linepref == ''){ continue; } $lineprefarray = explode(',', $linepref); $preferences->{$provider->component.'_'.$provider->name.'_'.$state} = array(); - foreach ($lineprefarray as $pref){ + foreach ($lineprefarray as $pref) { $preferences->{$provider->component.'_'.$provider->name.'_'.$state}[$pref] = 1; } } } +// Load all processors +$processors = get_message_processors(); /// For every processors put its options on the form (need to get function from processor's lib.php) -$processors = $DB->get_records('message_processors'); -foreach ( $processors as $processorid => $processor){ - $processorfile = $CFG->dirroot. '/message/output/'.$processor->name.'/message_output_'.$processor->name.'.php'; - if ( is_readable($processorfile) ) { - include_once( $processorfile ); - $processclass = 'message_output_' . $processor->name; - if ( class_exists($processclass) ){ - $pclass = new $processclass(); - $pclass->load_data($preferences, $user->id); - } else{ - print_error('errorcallingprocessor', 'message'); - } - } +foreach ($processors as $processor) { + $processor->object->load_data($preferences, $user->id); } //load general messaging preferences @@ -203,84 +179,17 @@ if ($course->id != SITEID) { } else { $PAGE->set_heading($course->fullname); } -echo $OUTPUT->header(); -// Start the form. We're not using mform here because of our special formatting needs ... -echo ''; - -/// Settings table... -echo '
'; -echo ''.get_string('providers_config', 'message').''; +// Grab the renderer +$renderer = $PAGE->get_renderer('core', 'message'); +// Fetch message providers $providers = message_get_my_providers(); -$processors = $DB->get_records('message_processors', null, 'name DESC'); -$number_procs = count($processors); -echo ''."\n"; -foreach ( $processors as $processorid => $processor){ - echo ''; -} -echo ''; +// Fetch default (site) preferences +$defaultpreferences = get_message_output_default_preferences(); -foreach ( $providers as $providerid => $provider){ - $providername = get_string('messageprovider:'.$provider->name, $provider->component); - - echo ''."\n"; - foreach (array('loggedin', 'loggedoff') as $state){ - $state_res = get_string($state.'description', 'message'); - echo ''."\n"; - foreach ( $processors as $processorid => $processor) { - if (!isset($preferences->{$provider->component.'_'.$provider->name.'_'.$state})) { - $checked = ''; - } else if (!isset($preferences->{$provider->component.'_'.$provider->name.'_'.$state}[$processor->name])) { - $checked = ''; - } else { - $checked = $preferences->{$provider->component.'_'.$provider->name.'_'.$state}[$processor->name]==1?" checked=\"checked\"":""; - } - echo ''."\n"; - } - echo ''."\n"; - } -} -echo '
 '.get_string('pluginname', 'message_'.$processor->name).'
'.$providername.'
'.$state_res.'
'; -echo '
'; - -/// Show all the message processors -$processors = $DB->get_records('message_processors'); - -$processorconfigform = null; -foreach ($processors as $processorid => $processor) { - $processorfile = $CFG->dirroot. '/message/output/'.$processor->name.'/message_output_'.$processor->name.'.php'; - if (is_readable($processorfile)) { - include_once($processorfile); - $processclass = 'message_output_' . $processor->name; - - if (class_exists($processclass)) { - $pclass = new $processclass(); - $processorconfigform = $pclass->config_form($preferences); - - if (!empty($processorconfigform)) { - echo '
'; - echo ''.get_string('pluginname', 'message_'.$processor->name).''; - - echo $processorconfigform; - - echo '
'; - } - } else{ - print_error('errorcallingprocessor', 'message'); - } - } -} - -echo '
'; -echo ''.get_string('generalsettings','admin').''; -echo get_string('blocknoncontacts', 'message').': blocknoncontacts==1?' checked="checked"':''); -//get_string('beepnewmessage', 'message').': beepnewmessage==1?" checked=\"checked\"":"").' />'; -echo '
'; - -echo '
'; -echo '
'; - -echo ""; +$messagingoptions = $renderer->manage_messagingoptions($processors, $providers, $preferences, $defaultpreferences); +echo $OUTPUT->header(); +echo $messagingoptions; echo $OUTPUT->footer(); diff --git a/message/externallib.php b/message/externallib.php new file mode 100644 index 00000000000..00f24a8cf39 --- /dev/null +++ b/message/externallib.php @@ -0,0 +1,159 @@ +. + +/** + * External message API + * + * @package moodlecore + * @subpackage message + * @copyright 2011 Moodle Pty Ltd (http://moodle.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +require_once("$CFG->libdir/externallib.php"); + +class moodle_message_external extends external_api { + + /** + * Returns description of method parameters + * @return external_function_parameters + */ + public static function send_messages_parameters() { + return new external_function_parameters( + array( + 'messages' => new external_multiple_structure( + new external_single_structure( + array( + 'touserid' => new external_value(PARAM_INT, 'id of the user to send the private message'), + 'text' => new external_value(PARAM_RAW, 'the text of the message - not that you can send anything it will be automatically cleaned to PARAM_TEXT and used againt MOODLE_FORMAT'), + 'clientmsgid' => new external_value(PARAM_ALPHANUMEXT, 'your own client id for the message. If this id is provided, the fail message id will be returned to you', VALUE_OPTIONAL), + ) + ) + ) + ) + ); + } + + /** + * Send private messages from the current USER to other users + * + * @param $messages An array of message to send. + * @return boolean + */ + public static function send_messages($messages = array()) { + global $CFG, $USER, $DB; + require_once($CFG->dirroot . "/message/lib.php"); + + //check if messaging is enabled + if (!$CFG->messaging) { + throw new moodle_exception('disabled', 'message'); + } + + // Ensure the current user is allowed to run this function + $context = get_context_instance(CONTEXT_SYSTEM); + self::validate_context($context); + require_capability('moodle/site:sendmessage', $context); + + $params = self::validate_parameters(self::send_messages_parameters(), array('messages' => $messages)); + + //retrieve all tousers of the messages + $touserids = array(); + foreach($params['messages'] as $message) { + $touserids[] = $message['touserid']; + } + list($sqluserids, $sqlparams) = $DB->get_in_or_equal($touserids, SQL_PARAMS_NAMED, 'userid_'); + $tousers = $DB->get_records_select("user", "id " . $sqluserids . " AND deleted = 0", $sqlparams); + + //retrieve the tousers who are blocking the $USER + $sqlparams['contactid'] = $USER->id; + $sqlparams['blocked'] = 1; + //Note: return userid field should be unique for the below request, + //so we'll use this field as key of $blockingcontacts + $blockingcontacts = $DB->get_records_select("message_contacts", + "userid " . $sqluserids . " AND contactid = :contactid AND blocked = :blocked", + $sqlparams, '', "userid"); + + $canreadallmessages = has_capability('moodle/site:readallmessages', $context); + + $resultmessages = array(); + foreach ($params['messages'] as $message) { + $text = clean_param($message['text'], PARAM_TEXT); + $resultmsg = array(); //the infos about the success of the operation + + //we are going to do some checking + //code should match /messages/index.php checks + $success = true; + + //check the user exists + if (empty($tousers[$message['touserid']])) { + $success = false; + $errormessage = get_string('touserdoesntexist', 'message', $message['touserid']); + } + + //check that the touser is not blocking the current user + if ($success and isset($blockingcontacts[$message['touserid']]) and !$canreadallmessages) { + $success = false; + $errormessage = get_string('userisblockingyou', 'message'); + } + + // Check if the user is a contact + //TODO: performance improvement - edit the function so we can pass an array instead userid + if ($success && empty($contact) && get_user_preferences('message_blocknoncontacts', NULL, $message['touserid']) == null) { + // The user isn't a contact and they have selected to block non contacts so this message won't be sent. + $success = false; + $errormessage = get_string('userisblockingyounoncontact', 'message'); + } + + //now we can send the message (at least try) + if ($success) { + //TODO: performance improvement - edit the function so we can pass an array instead one touser object + $success = message_post_message($USER, $tousers[$message['touserid']], $text, FORMAT_MOODLE); + } + + //build the resultmsg + if (isset($message['clientmsgid'])) { + $resultmsg['clientmsgid'] = $message['clientmsgid']; + } + if ($success) { + $resultmsg['msgid'] = $success; + } else { + $resultmsg['msgid'] = -1; + $resultmsg['errormessage'] = $errormessage; + } + + $resultmessages[] = $resultmsg; + } + + return $resultmessages; + } + + /** + * Returns description of method result value + * @return external_description + */ + public static function send_messages_returns() { + return new external_multiple_structure( + new external_single_structure( + array( + 'clientmsgid' => new external_value(PARAM_ALPHANUMEXT, 'your own id for the message', VALUE_OPTIONAL), + 'msgid' => new external_value(PARAM_INT, 'test this to know if it succeeds: id of the created message if it succeeded, -1 when failed'), + 'errormessage' => new external_value(PARAM_TEXT, 'error message - if it failed', VALUE_OPTIONAL) + ) + ) + ); + } + +} diff --git a/message/lib.php b/message/lib.php index acf231b5fe0..bf585a75b18 100644 --- a/message/lib.php +++ b/message/lib.php @@ -53,6 +53,30 @@ define('MESSAGE_SEARCH_MAX_RESULTS', 200); define('MESSAGE_CONTACTS_PER_PAGE',10); define('MESSAGE_MAX_COURSE_NAME_LENGTH', 30); +/** + * Define contants for messaging default settings population. For unambiguity of + * plugin developer intentions we use 4-bit value (LSB numbering): + * bit 0 - whether to send message when user is loggedin (MESSAGE_DEFAULT_LOGGEDIN) + * bit 1 - whether to send message when user is loggedoff (MESSAGE_DEFAULT_LOGGEDOFF) + * bit 2..3 - messaging permission (MESSAGE_DISALLOWED|MESSAGE_PERMITTED|MESSAGE_FORCED) + * + * MESSAGE_PERMITTED_MASK contains the mask we use to distinguish permission setting + */ + +define('MESSAGE_DEFAULT_LOGGEDIN', 0x01); // 0001 +define('MESSAGE_DEFAULT_LOGGEDOFF', 0x02); // 0010 + +define('MESSAGE_DISALLOWED', 0x04); // 0100 +define('MESSAGE_PERMITTED', 0x08); // 1000 +define('MESSAGE_FORCED', 0x0c); // 1100 + +define('MESSAGE_PERMITTED_MASK', 0x0c); // 1100 + +/** + * Set default value for default outputs permitted setting + */ +define('MESSAGE_DEFAULT_PERMITTED', 'permitted'); + if (!isset($CFG->message_contacts_refresh)) { // Refresh the contacts list every 60 seconds $CFG->message_contacts_refresh = 60; } @@ -64,20 +88,21 @@ if (!isset($CFG->message_offline_time)) { } /** -* Print the selector that allows the user to view their contacts, course participants, their recent -* conversations etc -* @param int $countunreadtotal how many unread messages does the user have? -* @param int $viewing What is the user viewing? ie MESSAGE_VIEW_UNREAD_MESSAGES, MESSAGE_VIEW_SEARCH etc -* @param object $user1 the user whose messages are being viewed -* @param object $user2 the user $user1 is talking to -* @param array $blockedusers an array of users blocked by $user1 -* @param array $onlinecontacts an array of $user1's online contacts -* @param array $offlinecontacts an array of $user1's offline contacts -* @param array $strangers an array of users who have messaged $user1 who aren't contacts -* @param bool $showcontactactionlinks show action links (add/remove contact etc) next to the users in the contact selector -* @param int $page if there are so many users listed that they have to be split into pages what page are we viewing -* @return void -*/ + * Print the selector that allows the user to view their contacts, course participants, their recent + * conversations etc + * + * @param int $countunreadtotal how many unread messages does the user have? + * @param int $viewing What is the user viewing? ie MESSAGE_VIEW_UNREAD_MESSAGES, MESSAGE_VIEW_SEARCH etc + * @param object $user1 the user whose messages are being viewed + * @param object $user2 the user $user1 is talking to + * @param array $blockedusers an array of users blocked by $user1 + * @param array $onlinecontacts an array of $user1's online contacts + * @param array $offlinecontacts an array of $user1's offline contacts + * @param array $strangers an array of users who have messaged $user1 who aren't contacts + * @param bool $showcontactactionlinks show action links (add/remove contact etc) next to the users in the contact selector + * @param int $page if there are so many users listed that they have to be split into pages what page are we viewing + * @return void + */ function message_print_contact_selector($countunreadtotal, $viewing, $user1, $user2, $blockedusers, $onlinecontacts, $offlinecontacts, $strangers, $showcontactactionlinks, $page=0) { global $PAGE; @@ -140,16 +165,17 @@ function message_print_contact_selector($countunreadtotal, $viewing, $user1, $us } /** -* Print course participants. Called by message_print_contact_selector() -* @param object $context the course context -* @param int $courseid the course ID -* @param string $contactselecturl the url to send the user to when a contact's name is clicked -* @param bool $showactionlinks show action links (add/remove contact etc) next to the users -* @param string $titletodisplay Optionally specify a title to display above the participants -* @param int $page if there are so many users listed that they have to be split into pages what page are we viewing -* @param object $user2 the user $user1 is talking to. They will be highlighted if they appear in the list of participants -* @return void -*/ + * Print course participants. Called by message_print_contact_selector() + * + * @param object $context the course context + * @param int $courseid the course ID + * @param string $contactselecturl the url to send the user to when a contact's name is clicked + * @param bool $showactionlinks show action links (add/remove contact etc) next to the users + * @param string $titletodisplay Optionally specify a title to display above the participants + * @param int $page if there are so many users listed that they have to be split into pages what page are we viewing + * @param object $user2 the user $user1 is talking to. They will be highlighted if they appear in the list of participants + * @return void + */ function message_print_participants($context, $courseid, $contactselecturl=null, $showactionlinks=true, $titletodisplay=null, $page=0, $user2=null) { global $DB, $USER, $PAGE, $OUTPUT; @@ -183,12 +209,13 @@ function message_print_participants($context, $courseid, $contactselecturl=null, } /** -* Retrieve users blocked by $user1 -* @param object $user1 the user whose messages are being viewed -* @param object $user2 the user $user1 is talking to. If they are being blocked -* they will have a variable called 'isblocked' added to their user object -* @return array the users blocked by $user1 -*/ + * Retrieve users blocked by $user1 + * + * @param object $user1 the user whose messages are being viewed + * @param object $user2 the user $user1 is talking to. If they are being blocked + * they will have a variable called 'isblocked' added to their user object + * @return array the users blocked by $user1 + */ function message_get_blocked_users($user1=null, $user2=null) { global $DB, $USER; @@ -225,14 +252,15 @@ function message_get_blocked_users($user1=null, $user2=null) { } /** -* Print users blocked by $user1. Called by message_print_contact_selector() -* @param array $blockedusers the users blocked by $user1 -* @param string $contactselecturl the url to send the user to when a contact's name is clicked -* @param bool $showactionlinks show action links (add/remove contact etc) next to the users -* @param string $titletodisplay Optionally specify a title to display above the participants -* @param object $user2 the user $user1 is talking to. They will be highlighted if they appear in the list of blocked users -* @return void -*/ + * Print users blocked by $user1. Called by message_print_contact_selector() + * + * @param array $blockedusers the users blocked by $user1 + * @param string $contactselecturl the url to send the user to when a contact's name is clicked + * @param bool $showactionlinks show action links (add/remove contact etc) next to the users + * @param string $titletodisplay Optionally specify a title to display above the participants + * @param object $user2 the user $user1 is talking to. They will be highlighted if they appear in the list of blocked users + * @return void + */ function message_print_blocked_users($blockedusers, $contactselecturl=null, $showactionlinks=true, $titletodisplay=null, $user2=null) { global $DB, $USER; @@ -262,12 +290,13 @@ function message_print_blocked_users($blockedusers, $contactselecturl=null, $sho } /** -* Retrieve $user1's contacts (online, offline and strangers) -* @param object $user1 the user whose messages are being viewed -* @param object $user2 the user $user1 is talking to. If they are a contact -* they will have a variable called 'iscontact' added to their user object -* @return array containing 3 arrays. array($onlinecontacts, $offlinecontacts, $strangers) -*/ + * Retrieve $user1's contacts (online, offline and strangers) + * + * @param object $user1 the user whose messages are being viewed + * @param object $user2 the user $user1 is talking to. If they are a contact + * they will have a variable called 'iscontact' added to their user object + * @return array containing 3 arrays. array($onlinecontacts, $offlinecontacts, $strangers) + */ function message_get_contacts($user1=null, $user2=null) { global $DB, $CFG, $USER; @@ -342,18 +371,19 @@ function message_get_contacts($user1=null, $user2=null) { } /** -* Print $user1's contacts. Called by message_print_contact_selector() -* @param array $onlinecontacts $user1's contacts which are online -* @param array $offlinecontacts $user1's contacts which are offline -* @param array $strangers users which are not contacts but who have messaged $user1 -* @param string $contactselecturl the url to send the user to when a contact's name is clicked -* @param int $minmessages The minimum number of unread messages required from a user for them to be displayed -* Typically 0 (show all contacts) or 1 (only show contacts from whom we have a new message) -* @param bool $showactionlinks show action links (add/remove contact etc) next to the users -* @param string $titletodisplay Optionally specify a title to display above the participants -* @param object $user2 the user $user1 is talking to. They will be highlighted if they appear in the list of contacts -* @return void -*/ + * Print $user1's contacts. Called by message_print_contact_selector() + * + * @param array $onlinecontacts $user1's contacts which are online + * @param array $offlinecontacts $user1's contacts which are offline + * @param array $strangers users which are not contacts but who have messaged $user1 + * @param string $contactselecturl the url to send the user to when a contact's name is clicked + * @param int $minmessages The minimum number of unread messages required from a user for them to be displayed + * Typically 0 (show all contacts) or 1 (only show contacts from whom we have a new message) + * @param bool $showactionlinks show action links (add/remove contact etc) next to the users + * @param string $titletodisplay Optionally specify a title to display above the participants + * @param object $user2 the user $user1 is talking to. They will be highlighted if they appear in the list of contacts + * @return void + */ function message_print_contacts($onlinecontacts, $offlinecontacts, $strangers, $contactselecturl=null, $minmessages=0, $showactionlinks=true, $titletodisplay=null, $user2=null) { global $CFG, $PAGE, $OUTPUT; @@ -426,16 +456,17 @@ function message_print_contacts($onlinecontacts, $offlinecontacts, $strangers, $ } /** -* Print a select box allowing the user to choose to view new messages, course participants etc. -* Called by message_print_contact_selector() -* @param int $viewing What page is the user viewing ie MESSAGE_VIEW_UNREAD_MESSAGES, MESSAGE_VIEW_RECENT_CONVERSATIONS etc -* @param array $courses array of course objects. The courses the user is enrolled in. -* @param array $coursecontexts array of course contexts. Keyed on course id. -* @param int $countunreadtotal how many unread messages does the user have? -* @param int $countblocked how many users has the current user blocked? -* @param string $strunreadmessages a preconstructed message about the number of unread messages the user has -* @return void -*/ + * Print a select box allowing the user to choose to view new messages, course participants etc. + * + * Called by message_print_contact_selector() + * @param int $viewing What page is the user viewing ie MESSAGE_VIEW_UNREAD_MESSAGES, MESSAGE_VIEW_RECENT_CONVERSATIONS etc + * @param array $courses array of course objects. The courses the user is enrolled in. + * @param array $coursecontexts array of course contexts. Keyed on course id. + * @param int $countunreadtotal how many unread messages does the user have? + * @param int $countblocked how many users has the current user blocked? + * @param string $strunreadmessages a preconstructed message about the number of unread messages the user has + * @return void + */ function message_print_usergroup_selector($viewing, $courses, $coursecontexts, $countunreadtotal, $countblocked, $strunreadmessages) { $options = array(); $textlib = textlib_get_instance(); // going to use textlib services @@ -482,10 +513,11 @@ function message_print_usergroup_selector($viewing, $courses, $coursecontexts, $ } /** -* Load the course contexts for all of the users courses -* @param array $courses array of course objects. The courses the user is enrolled in. -* @return array of course contexts -*/ + * Load the course contexts for all of the users courses + * + * @param array $courses array of course objects. The courses the user is enrolled in. + * @return array of course contexts + */ function message_get_course_contexts($courses) { $coursecontexts = array(); @@ -498,6 +530,7 @@ function message_get_course_contexts($courses) { /** * strip off action parameters like 'removecontact' + * * @param moodle_url/string $moodleurl a URL. Typically the current page URL. * @return string the URL minus parameters that perform actions (like adding/removing/blocking a contact). */ @@ -511,6 +544,7 @@ function message_remove_url_params($moodleurl) { * Count the number of messages with a field having a specified value. * if $field is empty then return count of the whole array * if $field is non-existent then return 0 + * * @param array $messagearray array of message objects * @param string $field the field to inspect on the message objects * @param string $value the value to test the field against @@ -528,6 +562,7 @@ function message_count_messages($messagearray, $field='', $value='') { /** * Returns the count of unread messages for user. Either from a specific user or from all users. + * * @param object $user1 the first user. Defaults to $USER * @param object $user2 the second user. If null this function will count all of user 1's unread messages. * @return int the count of $user1's unread messages @@ -550,7 +585,9 @@ function message_count_unread_messages($user1=null, $user2=null) { /** * Count the number of users blocked by $user1 - * @param $user1 + * + * @param object $user1 user object + * @return int the number of blocked users */ function message_count_blocked_users($user1=null) { global $USER, $DB; @@ -569,6 +606,7 @@ function message_count_blocked_users($user1=null) { /** * Print the search form and search results if a search has been performed + * * @param boolean $advancedsearch show basic or advanced search form * @param object $user1 the current user * @return boolean true if a search was performed @@ -627,6 +665,7 @@ function message_print_search($advancedsearch = false, $user1=null) { /** * Get the users recent conversations meaning all the people they've recently * sent or received a message from plus the most recent message sent to or received from each other user + * * @param object $user the current user * @param int $limitfrom can be used for paging * @param int $limitto can be used for paging @@ -718,6 +757,7 @@ function message_get_recent_conversations($user, $limitfrom=0, $limitto=100) { /** * Sort function used to order conversations + * * @param object $a A conversation object * @param object $b A conversation object * @return integer @@ -732,6 +772,7 @@ function conversationsort($a, $b) /** * Get the users recent event notifications + * * @param object $user the current user * @param int $limitfrom can be used for paging * @param int $limitto can be used for paging @@ -754,8 +795,10 @@ function message_get_recent_notifications($user, $limitfrom=0, $limitto=100) { /** * Print the user's recent conversations + * * @param object $user1 the current user * @param bool $showicontext flag indicating whether or not to show text next to the action icons + * @return void */ function message_print_recent_conversations($user=null, $showicontext=false) { global $USER; @@ -776,7 +819,9 @@ function message_print_recent_conversations($user=null, $showicontext=false) { /** * Print the user's recent notifications + * * @param object $user1 the current user + * @return void */ function message_print_recent_notifications($user=null) { global $USER; @@ -798,11 +843,13 @@ function message_print_recent_notifications($user=null) { /** * Print a list of recent messages + * * @staticvar type $dateformat * @param array $messages the messages to display * @param object $user the current user * @param bool $showotheruser display information on the other user? * @param bool $showicontext show text next to the action icons? + * @return void */ function message_print_recent_messages_table($messages, $user=null, $showotheruser=true, $showicontext=false) { global $OUTPUT; @@ -871,6 +918,7 @@ function message_print_recent_messages_table($messages, $user=null, $showotherus /** * Add the selected user as a contact for the current user + * * @param int $contactid the ID of the user to add as a contact * @param int $blocked 1 if you wish to block the contact * @return bool/int false if the $contactid isnt a valid user id. True if no changes made. @@ -907,6 +955,7 @@ function message_add_contact($contactid, $blocked=0) { /** * remove a contact + * * @param type $contactid the user ID of the contact to remove * @return bool returns the result of delete_records() */ @@ -917,6 +966,7 @@ function message_remove_contact($contactid) { /** * Unblock a contact. Note that this reverts the previously blocked user back to a non-contact. + * * @param int $contactid the user ID of the contact to unblock * @return bool returns the result of delete_records() */ @@ -927,6 +977,7 @@ function message_unblock_contact($contactid) { /** * block a user + * * @param int $contactid the user ID of the user to block */ function message_block_contact($contactid) { @@ -935,7 +986,9 @@ function message_block_contact($contactid) { /** * Load a user's contact record + * * @param int $contactid the user ID of the user whose contact record you want + * @return array message contacts */ function message_get_contact($contactid) { global $USER, $DB; @@ -944,9 +997,11 @@ function message_get_contact($contactid) { /** * Print the results of a message search + * * @param mixed $frm submitted form data * @param bool $showicontext show text next to action icons? * @param object $user1 the current user + * @return void */ function message_print_search_results($frm, $showicontext=false, $user1=null) { global $USER, $DB, $OUTPUT; @@ -1212,10 +1267,12 @@ function message_print_search_results($frm, $showicontext=false, $user1=null) { /** * Print information on a user. Used when printing search results. + * * @param object/bool $user the user to display or false if you just want $USER * @param bool $iscontact is the user being displayed a contact? * @param bool $isblocked is the user being displayed blocked? * @param bool $includeicontext include text next to the action icons? + * @return void */ function message_print_user ($user=false, $iscontact=false, $isblocked=false, $includeicontext=false) { global $USER, $OUTPUT; @@ -1259,6 +1316,7 @@ function message_print_user ($user=false, $iscontact=false, $isblocked=false, $i /** * Print a message contact link + * * @staticvar type $str * @param int $userid the ID of the user to apply to action to * @param string $linktype can be add, remove, block or unblock @@ -1333,6 +1391,7 @@ function message_contact_link($userid, $linktype='add', $return=false, $script=n /** * echo or return a link to take the user to the full message history between themselves and another user + * * @staticvar type $strmessagehistory * @param int $userid1 the ID of the current user * @param int $userid2 the ID of the other user @@ -1456,6 +1515,7 @@ function message_search_users($courseid, $searchtext, $sort='', $exceptions='') /** * search a user's messages + * * @param array $searchterms an array of search terms (strings) * @param bool $fromme include messages from the user? * @param bool $tome include messages to the user? @@ -1597,6 +1657,7 @@ function message_search($searchterms, $fromme=true, $tome=true, $courseid='none' * Given a message object that we already know has a long message * this function truncates the message nicely to the first * sane place between $CFG->forum_longpost and $CFG->forum_shortpost + * * @param string $message the message * @param int $minlength the minimum length to trim the message to * @return string the shortened message @@ -1652,6 +1713,7 @@ function message_shorten_message($message, $minlength = 0) { * Given a string and an array of keywords, this function looks * for the first keyword in the string, and then chops out a * small section from the text that shows that word in context. + * * @param string $message the text to search * @param array $keywords array of keywords to find */ @@ -1699,6 +1761,7 @@ function message_get_fragment($message, $keywords) { /** * Retrieve the messages between two users + * * @param object $user1 the current user * @param object $user2 the other user * @param int $limitnum the maximum number of messages to retrieve @@ -1754,6 +1817,7 @@ function message_get_history($user1, $user2, $limitnum=0, $viewingnewmessages=fa /** * Print the message history between two users + * * @param object $user1 the current user * @param object $user2 the other user * @param string $search search terms to highlight @@ -1853,6 +1917,7 @@ function message_print_message_history($user1,$user2,$search='',$messagelimit=0, /** * Format a message for display in the message history + * * @param object $message the message object * @param string $format optional date format * @param string $keywords keywords to highlight @@ -1893,6 +1958,7 @@ function message_format_message($message, $format='', $keywords='', $class='othe /** * Format a the context url and context url name of a message for display + * * @param object $message the message object * @return string the formatted string */ @@ -1916,6 +1982,7 @@ function message_format_contexturl($message) { /** * Send a message from one user to another. Will be delivered according to the message recipients messaging preferences + * * @param object $userfrom the message sender * @param object $userto the message recipient * @param string $message the message @@ -1968,6 +2035,7 @@ function message_post_message($userfrom, $userto, $message, $format) { * on large datasets? * * @todo: deprecated - to be deleted in 2.2 + * @return array */ function message_get_participants() { global $CFG, $DB; @@ -1983,12 +2051,14 @@ function message_get_participants() { /** * Print a row of contactlist displaying user picture, messages waiting and * block links etc + * * @param object $contact contact object containing all fields required for $OUTPUT->user_picture() * @param bool $incontactlist is the user a contact of ours? * @param bool $isblocked is the user blocked? * @param string $selectcontacturl the url to send the user to when a contact's name is clicked * @param bool $showactionlinks display action links next to the other users (add contact, block user etc) * @param object $selecteduser the user the current user is viewing (if any). They will be highlighted. + * @return void */ function message_print_contactlist_user($contact, $incontactlist = true, $isblocked = false, $selectcontacturl = null, $showactionlinks = true, $selecteduser=null) { global $OUTPUT, $USER; @@ -2048,6 +2118,7 @@ function message_print_contactlist_user($contact, $incontactlist = true, $isbloc /** * Constructs the add/remove contact link to display next to other users + * * @param bool $incontactlist is the user a contact * @param bool $isblocked is the user blocked * @param type $contact contact object @@ -2072,6 +2143,7 @@ function message_get_contact_add_remove_link($incontactlist, $isblocked, $contac /** * Constructs the block contact link to display next to other users + * * @param bool $incontactlist is the user a contact * @param bool $isblocked is the user blocked * @param type $contact contact object @@ -2096,12 +2168,13 @@ function message_get_contact_block_link($incontactlist, $isblocked, $contact, $s return $strblock; } - /** - * Moves messages from a particular user from the message table (unread messages) to message_read - * This is typically only used when a user is deleted - * @param object $userid User id - * @return boolean success - */ +/** + * Moves messages from a particular user from the message table (unread messages) to message_read + * This is typically only used when a user is deleted + * + * @param object $userid User id + * @return boolean success + */ function message_move_userfrom_unread2read($userid) { global $DB; @@ -2115,11 +2188,12 @@ function message_move_userfrom_unread2read($userid) { } /** -* marks ALL messages being sent from $fromuserid to $touserid as read -* @param int $touserid the id of the message recipient -* @param int $fromuserid the id of the message sender -* @return void -*/ + * marks ALL messages being sent from $fromuserid to $touserid as read + * + * @param int $touserid the id of the message recipient + * @param int $fromuserid the id of the message sender + * @return void + */ function message_mark_messages_read($touserid, $fromuserid){ global $DB; @@ -2134,12 +2208,13 @@ function message_mark_messages_read($touserid, $fromuserid){ } /** -* Mark a single message as read -* @param message an object with an object property ie $message->id which is an id in the message table -* @param int $timeread the timestamp for when the message should be marked read. Usually time(). -* @param bool $messageworkingempty Is the message_working table already confirmed empty for this message? -* @return int the ID of the message in the message_read table -*/ + * Mark a single message as read + * + * @param message an object with an object property ie $message->id which is an id in the message table + * @param int $timeread the timestamp for when the message should be marked read. Usually time(). + * @param bool $messageworkingempty Is the message_working table already confirmed empty for this message? + * @return int the ID of the message in the message_read table + */ function message_mark_message_read($message, $timeread, $messageworkingempty=false) { global $DB; @@ -2159,11 +2234,131 @@ function message_mark_message_read($message, $timeread, $messageworkingempty=fal /** * A helper function that prints a formatted heading + * * @param string $title the heading to display * @param int $colspan + * @return void */ function message_print_heading($title, $colspan=3) { echo html_writer::start_tag('tr'); echo html_writer::tag('td', $title, array('colspan' => $colspan, 'class' => 'heading')); echo html_writer::end_tag('tr'); } + +/** + * Get all message processors, validate corresponding plugin existance and + * system configuration + * + * @param bool $ready only return ready-to-use processors + * @return mixed $processors array of objects containing information on message processors + */ +function get_message_processors($ready = false) { + global $DB, $CFG; + + static $processors; + + if (empty($processors)) { + // Get all processors, ensure the name column is the first so it will be the array key + $processors = $DB->get_records('message_processors', null, 'name DESC', 'name, id, enabled'); + foreach ($processors as &$processor){ + $processorfile = $CFG->dirroot. '/message/output/'.$processor->name.'/message_output_'.$processor->name.'.php'; + if (is_readable($processorfile)) { + include_once($processorfile); + $processclass = 'message_output_' . $processor->name; + if (class_exists($processclass)) { + $pclass = new $processclass(); + $processor->object = $pclass; + $processor->configured = 0; + if ($pclass->is_system_configured()) { + $processor->configured = 1; + } + $processor->hassettings = 0; + if (is_readable($CFG->dirroot.'/message/output/'.$processor->name.'/settings.php')) { + $processor->hassettings = 1; + } + $processor->available = 1; + } else { + print_error('errorcallingprocessor', 'message'); + } + } else { + $processor->available = 0; + } + } + } + if ($ready) { + // Filter out enabled and system_configured processors + $readyprocessors = $processors; + foreach ($readyprocessors as $readyprocessor) { + if (!($readyprocessor->enabled && $readyprocessor->configured)) { + unset($readyprocessors[$readyprocessor->name]); + } + } + return $readyprocessors; + } + + return $processors; +} + +/** + * Get messaging outputs default (site) preferences + * + * @return object $processors object containing information on message processors + */ +function get_message_output_default_preferences() { + $preferences = get_config('message'); + if (!$preferences) { + $preferences = new stdClass(); + } + return $preferences; +} + +/** + * Translate message default settings from binary value to the array of string + * representing the settings to be stored. Also validate the provided value and + * use default if it is malformed. + * + * @param int $plugindefault Default setting suggested by plugin + * @param string $processorname The name of processor + * @return array $settings array of strings in the order: $permitted, $loggedin, $loggedoff. + */ +function translate_message_default_setting($plugindefault, $processorname) { + // Preset translation arrays + $permittedvalues = array( + 0x04 => 'disallowed', + 0x08 => 'permitted', + 0x0c => 'forced', + ); + + $loggedinstatusvalues = array( + 0x00 => null, // use null if loggedin/loggedoff is not defined + 0x01 => 'loggedin', + 0x02 => 'loggedoff', + ); + + // define the default setting + if ($processorname == 'email') { + $default = MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF; + } else { + $default = MESSAGE_PERMITTED; + } + + // Validate the value. It should not exceed the maximum size + if (!is_int($plugindefault) || ($plugindefault > 0x0f)) { + $OUTPUT->notification(get_string('errortranslatingdefault', 'message'), 'notifyproblem'); + $plugindefault = $default; + } + // Use plugin default setting of 'permitted' is 0 + if (!($plugindefault & MESSAGE_PERMITTED_MASK)) { + $plugindefault = $default; + } + + $permitted = $permittedvalues[$plugindefault & MESSAGE_PERMITTED_MASK]; + $loggedin = $loggedoff = null; + + if (($plugindefault & MESSAGE_PERMITTED_MASK) == MESSAGE_PERMITTED) { + $loggedin = $loggedinstatusvalues[$plugindefault & MESSAGE_DEFAULT_LOGGEDIN]; + $loggedoff = $loggedinstatusvalues[$plugindefault & MESSAGE_DEFAULT_LOGGEDOFF]; + } + + return array($permitted, $loggedin, $loggedoff); +} diff --git a/message/module.js b/message/module.js index e0627d0cc65..d92f7484b8a 100644 --- a/message/module.js +++ b/message/module.js @@ -42,4 +42,49 @@ M.core_message.init_notification = function(Y, title, content, url) { return false; }, o); }); -}; \ No newline at end of file +}; + +M.core_message.init_defaultoutputs = function(Y) { + var defaultoutputs = { + + init : function() { + Y.all('#defaultmessageoutputs select').each(function(node) { + // attach event listener + node.on('change', defaultoutputs.changeState); + // set initial layout + node.simulate("change"); + }, this); + }, + + changeState : function(e) { + var value = e.target._node.options[e.target.get('selectedIndex')].value; + var parentnode = e.target.ancestor('td'); + switch (value) { + case 'forced': + defaultoutputs.updateCheckboxes(parentnode, 1, 1); + break; + case 'disallowed': + defaultoutputs.updateCheckboxes(parentnode, 1, 0); + break; + case 'permitted': + defaultoutputs.updateCheckboxes(parentnode, 0, 0); + break; + } + }, + + updateCheckboxes : function(blocknode, disabled, checked) { + blocknode.all('input[type=checkbox]').each(function(node) { + node.removeAttribute('disabled'); + if (disabled) { + node.setAttribute('disabled', 1) + node.removeAttribute('checked'); + } + if (checked) { + node.setAttribute('checked', 1) + } + }, this); + } + } + + defaultoutputs.init(); +} \ No newline at end of file diff --git a/message/output/email/lang/en/message_email.php b/message/output/email/lang/en/message_email.php index 2cb8fe295fa..ae06d11d976 100644 --- a/message/output/email/lang/en/message_email.php +++ b/message/output/email/lang/en/message_email.php @@ -23,5 +23,20 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -$string['pluginname'] = 'Email'; +$string['allowusermailcharset'] = 'Allow user to select character set'; +$string['configallowusermailcharset'] = 'Enabling this, every user in the site will be able to specify his own charset for email.'; +$string['configmailnewline'] = 'Newline characters used in mail messages. CRLF is required according to RFC 822bis, some mail servers do automatic conversion from LF to CRLF, other mail servers do incorrect conversion from CRLF to CRCRLF, yet others reject mails with bare LF (qmail for example). Try changing this setting if you are having problems with undelivered emails or double newlines.'; +$string['confignoreplyaddress'] = 'Emails are sometimes sent out on behalf of a user (eg forum posts). The email address you specify here will be used as the "From" address in those cases when the recipients should not be able to reply directly to the user (eg when a user chooses to keep their address private).'; +$string['configsitemailcharset'] = 'All the emails generated by your site will be sent in the charset specified here. Anyway, every individual user will be able to adjust it if the next setting is enabled.'; +$string['configsmtphosts'] = 'Give the full name of one or more local SMTP servers that Moodle should use to send mail (eg \'mail.a.com\' or \'mail.a.com;mail.b.com\'). To specify a non-default port (i.e other than port 25), you can use the [server]:[port] syntax (eg \'mail.a.com:587\'. If you leave it blank, Moodle will use the PHP default method of sending mail.'; +$string['configsmtpmaxbulk'] = 'Maximum number of messages sent per SMTP session. Grouping messages may speed up the sending of emails. Values lower than 2 force creation of new SMTP session for each email.'; +$string['configsmtpuser'] = 'If you have specified an SMTP server above, and the server requires authentication, then enter the username and password here.'; $string['email'] = 'Send email notifications to'; +$string['mailnewline'] = 'Newline characters in mail'; +$string['noreplyaddress'] = 'No-reply address'; +$string['pluginname'] = 'Email'; +$string['sitemailcharset'] = 'Character set'; +$string['smtphosts'] = 'SMTP hosts'; +$string['smtpmaxbulk'] = 'SMTP session limit'; +$string['smtppass'] = 'SMTP password'; +$string['smtpuser'] = 'SMTP username'; diff --git a/message/output/email/message_output_email.php b/message/output/email/message_output_email.php index b919eced361..7f27f62614d 100644 --- a/message/output/email/message_output_email.php +++ b/message/output/email/message_output_email.php @@ -83,7 +83,7 @@ class message_output_email extends message_output { * @param array $preferences preferences array */ function process_form($form, &$preferences){ - if (isset($form->email_email)) { + if (isset($form->email_email) && !empty($form->email_email)) { $preferences['message_processor_email_email'] = $form->email_email; } } diff --git a/message/output/email/settings.php b/message/output/email/settings.php new file mode 100644 index 00000000000..7b7e7230769 --- /dev/null +++ b/message/output/email/settings.php @@ -0,0 +1,44 @@ +. + +/** + * Email configuration page + * + * @package message + * @subpackage email + * @copyright 2011 Lancaster University Network Services Limited + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +if ($ADMIN->fulltree) { + $settings->add(new admin_setting_configtext('smtphosts', get_string('smtphosts', 'message_email'), get_string('configsmtphosts', 'message_email'), '', PARAM_RAW)); + $settings->add(new admin_setting_configtext('smtpuser', get_string('smtpuser', 'message_email'), get_string('configsmtpuser', 'message_email'), '', PARAM_NOTAGS)); + $settings->add(new admin_setting_configpasswordunmask('smtppass', get_string('smtppass', 'message_email'), get_string('configsmtpuser', 'message_email'), '')); + $settings->add(new admin_setting_configtext('smtpmaxbulk', get_string('smtpmaxbulk', 'message_email'), get_string('configsmtpmaxbulk', 'message_email'), 1, PARAM_INT)); + $settings->add(new admin_setting_configtext('noreplyaddress', get_string('noreplyaddress', 'message_email'), get_string('confignoreplyaddress', 'message_email'), 'noreply@' . get_host_from_url($CFG->wwwroot), PARAM_NOTAGS)); + + $charsets = get_list_of_charsets(); + unset($charsets['UTF-8']); // not needed here + $options = array(); + $options['0'] = 'UTF-8'; + $options = array_merge($options, $charsets); + $settings->add(new admin_setting_configselect('sitemailcharset', get_string('sitemailcharset', 'message_email'), get_string('configsitemailcharset','message_email'), '0', $options)); + $settings->add(new admin_setting_configcheckbox('allowusermailcharset', get_string('allowusermailcharset', 'message_email'), get_string('configallowusermailcharset', 'message_email'), 0)); + $options = array('LF'=>'LF', 'CRLF'=>'CRLF'); + $settings->add(new admin_setting_configselect('mailnewline', get_string('mailnewline', 'message_email'), get_string('configmailnewline','message_email'), 'LF', $options)); +} \ No newline at end of file diff --git a/message/output/jabber/lang/en/message_jabber.php b/message/output/jabber/lang/en/message_jabber.php index fee28ef13e4..e38f6999e66 100644 --- a/message/output/jabber/lang/en/message_jabber.php +++ b/message/output/jabber/lang/en/message_jabber.php @@ -23,6 +23,16 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -$string['pluginname'] = 'Jabber message'; +$string['configjabberhost'] = 'The server to connect to to send jabber message notifications'; +$string['configjabberserver'] = 'XMPP host ID (can be left empty if the same as Jabber host)'; +$string['configjabberusername'] = 'The user name to use when connecting to the Jabber server'; +$string['configjabberpassword'] = 'The password to use when connecting to the Jabber server'; +$string['configjabberport'] = 'The port to use when connecting to the Jabber server'; +$string['jabberhost'] = 'Jabber host'; $string['jabberid'] = 'Jabber ID'; +$string['jabberserver'] = 'Jabber server'; +$string['jabberusername'] = 'Jabber user name'; +$string['jabberpassword'] = 'Jabber password'; +$string['jabberport'] = 'Jabber port'; $string['notconfigured'] = 'The Jabber server hasn\'t been configured so Jabber messages cannot be sent'; +$string['pluginname'] = 'Jabber message'; diff --git a/message/output/jabber/message_output_jabber.php b/message/output/jabber/message_output_jabber.php index 8969277b482..92eb8a8a994 100644 --- a/message/output/jabber/message_output_jabber.php +++ b/message/output/jabber/message_output_jabber.php @@ -44,46 +44,42 @@ class message_output_jabber extends message_output { function send_message($eventdata){ global $CFG; - if (message_output_jabber::_jabber_configured()) { - if (!empty($CFG->noemailever)) { - // hidden setting for development sites, set in config.php if needed - debugging('$CFG->noemailever active, no jabber message sent.', DEBUG_MINIMAL); - return true; - } - - //hold onto jabber id preference because /admin/cron.php sends a lot of messages at once - static $jabberaddresses = array(); - - if (!array_key_exists($eventdata->userto->id, $jabberaddresses)) { - $jabberaddresses[$eventdata->userto->id] = get_user_preferences('message_processor_jabber_jabberid', $eventdata->userto->email, $eventdata->userto->id); - } - $jabberaddress = $jabberaddresses[$eventdata->userto->id]; - - //calling s() on smallmessage causes Jabber to display things like < Jabber != a browser - $jabbermessage = fullname($eventdata->userfrom).': '.$eventdata->smallmessage; - - if (!empty($eventdata->contexturl)) { - $jabbermessage .= "\n".get_string('view').': '.$eventdata->contexturl; - } - - $jabbermessage .= "\n(".get_string('noreply','message').')'; - - $conn = new XMPPHP_XMPP($CFG->jabberhost,$CFG->jabberport,$CFG->jabberusername,$CFG->jabberpassword,'moodle',$CFG->jabberserver); - - try { - //$conn->useEncryption(false); - $conn->connect(); - $conn->processUntil('session_start'); - $conn->presence(); - $conn->message($jabberaddress, $jabbermessage); - $conn->disconnect(); - } catch(XMPPHP_Exception $e) { - debugging($e->getMessage()); - return false; - } + if (!empty($CFG->noemailever)) { + // hidden setting for development sites, set in config.php if needed + debugging('$CFG->noemailever active, no jabber message sent.', DEBUG_MINIMAL); + return true; } - //note that we're reporting success if message was sent or if Jabber simply isnt configured + //hold onto jabber id preference because /admin/cron.php sends a lot of messages at once + static $jabberaddresses = array(); + + if (!array_key_exists($eventdata->userto->id, $jabberaddresses)) { + $jabberaddresses[$eventdata->userto->id] = get_user_preferences('message_processor_jabber_jabberid', null, $eventdata->userto->id); + } + $jabberaddress = $jabberaddresses[$eventdata->userto->id]; + + //calling s() on smallmessage causes Jabber to display things like < Jabber != a browser + $jabbermessage = fullname($eventdata->userfrom).': '.$eventdata->smallmessage; + + if (!empty($eventdata->contexturl)) { + $jabbermessage .= "\n".get_string('view').': '.$eventdata->contexturl; + } + + $jabbermessage .= "\n(".get_string('noreply','message').')'; + + $conn = new XMPPHP_XMPP($CFG->jabberhost,$CFG->jabberport,$CFG->jabberusername,$CFG->jabberpassword,'moodle',$CFG->jabberserver); + + try { + //$conn->useEncryption(false); + $conn->connect(); + $conn->processUntil('session_start'); + $conn->presence(); + $conn->message($jabberaddress, $jabbermessage); + $conn->disconnect(); + } catch(XMPPHP_Exception $e) { + debugging($e->getMessage()); + return false; + } return true; } @@ -94,7 +90,7 @@ class message_output_jabber extends message_output { function config_form($preferences){ global $CFG; - if (!message_output_jabber::_jabber_configured()) { + if (!$this->is_system_configured()) { return get_string('notconfigured','message_jabber'); } else { return get_string('jabberid', 'message_jabber').': '; @@ -107,7 +103,7 @@ class message_output_jabber extends message_output { * @param array $preferences preferences array */ function process_form($form, &$preferences){ - if (isset($form->jabber_jabberid)) { + if (isset($form->jabber_jabberid) && !empty($form->jabber_jabberid)) { $preferences['message_processor_jabber_jabberid'] = $form->jabber_jabberid; } } @@ -125,11 +121,25 @@ class message_output_jabber extends message_output { * Tests whether the Jabber settings have been configured * @return boolean true if Jabber is configured */ - private function _jabber_configured() { + function is_system_configured() { global $CFG; return (!empty($CFG->jabberhost) && !empty($CFG->jabberport) && !empty($CFG->jabberusername) && !empty($CFG->jabberpassword)); } + /** + * Tests whether the Jabber settings have been configured on user level + * @param object $user the user object, defaults to $USER. + * @return bool has the user made all the necessary settings + * in their profile to allow this plugin to be used. + */ + function is_user_configured($user = null) { + global $USER; + + if (is_null($user)) { + $user = $USER; + } + return (bool)get_user_preferences('message_processor_jabber_jabberid', null, $user->id); + } } /* diff --git a/message/output/jabber/settings.php b/message/output/jabber/settings.php new file mode 100644 index 00000000000..3f7428f786f --- /dev/null +++ b/message/output/jabber/settings.php @@ -0,0 +1,34 @@ +. + +/** + * Jabber configuration page + * + * @package message + * @subpackage jabber + * @copyright 2011 Lancaster University Network Services Limited + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +if ($ADMIN->fulltree) { + $settings->add(new admin_setting_configtext('jabberhost', get_string('jabberhost', 'message_jabber'), get_string('configjabberhost', 'message_jabber'), '', PARAM_RAW)); + $settings->add(new admin_setting_configtext('jabberserver', get_string('jabberserver', 'message_jabber'), get_string('configjabberserver', 'message_jabber'), '', PARAM_RAW)); + $settings->add(new admin_setting_configtext('jabberusername', get_string('jabberusername', 'message_jabber'), get_string('configjabberusername', 'message_jabber'), '', PARAM_RAW)); + $settings->add(new admin_setting_configpasswordunmask('jabberpassword', get_string('jabberpassword', 'message_jabber'), get_string('configjabberpassword', 'message_jabber'), '')); + $settings->add(new admin_setting_configtext('jabberport', get_string('jabberport', 'message_jabber'), get_string('configjabberport', 'message_jabber'), 5222, PARAM_INT)); +} \ No newline at end of file diff --git a/message/output/lib.php b/message/output/lib.php index b485bf2ac03..f669c50d0f5 100644 --- a/message/output/lib.php +++ b/message/output/lib.php @@ -39,6 +39,22 @@ abstract class message_output { public abstract function process_form($form, &$preferences); public abstract function load_data(&$preferences, $userid); public abstract function config_form($preferences); + /** + * @return bool have all the necessary config settings been + * made that allow this plugin to be used. + */ + public function is_system_configured() { + return true; + } + /** + * @param object $user the user object, defaults to $USER. + * @return bool has the user made all the necessary settings + * in their profile to allow this plugin to be used. + */ + public function is_user_configured($user = null) { + return true; + } + } diff --git a/message/renderer.php b/message/renderer.php new file mode 100644 index 00000000000..bf24fa1aeb0 --- /dev/null +++ b/message/renderer.php @@ -0,0 +1,346 @@ +. + +/** + * Messaging libraries + * + * @package message + * @copyright 2011 Lancaster University Network Services Limited + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * message Renderer + * + * Class for rendering various message objects + * + * @package message + * @copyright 2011 Lancaster University Network Services Limited + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_message_renderer extends plugin_renderer_base { + + /** + * Display the interface to manage message outputs + * + * @param mixed $processors array of objects containing message processors + * @return string The text to render + */ + public function manage_messageoutputs($processors) { + global $CFG; + // Display the current workflows + $table = new html_table(); + $table->attributes['class'] = 'generaltable'; + $table->data = array(); + $table->head = array( + get_string('name'), + get_string('enable'), + get_string('settings'), + ); + $table->colclasses = array( + 'displayname', 'availability', 'settings', + ); + + foreach ($processors as $processor) { + $row = new html_table_row(); + $row->attributes['class'] = 'messageoutputs'; + + // Name + $name = new html_table_cell($processor->name); + + // Enable + $enable = new html_table_cell(); + $enable->attributes['class'] = 'mdl-align'; + if (!$processor->available) { + $enable->text = html_writer::nonempty_tag('span', get_string('outputnotavailable', 'message'), array('class' => 'error')); + } else if (!$processor->configured) { + $enable->text = html_writer::nonempty_tag('span', get_string('outputnotconfigured', 'message'), array('class' => 'error')); + } else if ($processor->enabled) { + $url = new moodle_url('/admin/message.php', array('disable' => $processor->id, 'sesskey' => sesskey())); + $enable->text = html_writer::link($url, html_writer::empty_tag('img', + array('src' => $this->output->pix_url('i/hide'), + 'class' => 'icon', + 'title' => get_string('outputenabled', 'message'), + 'alt' => get_string('outputenabled', 'message'), + ) + )); + } else { + $name->attributes['class'] = 'dimmed_text'; + $url = new moodle_url('/admin/message.php', array('enable' => $processor->id, 'sesskey' => sesskey())); + $enable->text = html_writer::link($url, html_writer::empty_tag('img', + array('src' => $this->output->pix_url('i/show'), + 'class' => 'icon', + 'title' => get_string('outputdisabled', 'message'), + 'alt' => get_string('outputdisabled', 'message'), + ) + )); + } + // Settings + $settings = new html_table_cell(); + if ($processor->available && $processor->hassettings) { + $settingsurl = new moodle_url('settings.php', array('section' => 'messagesetting'.$processor->name)); + $settings->text = html_writer::link($settingsurl, get_string('settings', 'message')); + } + + $row->cells = array($name, $enable, $settings); + $table->data[] = $row; + } + return html_writer::table($table); + } + + /** + * Display the interface to manage default message outputs + * + * @param mixed $processors array of objects containing message processors + * @param mixed $providers array of objects containing message providers + * @param mixed $preferences array of objects containing current preferences + * @return string The text to render + */ + public function manage_defaultmessageoutputs($processors, $providers, $preferences) { + global $CFG; + + // Prepare list of options for dropdown menu + $options = array(); + foreach (array('disallowed', 'permitted', 'forced') as $setting) { + $options[$setting] = get_string($setting, 'message'); + } + + $output = html_writer::start_tag('form', array('id'=>'defaultmessageoutputs', 'method'=>'post')); + $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'sesskey', 'value'=>sesskey())); + + // Display users outputs table + $table = new html_table(); + $table->attributes['class'] = 'generaltable'; + $table->data = array(); + $table->head = array(''); + + // Populate the header row + foreach ($processors as $processor) { + $table->head[] = get_string('pluginname', 'message_'.$processor->name); + } + // Generate the matrix of settings for each provider and processor + foreach ($providers as $provider) { + $row = new html_table_row(); + $row->attributes['class'] = 'defaultmessageoutputs'; + $row->cells = array(); + + // Provider Name + $providername = get_string('messageprovider:'.$provider->name, $provider->component); + $row->cells[] = new html_table_cell($providername); + + // Settings for each processor + foreach ($processors as $processor) { + $cellcontent = ''; + foreach (array('permitted', 'loggedin', 'loggedoff') as $setting) { + // pepare element and preference names + $elementname = $provider->component.'_'.$provider->name.'_'.$setting.'['.$processor->name.']'; + $preferencebase = $provider->component.'_'.$provider->name.'_'.$setting; + // prepare language bits + $processorname = get_string('pluginname', 'message_'.$processor->name); + $statename = get_string($setting, 'message'); + $labelparams = array( + 'provider' => $providername, + 'processor' => $processorname, + 'state' => $statename + ); + if ($setting == 'permitted') { + $label = get_string('sendingvia', 'message', $labelparams); + // determine the current setting or use default + $select = MESSAGE_DEFAULT_PERMITTED; + $preference = $processor->name.'_provider_'.$preferencebase; + if (array_key_exists($preference, $preferences)) { + $select = $preferences->{$preference}; + } + // dropdown menu + $cellcontent = html_writer::label($label, $elementname, true, array('class' => 'accesshide')); + $cellcontent .= html_writer::select($options, $elementname, $select, false, array('id' => $elementname)); + $cellcontent .= html_writer::tag('div', get_string('defaults', 'message')); + } else { + $label = get_string('sendingviawhen', 'message', $labelparams); + // determine the current setting based on the 'permitted' setting above + $checked = false; + if ($select == 'forced') { + $checked = true; + } else if ($select == 'permitted') { + $preference = 'message_provider_'.$preferencebase; + if (array_key_exists($preference, $preferences)) { + $checked = (int)in_array($processor->name, explode(',', $preferences->{$preference})); + } + } + // generate content + $cellcontent .= html_writer::start_tag('div'); + $cellcontent .= html_writer::label($label, $elementname, true, array('class' => 'accesshide')); + $cellcontent .= html_writer::checkbox($elementname, 1, $checked, '', array('id' => $elementname)); + $cellcontent .= $statename; + $cellcontent .= html_writer::end_tag('div'); + } + } + $row->cells[] = new html_table_cell($cellcontent); + } + $table->data[] = $row; + } + + $output .= html_writer::table($table); + $output .= html_writer::start_tag('div', array('class' => 'form-buttons')); + $output .= html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('savechanges','admin'), 'class' => 'form-submit')); + $output .= html_writer::end_tag('div'); + $output .= html_writer::end_tag('form'); + return $output; + } + + /** + * Display the interface for messaging options + * + * @param mixed $processors array of objects containing message processors + * @param mixed $providers array of objects containing message providers + * @param mixed $preferences array of objects containing current preferences + * @param mixed $defaultpreferences array of objects containing site default preferences + * @return string The text to render + */ + public function manage_messagingoptions($processors, $providers, $preferences, $defaultpreferences) { + // Filter out enabled, available system_configured and user_configured processors only. + $readyprocessors = array_filter($processors, create_function('$a', 'return $a->enabled && $a->configured && $a->object->is_user_configured();')); + + // Start the form. We're not using mform here because of our special formatting needs ... + $output = html_writer::start_tag('form', array('method'=>'post', 'class' => 'mform')); + $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'sesskey', 'value'=>sesskey())); + + /// Settings table... + $output .= html_writer::start_tag('fieldset', array('id' => 'providers', 'class' => 'clearfix')); + $output .= html_writer::nonempty_tag('legend', get_string('providers_config', 'message'), array('class' => 'ftoggler')); + + // Display the messging options table + $table = new html_table(); + $table->attributes['class'] = 'generaltable'; + $table->data = array(); + $table->head = array(''); + + foreach ($readyprocessors as $processor) { + $table->head[] = get_string('pluginname', 'message_'.$processor->name); + } + + $number_procs = count($processors); + // Populate the table with rows + foreach ( $providers as $provider) { + $preferencebase = $provider->component.'_'.$provider->name; + + $headerrow = new html_table_row(); + $providername = get_string('messageprovider:'.$provider->name, $provider->component); + $providercell = new html_table_cell($providername); + $providercell->header = true; + $providercell->colspan = $number_procs + 1; + $providercell->attributes['class'] = 'c0'; + $headerrow->cells = array($providercell); + $table->data[] = $headerrow; + + foreach (array('loggedin', 'loggedoff') as $state) { + $optionrow = new html_table_row(); + $optionname = new html_table_cell(get_string($state.'description', 'message')); + $optionname->attributes['class'] = 'c0'; + $optionrow->cells = array($optionname); + foreach ($readyprocessors as $processor) { + // determine the default setting + $permitted = MESSAGE_DEFAULT_PERMITTED; + $defaultpreference = $processor->name.'_provider_'.$preferencebase.'_permitted'; + if (isset($defaultpreferences->{$defaultpreference})) { + $permitted = $defaultpreferences->{$defaultpreference}; + } + // If settings are disallowed, just display the message that + // the setting is not permitted, if not use user settings or + // force them. + if ($permitted == 'disallowed') { + if ($state == 'loggedoff') { + // skip if we are rendering the second line + continue; + } + $cellcontent = html_writer::nonempty_tag('div', get_string('notpermitted', 'message'), array('class' => 'dimmed_text')); + $optioncell = new html_table_cell($cellcontent); + $optioncell->rowspan = 2; + $optioncell->attributes['class'] = 'disallowed'; + } else { + // determine user preferences and use then unless we force + // the preferences. + $disabled = array(); + if ($permitted == 'forced') { + $checked = true; + $disabled['disabled'] = 1; + } else { + $checked = false; + // See if hser has touched this preference + if (isset($preferences->{$preferencebase.'_'.$state})) { + // User have some preferneces for this state in the database, use them + $checked = isset($preferences->{$preferencebase.'_'.$state}[$processor->name]); + } else { + // User has not set this preference yet, using site default preferences set by admin + $defaultpreference = 'message_provider_'.$preferencebase.'_'.$state; + if (isset($defaultpreferences->{$defaultpreference})) { + $checked = (int)in_array($processor->name, explode(',', $defaultpreferences->{$defaultpreference})); + } + } + } + $elementname = $preferencebase.'_'.$state.'['.$processor->name.']'; + // prepare language bits + $processorname = get_string('pluginname', 'message_'.$processor->name); + $statename = get_string($state, 'message'); + $labelparams = array( + 'provider' => $providername, + 'processor' => $processorname, + 'state' => $statename + ); + $label = get_string('sendingviawhen', 'message', $labelparams); + $cellcontent = html_writer::label($label, $elementname, true, array('class' => 'accesshide')); + $cellcontent .= html_writer::checkbox($elementname, 1, $checked, '', array_merge(array('id' => $elementname), $disabled)); + $optioncell = new html_table_cell($cellcontent); + $optioncell->attributes['class'] = 'mdl-align'; + } + $optionrow->cells[] = $optioncell; + } + $table->data[] = $optionrow; + } + } + $output .= html_writer::start_tag('div'); + $output .= html_writer::table($table); + $output .= html_writer::end_tag('div'); + $output .= html_writer::end_tag('fieldset'); + + foreach ($processors as $processor) { + if (($processorconfigform = $processor->object->config_form($preferences)) && $processor->enabled) { + $output .= html_writer::start_tag('fieldset', array('id' => 'messageprocessor_'.$processor->name, 'class' => 'clearfix')); + $output .= html_writer::nonempty_tag('legend', get_string('pluginname', 'message_'.$processor->name), array('class' => 'ftoggler')); + $output .= html_writer::start_tag('div'); + $output .= $processorconfigform; + $output .= html_writer::end_tag('div'); + $output .= html_writer::end_tag('fieldset'); + } + } + + $output .= html_writer::start_tag('fieldset', array('id' => 'messageprocessor_general', 'class' => 'clearfix')); + $output .= html_writer::nonempty_tag('legend', get_string('generalsettings','admin'), array('class' => 'ftoggler')); + $output .= html_writer::start_tag('div'); + $output .= get_string('blocknoncontacts', 'message').': '; + $output .= html_writer::checkbox('blocknoncontacts', 1, $preferences->blocknoncontacts, ''); + $output .= html_writer::end_tag('div'); + $output .= html_writer::end_tag('fieldset'); + $output .= html_writer::start_tag('div', array('class' => 'mdl-align')); + $output .= html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('updatemyprofile'), 'class' => 'form-submit')); + $output .= html_writer::end_tag('div'); + $output .= html_writer::end_tag('form'); + return $output; + } + +} diff --git a/mod/assignment/lib.php b/mod/assignment/lib.php index eed4da00656..09f80134816 100644 --- a/mod/assignment/lib.php +++ b/mod/assignment/lib.php @@ -971,8 +971,7 @@ class assignment_base { $userfields = user_picture::fields('u', array('lastaccess')); $select = "SELECT $userfields, s.id AS submissionid, s.grade, s.submissioncomment, - s.timemodified, s.timemarked, - COALESCE(SIGN(SIGN(s.timemarked) + SIGN(s.timemarked - s.timemodified)), 0) AS status "; + s.timemodified, s.timemarked "; $sql = 'FROM {user} u '. 'LEFT JOIN {assignment_submissions} s ON u.id = s.userid AND s.assignment = '.$this->assignment->id.' '. @@ -1285,8 +1284,7 @@ class assignment_base { if (!empty($users)) { $select = "SELECT $ufields, s.id AS submissionid, s.grade, s.submissioncomment, - s.timemodified, s.timemarked, - COALESCE(SIGN(SIGN(s.timemarked) + SIGN(s.timemarked - s.timemodified)), 0) AS status "; + s.timemodified, s.timemarked "; $sql = 'FROM {user} u '. 'LEFT JOIN {assignment_submissions} s ON u.id = s.userid AND s.assignment = '.$this->assignment->id.' '. diff --git a/mod/forum/lang/en/forum.php b/mod/forum/lang/en/forum.php index ad6051adde3..17d8ffb238b 100644 --- a/mod/forum/lang/en/forum.php +++ b/mod/forum/lang/en/forum.php @@ -84,6 +84,7 @@ $string['completionreplies'] = 'Student must post replies:'; $string['completionrepliesgroup'] = 'Require replies'; $string['completionreplieshelp'] = 'requiring replies to complete'; $string['configcleanreadtime'] = 'The hour of the day to clean old posts from the \'read\' table.'; +$string['configdigestmailtime'] = 'People who choose to have emails sent to them in digest form will be emailed the digest daily. This setting controls which time of day the daily mail will be sent (the next cron that runs after this hour will send it).'; $string['configdisplaymode'] = 'The default display mode for discussions if one isn\'t set.'; $string['configenablerssfeeds'] = 'This switch will enable the possibility of RSS feeds for all forums. You will still need to turn feeds on manually in the settings for each forum.'; $string['configenabletimedposts'] = 'Set to \'yes\' if you want to allow setting of display periods when posting a new forum discussion (Experimental as not yet fully tested)'; @@ -110,6 +111,7 @@ $string['deletesureplural'] = 'Are you sure you want to delete this post and all $string['digestmailheader'] = 'This is your daily digest of new posts from the {$a->sitename} forums. To change your forum email preferences, go to {$a->userprefs}.'; $string['digestmailprefs'] = 'your user profile'; $string['digestmailsubject'] = '{$a}: forum digest'; +$string['digestmailtime'] = 'Hour to send digest emails'; $string['digestsentusers'] = 'Email digests successfully sent to {$a} users.'; $string['disallowsubscribe'] = 'Subscriptions not allowed'; $string['disallowsubscribeteacher'] = 'Subscriptions not allowed (except for teachers)'; diff --git a/mod/forum/settings.php b/mod/forum/settings.php index d2d343eb5cd..8247e0ae1e6 100644 --- a/mod/forum/settings.php +++ b/mod/forum/settings.php @@ -65,14 +65,17 @@ if ($ADMIN->fulltree) { $settings->add(new admin_setting_configcheckbox('forum_usermarksread', get_string('usermarksread', 'forum'), get_string('configusermarksread', 'forum'), 0)); - // Default time (hour) to execute 'clean_read_records' cron $options = array(); - for ($i=0; $i<24; $i++) { - $options[$i] = $i; + for ($i = 0; $i < 24; $i++) { + $options[$i] = sprintf("%02d",$i); } + // Default time (hour) to execute 'clean_read_records' cron $settings->add(new admin_setting_configselect('forum_cleanreadtime', get_string('cleanreadtime', 'forum'), get_string('configcleanreadtime', 'forum'), 2, $options)); + // Default time (hour) to send digest email + $settings->add(new admin_setting_configselect('digestmailtime', get_string('digestmailtime', 'forum'), + get_string('configdigestmailtime', 'forum'), 17, $options)); if (empty($CFG->enablerssfeeds)) { $options = array(0 => get_string('rssglobaldisabled', 'admin')); diff --git a/mod/glossary/backup/moodle2/backup_glossary_stepslib.php b/mod/glossary/backup/moodle2/backup_glossary_stepslib.php index 3fa69cceba4..20b63285c4f 100644 --- a/mod/glossary/backup/moodle2/backup_glossary_stepslib.php +++ b/mod/glossary/backup/moodle2/backup_glossary_stepslib.php @@ -38,7 +38,7 @@ class backup_glossary_activity_structure_step extends backup_activity_structure_ // Define each element separated $glossary = new backup_nested_element('glossary', array('id'), array( - 'name', 'intro', 'allowduplicatedentries', 'displayformat', + 'name', 'intro', 'introformat', 'allowduplicatedentries', 'displayformat', 'mainglossary', 'showspecial', 'showalphabet', 'showall', 'allowcomments', 'allowprintview', 'usedynalink', 'defaultapproval', 'globalglossary', 'entbypage', 'editalways', 'rsstype', diff --git a/mod/quiz/accessrules.php b/mod/quiz/accessrules.php index f3f6ad0e32e..fc79ddbe539 100644 --- a/mod/quiz/accessrules.php +++ b/mod/quiz/accessrules.php @@ -1,7 +1,38 @@ . + +/** + * Classes to enforce the various access rules that can apply to a quiz. + * + * @package mod + * @subpackage quiz + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + + /** * This class keeps track of the various access rules that apply to a particular * quiz, with convinient methods for seeing whether access is allowed. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class quiz_access_manager { private $_quizobj; @@ -15,8 +46,8 @@ class quiz_access_manager { * Create an instance for a particular quiz. * @param object $quizobj An instance of the class quiz from attemptlib.php. * The quiz we will be controlling access to. - * @param integer $timenow The time to use as 'now'. - * @param boolean $canignoretimelimits Whether this user is exempt from time + * @param int $timenow The time to use as 'now'. + * @param bool $canignoretimelimits Whether this user is exempt from time * limits (has_capability('mod/quiz:ignoretimelimits', ...)). */ public function __construct($quizobj, $timenow, $canignoretimelimits) { @@ -46,10 +77,12 @@ class quiz_access_manager { } if (!empty($quiz->popup)) { if ($quiz->popup == 1) { - $this->_securewindowrule = new securewindow_access_rule($this->_quizobj, $this->_timenow); + $this->_securewindowrule = new securewindow_access_rule( + $this->_quizobj, $this->_timenow); $this->_rules[] = $this->_securewindowrule; - } elseif ($quiz->popup == 2) { - $this->_safebrowserrule = new safebrowser_access_rule($this->_quizobj, $this->_timenow); + } else if ($quiz->popup == 2) { + $this->_safebrowserrule = new safebrowser_access_rule( + $this->_quizobj, $this->_timenow); $this->_rules[] = $this->_safebrowserrule; } } @@ -63,27 +96,6 @@ class quiz_access_manager { } } - /** - * Print each message in an array, surrounded by <p>, </p> tags. - * - * @param array $messages the array of message strings. - * @param boolean $return if true, return a string, instead of outputting. - * - * @return mixed, if $return is true, return the string that would have been output, otherwise - * return null. - */ - public function print_messages($messages, $return=false) { - $output = ''; - foreach ($messages as $message) { - $output .= '

' . $message . "

\n"; - } - if ($return) { - return $output; - } else { - echo $output; - } - } - /** * Provide a description of the rules that apply to this quiz, such * as is shown at the top of the quiz view page. Note that not all @@ -105,7 +117,7 @@ class quiz_access_manager { * any restrictions in force now, return an array of reasons why access * should be blocked. If access is OK, return false. * - * @param integer $numattempts the number of previous attempts this user has made. + * @param int $numattempts the number of previous attempts this user has made. * @param object|false $lastattempt information about the user's last completed attempt. * if there is not a previous attempt, the false is passed. * @return mixed An array of reason why access is not allowed, or an empty array @@ -138,12 +150,13 @@ class quiz_access_manager { /** * Do any of the rules mean that this student will no be allowed any further attempts at this - * quiz. Used, for example, to change the label by the grade displayed on the view page from - * 'your current score is' to 'your final score is'. + * quiz. Used, for example, to change the label by the grade displayed on the view page from + * 'your current grade is' to 'your final grade is'. * - * @param integer $numattempts the number of previous attempts this user has made. + * @param int $numattempts the number of previous attempts this user has made. * @param object $lastattempt information about the user's last completed attempt. - * @return boolean true if there is no way the user will ever be allowed to attempt this quiz again. + * @return bool true if there is no way the user will ever be allowed to attempt + * this quiz again. */ public function is_finished($numprevattempts, $lastattempt) { foreach ($this->_rules as $rule) { @@ -174,8 +187,8 @@ class quiz_access_manager { } } if ($timeleft !== false) { - /// Make sure the timer starts just above zero. If $timeleft was <= 0, then - /// this will just have the effect of causing the quiz to be submitted immediately. + // Make sure the timer starts just above zero. If $timeleft was <= 0, then + // this will just have the effect of causing the quiz to be submitted immediately. $timerstartvalue = max($timeleft, 1); $PAGE->requires->js_init_call('M.mod_quiz.timer.init', array($timerstartvalue), false, quiz_get_js_module()); @@ -191,7 +204,7 @@ class quiz_access_manager { /** * @return bolean if this quiz should only be shown to students with safe browser. - */ + */ public function safebrowser_required($canpreview) { return !$canpreview && !is_null($this->_safebrowserrule); } @@ -201,13 +214,14 @@ class quiz_access_manager { * depending on the access restrictions. The link will pop up a 'secure' window, if * necessary. * - * @param boolean $canpreview whether this user can preview. This affects whether they must + * @param bool $canpreview whether this user can preview. This affects whether they must * use a secure window. * @param string $buttontext the label to put on the button. - * @param boolean $unfinished whether the button is to continue an existing attempt, + * @param bool $unfinished whether the button is to continue an existing attempt, * or start a new one. This affects whether a javascript alert is shown. */ - public function print_start_attempt_button($canpreview, $buttontext, $unfinished) { + //TODO: Add this function to renderer + public function print_start_attempt_button($canpreview, $buttontext, $unfinished) { global $OUTPUT; $url = $this->_quizobj->start_attempt_url(); @@ -233,14 +247,14 @@ class quiz_access_manager { $OUTPUT->heading(get_string('noscript', 'quiz'))); } - echo $OUTPUT->render($button) . $warning; + return $OUTPUT->render($button) . $warning; } /** * Send the user back to the quiz view page. Normally this is just a redirect, but * If we were in a secure window, we close this window, and reload the view window we came from. * - * @param boolean $canpreview This affects whether we have to worry about secure window stuff. + * @param bool $canpreview This affects whether we have to worry about secure window stuff. */ public function back_to_view_page($canpreview, $message = '') { global $CFG, $OUTPUT, $PAGE; @@ -256,7 +270,8 @@ class quiz_access_manager { echo '

' . get_string('pleaseclose', 'quiz') . '

'; $delay = 0; } - $PAGE->requires->js_function_call('M.mod_quiz.secure_window.close', array($url, $delay)); + $PAGE->requires->js_function_call('M.mod_quiz.secure_window.close', + array($url, $delay)); echo $OUTPUT->box_end(); echo $OUTPUT->footer(); die(); @@ -269,7 +284,7 @@ class quiz_access_manager { * Print a control to finish the review. Normally this is just a link, but if we are * in a secure window, it needs to be a button that does M.mod_quiz.secure_window.close. * - * @param boolean $canpreview This affects whether we have to worry about secure window stuff. + * @param bool $canpreview This affects whether we have to worry about secure window stuff. */ public function print_finish_review_link($canpreview, $return = false) { global $CFG; @@ -311,7 +326,7 @@ class quiz_access_manager { * Actually ask the user for the password, if they have not already given it this session. * This function only returns is access is OK. * - * @param boolean $canpreview used to enfore securewindow stuff. + * @param bool $canpreview used to enfore securewindow stuff. */ public function do_password_check($canpreview) { if (!is_null($this->_passwordrule)) { @@ -326,11 +341,11 @@ class quiz_access_manager { public function confirm_start_attempt_message() { $quiz = $this->_quizobj->get_quiz(); if ($quiz->timelimit && $quiz->attempts) { - return get_string('confirmstartattempttimelimit','quiz', $quiz->attempts); + return get_string('confirmstartattempttimelimit', 'quiz', $quiz->attempts); } else if ($quiz->timelimit) { - return get_string('confirmstarttimelimit','quiz'); + return get_string('confirmstarttimelimit', 'quiz'); } else if ($quiz->attempts) { - return get_string('confirmstartattemptlimit','quiz', $quiz->attempts); + return get_string('confirmstartattemptlimit', 'quiz', $quiz->attempts); } return ''; } @@ -340,17 +355,23 @@ class quiz_access_manager { * * @param string $linktext some text. * @param object $attempt the attempt object - * @return string some HTML, the $linktext either unmodified or wrapped in a link to the review page. + * @return string some HTML, the $linktext either unmodified or wrapped in a + * link to the review page. */ public function make_review_link($attempt, $canpreview, $reviewoptions) { global $CFG; - /// If review of responses is not allowed, or the attempt is still open, don't link. + // If review of responses is not allowed, or the attempt is still open, don't link. if (!$attempt->timefinish) { return ''; } - if (!$reviewoptions->responses) { - $message = $this->cannot_review_message($reviewoptions, true); + + $when = quiz_attempt_state($this->_quizobj->get_quiz(), $attempt); + $reviewoptions = mod_quiz_display_options::make_from_quiz( + $this->_quizobj->get_quiz(), $when); + + if (!$reviewoptions->attempt) { + $message = $this->cannot_review_message($when, true); if ($message) { return '' . $message . ''; } else { @@ -360,7 +381,7 @@ class quiz_access_manager { $linktext = get_string('review', 'quiz'); - /// It is OK to link, does it need to be in a secure window? + // It is OK to link, does it need to be in a secure window? if ($this->securewindow_required($canpreview)) { return $this->_securewindowrule->make_review_link($linktext, $attempt->id); } else { @@ -370,14 +391,15 @@ class quiz_access_manager { } /** - * If $reviewoptions->responses is false, meaning that students can't review this + * If $reviewoptions->attempt is false, meaning that students can't review this * attempt at the moment, return an appropriate string explaining why. * - * @param object $reviewoptions as obtained from quiz_get_reviewoptions. - * @param boolean $short if true, return a shorter string. + * @param int $when One of the mod_quiz_display_options::DURING, + * IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants. + * @param bool $short if true, return a shorter string. * @return string an appropraite message. */ - public function cannot_review_message($reviewoptions, $short = false) { + public function cannot_review_message($when, $short = false) { $quiz = $this->_quizobj->get_quiz(); if ($short) { $langstrsuffix = 'short'; @@ -386,17 +408,20 @@ class quiz_access_manager { $langstrsuffix = ''; $dateformat = ''; } - if ($reviewoptions->quizstate == QUIZ_STATE_IMMEDIATELY) { + if ($when == mod_quiz_display_options::DURING || + $when == mod_quiz_display_options::IMMEDIATELY_AFTER) { return ''; - } else if ($reviewoptions->quizstate == QUIZ_STATE_OPEN && $quiz->timeclose && - ($quiz->review & QUIZ_REVIEW_CLOSED & QUIZ_REVIEW_RESPONSES)) { - return get_string('noreviewuntil' . $langstrsuffix, 'quiz', userdate($quiz->timeclose, $dateformat)); + } else if ($when == mod_quiz_display_options::LATER_WHILE_OPEN && $quiz->timeclose && + $quiz->reviewattempt & mod_quiz_display_options::AFTER_CLOSE) { + return get_string('noreviewuntil' . $langstrsuffix, 'quiz', + userdate($quiz->timeclose, $dateformat)); } else { return get_string('noreview' . $langstrsuffix, 'quiz'); } } } + /** * A base class that defines the interface for the various quiz access rules. * Most of the methods are defined in a slightly unnatural way because we either @@ -405,6 +430,9 @@ class quiz_access_manager { * return false if access is permitted, or a string explanation (which is treated * as true) if access should be blocked. Slighly unnatural, but acutally the easist * way to implement this. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class quiz_access_rule_base { protected $_quiz; @@ -421,16 +449,18 @@ abstract class quiz_access_rule_base { } /** * Whether or not a user should be allowed to start a new attempt at this quiz now. - * @param integer $numattempts the number of previous attempts this user has made. + * @param int $numattempts the number of previous attempts this user has made. * @param object $lastattempt information about the user's last completed attempt. - * @return string false if access should be allowed, a message explaining the reason if access should be prevented. + * @return string false if access should be allowed, a message explaining the + * reason if access should be prevented. */ public function prevent_new_attempt($numprevattempts, $lastattempt) { return false; } /** * Whether or not a user should be allowed to start a new attempt at this quiz now. - * @return string false if access should be allowed, a message explaining the reason if access should be prevented. + * @return string false if access should be allowed, a message explaining the + * reason if access should be prevented. */ public function prevent_access() { return false; @@ -448,11 +478,11 @@ abstract class quiz_access_rule_base { /** * If this rule can determine that this user will never be allowed another attempt at * this quiz, then return true. This is used so we can know whether to display a - * final score on the view page. This will only be called if there is not a currently + * final grade on the view page. This will only be called if there is not a currently * active attempt for this user. - * @param integer $numattempts the number of previous attempts this user has made. + * @param int $numattempts the number of previous attempts this user has made. * @param object $lastattempt information about the user's last completed attempt. - * @return boolean true if this rule means that this user will never be allowed another + * @return bool true if this rule means that this user will never be allowed another * attempt at this quiz. */ public function is_finished($numprevattempts, $lastattempt) { @@ -463,7 +493,7 @@ abstract class quiz_access_rule_base { * If, becuase of this rule, the user has to finish their attempt by a certain time, * you should override this method to return the amount of time left in seconds. * @param object $attempt the current attempt - * @param integer $timenow the time now. We don't use $this->_timenow, so we can + * @param int $timenow the time now. We don't use $this->_timenow, so we can * give the user a more accurate indication of how much time is left. * @return mixed false if there is no deadline, of the time left in seconds if there is one. */ @@ -474,6 +504,9 @@ abstract class quiz_access_rule_base { /** * A rule controlling the number of attempts allowed. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class num_attempts_access_rule extends quiz_access_rule_base { public function description() { @@ -492,6 +525,9 @@ class num_attempts_access_rule extends quiz_access_rule_base { /** * A rule enforcing open and close dates. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class open_close_date_access_rule extends quiz_access_rule_base { public function description() { @@ -540,16 +576,19 @@ class open_close_date_access_rule extends quiz_access_rule_base { } /** - * A rule imposing the delay between attemtps settings. + * A rule imposing the delay between attempts settings. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class inter_attempt_delay_access_rule extends quiz_access_rule_base { public function prevent_new_attempt($numprevattempts, $lastattempt) { if ($this->_quiz->attempts > 0 && $numprevattempts >= $this->_quiz->attempts) { - /// No more attempts allowed anyway. + // No more attempts allowed anyway. return false; } if ($this->_quiz->timeclose != 0 && $this->_timenow > $this->_quiz->timeclose) { - /// No more attempts allowed anyway. + // No more attempts allowed anyway. return false; } $nextstarttime = $this->compute_next_start_time($numprevattempts, $lastattempt); @@ -566,7 +605,7 @@ class inter_attempt_delay_access_rule extends quiz_access_rule_base { /** * Compute the next time a student would be allowed to start an attempt, * according to this rule. - * @param integer $numprevattempts number of previous attempts. + * @param int $numprevattempts number of previous attempts. * @param object $lastattempt information about the previous attempt. * @return number the time. */ @@ -576,7 +615,7 @@ class inter_attempt_delay_access_rule extends quiz_access_rule_base { } $lastattemptfinish = $lastattempt->timefinish; - if ($this->_quiz->timelimit > 0){ + if ($this->_quiz->timelimit > 0) { $lastattemptfinish = min($lastattemptfinish, $lastattempt->timestart + $this->_quiz->timelimit); } @@ -598,6 +637,9 @@ class inter_attempt_delay_access_rule extends quiz_access_rule_base { /** * A rule implementing the ipaddress check against the ->submet setting. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class ipaddress_access_rule extends quiz_access_rule_base { public function prevent_access() { @@ -612,6 +654,9 @@ class ipaddress_access_rule extends quiz_access_rule_base { /** * A rule representing the password check. It does not actually implement the check, * that has to be done directly in attempt.php, but this facilitates telling users about it. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class password_access_rule extends quiz_access_rule_base { public function description() { @@ -630,23 +675,24 @@ class password_access_rule extends quiz_access_rule_base { * Actually ask the user for the password, if they have not already given it this session. * This function only returns is access is OK. * - * @param boolean $canpreview used to enfore securewindow stuff. + * @param bool $canpreview used to enfore securewindow stuff. * @param object $accessmanager the accessmanager calling us. + * @return mixed return null, unless $return is true, and a form needs to be displayed. */ public function do_password_check($canpreview, $accessmanager) { global $CFG, $SESSION, $OUTPUT, $PAGE; - /// We have already checked the password for this quiz this session, so don't ask again. + // We have already checked the password for this quiz this session, so don't ask again. if (!empty($SESSION->passwordcheckedquizzes[$this->_quiz->id])) { return; } - /// If the user cancelled the password form, send them back to the view page. + // If the user cancelled the password form, send them back to the view page. if (optional_param('cancelpassword', false, PARAM_BOOL)) { $accessmanager->back_to_view_page($canpreview); } - /// If they entered the right password, let them in. + // If they entered the right password, let them in. $enteredpassword = optional_param('quizpassword', '', PARAM_RAW); $validpassword = false; if (strcmp($this->_quiz->password, $enteredpassword) === 0) { @@ -665,15 +711,16 @@ class password_access_rule extends quiz_access_rule_base { return; } - /// User entered the wrong password, or has not entered one yet, so display the form. + // User entered the wrong password, or has not entered one yet, so display the form. $output = ''; - /// Start the page and print the quiz intro, if any. + // Start the page and print the quiz intro, if any. if ($accessmanager->securewindow_required($canpreview)) { $accessmanager->setup_secure_page($this->_quizobj->get_course()->shortname . ': ' . format_string($this->_quizobj->get_quiz_name())); } else if ($accessmanager->safebrowser_required($canpreview)) { - $PAGE->set_title($this->_quizobj->get_course()->shortname . ': '.format_string($this->_quizobj->get_quiz_name())); + $PAGE->set_title($this->_quizobj->get_course()->shortname . ': ' . + format_string($this->_quizobj->get_quiz_name())); $PAGE->set_cacheable(false); echo $OUTPUT->header(); } else { @@ -682,16 +729,17 @@ class password_access_rule extends quiz_access_rule_base { } if (trim(strip_tags($this->_quiz->intro))) { - $output .= $OUTPUT->box(format_module_intro('quiz', $this->_quiz, $this->_quizobj->get_cmid()), 'generalbox', 'intro'); + $output .= $OUTPUT->box(format_module_intro('quiz', $this->_quiz, + $this->_quizobj->get_cmid()), 'generalbox', 'intro'); } $output .= $OUTPUT->box_start('generalbox', 'passwordbox'); - /// If they have previously tried and failed to enter a password, tell them it was wrong. + // If they have previously tried and failed to enter a password, tell them it was wrong. if (!empty($enteredpassword)) { $output .= '

' . get_string('passworderror', 'quiz') . '

'; } - /// Print the password entry form. + // Print the password entry form. $output .= '

' . get_string('requirepasswordmessage', 'quiz') . "

\n"; $output .= '
' . "\n"; @@ -707,10 +755,10 @@ class password_access_rule extends quiz_access_rule_base { $output .= "
\n"; $output .= "\n"; - /// Finish page. + // Finish page. $output .= $OUTPUT->box_end(); - /// return or display form. + // return or display form. echo $output; echo $OUTPUT->footer(); exit; @@ -720,6 +768,9 @@ class password_access_rule extends quiz_access_rule_base { /** * A rule representing the time limit. It does not actually restrict access, but we use this * class to encapsulate some of the relevant code. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class time_limit_access_rule extends quiz_access_rule_base { public function description() { @@ -733,6 +784,9 @@ class time_limit_access_rule extends quiz_access_rule_base { /** * A rule for ensuring that the quiz is opened in a popup, with some JavaScript * to prevent copying and pasting, etc. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class securewindow_access_rule extends quiz_access_rule_base { /** @@ -756,7 +810,7 @@ class securewindow_access_rule extends quiz_access_rule_base { * Make a link to the review page for an attempt. * * @param string $linktext the desired link text. - * @param integer $attemptid the attempt id. + * @param int $attemptid the attempt id. * @return string HTML for the link. */ public function make_review_link($linktext, $attemptid) { @@ -787,9 +841,13 @@ class securewindow_access_rule extends quiz_access_rule_base { } } + /** * A rule representing the safe browser check. -*/ + * + * @copyright 2009 Oliver Rahs + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ class safebrowser_access_rule extends quiz_access_rule_base { public function prevent_access() { if (!$this->_quizobj->is_preview_user() && !quiz_check_safe_browser()) { @@ -803,4 +861,3 @@ class safebrowser_access_rule extends quiz_access_rule_base { return get_string("safebrowsernotice", "quiz"); } } - diff --git a/mod/quiz/addrandom.php b/mod/quiz/addrandom.php index 36d603f8ea1..9950735746d 100644 --- a/mod/quiz/addrandom.php +++ b/mod/quiz/addrandom.php @@ -1,13 +1,30 @@ . + /** * Fallback page of /mod/quiz/edit.php add random question dialog, * for users who do not use javascript. * - * @author Olli Savolainen, as a part of the Quiz UI Redesign project in Summer 2008 - * {@link http://docs.moodle.org/en/Development:Quiz_UI_redesign}. - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package quiz + * @package mod + * @subpackage quiz + * @copyright 2008 Olli Savolainen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ + + require_once('../../config.php'); require_once($CFG->dirroot . '/mod/quiz/editlib.php'); require_once($CFG->dirroot . '/mod/quiz/addrandomform.php'); @@ -21,6 +38,7 @@ list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) = $returnurl = optional_param('returnurl', '', PARAM_LOCALURL); $addonpage = optional_param('addonpage', 0, PARAM_INT); $category = optional_param('category', 0, PARAM_INT); +$scrollpos = optional_param('scrollpos', 0, PARAM_INT); // Get the course object and related bits. if (!$course = $DB->get_record('course', array('id' => $quiz->course))) { @@ -36,6 +54,9 @@ if ($returnurl) { } else { $returnurl = new moodle_url('/mod/quiz/edit.php', array('cmid' => $cmid)); } +if ($scrollpos) { + $returnurl->param('scrollpos', $scrollpos); +} $defaultcategoryobj = question_make_default_categories($contexts->all()); $defaultcategory = $defaultcategoryobj->id . ',' . $defaultcategoryobj->contextid; @@ -70,7 +91,8 @@ if ($data = $mform->get_data()) { $returnurl->param('cat', $categoryid . ',' . $contextid); } else { - throw new coding_exception('It seems a form was submitted without any button being pressed???'); + throw new coding_exception( + 'It seems a form was submitted without any button being pressed???'); } quiz_add_random_questions($quiz, $addonpage, $categoryid, 1, $includesubcategories); diff --git a/mod/quiz/addrandomform.php b/mod/quiz/addrandomform.php index 2db19f3526c..3d3e5c3c23d 100644 --- a/mod/quiz/addrandomform.php +++ b/mod/quiz/addrandomform.php @@ -1,17 +1,51 @@ . + +/** + * Defines the Moodle forum used to add random questions to the quiz. + * + * @package mod + * @subpackage quiz + * @copyright 2008 Olli Savolainen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/formslib.php'); + +/** + * The add random questions form. + * + * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ class quiz_add_random_form extends moodleform { - function definition() { + protected function definition() { global $CFG, $DB; $mform =& $this->_form; $contexts = $this->_customdata; -//-------------------------------------------------------------------------------- - $mform->addElement('header', 'categoryheader', get_string('randomfromexistingcategory', 'quiz')); + //-------------------------------------------------------------------------------- + $mform->addElement('header', 'categoryheader', + get_string('randomfromexistingcategory', 'quiz')); $mform->addElement('questioncategory', 'category', get_string('category'), array('contexts' => $contexts->all(), 'top' => false)); @@ -20,8 +54,9 @@ class quiz_add_random_form extends moodleform { $mform->addElement('submit', 'existingcategory', get_string('addrandomquestion', 'quiz')); -//-------------------------------------------------------------------------------- - $mform->addElement('header', 'categoryheader', get_string('randomquestionusinganewcategory', 'quiz')); + //-------------------------------------------------------------------------------- + $mform->addElement('header', 'categoryheader', + get_string('randomquestionusinganewcategory', 'quiz')); $mform->addElement('text', 'name', get_string('name'), 'maxlength="254" size="50"'); $mform->setType('name', PARAM_MULTILANG); @@ -30,9 +65,10 @@ class quiz_add_random_form extends moodleform { array('contexts' => $contexts->all(), 'top' => true)); $mform->addHelpButton('parent', 'parentcategory', 'question'); - $mform->addElement('submit', 'newcategory', get_string('createcategoryandaddrandomquestion', 'quiz')); + $mform->addElement('submit', 'newcategory', + get_string('createcategoryandaddrandomquestion', 'quiz')); -//-------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------- $mform->addElement('cancel'); $mform->closeHeaderBefore('cancel'); @@ -44,11 +80,11 @@ class quiz_add_random_form extends moodleform { $mform->setType('returnurl', PARAM_LOCALURL); } - function validation($fromform, $files) { + public function validation($fromform, $files) { $errors = parent::validation($fromform, $files); if (!empty($fromform['newcategory']) && trim($fromform['name']) == '') { - $errors['name'] = get_string('categorynamecantbeblank', 'quiz'); + $errors['name'] = get_string('categorynamecantbeblank', 'question'); } return $errors; diff --git a/mod/quiz/attempt.php b/mod/quiz/attempt.php index 252a03c81fc..0bbd37f0106 100644 --- a/mod/quiz/attempt.php +++ b/mod/quiz/attempt.php @@ -1,173 +1,133 @@ . + /** - * This page prints a particular instance of quiz + * This script displays a particular page of a quiz attempt that is in progress. * - * @author Martin Dougiamas and many others. This has recently been completely - * rewritten by Alex Smith, Julian Sedding and Gustav Delius as part of - * the Serving Mathematics project - * {@link http://maths.york.ac.uk/serving_maths} - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package quiz + * @package mod + * @subpackage quiz + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - require_once(dirname(__FILE__) . '/../../config.php'); - require_once($CFG->dirroot . '/mod/quiz/locallib.php'); +require_once(dirname(__FILE__) . '/../../config.php'); +require_once($CFG->dirroot . '/mod/quiz/locallib.php'); -/// Look for old-style URLs, such as may be in the logs, and redirect them to startattemtp.php - if ($id = optional_param('id', 0, PARAM_INTEGER)) { - redirect($CFG->wwwroot . '/mod/quiz/startattempt.php?cmid=' . $id . '&sesskey=' . sesskey()); - } else if ($qid = optional_param('q', 0, PARAM_INTEGER)) { - if (!$cm = get_coursemodule_from_instance('quiz', $qid)) { - print_error('invalidquizid', 'quiz'); - } - redirect($CFG->wwwroot . '/mod/quiz/startattempt.php?cmid=' . $cm->id . '&sesskey=' . sesskey()); +// Look for old-style URLs, such as may be in the logs, and redirect them to startattemtp.php +if ($id = optional_param('id', 0, PARAM_INTEGER)) { + redirect($CFG->wwwroot . '/mod/quiz/startattempt.php?cmid=' . $id . '&sesskey=' . sesskey()); +} else if ($qid = optional_param('q', 0, PARAM_INTEGER)) { + if (!$cm = get_coursemodule_from_instance('quiz', $qid)) { + print_error('invalidquizid', 'quiz'); } + redirect(new moodle_url('/mod/quiz/startattempt.php', + array('cmid' => $cm->id, 'sesskey' => sesskey()))); +} -/// Get submitted parameters. - $attemptid = required_param('attempt', PARAM_INT); - $page = optional_param('page', 0, PARAM_INT); +// Get submitted parameters. +$attemptid = required_param('attempt', PARAM_INT); +$page = optional_param('page', 0, PARAM_INT); - $url = new moodle_url('/mod/quiz/attempt.php', array('attempt' => $attemptid)); - if ($page !== 0) { - $url->param('page', $page); - } - $PAGE->set_url($url); +$attemptobj = quiz_attempt::create($attemptid); +$PAGE->set_url($attemptobj->attempt_url(0, $page)); - $attemptobj = quiz_attempt::create($attemptid); +// Check login. +require_login($attemptobj->get_course(), false, $attemptobj->get_cm()); -/// Check login. - require_login($attemptobj->get_course(), false, $attemptobj->get_cm()); - -/// Check that this attempt belongs to this user. - if ($attemptobj->get_userid() != $USER->id) { - if ($attemptobj->has_capability('mod/quiz:viewreports')) { - redirect($attemptobj->review_url(0, $page)); - } else { - quiz_error($attemptobj->get_quiz(), 'notyourattempt'); - } - } - -/// Check capabilities and block settings - if (!$attemptobj->is_preview_user()) { - $attemptobj->require_capability('mod/quiz:attempt'); - if (empty($attemptobj->get_quiz()->showblocks)) { - $PAGE->blocks->show_only_fake_blocks(); - } - - } else { - navigation_node::override_active_url($attemptobj->start_attempt_url()); - } - -/// If the attempt is already closed, send them to the review page. - if ($attemptobj->is_finished()) { +// Check that this attempt belongs to this user. +if ($attemptobj->get_userid() != $USER->id) { + if ($attemptobj->has_capability('mod/quiz:viewreports')) { redirect($attemptobj->review_url(0, $page)); - } - -/// Check the access rules. - $accessmanager = $attemptobj->get_access_manager(time()); - $messages = $accessmanager->prevent_access(); - if (!$attemptobj->is_preview_user() && $messages) { - print_error('attempterror', 'quiz', $quizobj->view_url(), - $accessmanager->print_messages($messages, true)); - } - $accessmanager->do_password_check($attemptobj->is_preview_user()); - - add_to_log($attemptobj->get_courseid(), 'quiz', 'continue attempt', - 'review.php?attempt=' . $attemptobj->get_attemptid(), - $attemptobj->get_quizid(), $attemptobj->get_cmid()); - -/// Get the list of questions needed by this page. - $questionids = $attemptobj->get_question_ids($page); - -/// Check. - if (empty($questionids)) { - quiz_error($quiz, 'noquestionsfound'); - } - -/// Load those questions and the associated states. - $attemptobj->load_questions($questionids); - $attemptobj->load_question_states($questionids); - -/// Print the quiz page //////////////////////////////////////////////////////// - - // Initialise the JavaScript. - $headtags = $attemptobj->get_html_head_contributions($page); - $PAGE->requires->js_init_call('M.mod_quiz.init_attempt_form', null, false, quiz_get_js_module()); - - // Arrange for the navigation to be displayed. - $navbc = $attemptobj->get_navigation_panel('quiz_attempt_nav_panel', $page); - $firstregion = reset($PAGE->blocks->get_regions()); - $PAGE->blocks->add_fake_block($navbc, $firstregion); - - // Print the page header - $title = get_string('attempt', 'quiz', $attemptobj->get_attempt_number()); - $PAGE->set_heading($attemptobj->get_course()->fullname); - if ($accessmanager->securewindow_required($attemptobj->is_preview_user())) { - $accessmanager->setup_secure_page($attemptobj->get_course()->shortname . ': ' . - format_string($attemptobj->get_quiz_name())); - } else if ($accessmanager->safebrowser_required($attemptobj->is_preview_user())) { - $PAGE->set_title($attemptobj->get_course()->shortname . ': '.format_string($attemptobj->get_quiz_name())); - $PAGE->set_cacheable(false); - echo $OUTPUT->header(); } else { - $PAGE->set_title(format_string($attemptobj->get_quiz_name())); - echo $OUTPUT->header(); + throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt'); + } +} + +// Check capabilities and block settings +if (!$attemptobj->is_preview_user()) { + $attemptobj->require_capability('mod/quiz:attempt'); + if (empty($attemptobj->get_quiz()->showblocks)) { + $PAGE->blocks->show_only_fake_blocks(); } - if ($attemptobj->is_preview_user()) { +} else { + navigation_node::override_active_url($attemptobj->start_attempt_url()); +} - $quiz = $attemptobj->get_quiz(); +// If the attempt is already closed, send them to the review page. +if ($attemptobj->is_finished()) { + redirect($attemptobj->review_url(0, $page)); +} - /// Heading and tab bar. - echo $OUTPUT->heading(get_string('previewquiz', 'quiz', format_string($quiz->name))); - $attemptobj->print_restart_preview_button(); +// Check the access rules. +$accessmanager = $attemptobj->get_access_manager(time()); +$messages = $accessmanager->prevent_access(); +$output = $PAGE->get_renderer('mod_quiz'); +if (!$attemptobj->is_preview_user() && $messages) { + print_error('attempterror', 'quiz', $attemptobj->view_url(), + $output->access_messages($messages)); +} +$accessmanager->do_password_check($attemptobj->is_preview_user()); - /// Inform teachers of any restrictions that would apply to students at this point. - if ($messages) { - echo $OUTPUT->box_start('quizaccessnotices'); - echo $OUTPUT->heading(get_string('accessnoticesheader', 'quiz'), 3); - $accessmanager->print_messages($messages); - echo $OUTPUT->box_end(); - } - } +add_to_log($attemptobj->get_courseid(), 'quiz', 'continue attempt', + 'review.php?attempt=' . $attemptobj->get_attemptid(), + $attemptobj->get_quizid(), $attemptobj->get_cmid()); - // Start the form - echo '
', "\n"; - echo '
'; +// Get the list of questions needed by this page. +$slots = $attemptobj->get_slots($page); -/// Print all the questions - foreach ($attemptobj->get_question_ids($page) as $id) { - $attemptobj->print_question($id, false, $attemptobj->attempt_url($id, $page)); - } +// Check. +if (empty($slots)) { + throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noquestionsfound'); +} -/// Print a link to the next page. - echo '
'; - if ($attemptobj->is_last_page($page)) { - $nextpage = -1; - } else { - $nextpage = $page + 1; - } - echo ''; - echo "
"; +// Initialise the JavaScript. +$headtags = $attemptobj->get_html_head_contributions($page); +$PAGE->requires->js_init_call('M.mod_quiz.init_attempt_form', null, false, quiz_get_js_module()); - // Some hidden fields to trach what is going on. - echo ''; - echo ''; - echo ''; - echo ''; +// Arrange for the navigation to be displayed. +$navbc = $attemptobj->get_navigation_panel($output, 'quiz_attempt_nav_panel', $page); +$firstregion = reset($PAGE->blocks->get_regions()); +$PAGE->blocks->add_fake_block($navbc, $firstregion); - // Add a hidden field with questionids. Do this at the end of the form, so - // if you navigate before the form has finished loading, it does not wipe all - // the student's answers. - echo '\n"; +$title = get_string('attempt', 'quiz', $attemptobj->get_attempt_number()); +$headtags = $attemptobj->get_html_head_contributions($page); +$PAGE->set_heading($attemptobj->get_course()->fullname); +if ($accessmanager->securewindow_required($attemptobj->is_preview_user())) { + $accessmanager->setup_secure_page($attemptobj->get_course()->shortname . ': ' . + format_string($attemptobj->get_quiz_name())); - // Finish the form - echo '
'; - echo "
\n"; +} else if ($accessmanager->safebrowser_required($attemptobj->is_preview_user())) { + $PAGE->set_title($attemptobj->get_course()->shortname . ': ' . + format_string($attemptobj->get_quiz_name())); + $PAGE->set_cacheable(false); + echo $OUTPUT->header(); - // Finish the page - $accessmanager->show_attempt_timer_if_needed($attemptobj->get_attempt(), time()); - echo $OUTPUT->footer(); +} else { + $PAGE->set_title(format_string($attemptobj->get_quiz_name())); + echo $OUTPUT->header(); +} +if ($attemptobj->is_last_page($page)) { + $nextpage = -1; +} else { + $nextpage = $page + 1; +} + +echo $output->attempt_page($attemptobj, $page, $accessmanager, $messages, $slots, $id, $nextpage); + +$accessmanager->show_attempt_timer_if_needed($attemptobj->get_attempt(), time()); +echo $OUTPUT->footer(); diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index 1efe7a87924..2ed1246ac96 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -1,5 +1,4 @@ view_url(); } @@ -44,6 +47,7 @@ class moodle_quiz_exception extends moodle_exception { } } + /** * A class encapsulating a quiz and the questions it contains, and making the * information available to scripts like view.php. @@ -52,9 +56,9 @@ class moodle_quiz_exception extends moodle_exception { * extra information only when necessary or when asked. The class tracks which questions * are loaded. * - * @copyright 2008 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 + * @copyright 2008 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 */ class quiz { // Fields initialised in the constructor. @@ -62,8 +66,7 @@ class quiz { protected $cm; protected $quiz; protected $context; - protected $questionids; // All question ids in order that they appear in the quiz. - protected $pagequestionids; // array page no => array of questionids on the page in order. + protected $questionids; // Fields set later if that data is needed. protected $questions = null; @@ -77,9 +80,9 @@ class quiz { * @param object $quiz the row from the quiz table. * @param object $cm the course_module object for this quiz. * @param object $course the row from the course table for the course we belong to. - * @param boolean $getcontext intended for testing - stops the constructor getting the context. + * @param bool $getcontext intended for testing - stops the constructor getting the context. */ - function __construct($quiz, $cm, $course, $getcontext = true) { + public function __construct($quiz, $cm, $course, $getcontext = true) { $this->quiz = $quiz; $this->cm = $cm; $this->quiz->cmid = $this->cm->id; @@ -87,28 +90,22 @@ class quiz { if ($getcontext && !empty($cm->id)) { $this->context = get_context_instance(CONTEXT_MODULE, $cm->id); } - $this->determine_layout(); + $this->questionids = explode(',', quiz_questions_in_quiz($this->quiz->questions)); } /** * Static function to create a new quiz object for a specific user. * - * @param integer $quizid the the quiz id. - * @param integer $userid the the userid. + * @param int $quizid the the quiz id. + * @param int $userid the the userid. * @return quiz the new quiz object */ - static public function create($quizid, $userid) { + public static function create($quizid, $userid) { global $DB; - if (!$quiz = $DB->get_record('quiz', array('id' => $quizid))) { - throw new moodle_exception('invalidquizid', 'quiz'); - } - if (!$course = $DB->get_record('course', array('id' => $quiz->course))) { - throw new moodle_exception('invalidcoursemodule'); - } - if (!$cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id)) { - throw new moodle_exception('invalidcoursemodule'); - } + $quiz = $DB->get_record('quiz', array('id' => $quizid), '*', MUST_EXIST); + $course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST); + $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST); // Update quiz with override information $quiz = quiz_update_effective_access($quiz, $userid); @@ -117,15 +114,6 @@ class quiz { } // Functions for loading more data ===================================================== - /** - * Convenience method. Calls {@link load_questions()} with the list of - * question ids for a given page. - * - * @param integer $page a page number. - */ - public function load_questions_on_page($page) { - $this->load_questions($this->pagequestionids[$page]); - } /** * Load just basic information about all the questions in this quiz. @@ -135,14 +123,14 @@ class quiz { throw new moodle_quiz_exception($this, 'noquestions', $this->edit_url()); } $this->questions = question_preload_questions($this->questionids, - 'qqi.grade AS maxgrade, qqi.id AS instance', + 'qqi.grade AS maxmark, qqi.id AS instance', '{quiz_question_instances} qqi ON qqi.quiz = :quizid AND q.id = qqi.question', array('quizid' => $this->quiz->id)); - $this->number_questions(); } - /** - * Fully load some or all of the questions for this quiz. You must call {@link preload_questions()} first. + /** + * Fully load some or all of the questions for this quiz. You must call + * {@link preload_questions()} first. * * @param array $questionids question ids of the questions to load. null for all. */ @@ -152,15 +140,15 @@ class quiz { } $questionstoprocess = array(); foreach ($questionids as $id) { - $questionstoprocess[$id] = $this->questions[$id]; - } - if (!get_question_options($questionstoprocess)) { - throw new moodle_quiz_exception($this, 'loadingquestionsfailed', implode(', ', $questionids)); + if (array_key_exists($id, $this->questions)) { + $questionstoprocess[$id] = $this->questions[$id]; + } } + get_question_options($questionstoprocess); } // Simple getters ====================================================================== - /** @return integer the course id. */ + /** @return int the course id. */ public function get_courseid() { return $this->course->id; } @@ -170,7 +158,7 @@ class quiz { return $this->course; } - /** @return integer the quiz id. */ + /** @return int the quiz id. */ public function get_quizid() { return $this->quiz->id; } @@ -185,12 +173,12 @@ class quiz { return $this->quiz->name; } - /** @return integer the number of attempts allowed at this quiz (0 = infinite). */ + /** @return int the number of attempts allowed at this quiz (0 = infinite). */ public function get_num_attempts_allowed() { return $this->quiz->attempts; } - /** @return integer the course_module id. */ + /** @return int the course_module id. */ public function get_cmid() { return $this->cm->id; } @@ -200,8 +188,13 @@ class quiz { return $this->cm; } + /** @return object the module context for this quiz. */ + public function get_context() { + return $this->context; + } + /** - * @return boolean wether the current user is someone who previews the quiz, + * @return bool wether the current user is someone who previews the quiz, * rather than attempting it. */ public function is_preview_user() { @@ -212,23 +205,14 @@ class quiz { } /** - * @return integer number fo pages in this quiz. + * @return whether any questions have been added to this quiz. */ - public function get_num_pages() { - return count($this->pagequestionids); - } - - - /** - * @param int $page page number - * @return boolean true if this is the last page of the quiz. - */ - public function is_last_page($page) { - return $page == count($this->pagequestionids) - 1; + public function has_questions() { + return !empty($this->questionids); } /** - * @param integer $id the question id. + * @param int $id the question id. * @return object the question object with that id. */ public function get_question($id) { @@ -244,6 +228,9 @@ class quiz { } $questions = array(); foreach ($questionids as $id) { + if (!array_key_exists($id, $this->questions)) { + throw new moodle_exception('cannotstartmissingquestion', 'quiz', $this->view_url()); + } $questions[$id] = $this->questions[$id]; $this->ensure_question_loaded($id); } @@ -251,53 +238,29 @@ class quiz { } /** - * Return the list of question ids for either a given page of the quiz, or for the - * whole quiz. - * - * @param mixed $page string 'all' or integer page number. - * @return array the reqested list of question ids. - */ - public function get_question_ids($page = 'all') { - if ($page === 'all') { - $list = $this->questionids; - } else { - $list = $this->pagequestionids[$page]; - } - // Clone the array, so our private arrays cannot be modified. - $result = array(); - foreach ($list as $id) { - $result[] = $id; - } - return $result; - } - - /** - * @param integer $timenow the current time as a unix timestamp. - * @return quiz_access_manager and instance of the quiz_access_manager class for this quiz at this time. + * @param int $timenow the current time as a unix timestamp. + * @return quiz_access_manager and instance of the quiz_access_manager class + * for this quiz at this time. */ public function get_access_manager($timenow) { if (is_null($this->accessmanager)) { $this->accessmanager = new quiz_access_manager($this, $timenow, - has_capability('mod/quiz:ignoretimelimits', $this->context, NULL, false)); + has_capability('mod/quiz:ignoretimelimits', $this->context, null, false)); } return $this->accessmanager; } - public function get_overall_feedback($grade) { - return quiz_feedback_for_grade($grade, $this->quiz, $this->context, $this->cm); - } - /** * Wrapper round the has_capability funciton that automatically passes in the quiz context. */ - public function has_capability($capability, $userid = NULL, $doanything = true) { + public function has_capability($capability, $userid = null, $doanything = true) { return has_capability($capability, $this->context, $userid, $doanything); } /** * Wrapper round the require_capability funciton that automatically passes in the quiz context. */ - public function require_capability($capability, $userid = NULL, $doanything = true) { + public function require_capability($capability, $userid = null, $doanything = true) { return require_capability($capability, $this->context, $userid, $doanything); } @@ -319,7 +282,7 @@ class quiz { } /** - * @param integer $attemptid the id of an attempt. + * @param int $attemptid the id of an attempt. * @return string the URL of that attempt. */ public function attempt_url($attemptid) { @@ -336,7 +299,7 @@ class quiz { } /** - * @param integer $attemptid the id of an attempt. + * @param int $attemptid the id of an attempt. * @return string the URL of the review of that attempt. */ public function review_url($attemptid) { @@ -358,91 +321,33 @@ class quiz { // Private methods ===================================================================== /** - * Check that the definition of a particular question is loaded, and if not throw an exception. - * @param $id a questionid. + * Check that the definition of a particular question is loaded, and if not throw an exception. + * @param $id a questionid. */ protected function ensure_question_loaded($id) { if (isset($this->questions[$id]->_partiallyloaded)) { throw new moodle_quiz_exception($this, 'questionnotloaded', $id); } } - - /** - * Populate {@link $questionids} and {@link $pagequestionids} from the layout. - */ - protected function determine_layout() { - $this->questionids = array(); - $this->pagequestionids = array(); - - // Get the appropriate layout string (from quiz or attempt). - $layout = quiz_clean_layout($this->get_layout_string(), true); - if (empty($layout)) { - // Nothing to do. - return; - } - - // Break up the layout string into pages. - $pagelayouts = explode(',0', $layout); - - // Strip off any empty last page (normally there is one). - if (end($pagelayouts) == '') { - array_pop($pagelayouts); - } - - // File the ids into the arrays. - $this->questionids = array(); - $this->pagequestionids = array(); - foreach ($pagelayouts as $page => $pagelayout) { - $pagelayout = trim($pagelayout, ','); - if ($pagelayout == '') continue; - $this->pagequestionids[$page] = explode(',', $pagelayout); - foreach ($this->pagequestionids[$page] as $id) { - $this->questionids[] = $id; - } - } - } - - /** - * Number the questions, adding a _number field to each one. - */ - private function number_questions() { - $number = 1; - foreach ($this->pagequestionids as $page => $questionids) { - foreach ($questionids as $id) { - if ($this->questions[$id]->length > 0) { - $this->questions[$id]->_number = $number; - $number += $this->questions[$id]->length; - } else { - $this->questions[$id]->_number = get_string('infoshort', 'quiz'); - } - $this->questions[$id]->_page = $page; - } - } - } - - /** - * @return string the layout of this quiz. Used by number_questions to - * work out which questions are on which pages. - */ - protected function get_layout_string() { - return $this->quiz->questions; - } } + /** * This class extends the quiz class to hold data about the state of a particular attempt, * in addition to the data about the quiz. * - * @copyright 2008 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 + * @copyright 2008 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 */ -class quiz_attempt extends quiz { +class quiz_attempt { // Fields initialised in the constructor. + protected $quizobj; protected $attempt; + protected $quba; // Fields set later if that data is needed. - protected $states = array(); + protected $pagelayout; // array page no => array of numbers on the page in order. protected $reviewoptions = null; // Constructor ========================================================================= @@ -454,18 +359,19 @@ class quiz_attempt extends quiz { * @param object $cm the course_module object for this quiz. * @param object $course the row from the course table for the course we belong to. */ - function __construct($attempt, $quiz, $cm, $course) { + public function __construct($attempt, $quiz, $cm, $course) { $this->attempt = $attempt; - parent::__construct($quiz, $cm, $course); - $this->preload_questions(); - $this->preload_question_states(); + $this->quizobj = new quiz($quiz, $cm, $course); + $this->quba = question_engine::load_questions_usage_by_activity($this->attempt->uniqueid); + $this->determine_layout(); + $this->number_questions(); } /** * Used by {create()} and {create_from_usage_id()}. * @param array $conditions passed to $DB->get_record('quiz_attempts', $conditions). */ - static protected function create_helper($conditions) { + protected static function create_helper($conditions) { global $DB; $attempt = $DB->get_record('quiz_attempts', $conditions, '*', MUST_EXIST); @@ -485,7 +391,7 @@ class quiz_attempt extends quiz { * @param int $attemptid the attempt id. * @return quiz_attempt the new quiz_attempt object */ - static public function create($attemptid) { + public static function create($attemptid) { return self::create_helper(array('id' => $attemptid)); } @@ -495,72 +401,121 @@ class quiz_attempt extends quiz { * @param int $usageid the attempt usage id. * @return quiz_attempt the new quiz_attempt object */ - static public function create_from_unique_id($usageid) { + public static function create_from_usage_id($usageid) { return self::create_helper(array('uniqueid' => $usageid)); } - // Functions for loading more data ===================================================== - /** - * Load the state of a number of questions that have already been loaded. - * - * @param array $questionids question ids to process. Blank = all. - */ - public function load_question_states($questionids = null) { - if (is_null($questionids)) { - $questionids = $this->questionids; + private function determine_layout() { + $this->pagelayout = array(); + + // Break up the layout string into pages. + $pagelayouts = explode(',0', quiz_clean_layout($this->attempt->layout, true)); + + // Strip off any empty last page (normally there is one). + if (end($pagelayouts) == '') { + array_pop($pagelayouts); } - $questionstoprocess = array(); - foreach ($questionids as $id) { - $this->ensure_question_loaded($id); - $questionstoprocess[$id] = $this->questions[$id]; - } - if (!question_load_states($questionstoprocess, $this->states, - $this->quiz, $this->attempt)) { - throw new moodle_quiz_exception($this, 'cannotrestore'); + + // File the ids into the arrays. + $this->pagelayout = array(); + foreach ($pagelayouts as $page => $pagelayout) { + $pagelayout = trim($pagelayout, ','); + if ($pagelayout == '') { + continue; + } + $this->pagelayout[$page] = explode(',', $pagelayout); } } - /** - * Load basic information about the state of each question. - * - * This is enough to, for example, show the state of each question in the - * navigation panel, but only takes one DB query. - */ - public function preload_question_states() { - if (empty($this->questionids)) { - throw new moodle_quiz_exception($this, 'noquestions', $this->edit_url()); + // Number the questions. + private function number_questions() { + $number = 1; + foreach ($this->pagelayout as $page => $slots) { + foreach ($slots as $slot) { + $question = $this->quba->get_question($slot); + if ($question->length > 0) { + $question->_number = $number; + $number += $question->length; + } else { + $question->_number = get_string('infoshort', 'quiz'); + } + $question->_page = $page; + } } - $this->states = question_preload_states($this->attempt->uniqueid); - if (!$this->states) { - $this->states = array(); - } - } - - /** - * Load a particular state of a particular question. Used by the reviewquestion.php - * script to let the teacher walk through the entire sequence of a student's - * interaction with a question. - * - * @param $questionid the question id - * @param $stateid the id of the particular state to load. - */ - public function load_specific_question_state($questionid, $stateid) { - global $DB; - $state = question_load_specific_state($this->questions[$questionid], - $this->quiz, $this->attempt->uniqueid, $stateid); - if ($state === false) { - throw new moodle_quiz_exception($this, 'invalidstateid'); - } - $this->states[$questionid] = $state; } // Simple getters ====================================================================== - /** @return integer the attempt id. */ + public function get_quiz() { + return $this->quizobj->get_quiz(); + } + + public function get_quizobj() { + return $this->quizobj; + } + + /** @return int the course id. */ + public function get_courseid() { + return $this->quizobj->get_courseid(); + } + + /** @return int the course id. */ + public function get_course() { + return $this->quizobj->get_course(); + } + + /** @return int the quiz id. */ + public function get_quizid() { + return $this->quizobj->get_quizid(); + } + + /** @return string the name of this quiz. */ + public function get_quiz_name() { + return $this->quizobj->get_quiz_name(); + } + + /** @return object the course_module object. */ + public function get_cm() { + return $this->quizobj->get_cm(); + } + + /** @return object the course_module object. */ + public function get_cmid() { + return $this->quizobj->get_cmid(); + } + + /** + * @return bool wether the current user is someone who previews the quiz, + * rather than attempting it. + */ + public function is_preview_user() { + return $this->quizobj->is_preview_user(); + } + + /** @return int the number of attempts allowed at this quiz (0 = infinite). */ + public function get_num_attempts_allowed() { + return $this->quizobj->get_num_attempts_allowed(); + } + + /** @return int number fo pages in this quiz. */ + public function get_num_pages() { + return count($this->pagelayout); + } + + /** + * @param int $timenow the current time as a unix timestamp. + * @return quiz_access_manager and instance of the quiz_access_manager class + * for this quiz at this time. + */ + public function get_access_manager($timenow) { + return $this->quizobj->get_access_manager($timenow); + } + + /** @return int the attempt id. */ public function get_attemptid() { return $this->attempt->id; } - /** @return integer the attempt unique id. */ + /** @return int the attempt unique id. */ public function get_uniqueid() { return $this->attempt->uniqueid; } @@ -570,22 +525,25 @@ class quiz_attempt extends quiz { return $this->attempt; } - /** @return integer the number of this attemp (is it this user's first, second, ... attempt). */ + /** @return int the number of this attemp (is it this user's first, second, ... attempt). */ public function get_attempt_number() { return $this->attempt->attempt; } - /** @return integer the id of the user this attempt belongs to. */ + /** @return int the id of the user this attempt belongs to. */ public function get_userid() { return $this->attempt->userid; } - /** @return boolean whether this attempt has been finished (true) or is still in progress (false). */ + /** + * @return bool whether this attempt has been finished (true) or is still + * in progress (false). + */ public function is_finished() { return $this->attempt->timefinish != 0; } - /** @return boolean whether this attempt is a preview attempt. */ + /** @return bool whether this attempt is a preview attempt. */ public function is_preview() { return $this->attempt->preview; } @@ -594,7 +552,7 @@ class quiz_attempt extends quiz { * Is this a student dealing with their own attempt/teacher previewing, * or someone with 'mod/quiz:viewreports' reviewing someone elses attempt. * - * @return boolean whether this situation should be treated as someone looking at their own + * @return bool whether this situation should be treated as someone looking at their own * attempt. The distinction normally only matters when an attempt is being reviewed. */ public function is_own_attempt() { @@ -603,6 +561,15 @@ class quiz_attempt extends quiz { (!$this->is_preview_user() || $this->attempt->preview); } + /** + * @return bool whether this attempt is a preview belonging to the current user. + */ + public function is_own_preview() { + global $USER; + return $this->attempt->userid == $USER->id && + $this->is_preview_user() && $this->attempt->preview; + } + /** * Is the current user allowed to review this attempt. This applies when * {@link is_own_attempt()} returns false. @@ -621,18 +588,42 @@ class quiz_attempt extends quiz { // Check the users have at least one group in common. $teachersgroups = groups_get_activity_allowed_groups($cm); - $studentsgroups = groups_get_all_groups($cm->course, $this->attempt->userid, $cm->groupingid); + $studentsgroups = groups_get_all_groups( + $cm->course, $this->attempt->userid, $cm->groupingid); return $teachersgroups && $studentsgroups && array_intersect(array_keys($teachersgroups), array_keys($studentsgroups)); } + /** + * Get the overall feedback corresponding to a particular mark. + * @param $grade a particular grade. + */ + public function get_overall_feedback($grade) { + return quiz_feedback_for_grade($grade, $this->get_quiz(), + $this->quizobj->get_context()); + } + + /** + * Wrapper round the has_capability funciton that automatically passes in the quiz context. + */ + public function has_capability($capability, $userid = null, $doanything = true) { + return $this->quizobj->has_capability($capability, $userid, $doanything); + } + + /** + * Wrapper round the require_capability funciton that automatically passes in the quiz context. + */ + public function require_capability($capability, $userid = null, $doanything = true) { + return $this->quizobj->require_capability($capability, $userid, $doanything); + } + /** * Check the appropriate capability to see whether this user may review their own attempt. * If not, prints an error. */ public function check_review_capability() { if (!$this->has_capability('mod/quiz:viewreports')) { - if ($this->get_review_options()->quizstate == QUIZ_STATE_IMMEDIATELY) { + if ($this->get_attempt_state() == mod_quiz_display_options::IMMEDIATELY_AFTER) { $this->require_capability('mod/quiz:attempt'); } else { $this->require_capability('mod/quiz:reviewmyattempts'); @@ -641,235 +632,321 @@ class quiz_attempt extends quiz { } /** - * Get the current state of a question in the attempt. - * - * @param $questionid a questionid. - * @return object the state. + * @return int one of the mod_quiz_display_options::DURING, + * IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants. */ - public function get_question_state($questionid) { - return $this->states[$questionid]; + public function get_attempt_state() { + return quiz_attempt_state($this->get_quiz(), $this->attempt); } /** - * Wrapper that calls quiz_get_reviewoptions with the appropriate arguments. + * Wrapper that the correct mod_quiz_display_options for this quiz at the + * moment. * - * @return object the review options for this user on this attempt. + * @return question_display_options the render options for this user on this attempt. */ - public function get_review_options() { - if (is_null($this->reviewoptions)) { - $this->reviewoptions = quiz_get_reviewoptions($this->quiz, $this->attempt, $this->context); - } - return $this->reviewoptions; - } + public function get_display_options($reviewing) { + if ($reviewing) { + if (is_null($this->reviewoptions)) { + $this->reviewoptions = quiz_get_review_options($this->get_quiz(), + $this->attempt, $this->quizobj->get_context()); + } + return $this->reviewoptions; - /** - * Wrapper that calls get_render_options with the appropriate arguments. - * - * @param integer questionid the quetsion to get the render options for. - * @return object the render options for this user on this attempt. - */ - public function get_render_options($questionid) { - return quiz_get_renderoptions($this->quiz, $this->attempt, $this->context, - $this->get_question_state($questionid)); - } - - /** - * Get a quiz_attempt_question_iterator for either a page of the quiz, or a whole quiz. - * You must have called load_questions with an appropriate argument first. - * - * @param mixed $page as for the @see{get_question_ids} method. - * @return quiz_attempt_question_iterator the requested iterator. - */ - public function get_question_iterator($page = 'all') { - return new quiz_attempt_question_iterator($this, $page); - } - - /** - * Return a summary of the current state of a question in this attempt. You must previously - * have called load_question_states to load the state data about this question. - * - * @param integer $questionid question id of a question that belongs to this quiz. - * @return string a brief string (that could be used as a CSS class name, for example) - * that describes the current state of a question in this attempt. Possible results are: - * open|saved|closed|correct|partiallycorrect|incorrect. - */ - public function get_question_status($questionid) { - $state = $this->states[$questionid]; - switch ($state->event) { - case QUESTION_EVENTOPEN: - return 'open'; - - case QUESTION_EVENTSAVE: - case QUESTION_EVENTGRADE: - case QUESTION_EVENTSUBMIT: - return 'answered'; - - case QUESTION_EVENTCLOSEANDGRADE: - case QUESTION_EVENTCLOSE: - case QUESTION_EVENTMANUALGRADE: - $options = $this->get_render_options($questionid); - if ($options->scores && $this->questions[$questionid]->maxgrade > 0) { - return question_get_feedback_class($state->last_graded->raw_grade / - $this->questions[$questionid]->maxgrade); - } else { - return 'closed'; - } - - default: - $a = new stdClass; - $a->event = $state->event; - $a->questionid = $questionid; - $a->attemptid = $this->attempt->id; - throw new moodle_quiz_exception($this, 'errorunexpectedevent', $a); + } else { + $options = mod_quiz_display_options::make_from_quiz($this->get_quiz(), + mod_quiz_display_options::DURING); + $options->flags = quiz_get_flag_option($this->attempt, $this->quizobj->get_context()); + return $options; } } /** - * @param integer $questionid question id of a question that belongs to this quiz. - * @return boolean whether this question hss been flagged by the attempter. + * @param int $page page number + * @return bool true if this is the last page of the quiz. */ - public function is_question_flagged($questionid) { - $state = $this->states[$questionid]; - return $state->flagged; + public function is_last_page($page) { + return $page == count($this->pagelayout) - 1; } /** - * Return the grade obtained on a particular question, if the user is permitted to see it. - * You must previously have called load_question_states to load the state data about this question. + * Return the list of question ids for either a given page of the quiz, or for the + * whole quiz. * - * @param integer $questionid question id of a question that belongs to this quiz. + * @param mixed $page string 'all' or integer page number. + * @return array the reqested list of question ids. + */ + public function get_slots($page = 'all') { + if ($page === 'all') { + $numbers = array(); + foreach ($this->pagelayout as $numbersonpage) { + $numbers = array_merge($numbers, $numbersonpage); + } + return $numbers; + } else { + return $this->pagelayout[$page]; + } + } + + /** + * Get the question_attempt object for a particular question in this attempt. + * @param int $slot the number used to identify this question within this attempt. + * @return question_attempt + */ + public function get_question_attempt($slot) { + return $this->quba->get_question_attempt($slot); + } + + /** + * Is a particular question in this attempt a real question, or something like a description. + * @param int $slot the number used to identify this question within this attempt. + * @return bool whether that question is a real question. + */ + public function is_real_question($slot) { + return $this->quba->get_question($slot)->length != 0; + } + + /** + * Is a particular question in this attempt a real question, or something like a description. + * @param int $slot the number used to identify this question within this attempt. + * @return bool whether that question is a real question. + */ + public function is_question_flagged($slot) { + return $this->quba->get_question_attempt($slot)->is_flagged(); + } + + /** + * Return the grade obtained on a particular question, if the user is permitted + * to see it. You must previously have called load_question_states to load the + * state data about this question. + * + * @param int $slot the number used to identify this question within this attempt. + * @return string the formatted grade, to the number of decimal places specified + * by the quiz. + */ + public function get_question_number($slot) { + return $this->quba->get_question($slot)->_number; + } + + /** + * Return the grade obtained on a particular question, if the user is permitted + * to see it. You must previously have called load_question_states to load the + * state data about this question. + * + * @param int $slot the number used to identify this question within this attempt. + * @return string the formatted grade, to the number of decimal places specified + * by the quiz. + */ + public function get_question_name($slot) { + return $this->quba->get_question($slot)->name; + } + + /** + * Return the grade obtained on a particular question, if the user is permitted + * to see it. You must previously have called load_question_states to load the + * state data about this question. + * + * @param int $slot the number used to identify this question within this attempt. + * @param bool $showcorrectness Whether right/partial/wrong states should + * be distinguised. + * @return string the formatted grade, to the number of decimal places specified + * by the quiz. + */ + public function get_question_status($slot, $showcorrectness) { + return $this->quba->get_question_state_string($slot, $showcorrectness); + } + + /** + * Return the grade obtained on a particular question, if the user is permitted + * to see it. You must previously have called load_question_states to load the + * state data about this question. + * + * @param int $slot the number used to identify this question within this attempt. + * @param bool $showcorrectness Whether right/partial/wrong states should + * be distinguised. + * @return string class name for this state. + */ + public function get_question_state_class($slot, $showcorrectness) { + return $this->quba->get_question_state_class($slot, $showcorrectness); + } + + /** + * Return the grade obtained on a particular question. + * You must previously have called load_question_states to load the state + * data about this question. + * + * @param int $slot the number used to identify this question within this attempt. * @return string the formatted grade, to the number of decimal places specified by the quiz. */ - public function get_question_score($questionid) { - $options = $this->get_render_options($questionid); - if ($options->scores) { - return quiz_format_question_grade($this->quiz, $this->states[$questionid]->last_graded->grade); - } else { - return ''; - } + public function get_question_mark($slot) { + return quiz_format_question_grade($this->get_quiz(), $this->quba->get_question_mark($slot)); + } + + /** + * Get the time of the most recent action performed on a question. + * @param int $slot the number used to identify this question within this usage. + * @return int timestamp. + */ + public function get_question_action_time($slot) { + return $this->quba->get_question_action_time($slot); } // URLs related to this attempt ======================================================== /** - * @param integer $questionid a question id. If set, will add a fragment to the URL + * @return string quiz view url. + */ + public function view_url() { + return $this->quizobj->view_url(); + } + + /** + * @return string the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter. + */ + public function start_attempt_url() { + return $this->quizobj->start_attempt_url(); + } + + /** + * @param int $slot if speified, the slot number of a specific question to link to. + * @param int $page if specified, a particular page to link to. If not givem deduced + * from $slot, or goes to the first page. + * @param int $questionid a question id. If set, will add a fragment to the URL * to jump to a particuar question on the page. - * @param integer $page if specified, the URL of this particular page of the attempt, otherwise - * the URL will go to the first page. If -1, deduce $page from $questionid. - * @param integer $thispage if not -1, the current page. Will cause links to other things on + * @param int $thispage if not -1, the current page. Will cause links to other things on * this page to be output as only a fragment. * @return string the URL to continue this attempt. */ - public function attempt_url($questionid = 0, $page = -1, $thispage = -1) { - return $this->page_and_question_url('attempt', $questionid, $page, false, $thispage); + public function attempt_url($slot = null, $page = -1, $thispage = -1) { + return $this->page_and_question_url('attempt', $slot, $page, false, $thispage); } /** * @return string the URL of this quiz's summary page. */ public function summary_url() { - global $CFG; - return $CFG->wwwroot . '/mod/quiz/summary.php?attempt=' . $this->attempt->id; + return new moodle_url('/mod/quiz/summary.php', array('attempt' => $this->attempt->id)); } /** * @return string the URL of this quiz's summary page. */ public function processattempt_url() { - global $CFG; - return $CFG->wwwroot . '/mod/quiz/processattempt.php'; + return new moodle_url('/mod/quiz/processattempt.php'); } /** - * @param integer $questionid a question id. If set, will add a fragment to the URL - * to jump to a particuar question on the page. If -1, deduce $page from $questionid. - * @param integer $page if specified, the URL of this particular page of the attempt, otherwise - * the URL will go to the first page. - * @param boolean $showall if true, the URL will be to review the entire attempt on one page, + * @param int $slot indicates which question to link to. + * @param int $page if specified, the URL of this particular page of the attempt, otherwise + * the URL will go to the first page. If -1, deduce $page from $slot. + * @param bool $showall if true, the URL will be to review the entire attempt on one page, * and $page will be ignored. - * @param integer $thispage if not -1, the current page. Will cause links to other things on + * @param int $thispage if not -1, the current page. Will cause links to other things on * this page to be output as only a fragment. * @return string the URL to review this attempt. */ - public function review_url($questionid = 0, $page = -1, $showall = false, $thispage = -1) { - return $this->page_and_question_url('review', $questionid, $page, $showall, $thispage); + public function review_url($slot = null, $page = -1, $showall = false, $thispage = -1) { + return $this->page_and_question_url('review', $slot, $page, $showall, $thispage); } // Bits of content ===================================================================== + /** * Initialise the JS etc. required all the questions on a page.. * @param mixed $page a page number, or 'all'. */ - public function get_html_head_contributions($page = 'all') { - global $PAGE; - question_get_html_head_contributions($this->get_question_ids($page), $this->questions, $this->states); + public function get_html_head_contributions($page = 'all', $showall = false) { + if ($showall) { + $page = 'all'; + } + $result = ''; + foreach ($this->get_slots($page) as $slot) { + $result .= $this->quba->render_question_head_html($slot); + } + $result .= question_engine::initialise_js(); + return $result; } /** * Initialise the JS etc. required by one question. - * @param integer $questionid the question id. + * @param int $questionid the question id. */ - public function get_question_html_head_contributions($questionid) { - question_get_html_head_contributions(array($questionid), $this->questions, $this->states); + public function get_question_html_head_contributions($slot) { + return $this->quba->render_question_head_html($slot) . + question_engine::initialise_js(); } /** - * Print the HTML for the start new preview button. + * Print the HTML for the start new preview button, if the current user + * is allowed to see one. */ - public function print_restart_preview_button() { - global $CFG, $OUTPUT; - echo $OUTPUT->container_start('controls'); - $url = new moodle_url($this->start_attempt_url(), array('forcenew' => true)); - echo $OUTPUT->single_button($url, get_string('startagain', 'quiz')); - echo $OUTPUT->container_end(); + public function restart_preview_button() { + global $OUTPUT; + if ($this->is_preview() && $this->is_preview_user()) { + return $OUTPUT->single_button(new moodle_url( + $this->start_attempt_url(), array('forcenew' => true)), + get_string('startnewpreview', 'quiz')); + } else { + return ''; + } } /** - * Return the HTML of the quiz timer. - * @return string HTML content. + * Generate the HTML that displayes the question in its current state, with + * the appropriate display options. + * + * @param int $id the id of a question in this quiz attempt. + * @param bool $reviewing is the being printed on an attempt or a review page. + * @param string $thispageurl the URL of the page this question is being printed on. + * @return string HTML for the question in its current state. */ - public function get_timer_html() { - return '
' . get_string('timeleft', 'quiz') . - '
'; + public function render_question($slot, $reviewing, $thispageurl = '') { + return $this->quba->render_question($slot, + $this->get_display_options($reviewing), + $this->quba->get_question($slot)->_number); + } + + /** + * Like {@link render_question()} but displays the question at the past step + * indicated by $seq, rather than showing the latest step. + * + * @param int $id the id of a question in this quiz attempt. + * @param int $seq the seq number of the past state to display. + * @param bool $reviewing is the being printed on an attempt or a review page. + * @param string $thispageurl the URL of the page this question is being printed on. + * @return string HTML for the question in its current state. + */ + public function render_question_at_step($slot, $seq, $reviewing, $thispageurl = '') { + return $this->quba->render_question_at_step($slot, $seq, + $this->get_display_options($reviewing), + $this->quba->get_question($slot)->_number); } /** * Wrapper round print_question from lib/questionlib.php. * - * @param integer $id the id of a question in this quiz attempt. - * @param boolean $reviewing is the being printed on an attempt or a review page. + * @param int $id the id of a question in this quiz attempt. + * @param bool $reviewing is the being printed on an attempt or a review page. * @param string $thispageurl the URL of the page this question is being printed on. */ - public function print_question($id, $reviewing, $thispageurl = '') { - global $CFG; - - if ($reviewing) { - $options = $this->get_review_options(); - } else { - $options = $this->get_render_options($id); - } - if ($thispageurl instanceof moodle_url) { - $thispageurl = $thispageurl->out(false); - } - if ($thispageurl) { - $this->quiz->thispageurl = str_replace($CFG->wwwroot, '', $thispageurl); - } else { - unset($thispageurl); - } - print_question($this->questions[$id], $this->states[$id], $this->questions[$id]->_number, - $this->quiz, $options); + public function render_question_for_commenting($slot) { + $options = $this->get_display_options(true); + $options->hide_all_feedback(); + $options->manualcomment = question_display_options::EDITABLE; + return $this->quba->render_question($slot, $options, + $this->quba->get_question($slot)->_number); } - public function check_file_access($questionid, $isreviewing, $contextid, $component, + /** + * Check wheter access should be allowed to a particular file. + * + * @param int $id the id of a question in this quiz attempt. + * @param bool $reviewing is the being printed on an attempt or a review page. + * @param string $thispageurl the URL of the page this question is being printed on. + * @return string HTML for the question in its current state. + */ + public function check_file_access($slot, $reviewing, $contextid, $component, $filearea, $args, $forcedownload) { - if ($isreviewing) { - $options = $this->get_review_options(); - } else { - $options = $this->get_render_options($questionid); - } - // XXX: mulitichoice type needs quiz id to get maxgrade - $options->quizid = $this->attempt->quiz; - return question_check_file_access($this->questions[$questionid], - $this->get_question_state($questionid), $options, $contextid, + return $this->quba->check_file_access($slot, $this->get_display_options($reviewing), $component, $filearea, $args, $forcedownload); } @@ -877,8 +954,8 @@ class quiz_attempt extends quiz { * Triggers the sending of the notification emails at the end of this attempt. */ public function quiz_send_notification_emails() { - quiz_send_notification_emails($this->course, $this->quiz, $this->attempt, - $this->context, $this->cm); + quiz_send_notification_emails($this->get_course(), $this->get_quiz(), $this->attempt, + $this->quizobj->get_context(), $this->get_cm()); } /** @@ -889,230 +966,193 @@ class quiz_attempt extends quiz { * @param $showall whether we are showing the whole quiz on one page. (Used by review.php) * @return quiz_nav_panel_base the requested object. */ - public function get_navigation_panel($panelclass, $page, $showall = false) { - $panel = new $panelclass($this, $this->get_review_options(), $page, $showall); - return $panel->get_contents(); + public function get_navigation_panel(mod_quiz_renderer $output, + $panelclass, $page, $showall = false) { + $panel = new $panelclass($this, $this->get_display_options(true), $page, $showall); + + $bc = new block_contents(); + $bc->id = 'quiznavigation'; + $bc->title = get_string('quiznavigation', 'quiz'); + $bc->content = $output->navigation_panel($panel); + return $bc; } /** * Given a URL containing attempt={this attempt id}, return an array of variant URLs - * @param $url a URL. + * @param moodle_url $url a URL. * @return string HTML fragment. Comma-separated list of links to the other * attempts with the attempt number as the link text. The curent attempt is * included but is not a link. */ - public function links_to_other_attempts($url) { - $search = '/\battempt=' . $this->attempt->id . '\b/'; - $attempts = quiz_get_user_attempts($this->quiz->id, $this->attempt->userid, 'all'); + public function links_to_other_attempts(moodle_url $url) { + $attempts = quiz_get_user_attempts($this->get_quiz()->id, $this->attempt->userid, 'all'); if (count($attempts) <= 1) { return false; } - $attemptlist = array(); + + $links = new mod_quiz_links_to_other_attempts(); foreach ($attempts as $at) { if ($at->id == $this->attempt->id) { - $attemptlist[] = '' . $at->attempt . ''; + $links->links[$at->attempt] = null; } else { - $changedurl = preg_replace($search, 'attempt=' . $at->id, $url); - $attemptlist[] = '' . $at->attempt . ''; + $links->links[$at->attempt] = new moodle_url($url, array('attempt' => $at->id)); } } - return implode(', ', $attemptlist); + return $links; } - // Methods for processing manual comments ============================================== + // Methods for processing ================================================== + /** - * Process a manual comment for a question in this attempt. - * @param $questionid - * @param integer $questionid the question id - * @param string $comment the new comment from the teacher. - * @param mixed $grade the grade the teacher assigned, or '' to not change the grade. - * @return mixed true on success, a string error message if a problem is detected - * (for example score out of range). + * Process all the actions that were submitted as part of the current request. + * + * @param int $timestamp the timestamp that should be stored as the modifed + * time in the database for these actions. If null, will use the current time. */ - public function process_comment($questionid, $comment, $commentformat, $grade) { - // I am not sure it is a good idea to have update methods here - this - // class is only about getting data out of the question engine, and - // helping to display it, apart from this. - $this->ensure_question_loaded($questionid); - $this->ensure_state_loaded($questionid); - $state = $this->states[$questionid]; + public function process_all_actions($timestamp) { + global $DB; + $this->quba->process_all_actions($timestamp); + question_engine::save_questions_usage_by_activity($this->quba); - $error = question_process_comment($this->questions[$questionid], - $state, $this->attempt, $comment, $commentformat, $grade); - - // If the state was update (successfully), save the changes. - if (!is_string($error) && $state->changed) { - if (!save_question_session($this->questions[$questionid], $state)) { - $error = get_string('errorudpatingquestionsession', 'quiz'); - } - if (!quiz_save_best_grade($this->quiz, $this->attempt->userid)) { - $error = get_string('errorudpatingbestgrade', 'quiz'); - } + $this->attempt->timemodified = $timestamp; + if ($this->attempt->timefinish) { + $this->attempt->sumgrades = $this->quba->get_total_mark(); + } + $DB->update_record('quiz_attempts', $this->attempt); + + if (!$this->is_preview() && $this->attempt->timefinish) { + quiz_save_best_grade($this->get_quiz(), $this->get_userid()); + } + } + + /** + * Update the flagged state for all question_attempts in this usage, if their + * flagged state was changed in the request. + */ + public function save_question_flags() { + $this->quba->update_question_flags(); + question_engine::save_questions_usage_by_activity($this->quba); + } + + public function finish_attempt($timestamp) { + global $DB; + $this->quba->process_all_actions($timestamp); + $this->quba->finish_all_questions($timestamp); + + question_engine::save_questions_usage_by_activity($this->quba); + + $this->attempt->timemodified = $timestamp; + $this->attempt->timefinish = $timestamp; + $this->attempt->sumgrades = $this->quba->get_total_mark(); + $DB->update_record('quiz_attempts', $this->attempt); + + if (!$this->is_preview()) { + quiz_save_best_grade($this->get_quiz()); + $this->quiz_send_notification_emails(); } - return $error; } /** * Print the fields of the comment form for questions in this attempt. - * @param $questionid a question id. + * @param $slot which question to output the fields for. * @param $prefix Prefix to add to all field names. */ - public function question_print_comment_fields($questionid, $prefix) { - global $DB; - - $this->ensure_question_loaded($questionid); - $this->ensure_state_loaded($questionid); - - /// Work out a nice title. - $student = $DB->get_record('user', array('id' => $this->get_userid())); - $a = new stdClass(); + public function question_print_comment_fields($slot, $prefix) { + // Work out a nice title. + $student = get_record('user', 'id', $this->get_userid()); + $a = new object(); $a->fullname = fullname($student, true); $a->attempt = $this->get_attempt_number(); - question_print_comment_fields($this->questions[$questionid], - $this->states[$questionid], $prefix, $this->quiz, get_string('gradingattempt', 'quiz_grading', $a)); + question_print_comment_fields($this->quba->get_question_attempt($slot), + $prefix, $this->get_display_options(true)->markdp, + get_string('gradingattempt', 'quiz_grading', $a)); } // Private methods ===================================================================== - /** - * Check that the state of a particular question is loaded, and if not throw an exception. - * @param integer $id a question id. - */ - private function ensure_state_loaded($id) { - if (!array_key_exists($id, $this->states) || isset($this->states[$id]->_partiallyloaded)) { - throw new moodle_quiz_exception($this, 'statenotloaded', $id); - } - } - - /** - * @return string the layout of this quiz. Used by number_questions to - * work out which questions are on which pages. - */ - protected function get_layout_string() { - return $this->attempt->layout; - } /** * Get a URL for a particular question on a particular page of the quiz. * Used by {@link attempt_url()} and {@link review_url()}. * * @param string $script. Used in the URL like /mod/quiz/$script.php - * @param integer $questionid the id of a particular question on the page to jump to. 0 to just use the $page parameter. - * @param integer $page -1 to look up the page number from the questionid, otherwise the page number to go to. - * @param boolean $showall if true, return a URL with showall=1, and not page number - * @param integer $thispage the page we are currently on. Links to questoins on this + * @param int $slot identifies the specific question on the page to jump to. + * 0 to just use the $page parameter. + * @param int $page -1 to look up the page number from the slot, otherwise + * the page number to go to. + * @param bool $showall if true, return a URL with showall=1, and not page number + * @param int $thispage the page we are currently on. Links to questions on this * page will just be a fragment #q123. -1 to disable this. * @return The requested URL. */ - protected function page_and_question_url($script, $questionid, $page, $showall, $thispage) { - global $CFG; - + protected function page_and_question_url($script, $slot, $page, $showall, $thispage) { // Fix up $page if ($page == -1) { - if ($questionid && !$showall) { - $page = $this->questions[$questionid]->_page; + if (!is_null($slot) && !$showall) { + $page = $this->quba->get_question($slot)->_page; } else { $page = 0; } } + if ($showall) { $page = 0; } + // Add a fragment to scroll down to the question. + $fragment = ''; + if (!is_null($slot)) { + if ($slot == reset($this->pagelayout[$page])) { + // First question on page, go to top. + $fragment = '#'; + } else { + $fragment = '#q' . $slot; + } + } + // Work out the correct start to the URL. if ($thispage == $page) { - $url = ''; + return new moodle_url($fragment); + } else { - $url = $CFG->wwwroot . '/mod/quiz/' . $script . '.php?attempt=' . $this->attempt->id; + $url = new moodle_url('/mod/quiz/' . $script . '.php' . $fragment, + array('attempt' => $this->attempt->id)); if ($showall) { - $url .= '&showall=1'; + $url->param('showall', 1); } else if ($page > 0) { - $url .= '&page=' . $page; + $url->param('page', $page); } + return $url; } - - // Add a fragment to scroll down to the question. - if ($questionid) { - if ($questionid == reset($this->pagequestionids[$page])) { - // First question on page, go to top. - $url .= '#'; - } else { - $url .= '#q' . $questionid; - } - } - - return $url; } } + /** - * A PHP Iterator for conviniently looping over the questions in a quiz. The keys are the question - * numbers (with 'i' for descriptions) and the values are the question objects. + * Represents a single link in the navigation panel. * - * @copyright 2008 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.1 */ -class quiz_attempt_question_iterator implements Iterator { - private $attemptobj; // Reference to the quiz_attempt object we provide access to. - private $questionids; // Array of the question ids within that attempt we are iterating over. - - /** - * Constructor. Normally, you don't want to call this directly. Instead call - * quiz_attempt::get_question_iterator - * - * @param quiz_attempt $attemptobj the quiz_attempt object we will be providing access to. - * @param mixed $page as for @see{quiz_attempt::get_question_iterator}. - */ - public function __construct(quiz_attempt $attemptobj, $page = 'all') { - $this->attemptobj = $attemptobj; - $this->questionids = $attemptobj->get_question_ids($page); - } - - // Implementation of the Iterator interface ============================================ - public function rewind() { - reset($this->questionids); - } - - public function current() { - $id = current($this->questionids); - if ($id) { - return $this->attemptobj->get_question($id); - } else { - return false; - } - } - - public function key() { - $id = current($this->questionids); - if ($id) { - return $this->attemptobj->get_question($id)->_number; - } else { - return false; - } - } - - public function next() { - $id = next($this->questionids); - if ($id) { - return $this->attemptobj->get_question($id); - } else { - return false; - } - } - - public function valid() { - return $this->current() !== false; - } +class quiz_nav_question_button implements renderable { + public $id; + public $number; + public $stateclass; + public $statestring; + public $currentpage; + public $flagged; + public $url; } + /** * Represents the navigation panel, and builds a {@link block_contents} to allow * it to be output. * - * @copyright 2008 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 + * @copyright 2008 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 */ abstract class quiz_nav_panel_base { /** @var quiz_attempt */ @@ -1124,138 +1164,114 @@ abstract class quiz_nav_panel_base { /** @var boolean */ protected $showall; - public function __construct(quiz_attempt $attemptobj, $options, $page, $showall) { + public function __construct(quiz_attempt $attemptobj, + question_display_options $options, $page, $showall) { $this->attemptobj = $attemptobj; $this->options = $options; $this->page = $page; $this->showall = $showall; } - protected function get_question_buttons() { - $html = '
' . "\n"; - foreach ($this->attemptobj->get_question_iterator() as $number => $question) { - $html .= $this->get_question_button($number, $question) . "\n"; + public function get_question_buttons() { + $buttons = array(); + foreach ($this->attemptobj->get_slots() as $slot) { + $qa = $this->attemptobj->get_question_attempt($slot); + $showcorrectness = $this->options->correctness && $qa->has_marks(); + + $button = new quiz_nav_question_button(); + $button->id = 'quiznavbutton' . $slot; + $button->number = $qa->get_question()->_number; + $button->stateclass = $qa->get_state_class($showcorrectness); + if (!$showcorrectness && $button->stateclass == 'notanswered') { + $button->stateclass = 'complete'; + } + $button->statestring = $qa->get_state_string($showcorrectness); + $button->currentpage = $qa->get_question()->_page == $this->page; + $button->flagged = $qa->is_flagged(); + $button->url = $this->get_question_url($slot); + $buttons[] = $button; } - $html .= "
\n"; - return $html; + + return $buttons; } - protected function get_question_button($number, $question) { - $strstate = get_string($this->attemptobj->get_question_status($question->id), 'quiz'); - $flagstate = ''; - if ($this->attemptobj->is_question_flagged($question->id)) { - $flagstate = get_string('flagged', 'question'); - } - return '' . - $number . ' (' . $strstate . ' - ' . $flagstate . ')'; - } - - protected function get_before_button_bits() { + public function render_before_button_bits(mod_quiz_renderer $output) { return ''; } - abstract protected function get_end_bits(); + abstract public function render_end_bits(mod_quiz_renderer $output); - abstract protected function get_question_url($question); + protected function render_restart_preview_link($output) { + if (!$this->attemptobj->is_own_preview()) { + return ''; + } + return $output->restart_preview_button(new moodle_url( + $this->attemptobj->start_attempt_url(), array('forcenew' => true))); + } - protected function get_user_picture() { - global $DB, $OUTPUT; + protected abstract function get_question_url($slot); + + public function user_picture() { + global $DB; $user = $DB->get_record('user', array('id' => $this->attemptobj->get_userid())); - $output = ''; - $output .= '
'; - $output .= $OUTPUT->user_picture($user, array('courseid'=>$this->attemptobj->get_courseid())); - $output .= ' ' . fullname($user); - $output .= '
'; - return $output; - } - - protected function get_question_state_classes($question) { - // The current status of the question. - $classes = $this->attemptobj->get_question_status($question->id); - - // Plus a marker for the current page. - if ($this->showall || $question->_page == $this->page) { - $classes .= ' thispage'; - } - - // Plus a marker for flagged questions. - if ($this->attemptobj->is_question_flagged($question->id)) { - $classes .= ' flagged'; - } - return $classes; - } - - public function get_contents() { - global $PAGE; - $PAGE->requires->js_init_call('M.mod_quiz.nav.init', null, false, quiz_get_js_module()); - - $content = ''; - if ($this->attemptobj->get_quiz()->showuserpicture) { - $content .= $this->get_user_picture() . "\n"; - } - $content .= $this->get_before_button_bits(); - $content .= $this->get_question_buttons() . "\n"; - $content .= '
' . "\n" . $this->get_end_bits() . "\n
\n"; - - $bc = new block_contents(); - $bc->id = 'quiznavigation'; - $bc->title = get_string('quiznavigation', 'quiz'); - $bc->content = $content; - return $bc; + $userpicture = new user_picture($user); + $userpicture->courseid = $this->attemptobj->get_courseid(); + return $userpicture; } } + /** * Specialisation of {@link quiz_nav_panel_base} for the attempt quiz page. * - * @copyright 2008 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 + * @copyright 2008 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 */ class quiz_attempt_nav_panel extends quiz_nav_panel_base { - protected function get_question_url($question) { - return $this->attemptobj->attempt_url($question->id, -1, $this->page); + public function get_question_url($slot) { + return $this->attemptobj->attempt_url($slot, -1, $this->page); } - protected function get_before_button_bits() { - return '
' . get_string('navnojswarning', 'quiz') . "
\n"; + public function render_before_button_bits(mod_quiz_renderer $output) { + return html_writer::tag('div', get_string('navnojswarning', 'quiz'), + array('id' => 'quiznojswarning')); } - protected function get_end_bits() { - global $PAGE; - $output = ''; - $output .= '' . get_string('finishattemptdots', 'quiz') . ''; - $output .= $this->attemptobj->get_timer_html(); - return $output; + public function render_end_bits(mod_quiz_renderer $output) { + return html_writer::link($this->attemptobj->summary_url(), + get_string('endtest', 'quiz'), array('class' => 'endtestlink')) . + $output->countdown_timer() . + $this->render_restart_preview_link($output); } } + /** * Specialisation of {@link quiz_nav_panel_base} for the review quiz page. * - * @copyright 2008 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 + * @copyright 2008 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 */ class quiz_review_nav_panel extends quiz_nav_panel_base { - protected function get_question_url($question) { - return $this->attemptobj->review_url($question->id, -1, $this->showall, $this->page); + public function get_question_url($slot) { + return $this->attemptobj->review_url($slot, -1, $this->showall, $this->page); } - protected function get_end_bits() { + public function render_end_bits(mod_quiz_renderer $output) { $html = ''; if ($this->attemptobj->get_num_pages() > 1) { if ($this->showall) { - $html .= '' . get_string('showeachpage', 'quiz') . ''; + $html .= html_writer::link($this->attemptobj->review_url(null, 0, false), + get_string('showeachpage', 'quiz')); } else { - $html .= '' . get_string('showall', 'quiz') . ''; + $html .= html_writer::link($this->attemptobj->review_url(null, 0, true), + get_string('showall', 'quiz')); } } - $accessmanager = $this->attemptobj->get_access_manager(time()); - $html .= $accessmanager->print_finish_review_link($this->attemptobj->is_preview_user(), true); + $html .= $output->finish_review_link($this->attemptobj->view_url()); + $html .= $this->render_restart_preview_link($output); return $html; } } diff --git a/mod/quiz/backup/moodle2/backup_quiz_activity_task.class.php b/mod/quiz/backup/moodle2/backup_quiz_activity_task.class.php index 3f286dcd46f..e5092f4b42f 100644 --- a/mod/quiz/backup/moodle2/backup_quiz_activity_task.class.php +++ b/mod/quiz/backup/moodle2/backup_quiz_activity_task.class.php @@ -1,5 +1,4 @@ . /** - * @package moodlecore + * @package moodlecore * @subpackage backup-moodle2 - * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -require_once($CFG->dirroot . '/mod/quiz/backup/moodle2/backup_quiz_stepslib.php'); // Because it exists (must) + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/quiz/backup/moodle2/backup_quiz_stepslib.php'); + /** * quiz backup task that provides all the settings and steps to perform one * complete backup of the activity + * + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_quiz_activity_task extends backup_activity_task { @@ -64,10 +70,10 @@ class backup_quiz_activity_task extends backup_activity_task { * Code the transformations to perform in the activity in * order to get transportable (encoded) links */ - static public function encode_content_links($content) { + public static function encode_content_links($content) { global $CFG; - $base = preg_quote($CFG->wwwroot,"/"); + $base = preg_quote($CFG->wwwroot, '/'); // Link to the list of quizzes $search="/(".$base."\/mod\/quiz\/index.php\?id\=)([0-9]+)/"; diff --git a/mod/quiz/backup/moodle2/backup_quiz_stepslib.php b/mod/quiz/backup/moodle2/backup_quiz_stepslib.php index 655cb5f8474..f047f4a9a18 100644 --- a/mod/quiz/backup/moodle2/backup_quiz_stepslib.php +++ b/mod/quiz/backup/moodle2/backup_quiz_stepslib.php @@ -1,5 +1,4 @@ . /** - * @package moodlecore + * @package moodlecore * @subpackage backup-moodle2 - * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ + +defined('MOODLE_INTERNAL') || die(); + + /** * Define all the backup steps that will be used by the backup_quiz_activity_task - */ - -/** - * Define the complete quiz structure for backup, with file and id annotations + * + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_quiz_activity_structure_step extends backup_questions_activity_structure_step { @@ -39,9 +41,12 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru // Define each element separated $quiz = new backup_nested_element('quiz', array('id'), array( 'name', 'intro', 'introformat', 'timeopen', - 'timeclose', 'optionflags', 'penaltyscheme', 'attempts_number', + 'timeclose', 'preferredbehaviour', 'attempts_number', 'attemptonlast', 'grademethod', 'decimalpoints', 'questiondecimalpoints', - 'review', 'questionsperpage', 'shufflequestions', 'shuffleanswers', + 'reviewattempt', 'reviewcorrectness', 'reviewmarks', + 'reviewspecificfeedback', 'reviewgeneralfeedback', + 'reviewrightanswer', 'reviewoverallfeedback', + 'questionsperpage', 'shufflequestions', 'shuffleanswers', 'questions', 'sumgrades', 'grade', 'timecreated', 'timemodified', 'timelimit', 'password', 'subnet', 'popup', 'delay1', 'delay2', 'showuserpicture', @@ -77,8 +82,7 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru // This module is using questions, so produce the related question states and sessions // attaching them to the $attempt element based in 'uniqueid' matching - $this->add_question_attempts_states($attempt, 'uniqueid'); - $this->add_question_attempts_sessions($attempt, 'uniqueid'); + $this->add_question_usages($attempt, 'uniqueid'); // Build the tree @@ -100,9 +104,11 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru // Define sources $quiz->set_source_table('quiz', array('id' => backup::VAR_ACTIVITYID)); - $qinstance->set_source_table('quiz_question_instances', array('quiz' => backup::VAR_PARENTID)); + $qinstance->set_source_table('quiz_question_instances', + array('quiz' => backup::VAR_PARENTID)); - $feedback->set_source_table('quiz_feedback', array('quizid' => backup::VAR_PARENTID)); + $feedback->set_source_table('quiz_feedback', + array('quizid' => backup::VAR_PARENTID)); // Quiz overrides to backup are different depending of user info $overrideparams = array('quiz' => backup::VAR_PARENTID); @@ -115,7 +121,11 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru // All the rest of elements only happen if we are including user info if ($userinfo) { $grade->set_source_table('quiz_grades', array('quiz' => backup::VAR_PARENTID)); - $attempt->set_source_table('quiz_attempts', array('quiz' => backup::VAR_PARENTID)); + $attempt->set_source_sql(' + SELECT * + FROM {quiz_attempts} + WHERE quiz = :quiz AND preview = 0', + array('quiz' => backup::VAR_PARENTID)); } // Define source alias diff --git a/mod/quiz/backup/moodle2/restore_quiz_activity_task.class.php b/mod/quiz/backup/moodle2/restore_quiz_activity_task.class.php index baedede8d64..6b53c5ec3ae 100644 --- a/mod/quiz/backup/moodle2/restore_quiz_activity_task.class.php +++ b/mod/quiz/backup/moodle2/restore_quiz_activity_task.class.php @@ -1,5 +1,4 @@ . /** - * @package moodlecore + * @package moodlecore * @subpackage backup-moodle2 - * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ + defined('MOODLE_INTERNAL') || die(); -require_once($CFG->dirroot . '/mod/quiz/backup/moodle2/restore_quiz_stepslib.php'); // Because it exists (must) +require_once($CFG->dirroot . '/mod/quiz/backup/moodle2/restore_quiz_stepslib.php'); +// Because it exists (must) + /** * quiz restore task that provides all the settings and steps to perform one * complete restore of the activity + * + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_quiz_activity_task extends restore_activity_task { @@ -51,11 +56,12 @@ class restore_quiz_activity_task extends restore_activity_task { * Define the contents in the activity that must be * processed by the link decoder */ - static public function define_decode_contents() { + public static function define_decode_contents() { $contents = array(); $contents[] = new restore_decode_content('quiz', array('intro'), 'quiz'); - $contents[] = new restore_decode_content('quiz_feedback', array('feedbacktext'), 'quiz_feedback'); + $contents[] = new restore_decode_content('quiz_feedback', + array('feedbacktext'), 'quiz_feedback'); return $contents; } @@ -64,12 +70,15 @@ class restore_quiz_activity_task extends restore_activity_task { * Define the decoding rules for links belonging * to the activity to be executed by the link decoder */ - static public function define_decode_rules() { + public static function define_decode_rules() { $rules = array(); - $rules[] = new restore_decode_rule('QUIZVIEWBYID', '/mod/quiz/view.php?id=$1', 'course_module'); - $rules[] = new restore_decode_rule('QUIZVIEWBYQ', '/mod/quiz/view.php?q=$1', 'quiz'); - $rules[] = new restore_decode_rule('QUIZINDEX', '/mod/quiz/index.php?id=$1', 'course'); + $rules[] = new restore_decode_rule('QUIZVIEWBYID', + '/mod/quiz/view.php?id=$1', 'course_module'); + $rules[] = new restore_decode_rule('QUIZVIEWBYQ', + '/mod/quiz/view.php?q=$1', 'quiz'); + $rules[] = new restore_decode_rule('QUIZINDEX', + '/mod/quiz/index.php?id=$1', 'course'); return $rules; @@ -81,54 +90,80 @@ class restore_quiz_activity_task extends restore_activity_task { * quiz logs. It must return one array * of {@link restore_log_rule} objects */ - static public function define_restore_log_rules() { + public static function define_restore_log_rules() { $rules = array(); - $rules[] = new restore_log_rule('quiz', 'add', 'view.php?id={course_module}', '{quiz}'); - $rules[] = new restore_log_rule('quiz', 'update', 'view.php?id={course_module}', '{quiz}'); - $rules[] = new restore_log_rule('quiz', 'view', 'view.php?id={course_module}', '{quiz}'); - $rules[] = new restore_log_rule('quiz', 'preview', 'view.php?id={course_module}', '{quiz}'); - $rules[] = new restore_log_rule('quiz', 'report', 'report.php?id={course_module}', '{quiz}'); - $rules[] = new restore_log_rule('quiz', 'editquestions', 'view.php?id={course_module}', '{quiz}'); - $rules[] = new restore_log_rule('quiz', 'delete attempt', 'report.php?id={course_module}', '[oldattempt]'); - $rules[] = new restore_log_rule('quiz', 'edit override', 'overrideedit.php?id={quiz_override}', '{quiz}'); - $rules[] = new restore_log_rule('quiz', 'delete override', 'overrides.php.php?cmid={course_module}', '{quiz}'); - $rules[] = new restore_log_rule('quiz', 'addcategory', 'view.php?id={course_module}', '{question_category}'); - $rules[] = new restore_log_rule('quiz', 'view summary', 'summary.php?attempt={quiz_attempt_id}', '{quiz}'); - $rules[] = new restore_log_rule('quiz', 'manualgrade', 'comment.php?attempt={quiz_attempt_id}&question={question}', '{quiz}'); - $rules[] = new restore_log_rule('quiz', 'manualgrading', 'report.php?mode=grading&q={quiz}', '{quiz}'); + $rules[] = new restore_log_rule('quiz', 'add', + 'view.php?id={course_module}', '{quiz}'); + $rules[] = new restore_log_rule('quiz', 'update', + 'view.php?id={course_module}', '{quiz}'); + $rules[] = new restore_log_rule('quiz', 'view', + 'view.php?id={course_module}', '{quiz}'); + $rules[] = new restore_log_rule('quiz', 'preview', + 'view.php?id={course_module}', '{quiz}'); + $rules[] = new restore_log_rule('quiz', 'report', + 'report.php?id={course_module}', '{quiz}'); + $rules[] = new restore_log_rule('quiz', 'editquestions', + 'view.php?id={course_module}', '{quiz}'); + $rules[] = new restore_log_rule('quiz', 'delete attempt', + 'report.php?id={course_module}', '[oldattempt]'); + $rules[] = new restore_log_rule('quiz', 'edit override', + 'overrideedit.php?id={quiz_override}', '{quiz}'); + $rules[] = new restore_log_rule('quiz', 'delete override', + 'overrides.php.php?cmid={course_module}', '{quiz}'); + $rules[] = new restore_log_rule('quiz', 'addcategory', + 'view.php?id={course_module}', '{question_category}'); + $rules[] = new restore_log_rule('quiz', 'view summary', + 'summary.php?attempt={quiz_attempt_id}', '{quiz}'); + $rules[] = new restore_log_rule('quiz', 'manualgrade', + 'comment.php?attempt={quiz_attempt_id}&question={question}', '{quiz}'); + $rules[] = new restore_log_rule('quiz', 'manualgrading', + 'report.php?mode=grading&q={quiz}', '{quiz}'); // All the ones calling to review.php have two rules to handle both old and new urls // in any case they are always converted to new urls on restore // TODO: In Moodle 2.x (x >= 5) kill the old rules - // Note we are using the 'quiz_attempt_id' mapping becuase that is the one containing the quiz_attempt->ids - // old an new for quiz-attempt - $rules[] = new restore_log_rule('quiz', 'attempt', 'review.php?id={course_module}&attempt={quiz_attempt}', '{quiz}', - null, null, 'review.php?attempt={quiz_attempt}'); + // Note we are using the 'quiz_attempt_id' mapping becuase that is the + // one containing the quiz_attempt->ids old an new for quiz-attempt + $rules[] = new restore_log_rule('quiz', 'attempt', + 'review.php?id={course_module}&attempt={quiz_attempt}', '{quiz}', + null, null, 'review.php?attempt={quiz_attempt}'); // old an new for quiz-submit - $rules[] = new restore_log_rule('quiz', 'submit', 'review.php?id={course_module}&attempt={quiz_attempt_id}', '{quiz}', - null, null, 'review.php?attempt={quiz_attempt_id}'); - $rules[] = new restore_log_rule('quiz', 'submit', 'review.php?attempt={quiz_attempt_id}', '{quiz}'); + $rules[] = new restore_log_rule('quiz', 'submit', + 'review.php?id={course_module}&attempt={quiz_attempt_id}', '{quiz}', + null, null, 'review.php?attempt={quiz_attempt_id}'); + $rules[] = new restore_log_rule('quiz', 'submit', + 'review.php?attempt={quiz_attempt_id}', '{quiz}'); // old an new for quiz-review - $rules[] = new restore_log_rule('quiz', 'review', 'review.php?id={course_module}&attempt={quiz_attempt_id}', '{quiz}', - null, null, 'review.php?attempt={quiz_attempt_id}'); - $rules[] = new restore_log_rule('quiz', 'review', 'review.php?attempt={quiz_attempt_id}', '{quiz}'); + $rules[] = new restore_log_rule('quiz', 'review', + 'review.php?id={course_module}&attempt={quiz_attempt_id}', '{quiz}', + null, null, 'review.php?attempt={quiz_attempt_id}'); + $rules[] = new restore_log_rule('quiz', 'review', + 'review.php?attempt={quiz_attempt_id}', '{quiz}'); // old an new for quiz-start attemp - $rules[] = new restore_log_rule('quiz', 'start attempt', 'review.php?id={course_module}&attempt={quiz_attempt_id}', '{quiz}', - null, null, 'review.php?attempt={quiz_attempt_id}'); - $rules[] = new restore_log_rule('quiz', 'start attempt', 'review.php?attempt={quiz_attempt_id}', '{quiz}'); + $rules[] = new restore_log_rule('quiz', 'start attempt', + 'review.php?id={course_module}&attempt={quiz_attempt_id}', '{quiz}', + null, null, 'review.php?attempt={quiz_attempt_id}'); + $rules[] = new restore_log_rule('quiz', 'start attempt', + 'review.php?attempt={quiz_attempt_id}', '{quiz}'); // old an new for quiz-close attemp - $rules[] = new restore_log_rule('quiz', 'close attempt', 'review.php?id={course_module}&attempt={quiz_attempt_id}', '{quiz}', - null, null, 'review.php?attempt={quiz_attempt_id}'); - $rules[] = new restore_log_rule('quiz', 'close attempt', 'review.php?attempt={quiz_attempt_id}', '{quiz}'); + $rules[] = new restore_log_rule('quiz', 'close attempt', + 'review.php?id={course_module}&attempt={quiz_attempt_id}', '{quiz}', + null, null, 'review.php?attempt={quiz_attempt_id}'); + $rules[] = new restore_log_rule('quiz', 'close attempt', + 'review.php?attempt={quiz_attempt_id}', '{quiz}'); // old an new for quiz-continue attempt - $rules[] = new restore_log_rule('quiz', 'continue attempt', 'review.php?id={course_module}&attempt={quiz_attempt_id}', '{quiz}', - null, null, 'review.php?attempt={quiz_attempt_id}'); - $rules[] = new restore_log_rule('quiz', 'continue attempt', 'review.php?attempt={quiz_attempt_id}', '{quiz}'); + $rules[] = new restore_log_rule('quiz', 'continue attempt', + 'review.php?id={course_module}&attempt={quiz_attempt_id}', '{quiz}', + null, null, 'review.php?attempt={quiz_attempt_id}'); + $rules[] = new restore_log_rule('quiz', 'continue attempt', + 'review.php?attempt={quiz_attempt_id}', '{quiz}'); // old an new for quiz-continue attemp - $rules[] = new restore_log_rule('quiz', 'continue attemp', 'review.php?id={course_module}&attempt={quiz_attempt_id}', '{quiz}', - null, 'continue attempt', 'review.php?attempt={quiz_attempt_id}'); - $rules[] = new restore_log_rule('quiz', 'continue attemp', 'review.php?attempt={quiz_attempt_id}', '{quiz}', - null, 'continue attempt'); + $rules[] = new restore_log_rule('quiz', 'continue attemp', + 'review.php?id={course_module}&attempt={quiz_attempt_id}', '{quiz}', + null, 'continue attempt', 'review.php?attempt={quiz_attempt_id}'); + $rules[] = new restore_log_rule('quiz', 'continue attemp', + 'review.php?attempt={quiz_attempt_id}', '{quiz}', + null, 'continue attempt'); return $rules; } @@ -143,7 +178,7 @@ class restore_quiz_activity_task extends restore_activity_task { * by the restore final task, but are defined here at * activity level. All them are rules not linked to any module instance (cmid = 0) */ - static public function define_restore_log_rules_for_course() { + public static function define_restore_log_rules_for_course() { $rules = array(); $rules[] = new restore_log_rule('quiz', 'view all', 'index.php?id={course}', null); diff --git a/mod/quiz/backup/moodle2/restore_quiz_stepslib.php b/mod/quiz/backup/moodle2/restore_quiz_stepslib.php index 72222419322..68da356999f 100644 --- a/mod/quiz/backup/moodle2/restore_quiz_stepslib.php +++ b/mod/quiz/backup/moodle2/restore_quiz_stepslib.php @@ -1,5 +1,4 @@ . /** - * @package moodlecore + * @package moodlecore * @subpackage backup-moodle2 - * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -/** - * Define all the restore steps that will be used by the restore_quiz_activity_task - */ + +defined('MOODLE_INTERNAL') || die(); + /** * Structure step to restore one quiz activity + * + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_quiz_activity_structure_step extends restore_questions_activity_structure_step { @@ -37,16 +39,17 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st $userinfo = $this->get_setting_value('userinfo'); $paths[] = new restore_path_element('quiz', '/activity/quiz'); - $paths[] = new restore_path_element('quiz_question_instance', '/activity/quiz/question_instances/question_instance'); + $paths[] = new restore_path_element('quiz_question_instance', + '/activity/quiz/question_instances/question_instance'); $paths[] = new restore_path_element('quiz_feedback', '/activity/quiz/feedbacks/feedback'); $paths[] = new restore_path_element('quiz_override', '/activity/quiz/overrides/override'); if ($userinfo) { $paths[] = new restore_path_element('quiz_grade', '/activity/quiz/grades/grade'); - $quizattempt = new restore_path_element('quiz_attempt', '/activity/quiz/attempts/attempt'); + $quizattempt = new restore_path_element('quiz_attempt', + '/activity/quiz/attempts/attempt'); $paths[] = $quizattempt; // Add states and sessions - $this->add_question_attempts_states($quizattempt, $paths); - $this->add_question_attempts_sessions($quizattempt, $paths); + $this->add_question_usages($quizattempt, $paths); } // Return the paths wrapped into standard activity structure @@ -54,7 +57,7 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st } protected function process_quiz($data) { - global $DB; + global $CFG, $DB; $data = (object)$data; $oldid = $data->id; @@ -74,6 +77,108 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st unset($data->attempts_number); } + // The old optionflags and penaltyscheme from 2.0 need to be mapped to + // the new preferredbehaviour. MDL-20636 + if (!isset($data->preferredbehaviour)) { + if (empty($data->optionflags)) { + $data->preferredbehaviour = 'deferredfeedback'; + } else if (empty($data->penaltyscheme)) { + $data->preferredbehaviour = 'adaptivenopenalty'; + } else { + $data->preferredbehaviour = 'adaptive'; + } + unset($data->optionflags); + unset($data->penaltyscheme); + } + + // The old review column from 2.0 need to be split into the seven new + // review columns. MDL-20636 + if (isset($data->review)) { + require_once($CFG->dirroot . '/mod/quiz/locallib.php'); + + if (!defined('QUIZ_OLD_IMMEDIATELY')) { + define('QUIZ_OLD_IMMEDIATELY', 0x3c003f); + define('QUIZ_OLD_OPEN', 0x3c00fc0); + define('QUIZ_OLD_CLOSED', 0x3c03f000); + + define('QUIZ_OLD_RESPONSES', 1*0x1041); + define('QUIZ_OLD_SCORES', 2*0x1041); + define('QUIZ_OLD_FEEDBACK', 4*0x1041); + define('QUIZ_OLD_ANSWERS', 8*0x1041); + define('QUIZ_OLD_SOLUTIONS', 16*0x1041); + define('QUIZ_OLD_GENERALFEEDBACK', 32*0x1041); + define('QUIZ_OLD_OVERALLFEEDBACK', 1*0x4440000); + } + + $oldreview = $data->review; + + $data->reviewattempt = + mod_quiz_display_options::DURING | + ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_RESPONSES ? + mod_quiz_display_options::IMMEDIATELY_AFTER : 0) | + ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_RESPONSES ? + mod_quiz_display_options::LATER_WHILE_OPEN : 0) | + ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_RESPONSES ? + mod_quiz_display_options::AFTER_CLOSE : 0); + + $data->reviewcorrectness = + mod_quiz_display_options::DURING | + ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_SCORES ? + mod_quiz_display_options::IMMEDIATELY_AFTER : 0) | + ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_SCORES ? + mod_quiz_display_options::LATER_WHILE_OPEN : 0) | + ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_SCORES ? + mod_quiz_display_options::AFTER_CLOSE : 0); + + $data->reviewmarks = + mod_quiz_display_options::DURING | + ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_SCORES ? + mod_quiz_display_options::IMMEDIATELY_AFTER : 0) | + ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_SCORES ? + mod_quiz_display_options::LATER_WHILE_OPEN : 0) | + ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_SCORES ? + mod_quiz_display_options::AFTER_CLOSE : 0); + + $data->reviewspecificfeedback = + ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_FEEDBACK ? + mod_quiz_display_options::DURING : 0) | + ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_FEEDBACK ? + mod_quiz_display_options::IMMEDIATELY_AFTER : 0) | + ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_FEEDBACK ? + mod_quiz_display_options::LATER_WHILE_OPEN : 0) | + ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_FEEDBACK ? + mod_quiz_display_options::AFTER_CLOSE : 0); + + $data->reviewgeneralfeedback = + ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_GENERALFEEDBACK ? + mod_quiz_display_options::DURING : 0) | + ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_GENERALFEEDBACK ? + mod_quiz_display_options::IMMEDIATELY_AFTER : 0) | + ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_GENERALFEEDBACK ? + mod_quiz_display_options::LATER_WHILE_OPEN : 0) | + ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_GENERALFEEDBACK ? + mod_quiz_display_options::AFTER_CLOSE : 0); + + $data->reviewrightanswer = + ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_ANSWERS ? + mod_quiz_display_options::DURING : 0) | + ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_ANSWERS ? + mod_quiz_display_options::IMMEDIATELY_AFTER : 0) | + ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_ANSWERS ? + mod_quiz_display_options::LATER_WHILE_OPEN : 0) | + ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_ANSWERS ? + mod_quiz_display_options::AFTER_CLOSE : 0); + + $data->reviewoverallfeedback = + 0 | + ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_OVERALLFEEDBACK ? + mod_quiz_display_options::IMMEDIATELY_AFTER : 0) | + ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_OVERALLFEEDBACK ? + mod_quiz_display_options::LATER_WHILE_OPEN : 0) | + ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_OVERALLFEEDBACK ? + mod_quiz_display_options::AFTER_CLOSE : 0); + } + // insert the quiz record $newitemid = $DB->insert_record('quiz', $data); // immediately after inserting "activity" record, call this @@ -159,7 +264,7 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st $data->quiz = $this->get_new_parentid('quiz'); $data->attempt = $data->attemptnum; - $data->uniqueid = question_new_attempt_uniqueid('quiz'); + $data->uniqueid = 0; // filled in later by {@link inform_new_usage_id()} $data->userid = $this->get_mappingid('user', $data->userid); @@ -167,18 +272,20 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st $data->timefinish = $this->apply_date_offset($data->timefinish); $data->timemodified = $this->apply_date_offset($data->timemodified); - $data->layout = $this->questions_recode_layout($data->layout); - $newitemid = $DB->insert_record('quiz_attempts', $data); - // Save quiz_attempt->uniqueid as quiz_attempt mapping, both question_states and - // question_sessions have Fk to it and not to quiz_attempts->id at all. - $this->set_mapping('quiz_attempt', $olduniqueid, $data->uniqueid, false); - // Also save quiz_attempt->id mapping, because logs use it - $this->set_mapping('quiz_attempt_id', $oldid, $newitemid, false); + // Save quiz_attempt->id mapping, because logs use it + $this->set_mapping('quiz_attempt', $oldid, $newitemid, false); + } + + protected function inform_new_usage_id($newusageid) { + global $DB; + $DB->set_field('quiz_attempts', 'uniqueid', $newusageid, array('id' => + $this->get_new_parentid('quiz_attempt'))); } protected function after_execute() { + parent::after_execute(); // Add quiz related files, no need to match by itemname (just internally handled context) $this->add_related_files('mod_quiz', 'intro', null); // Add feedback related files, matching by itemname = 'quiz_feedback' diff --git a/mod/quiz/comment.php b/mod/quiz/comment.php index ea8da3a8454..515e08f709b 100644 --- a/mod/quiz/comment.php +++ b/mod/quiz/comment.php @@ -1,70 +1,79 @@ . + /** * This page allows the teacher to enter a manual grade for a particular question. * This page is expected to only be used in a popup window. * - * @package mod + * @package mod * @subpackage quiz - * @copyright gustav delius 2006 - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @copyright gustav delius 2006 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - require_once('../../config.php'); - require_once('locallib.php'); +require_once('../../config.php'); +require_once('locallib.php'); - $attemptid = required_param('attempt', PARAM_INT); // attempt id - $questionid = required_param('question', PARAM_INT); // question id +$attemptid = required_param('attempt', PARAM_INT); // attempt id +$slot = required_param('slot', PARAM_INT); // question number in attempt - $PAGE->set_url('/mod/quiz/comment.php', array('attempt'=>$attemptid, 'question'=>$questionid)); +$PAGE->set_url('/mod/quiz/comment.php', array('attempt' => $attemptid, 'slot' => $slot)); - $attemptobj = quiz_attempt::create($attemptid); +$attemptobj = quiz_attempt::create($attemptid); -/// Can only grade finished attempts. - if (!$attemptobj->is_finished()) { - print_error('attemptclosed', 'quiz'); +// Can only grade finished attempts. +if (!$attemptobj->is_finished()) { + print_error('attemptclosed', 'quiz'); +} + +// Check login and permissions. +require_login($attemptobj->get_courseid(), false, $attemptobj->get_cm()); +$attemptobj->require_capability('mod/quiz:grade'); + +// Log this action. +add_to_log($attemptobj->get_courseid(), 'quiz', 'manualgrade', 'comment.php?attempt=' . + $attemptobj->get_attemptid() . '&slot=' . $slot, + $attemptobj->get_quizid(), $attemptobj->get_cmid()); + +// Print the page header +$PAGE->set_pagelayout('popup'); +echo $OUTPUT->header(); +echo $OUTPUT->heading(format_string($attemptobj->get_question_name($slot))); + +// Process any data that was submitted. +if (data_submitted() && confirm_sesskey()) { + if (optional_param('submit', false, PARAM_BOOL)) { + $transaction = $DB->start_delegated_transaction(); + $attemptobj->process_all_actions(time()); + $transaction->allow_commit(); + echo $OUTPUT->notification(get_string('changessaved'), 'notifysuccess'); + close_window(2, true); + die; } +} -/// Check login and permissions. - require_login($attemptobj->get_courseid(), false, $attemptobj->get_cm()); - $attemptobj->require_capability('mod/quiz:grade'); - -/// Load the questions and states. - $questionids = array($questionid); - $attemptobj->load_questions($questionids); - $attemptobj->load_question_states($questionids); - -/// Log this action. - add_to_log($attemptobj->get_courseid(), 'quiz', 'manualgrade', 'comment.php?attempt=' . - $attemptobj->get_attemptid() . '&question=' . $questionid, - $attemptobj->get_quizid(), $attemptobj->get_cmid()); - -/// Print the page header - $PAGE->set_pagelayout('popup'); - echo $OUTPUT->header(); - echo $OUTPUT->heading(format_string($attemptobj->get_question($questionid)->name)); - -/// Process any data that was submitted. - if ($data = data_submitted() and confirm_sesskey()) { - $error = $attemptobj->process_comment($questionid, - $data->response['comment'], FORMAT_HTML, $data->response['grade']); - - /// If success, notify and print a close button. - if (!is_string($error)) { - echo $OUTPUT->notification(get_string('changessaved'), 'notifysuccess'); - close_window(2, true); - } - - /// Otherwise, display the error and fall throug to re-display the form. - echo $OUTPUT->notification($error); - } - -/// Print the comment form. - echo '
'; - $attemptobj->question_print_comment_fields($questionid, 'response'); +// Print the comment form. +echo ''; +echo $attemptobj->render_question_for_commenting($slot); ?>
- + +
'; +echo '
'; -/// End of the page. - echo $OUTPUT->footer(); -?> +// End of the page. +echo $OUTPUT->footer(); diff --git a/mod/quiz/db/access.php b/mod/quiz/db/access.php index d4dfdd6ffdc..a792b6ac0aa 100644 --- a/mod/quiz/db/access.php +++ b/mod/quiz/db/access.php @@ -1,9 +1,30 @@ . + /** * Capability definitions for the quiz module. * - * For naming conventions, see lib/db/access.php. + * @package mod + * @subpackage quiz + * @copyright 2006 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ + +defined('MOODLE_INTERNAL') || die(); + $capabilities = array( // Ability to see that the quiz exists, and the basic information diff --git a/mod/quiz/db/install.php b/mod/quiz/db/install.php index cb9036e8396..68be5cb4611 100644 --- a/mod/quiz/db/install.php +++ b/mod/quiz/db/install.php @@ -1,26 +1,50 @@ . -// This file replaces: -// * STATEMENTS section in db/install.xml -// * lib.php/modulename_install() post installation hook -// * partially defaults.php +/** + * Post-install code for the quiz module. + * + * @package mod + * @subpackage quiz + * @copyright 2009 Petr Skoda + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Code run after the quiz module database tables have been created. + */ function xmldb_quiz_install() { global $DB; $record = new stdClass(); $record->name = 'overview'; $record->displayorder = '10000'; - $DB->insert_record('quiz_report', $record); + $DB->insert_record('quiz_reports', $record); $record = new stdClass(); $record->name = 'responses'; $record->displayorder = '9000'; - $DB->insert_record('quiz_report', $record); + $DB->insert_record('quiz_reports', $record); $record = new stdClass(); $record->name = 'grading'; $record->displayorder = '6000'; - $DB->insert_record('quiz_report', $record); - + $DB->insert_record('quiz_reports', $record); } diff --git a/mod/quiz/db/install.xml b/mod/quiz/db/install.xml index a8b41442595..f9f0af99d16 100644 --- a/mod/quiz/db/install.xml +++ b/mod/quiz/db/install.xml @@ -1,5 +1,5 @@ - @@ -7,34 +7,39 @@ - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -48,16 +53,17 @@
- - - - - - - - - - + + + + + + + + + + + @@ -97,7 +103,7 @@
- +
@@ -111,20 +117,7 @@
- - - - - - - - - - - - -
- +
@@ -143,5 +136,21 @@
+ + + + + + + + + + + + + + + +
-
\ No newline at end of file + diff --git a/mod/quiz/db/log.php b/mod/quiz/db/log.php index 8db9d268753..c5a31e72529 100644 --- a/mod/quiz/db/log.php +++ b/mod/quiz/db/log.php @@ -1,5 +1,4 @@ . /** - * Definition of log events + * Definition of log events for the quiz module. * * @package mod * @subpackage quiz diff --git a/mod/quiz/db/messages.php b/mod/quiz/db/messages.php index 32a05faa014..e8094070f62 100644 --- a/mod/quiz/db/messages.php +++ b/mod/quiz/db/messages.php @@ -1,5 +1,4 @@ . /** - * Defines message providers (types of messages being sent) + * Defines message providers (types of message sent) for the quiz module. * - * @package mod-quiz - * @copyright 2010 onwards Andrew Davis http://moodle.com - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package mod + * @subpackage quiz + * @copyright 2010 Andrew Davis http://moodle.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +defined('MOODLE_INTERNAL') || die(); + $messageproviders = array ( - - // notify teacher that a student has submitted a quiz attempt + // Notify teacher that a student has submitted a quiz attempt 'submission' => array ( 'capability' => 'mod/quiz:emailnotifysubmission' ), - - // confirm a student's quiz attempt + + // Confirm a student's quiz attempt 'confirmation' => array ( 'capability' => 'mod/quiz:emailconfirmsubmission' ) - ); - - - diff --git a/mod/quiz/db/subplugins.php b/mod/quiz/db/subplugins.php index 06f8ac16bf4..9b63ff1896f 100644 --- a/mod/quiz/db/subplugins.php +++ b/mod/quiz/db/subplugins.php @@ -1,3 +1,28 @@ . -$subplugins = array('quiz'=>'mod/quiz/report'); +/** + * Sub-plugin definitions for the quiz module. + * + * @package mod + * @subpackage quiz + * @copyright 2010 Petr Skoda + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$subplugins = array('quiz' => 'mod/quiz/report'); diff --git a/mod/quiz/db/upgrade.php b/mod/quiz/db/upgrade.php index 28c59c654d4..fe68db01787 100644 --- a/mod/quiz/db/upgrade.php +++ b/mod/quiz/db/upgrade.php @@ -1,46 +1,60 @@ . -// This file keeps track of upgrades to -// the quiz module -// -// Sometimes, changes between versions involve -// alterations to database structures and other -// major things that may break installations. -// -// The upgrade function in this file will attempt -// to perform all the necessary actions to upgrade -// your older installation to the current version. -// -// If there's something it cannot do itself, it -// will tell you what you need to do. -// -// The commands in here will all be database-neutral, -// using the methods of database_manager class -// -// Please do not forget to use upgrade_set_timeout() -// before any action that may take longer time to finish. +/** + * Upgrade script for the quiz module. + * + * @package mod + * @subpackage quiz + * @copyright 2006 Eloy Lafuente (stronk7) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Quiz module upgrade function. + * @param string $oldversion the version we are upgrading from. + */ function xmldb_quiz_upgrade($oldversion) { global $CFG, $DB; $dbman = $DB->get_manager(); -//===== 1.9.0 upgrade line ======// + //===== 1.9.0 upgrade line ======// if ($oldversion < 2008062000) { - /// Define table quiz_report to be created + // Define table quiz_report to be created $table = new xmldb_table('quiz_report'); - /// Adding fields to table quiz_report - $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); - $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null); - $table->add_field('displayorder', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); + // Adding fields to table quiz_report + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, + null, null, null); + $table->add_field('displayorder', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, null); - /// Adding keys to table quiz_report + // Adding keys to table quiz_report $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); - /// Conditionally launch create table for quiz_report + // Conditionally launch create table for quiz_report if (!$dbman->table_exists($table)) { $dbman->create_table($table); } @@ -74,60 +88,64 @@ function xmldb_quiz_upgrade($oldversion) { if ($oldversion < 2008072402) { - /// Define field lastcron to be added to quiz_report + // Define field lastcron to be added to quiz_report $table = new xmldb_table('quiz_report'); - $field = new xmldb_field('lastcron', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', 'displayorder'); + $field = new xmldb_field('lastcron', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0', 'displayorder'); - /// Conditionally launch add field lastcron + // Conditionally launch add field lastcron if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } - /// Define field cron to be added to quiz_report - $field = new xmldb_field('cron', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', 'lastcron'); + // Define field cron to be added to quiz_report + $field = new xmldb_field('cron', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0', 'lastcron'); - /// Conditionally launch add field cron + // Conditionally launch add field cron if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } - /// quiz savepoint reached + // quiz savepoint reached upgrade_mod_savepoint(true, 2008072402, 'quiz'); } if ($oldversion < 2008072900) { - /// Delete the regrade report - it is now part of the overview report. + // Delete the regrade report - it is now part of the overview report. $DB->delete_records('quiz_report', array('name' => 'regrade')); - /// quiz savepoint reached + // quiz savepoint reached upgrade_mod_savepoint(true, 2008072900, 'quiz'); } if ($oldversion < 2008081500) { - /// Define table quiz_question_versions to be dropped + // Define table quiz_question_versions to be dropped $table = new xmldb_table('quiz_question_versions'); - /// Launch drop table for quiz_question_versions + // Launch drop table for quiz_question_versions $dbman->drop_table($table); - /// quiz savepoint reached + // quiz savepoint reached upgrade_mod_savepoint(true, 2008081500, 'quiz'); } - /// Changing the type of all the columns that store grades to be NUMBER(10, 5) or similar. + // Changing the type of all the columns that store grades to be NUMBER(10, 5) or similar. if ($oldversion < 2008081501) { // First set all quiz.sumgrades to 0 if they are null. This should never // happen however some users have encountered a null value there. $DB->execute('UPDATE {quiz} SET sumgrades=0 WHERE sumgrades IS NULL'); $table = new xmldb_table('quiz'); - $field = new xmldb_field('sumgrades', XMLDB_TYPE_NUMBER, '10, 5', null, XMLDB_NOTNULL, null, '0', 'questions'); + $field = new xmldb_field('sumgrades', XMLDB_TYPE_NUMBER, '10, 5', null, + XMLDB_NOTNULL, null, '0', 'questions'); $dbman->change_field_type($table, $field); upgrade_mod_savepoint(true, 2008081501, 'quiz'); } if ($oldversion < 2008081502) { $table = new xmldb_table('quiz'); - $field = new xmldb_field('grade', XMLDB_TYPE_NUMBER, '10, 5', null, XMLDB_NOTNULL, null, '0', 'sumgrades'); + $field = new xmldb_field('grade', XMLDB_TYPE_NUMBER, '10, 5', null, + XMLDB_NOTNULL, null, '0', 'sumgrades'); $dbman->change_field_type($table, $field); upgrade_mod_savepoint(true, 2008081502, 'quiz'); } @@ -137,40 +155,45 @@ function xmldb_quiz_upgrade($oldversion) { // happen however some users have encountered a null value there. $DB->execute('UPDATE {quiz_attempts} SET sumgrades=0 WHERE sumgrades IS NULL'); $table = new xmldb_table('quiz_attempts'); - $field = new xmldb_field('sumgrades', XMLDB_TYPE_NUMBER, '10, 5', null, XMLDB_NOTNULL, null, '0', 'attempt'); + $field = new xmldb_field('sumgrades', XMLDB_TYPE_NUMBER, '10, 5', null, + XMLDB_NOTNULL, null, '0', 'attempt'); $dbman->change_field_type($table, $field); upgrade_mod_savepoint(true, 2008081503, 'quiz'); } if ($oldversion < 2008081504) { $table = new xmldb_table('quiz_feedback'); - $field = new xmldb_field('mingrade', XMLDB_TYPE_NUMBER, '10, 5', null, XMLDB_NOTNULL, null, '0', 'feedbacktext'); + $field = new xmldb_field('mingrade', XMLDB_TYPE_NUMBER, '10, 5', null, + XMLDB_NOTNULL, null, '0', 'feedbacktext'); $dbman->change_field_type($table, $field); upgrade_mod_savepoint(true, 2008081504, 'quiz'); } if ($oldversion < 2008081505) { $table = new xmldb_table('quiz_feedback'); - $field = new xmldb_field('maxgrade', XMLDB_TYPE_NUMBER, '10, 5', null, XMLDB_NOTNULL, null, '0', 'mingrade'); + $field = new xmldb_field('maxgrade', XMLDB_TYPE_NUMBER, '10, 5', null, + XMLDB_NOTNULL, null, '0', 'mingrade'); $dbman->change_field_type($table, $field); upgrade_mod_savepoint(true, 2008081505, 'quiz'); } if ($oldversion < 2008081506) { $table = new xmldb_table('quiz_grades'); - $field = new xmldb_field('grade', XMLDB_TYPE_NUMBER, '10, 5', null, XMLDB_NOTNULL, null, '0', 'userid'); + $field = new xmldb_field('grade', XMLDB_TYPE_NUMBER, '10, 5', null, + XMLDB_NOTNULL, null, '0', 'userid'); $dbman->change_field_type($table, $field); upgrade_mod_savepoint(true, 2008081506, 'quiz'); } if ($oldversion < 2008081507) { $table = new xmldb_table('quiz_question_instances'); - $field = new xmldb_field('grade', XMLDB_TYPE_NUMBER, '12, 7', null, XMLDB_NOTNULL, null, '0', 'question'); + $field = new xmldb_field('grade', XMLDB_TYPE_NUMBER, '12, 7', null, + XMLDB_NOTNULL, null, '0', 'question'); $dbman->change_field_type($table, $field); upgrade_mod_savepoint(true, 2008081507, 'quiz'); } - /// Move all of the quiz config settings from $CFG to the config_plugins table. + // Move all of the quiz config settings from $CFG to the config_plugins table. if ($oldversion < 2008082200) { foreach (get_object_vars($CFG) as $name => $value) { if (strpos($name, 'quiz_') === 0) { @@ -186,11 +209,11 @@ function xmldb_quiz_upgrade($oldversion) { upgrade_mod_savepoint(true, 2008082200, 'quiz'); } - /// Now that the quiz is no longer responsible for creating all the question - /// bank tables, and some of the tables are now the responsibility of the - /// datasetdependent question type, which did not have a version.php file before, - /// we need to say that these tables are already installed, otherwise XMLDB - /// will try to create them again and give an error. + // Now that the quiz is no longer responsible for creating all the question + // bank tables, and some of the tables are now the responsibility of the + // datasetdependent question type, which did not have a version.php file before, + // we need to say that these tables are already installed, otherwise XMLDB + // will try to create them again and give an error. if ($oldversion < 2008082600) { // Since MDL-16505 was fixed, and we eliminated the datasetdependent // question type, this is now a no-op. @@ -199,37 +222,39 @@ function xmldb_quiz_upgrade($oldversion) { if ($oldversion < 2008112101) { - /// Define field lastcron to be added to quiz_report + // Define field lastcron to be added to quiz_report $table = new xmldb_table('quiz_report'); - $field = new xmldb_field('capability', XMLDB_TYPE_CHAR, '255', null, null, null, null, 'cron'); + $field = new xmldb_field('capability', XMLDB_TYPE_CHAR, '255', null, + null, null, null, 'cron'); - /// Conditionally launch add field lastcron + // Conditionally launch add field lastcron if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } - /// quiz savepoint reached + // quiz savepoint reached upgrade_mod_savepoint(true, 2008112101, 'quiz'); } if ($oldversion < 2009010700) { - /// Define field showuserpicture to be added to quiz + // Define field showuserpicture to be added to quiz $table = new xmldb_table('quiz'); - $field = new xmldb_field('showuserpicture', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0', 'delay2'); + $field = new xmldb_field('showuserpicture', XMLDB_TYPE_INTEGER, '4', null, + XMLDB_NOTNULL, null, '0', 'delay2'); - /// Conditionally launch add field showuserpicture + // Conditionally launch add field showuserpicture if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } - /// quiz savepoint reached + // quiz savepoint reached upgrade_mod_savepoint(true, 2009010700, 'quiz'); } if ($oldversion < 2009030900) { - /// If there are no quiz settings set to advanced yet, the set up the default - /// advanced fields from Moodle 2.0. + // If there are no quiz settings set to advanced yet, the set up the default + // advanced fields from Moodle 2.0. $quizconfig = get_config('quiz'); $arealreadyadvanced = false; foreach (array($quizconfig) as $name => $value) { @@ -250,37 +275,39 @@ function xmldb_quiz_upgrade($oldversion) { set_config('fix_popup', 1, 'quiz'); } - /// quiz savepoint reached + // quiz savepoint reached upgrade_mod_savepoint(true, 2009030900, 'quiz'); } if ($oldversion < 2009031000) { - /// Add new questiondecimaldigits setting, separate form the overall decimaldigits one. + // Add new questiondecimaldigits setting, separate form the overall decimaldigits one. $table = new xmldb_table('quiz'); - $field = new xmldb_field('questiondecimalpoints', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '-2', 'decimalpoints'); + $field = new xmldb_field('questiondecimalpoints', XMLDB_TYPE_INTEGER, '4', null, + XMLDB_NOTNULL, null, '-2', 'decimalpoints'); if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } - /// quiz savepoint reached + // quiz savepoint reached upgrade_mod_savepoint(true, 2009031000, 'quiz'); } if ($oldversion < 2009031001) { - /// Convert quiz.timelimit from minutes to seconds. + // Convert quiz.timelimit from minutes to seconds. $DB->execute('UPDATE {quiz} SET timelimit = timelimit * 60'); $default = get_config('quiz', 'timelimit'); set_config('timelimit', 60 * $default, 'quiz'); - /// quiz savepoint reached + // quiz savepoint reached upgrade_mod_savepoint(true, 2009031001, 'quiz'); } if ($oldversion < 2009042000) { - /// Define field introformat to be added to quiz + // Define field introformat to be added to quiz $table = new xmldb_table('quiz'); - $field = new xmldb_field('introformat', XMLDB_TYPE_INTEGER, '4', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', 'intro'); + $field = new xmldb_field('introformat', XMLDB_TYPE_INTEGER, '4', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0', 'intro'); if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); @@ -288,7 +315,8 @@ function xmldb_quiz_upgrade($oldversion) { // conditionally migrate to html format in intro if ($CFG->texteditors !== 'textarea') { - $rs = $DB->get_recordset('quiz', array('introformat' => FORMAT_MOODLE), '', 'id,intro,introformat'); + $rs = $DB->get_recordset('quiz', array('introformat' => FORMAT_MOODLE), + '', 'id, intro, introformat'); foreach ($rs as $q) { $q->intro = text_to_html($q->intro, false, false, true); $q->introformat = FORMAT_HTML; @@ -298,37 +326,45 @@ function xmldb_quiz_upgrade($oldversion) { $rs->close(); } - /// quiz savepoint reached + // quiz savepoint reached upgrade_mod_savepoint(true, 2009042000, 'quiz'); } if ($oldversion < 2010030501) { - /// Define table quiz_overrides to be created + // Define table quiz_overrides to be created $table = new xmldb_table('quiz_overrides'); - /// Adding fields to table quiz_overrides - $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); - $table->add_field('quiz', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0'); - $table->add_field('groupid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null); - $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null); - $table->add_field('timeopen', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null); - $table->add_field('timeclose', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null); - $table->add_field('timelimit', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null); - $table->add_field('attempts', XMLDB_TYPE_INTEGER, '6', XMLDB_UNSIGNED, null, null, null); + // Adding fields to table quiz_overrides + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('quiz', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0'); + $table->add_field('groupid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + null, null, null); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + null, null, null); + $table->add_field('timeopen', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + null, null, null); + $table->add_field('timeclose', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + null, null, null); + $table->add_field('timelimit', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + null, null, null); + $table->add_field('attempts', XMLDB_TYPE_INTEGER, '6', XMLDB_UNSIGNED, + null, null, null); $table->add_field('password', XMLDB_TYPE_CHAR, '255', null, null, null, null); - /// Adding keys to table quiz_overrides + // Adding keys to table quiz_overrides $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); $table->add_key('quiz', XMLDB_KEY_FOREIGN, array('quiz'), 'quiz', array('id')); $table->add_key('groupid', XMLDB_KEY_FOREIGN, array('groupid'), 'groups', array('id')); $table->add_key('userid', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id')); - /// Conditionally launch create table for quiz_overrides + // Conditionally launch create table for quiz_overrides if (!$dbman->table_exists($table)) { $dbman->create_table($table); } - /// quiz savepoint reached + // quiz savepoint reached upgrade_mod_savepoint(true, 2010030501, 'quiz'); } @@ -336,7 +372,8 @@ function xmldb_quiz_upgrade($oldversion) { // Define field showblocks to be added to quiz $table = new xmldb_table('quiz'); - $field = new xmldb_field('showblocks', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0', 'showuserpicture'); + $field = new xmldb_field('showblocks', XMLDB_TYPE_INTEGER, '4', null, + XMLDB_NOTNULL, null, '0', 'showuserpicture'); // Conditionally launch add field showblocks if (!$dbman->field_exists($table, $field)) { @@ -351,7 +388,8 @@ function xmldb_quiz_upgrade($oldversion) { // Define field feedbacktextformat to be added to quiz_feedback $table = new xmldb_table('quiz_feedback'); - $field = new xmldb_field('feedbacktextformat', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'feedbacktext'); + $field = new xmldb_field('feedbacktextformat', XMLDB_TYPE_INTEGER, '2', null, + XMLDB_NOTNULL, null, '0', 'feedbacktext'); // Conditionally launch add field feedbacktextformat if (!$dbman->field_exists($table, $field)) { @@ -369,7 +407,8 @@ function xmldb_quiz_upgrade($oldversion) { // Define field showblocks to be added to quiz // Repeat this step, because the column was missing from install.xml for a time. $table = new xmldb_table('quiz'); - $field = new xmldb_field('showblocks', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0', 'showuserpicture'); + $field = new xmldb_field('showblocks', XMLDB_TYPE_INTEGER, '4', null, + XMLDB_NOTNULL, null, '0', 'showuserpicture'); // Conditionally launch add field showblocks if (!$dbman->field_exists($table, $field)) { @@ -386,28 +425,32 @@ function xmldb_quiz_upgrade($oldversion) { $columns = $DB->get_columns('quiz'); // quiz.questiondecimalpoints should be int (4) not null default -2 - if (array_key_exists('questiondecimalpoints', $columns) && $columns['questiondecimalpoints']->default_value != '-2') { - $field = new xmldb_field('questiondecimalpoints', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, -2, 'decimalpoints'); + if (array_key_exists('questiondecimalpoints', $columns) && + $columns['questiondecimalpoints']->default_value != '-2') { + $field = new xmldb_field('questiondecimalpoints', XMLDB_TYPE_INTEGER, '4', null, + XMLDB_NOTNULL, null, -2, 'decimalpoints'); if ($dbman->field_exists($table, $field)) { $dbman->change_field_default($table, $field); } } - // quiz.sumgrades should be decimal(10,5) not null default 0 + // quiz.sumgrades should be decimal(10, 5) not null default 0 if (array_key_exists('sumgrades', $columns) && empty($columns['sumgrades']->not_null)) { // First set all quiz.sumgrades to 0 if they are null. This should never // happen however some users have encountered a null value there. $DB->execute('UPDATE {quiz} SET sumgrades=0 WHERE sumgrades IS NULL'); - $field = new xmldb_field('sumgrades', XMLDB_TYPE_NUMBER, '10, 5', null, XMLDB_NOTNULL, null, '0', 'questions'); + $field = new xmldb_field('sumgrades', XMLDB_TYPE_NUMBER, '10, 5', null, + XMLDB_NOTNULL, null, '0', 'questions'); if ($dbman->field_exists($table, $field)) { $dbman->change_field_default($table, $field); } } - // quiz.grade should be decimal(10,5) not null default 0 + // quiz.grade should be decimal(10, 5) not null default 0 if (array_key_exists('grade', $columns) && empty($columns['grade']->not_null)) { - $field = new xmldb_field('grade', XMLDB_TYPE_NUMBER, '10, 5', null, XMLDB_NOTNULL, null, '0', 'sumgrades'); + $field = new xmldb_field('grade', XMLDB_TYPE_NUMBER, '10, 5', null, + XMLDB_NOTNULL, null, '0', 'sumgrades'); if ($dbman->field_exists($table, $field)) { $dbman->change_field_default($table, $field); } @@ -421,13 +464,14 @@ function xmldb_quiz_upgrade($oldversion) { $table = new xmldb_table('quiz_attempts'); $columns = $DB->get_columns('quiz_attempts'); - // quiz_attempts.sumgrades should be decimal(10,5) not null default 0 + // quiz_attempts.sumgrades should be decimal(10, 5) not null default 0 if (array_key_exists('sumgrades', $columns) && empty($columns['sumgrades']->not_null)) { // First set all quiz.sumgrades to 0 if they are null. This should never // happen however some users have encountered a null value there. $DB->execute('UPDATE {quiz_attempts} SET sumgrades=0 WHERE sumgrades IS NULL'); - $field = new xmldb_field('sumgrades', XMLDB_TYPE_NUMBER, '10, 5', null, XMLDB_NOTNULL, null, '0', 'attempt'); + $field = new xmldb_field('sumgrades', XMLDB_TYPE_NUMBER, '10, 5', null, + XMLDB_NOTNULL, null, '0', 'attempt'); if ($dbman->field_exists($table, $field)) { $dbman->change_field_default($table, $field); } @@ -441,18 +485,20 @@ function xmldb_quiz_upgrade($oldversion) { $table = new xmldb_table('quiz_feedback'); $columns = $DB->get_columns('quiz_feedback'); - // quiz_feedback.mingrade should be decimal(10,5) not null default 0 + // quiz_feedback.mingrade should be decimal(10, 5) not null default 0 if (array_key_exists('mingrade', $columns) && empty($columns['mingrade']->not_null)) { - $field = new xmldb_field('mingrade', XMLDB_TYPE_NUMBER, '10, 5', null, XMLDB_NOTNULL, null, '0', 'feedbacktextformat'); + $field = new xmldb_field('mingrade', XMLDB_TYPE_NUMBER, '10, 5', null, + XMLDB_NOTNULL, null, '0', 'feedbacktextformat'); if ($dbman->field_exists($table, $field)) { $dbman->change_field_default($table, $field); } } - // quiz_feedback.maxgrade should be decimal(10,5) not null default 0 + // quiz_feedback.maxgrade should be decimal(10, 5) not null default 0 if (array_key_exists('maxgrade', $columns) && empty($columns['maxgrade']->not_null)) { // Fixed in earlier upgrade code - $field = new xmldb_field('maxgrade', XMLDB_TYPE_NUMBER, '10, 5', null, XMLDB_NOTNULL, null, '0', 'mingrade'); + $field = new xmldb_field('maxgrade', XMLDB_TYPE_NUMBER, '10, 5', null, + XMLDB_NOTNULL, null, '0', 'mingrade'); if ($dbman->field_exists($table, $field)) { $dbman->change_field_default($table, $field); } @@ -466,9 +512,10 @@ function xmldb_quiz_upgrade($oldversion) { $table = new xmldb_table('quiz_grades'); $columns = $DB->get_columns('quiz_grades'); - // quiz_grades.grade should be decimal(10,5) not null default 0 + // quiz_grades.grade should be decimal(10, 5) not null default 0 if (array_key_exists('grade', $columns) && empty($columns['grade']->not_null)) { - $field = new xmldb_field('grade', XMLDB_TYPE_NUMBER, '10, 5', null, XMLDB_NOTNULL, null, '0', 'userid'); + $field = new xmldb_field('grade', XMLDB_TYPE_NUMBER, '10, 5', null, + XMLDB_NOTNULL, null, '0', 'userid'); if ($dbman->field_exists($table, $field)) { $dbman->change_field_default($table, $field); } @@ -482,9 +529,10 @@ function xmldb_quiz_upgrade($oldversion) { $table = new xmldb_table('quiz_question_instances'); $columns = $DB->get_columns('quiz_question_instances'); - // quiz_question_instances.grade should be decimal(12,7) not null default 0 + // quiz_question_instances.grade should be decimal(12, 7) not null default 0 if (array_key_exists('grade', $columns) && empty($columns['grade']->not_null)) { - $field = new xmldb_field('grade', XMLDB_TYPE_NUMBER, '12, 7', null, XMLDB_NOTNULL, null, '0', 'question'); + $field = new xmldb_field('grade', XMLDB_TYPE_NUMBER, '12, 7', null, + XMLDB_NOTNULL, null, '0', 'question'); if ($dbman->field_exists($table, $field)) { $dbman->change_field_default($table, $field); } @@ -493,6 +541,580 @@ function xmldb_quiz_upgrade($oldversion) { upgrade_mod_savepoint(true, 2010122304, 'quiz'); } + //===== 2.1.0 upgrade line ======// + + // Complete any old upgrade from 1.5 that was never finished. + if ($oldversion < 2011051199) { + $table = new xmldb_table('question_states'); + if ($dbman->table_exists($table)) { + $transaction = $DB->start_delegated_transaction(); + + $oldattempts = $DB->get_records_sql(' + SELECT * + FROM {quiz_attempts} quiza + WHERE uniqueid IN ( + SELECT DISTINCT qst.attempt + FROM {question_states} qst + LEFT JOIN {question_sessions} qsess ON + qst.question = qsess.questionid AND qst.attempt = qsess.attemptid + WHERE qsess.id IS NULL + ) + '); + + if ($oldattempts) { + $pbar = new progress_bar('q15upgrade'); + $a = new stdClass(); + $a->todo = count($oldattempts); + $a->done = 0; + $pbar->update($a->done, $a->todo, + get_string('upgradingveryoldquizattempts', 'quiz', $a)); + + foreach ($oldattempts as $oldattempt) { + quiz_upgrade_very_old_question_sessions($oldattempt); + + $a->done += 1; + $pbar->update($a->done, $a->todo, + get_string('upgradingveryoldquizattempts', 'quiz', $a)); + } + } + + $transaction->allow_commit(); + } + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051199, 'quiz'); + } + + // Add new preferredbehaviour column to the quiz table. + if ($oldversion < 2011051200) { + $table = new xmldb_table('quiz'); + $field = new xmldb_field('preferredbehaviour'); + $field->set_attributes(XMLDB_TYPE_CHAR, '32', null, + null, null, null, 'timeclose'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051200, 'quiz'); + } + + // Populate preferredbehaviour column based on old optionflags column. + if ($oldversion < 2011051201) { + if ($dbman->field_exists('quiz', 'optionflags')) { + $DB->set_field_select('quiz', 'preferredbehaviour', 'deferredfeedback', + 'optionflags = 0'); + $DB->set_field_select('quiz', 'preferredbehaviour', 'adaptive', + 'optionflags <> 0 AND penaltyscheme <> 0'); + $DB->set_field_select('quiz', 'preferredbehaviour', 'adaptivenopenalty', + 'optionflags <> 0 AND penaltyscheme = 0'); + + set_config('preferredbehaviour', 'deferredfeedback', 'quiz'); + set_config('fix_preferredbehaviour', 0, 'quiz'); + } + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051201, 'quiz'); + } + + // Add a not-NULL constraint to the preferredmodel field now that it is populated. + if ($oldversion < 2011051202) { + $table = new xmldb_table('quiz'); + $field = new xmldb_field('preferredbehaviour'); + $field->set_attributes(XMLDB_TYPE_CHAR, '32', null, + XMLDB_NOTNULL, null, null, 'timeclose'); + + $dbman->change_field_notnull($table, $field); + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051202, 'quiz'); + } + + // Drop the old optionflags field. + if ($oldversion < 2011051203) { + $table = new xmldb_table('quiz'); + $field = new xmldb_field('optionflags'); + + if ($dbman->field_exists($table, $field)) { + $dbman->drop_field($table, $field); + } + + unset_config('optionflags', 'quiz'); + unset_config('fix_optionflags', 'quiz'); + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051203, 'quiz'); + } + + // Drop the old penaltyscheme field. + if ($oldversion < 2011051204) { + $table = new xmldb_table('quiz'); + $field = new xmldb_field('penaltyscheme'); + + if ($dbman->field_exists($table, $field)) { + $dbman->drop_field($table, $field); + } + + unset_config('penaltyscheme', 'quiz'); + unset_config('fix_penaltyscheme', 'quiz'); + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051204, 'quiz'); + } + + if ($oldversion < 2011051205) { + + // Changing nullability of field sumgrades on table quiz_attempts to null + $table = new xmldb_table('quiz_attempts'); + $field = new xmldb_field('sumgrades'); + $field->set_attributes(XMLDB_TYPE_NUMBER, '10, 5', null, + null, null, null, 'attempt'); + + // Launch change of nullability for field sumgrades + $dbman->change_field_notnull($table, $field); + + // Launch change of default for field sumgrades + $dbman->change_field_default($table, $field); + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051205, 'quiz'); + } + + if ($oldversion < 2011051207) { + + // Define field reviewattempt to be added to quiz + $table = new xmldb_table('quiz'); + $field = new xmldb_field('reviewattempt'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '6', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0', 'review'); + + // Launch add field reviewattempt + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051207, 'quiz'); + } + + if ($oldversion < 2011051208) { + + // Define field reviewattempt to be added to quiz + $table = new xmldb_table('quiz'); + $field = new xmldb_field('reviewcorrectness'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '6', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0', 'reviewattempt'); + + // Launch add field reviewattempt + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051208, 'quiz'); + } + + if ($oldversion < 2011051209) { + + // Define field reviewattempt to be added to quiz + $table = new xmldb_table('quiz'); + $field = new xmldb_field('reviewmarks'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '6', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0', 'reviewcorrectness'); + + // Launch add field reviewattempt + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051209, 'quiz'); + } + + if ($oldversion < 2011051210) { + + // Define field reviewattempt to be added to quiz + $table = new xmldb_table('quiz'); + $field = new xmldb_field('reviewspecificfeedback'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '6', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0', 'reviewmarks'); + + // Launch add field reviewattempt + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051210, 'quiz'); + } + + if ($oldversion < 2011051211) { + + // Define field reviewattempt to be added to quiz + $table = new xmldb_table('quiz'); + $field = new xmldb_field('reviewgeneralfeedback'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '6', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0', 'reviewspecificfeedback'); + + // Launch add field reviewattempt + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051211, 'quiz'); + } + + if ($oldversion < 2011051212) { + + // Define field reviewattempt to be added to quiz + $table = new xmldb_table('quiz'); + $field = new xmldb_field('reviewrightanswer'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '6', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0', 'reviewgeneralfeedback'); + + // Launch add field reviewattempt + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051212, 'quiz'); + } + + if ($oldversion < 2011051213) { + + // Define field reviewattempt to be added to quiz + $table = new xmldb_table('quiz'); + $field = new xmldb_field('reviewoverallfeedback'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '6', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0', 'reviewrightanswer'); + + // Launch add field reviewattempt + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051213, 'quiz'); + } + + define('QUIZ_NEW_DURING', 0x10000); + define('QUIZ_NEW_IMMEDIATELY_AFTER', 0x01000); + define('QUIZ_NEW_LATER_WHILE_OPEN', 0x00100); + define('QUIZ_NEW_AFTER_CLOSE', 0x00010); + + define('QUIZ_OLD_IMMEDIATELY', 0x3c003f); + define('QUIZ_OLD_OPEN', 0x3c00fc0); + define('QUIZ_OLD_CLOSED', 0x3c03f000); + + define('QUIZ_OLD_RESPONSES', 1*0x1041); // Show responses + define('QUIZ_OLD_SCORES', 2*0x1041); // Show scores + define('QUIZ_OLD_FEEDBACK', 4*0x1041); // Show question feedback + define('QUIZ_OLD_ANSWERS', 8*0x1041); // Show correct answers + define('QUIZ_OLD_SOLUTIONS', 16*0x1041); // Show solutions + define('QUIZ_OLD_GENERALFEEDBACK', 32*0x1041); // Show question general feedback + define('QUIZ_OLD_OVERALLFEEDBACK', 1*0x4440000); // Show quiz overall feedback + + // Copy the old review settings + if ($oldversion < 2011051214) { + if ($dbman->field_exists('quiz', 'review')) { + $DB->execute(" + UPDATE {quiz} + SET reviewattempt = " . $DB->sql_bitor($DB->sql_bitor( + QUIZ_NEW_DURING, + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_RESPONSES) . + ' <> 0 THEN ' . QUIZ_NEW_IMMEDIATELY_AFTER . ' ELSE 0 END'), $DB->sql_bitor( + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_OPEN & QUIZ_OLD_RESPONSES) . + ' <> 0 THEN ' . QUIZ_NEW_LATER_WHILE_OPEN . ' ELSE 0 END', + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_CLOSED & QUIZ_OLD_RESPONSES) . + ' <> 0 THEN ' . QUIZ_NEW_AFTER_CLOSE . ' ELSE 0 END')) . " + "); + } + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051214, 'quiz'); + } + + if ($oldversion < 2011051215) { + if ($dbman->field_exists('quiz', 'review')) { + $DB->execute(" + UPDATE {quiz} + SET reviewcorrectness = " . $DB->sql_bitor($DB->sql_bitor( + QUIZ_NEW_DURING, + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_SCORES) . + ' <> 0 THEN ' . QUIZ_NEW_IMMEDIATELY_AFTER . ' ELSE 0 END'), $DB->sql_bitor( + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_OPEN & QUIZ_OLD_SCORES) . + ' <> 0 THEN ' . QUIZ_NEW_LATER_WHILE_OPEN . ' ELSE 0 END', + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_CLOSED & QUIZ_OLD_SCORES) . + ' <> 0 THEN ' . QUIZ_NEW_AFTER_CLOSE . ' ELSE 0 END')) . " + "); + } + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051215, 'quiz'); + } + + if ($oldversion < 2011051216) { + if ($dbman->field_exists('quiz', 'review')) { + $DB->execute(" + UPDATE {quiz} + SET reviewmarks = " . $DB->sql_bitor($DB->sql_bitor( + QUIZ_NEW_DURING, + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_SCORES) . + ' <> 0 THEN ' . QUIZ_NEW_IMMEDIATELY_AFTER . ' ELSE 0 END'), $DB->sql_bitor( + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_OPEN & QUIZ_OLD_SCORES) . + ' <> 0 THEN ' . QUIZ_NEW_LATER_WHILE_OPEN . ' ELSE 0 END', + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_CLOSED & QUIZ_OLD_SCORES) . + ' <> 0 THEN ' . QUIZ_NEW_AFTER_CLOSE . ' ELSE 0 END')) . " + "); + } + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051216, 'quiz'); + } + + if ($oldversion < 2011051217) { + if ($dbman->field_exists('quiz', 'review')) { + $DB->execute(" + UPDATE {quiz} + SET reviewspecificfeedback = " . $DB->sql_bitor($DB->sql_bitor( + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_FEEDBACK) . + ' <> 0 THEN ' . QUIZ_NEW_DURING . ' ELSE 0 END', + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_FEEDBACK) . + ' <> 0 THEN ' . QUIZ_NEW_IMMEDIATELY_AFTER . ' ELSE 0 END'), $DB->sql_bitor( + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_OPEN & QUIZ_OLD_FEEDBACK) . + ' <> 0 THEN ' . QUIZ_NEW_LATER_WHILE_OPEN . ' ELSE 0 END', + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_CLOSED & QUIZ_OLD_FEEDBACK) . + ' <> 0 THEN ' . QUIZ_NEW_AFTER_CLOSE . ' ELSE 0 END')) . " + "); + } + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051217, 'quiz'); + } + + if ($oldversion < 2011051218) { + if ($dbman->field_exists('quiz', 'review')) { + $DB->execute(" + UPDATE {quiz} + SET reviewgeneralfeedback = " . $DB->sql_bitor($DB->sql_bitor( + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_GENERALFEEDBACK) . + ' <> 0 THEN ' . QUIZ_NEW_DURING . ' ELSE 0 END', + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_GENERALFEEDBACK) . + ' <> 0 THEN ' . QUIZ_NEW_IMMEDIATELY_AFTER . ' ELSE 0 END'), $DB->sql_bitor( + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_OPEN & QUIZ_OLD_GENERALFEEDBACK) . + ' <> 0 THEN ' . QUIZ_NEW_LATER_WHILE_OPEN . ' ELSE 0 END', + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_CLOSED & QUIZ_OLD_GENERALFEEDBACK) . + ' <> 0 THEN ' . QUIZ_NEW_AFTER_CLOSE . ' ELSE 0 END')) . " + "); + } + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051218, 'quiz'); + } + + if ($oldversion < 2011051219) { + if ($dbman->field_exists('quiz', 'review')) { + $DB->execute(" + UPDATE {quiz} + SET reviewrightanswer = " . $DB->sql_bitor($DB->sql_bitor( + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_ANSWERS) . + ' <> 0 THEN ' . QUIZ_NEW_DURING . ' ELSE 0 END', + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_ANSWERS) . + ' <> 0 THEN ' . QUIZ_NEW_IMMEDIATELY_AFTER . ' ELSE 0 END'), $DB->sql_bitor( + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_OPEN & QUIZ_OLD_ANSWERS) . + ' <> 0 THEN ' . QUIZ_NEW_LATER_WHILE_OPEN . ' ELSE 0 END', + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_CLOSED & QUIZ_OLD_ANSWERS) . + ' <> 0 THEN ' . QUIZ_NEW_AFTER_CLOSE . ' ELSE 0 END')) . " + "); + } + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051219, 'quiz'); + } + + if ($oldversion < 2011051220) { + if ($dbman->field_exists('quiz', 'review')) { + $DB->execute(" + UPDATE {quiz} + SET reviewoverallfeedback = " . $DB->sql_bitor($DB->sql_bitor( + 0, + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_OVERALLFEEDBACK) . + ' <> 0 THEN ' . QUIZ_NEW_IMMEDIATELY_AFTER . ' ELSE 0 END'), $DB->sql_bitor( + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_OPEN & QUIZ_OLD_OVERALLFEEDBACK) . + ' <> 0 THEN ' . QUIZ_NEW_LATER_WHILE_OPEN . ' ELSE 0 END', + 'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_CLOSED & QUIZ_OLD_OVERALLFEEDBACK) . + ' <> 0 THEN ' . QUIZ_NEW_AFTER_CLOSE . ' ELSE 0 END')) . " + "); + } + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051220, 'quiz'); + } + + // And, do the same for the defaults + if ($oldversion < 2011051221) { + $quizrevew = get_config('quiz', 'review'); + if (!empty($quizrevew)) { + + set_config('reviewattempt', + QUIZ_NEW_DURING | + ($quizrevew & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_RESPONSES ? QUIZ_NEW_IMMEDIATELY_AFTER : 0) | + ($quizrevew & QUIZ_OLD_OPEN & QUIZ_OLD_RESPONSES ? QUIZ_NEW_LATER_WHILE_OPEN : 0) | + ($quizrevew & QUIZ_OLD_CLOSED & QUIZ_OLD_RESPONSES ? QUIZ_NEW_AFTER_CLOSE : 0), + 'quiz'); + + set_config('reviewcorrectness', + QUIZ_NEW_DURING | + ($quizrevew & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_SCORES ? QUIZ_NEW_IMMEDIATELY_AFTER : 0) | + ($quizrevew & QUIZ_OLD_OPEN & QUIZ_OLD_SCORES ? QUIZ_NEW_LATER_WHILE_OPEN : 0) | + ($quizrevew & QUIZ_OLD_CLOSED & QUIZ_OLD_SCORES ? QUIZ_NEW_AFTER_CLOSE : 0), + 'quiz'); + + set_config('reviewmarks', + QUIZ_NEW_DURING | + ($quizrevew & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_SCORES ? QUIZ_NEW_IMMEDIATELY_AFTER : 0) | + ($quizrevew & QUIZ_OLD_OPEN & QUIZ_OLD_SCORES ? QUIZ_NEW_LATER_WHILE_OPEN : 0) | + ($quizrevew & QUIZ_OLD_CLOSED & QUIZ_OLD_SCORES ? QUIZ_NEW_AFTER_CLOSE : 0), + 'quiz'); + + set_config('reviewspecificfeedback', + ($quizrevew & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_FEEDBACK ? QUIZ_NEW_DURING : 0) | + ($quizrevew & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_FEEDBACK ? QUIZ_NEW_IMMEDIATELY_AFTER : 0) | + ($quizrevew & QUIZ_OLD_OPEN & QUIZ_OLD_FEEDBACK ? QUIZ_NEW_LATER_WHILE_OPEN : 0) | + ($quizrevew & QUIZ_OLD_CLOSED & QUIZ_OLD_FEEDBACK ? QUIZ_NEW_AFTER_CLOSE : 0), + 'quiz'); + + set_config('reviewgeneralfeedback', + ($quizrevew & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_GENERALFEEDBACK ? QUIZ_NEW_DURING : 0) | + ($quizrevew & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_GENERALFEEDBACK ? QUIZ_NEW_IMMEDIATELY_AFTER : 0) | + ($quizrevew & QUIZ_OLD_OPEN & QUIZ_OLD_GENERALFEEDBACK ? QUIZ_NEW_LATER_WHILE_OPEN : 0) | + ($quizrevew & QUIZ_OLD_CLOSED & QUIZ_OLD_GENERALFEEDBACK ? QUIZ_NEW_AFTER_CLOSE : 0), + 'quiz'); + + set_config('reviewrightanswer', + ($quizrevew & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_ANSWERS ? QUIZ_NEW_DURING : 0) | + ($quizrevew & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_ANSWERS ? QUIZ_NEW_IMMEDIATELY_AFTER : 0) | + ($quizrevew & QUIZ_OLD_OPEN & QUIZ_OLD_ANSWERS ? QUIZ_NEW_LATER_WHILE_OPEN : 0) | + ($quizrevew & QUIZ_OLD_CLOSED & QUIZ_OLD_ANSWERS ? QUIZ_NEW_AFTER_CLOSE : 0), + 'quiz'); + + set_config('reviewoverallfeedback', + 0 | + ($quizrevew & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_OVERALLFEEDBACK ? QUIZ_NEW_IMMEDIATELY_AFTER : 0) | + ($quizrevew & QUIZ_OLD_OPEN & QUIZ_OLD_OVERALLFEEDBACK ? QUIZ_NEW_LATER_WHILE_OPEN : 0) | + ($quizrevew & QUIZ_OLD_CLOSED & QUIZ_OLD_OVERALLFEEDBACK ? QUIZ_NEW_AFTER_CLOSE : 0), + 'quiz'); + } + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051221, 'quiz'); + } + + // Finally drop the old column + if ($oldversion < 2011051222) { + // Define field review to be dropped from quiz + $table = new xmldb_table('quiz'); + $field = new xmldb_field('review'); + + // Launch drop field review + if ($dbman->field_exists($table, $field)) { + $dbman->drop_field($table, $field); + } + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051222, 'quiz'); + } + + if ($oldversion < 2011051223) { + unset_config('review', 'quiz'); + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051223, 'quiz'); + } + + if ($oldversion < 2011051225) { + // Define table quiz_report to be renamed to quiz_reports + $table = new xmldb_table('quiz_report'); + + // Launch rename table for quiz_reports + if ($dbman->table_exists($table)) { + $dbman->rename_table($table, 'quiz_reports'); + } + + upgrade_mod_savepoint(true, 2011051225, 'quiz'); + } + + if ($oldversion < 2011051226) { + // Define index name (unique) to be added to quiz_reports + $table = new xmldb_table('quiz_reports'); + $index = new xmldb_index('name', XMLDB_INDEX_UNIQUE, array('name')); + + // Conditionally launch add index name + if (!$dbman->index_exists($table, $index)) { + $dbman->add_index($table, $index); + } + + upgrade_mod_savepoint(true, 2011051226, 'quiz'); + } + + if ($oldversion < 2011051227) { + + // Changing nullability of field sumgrades on table quiz_attempts to null + $table = new xmldb_table('quiz_attempts'); + $field = new xmldb_field('sumgrades', XMLDB_TYPE_NUMBER, '10, 5', null, + null, null, null, 'attempt'); + + // Launch change of nullability for field sumgrades + $dbman->change_field_notnull($table, $field); + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051227, 'quiz'); + } + + if ($oldversion < 2011051228) { + // Define field needsupgradetonewqe to be added to quiz_attempts + $table = new xmldb_table('quiz_attempts'); + $field = new xmldb_field('needsupgradetonewqe', XMLDB_TYPE_INTEGER, '3', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0', 'preview'); + + // Launch add field needsupgradetonewqe + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + $DB->set_field('quiz_attempts', 'needsupgradetonewqe', 1); + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051228, 'quiz'); + } + + if ($oldversion < 2011051229) { + $table = new xmldb_table('question_states'); + if ($dbman->table_exists($table)) { + // First delete all data from preview attempts. + $DB->delete_records_select('question_states', + "attempt IN (SELECT uniqueid FROM {quiz_attempts} WHERE preview = 1)"); + $DB->delete_records_select('question_sessions', + "attemptid IN (SELECT uniqueid FROM {quiz_attempts} WHERE preview = 1)"); + $DB->delete_records('quiz_attempts', array('preview' => 1)); + + // Now update all the old attempt data. + $oldrcachesetting = $CFG->rcache; + $CFG->rcache = false; + + require_once($CFG->dirroot . '/question/engine/upgrade/upgradelib.php'); + $upgrader = new question_engine_attempt_upgrader(); + $upgrader->convert_all_quiz_attempts(); + + $CFG->rcache = $oldrcachesetting; + } + + // quiz savepoint reached + upgrade_mod_savepoint(true, 2011051229, 'quiz'); + } + return true; } diff --git a/mod/quiz/db/upgradelib.php b/mod/quiz/db/upgradelib.php new file mode 100644 index 00000000000..bc2874dda0a --- /dev/null +++ b/mod/quiz/db/upgradelib.php @@ -0,0 +1,76 @@ +. + +/** + * Upgrade helper code for the quiz module. + * + * @package mod + * @subpackage quiz + * @copyright 2006 Eloy Lafuente (stronk7) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Upgrade states for an attempt to Moodle 1.5 model + * + * Any state that does not yet have its timestamp set to nonzero has not yet been + * upgraded from Moodle 1.4. The reason these are still around is that for large + * sites it would have taken too long to upgrade all states at once. This function + * sets the timestamp field and creates an entry in the question_sessions table. + * @param object $attempt The attempt whose states need upgrading + */ +function quiz_upgrade_very_old_question_sessions($attempt) { + global $DB; + // The old quiz model only allowed a single response per quiz attempt so that there will be + // only one state record per question for this attempt. + + // We set the timestamp of all states to the timemodified field of the attempt. + $DB->execute("UPDATE {question_states} SET timestamp = ? WHERE attempt = ?", + array($attempt->timemodified, $attempt->uniqueid)); + + // For each state we create an entry in the question_sessions table, with both newest and + // newgraded pointing to this state. + // Actually we only do this for states whose question is actually listed in $attempt->layout. + // We do not do it for states associated to wrapped questions like for example the questions + // used by a RANDOM question + $session = new stdClass(); + $session->attemptid = $attempt->uniqueid; + $session->sumpenalty = 0; + $session->manualcomment = ''; + $session->manualcommentformat = FORMAT_HTML; + $session->flagged = 0; + + $questionlist = str_replace(',0', '', quiz_clean_layout($layout, true)); + if (!$questionlist) { + return; + } + list($usql, $question_params) = $DB->get_in_or_equal(explode(',', $questionlist)); + $params = array_merge(array($attempt->uniqueid), $question_params); + + if ($states = $DB->get_records_select('question_states', + "attempt = ? AND question $usql", $params)) { + foreach ($states as $state) { + $session->newgraded = $state->id; + $session->newest = $state->id; + $session->questionid = $state->question; + $DB->insert_record('question_sessions', $session, false); + } + } +} diff --git a/mod/quiz/edit.js b/mod/quiz/edit.js index a5d43f1b559..86b1c016179 100644 --- a/mod/quiz/edit.js +++ b/mod/quiz/edit.js @@ -1,11 +1,40 @@ -/** JavaScript for /mod/quiz/edit.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 . + +/** + * JavaScript library for the quiz module editing interface. + * + * @package mod + * @subpackage quiz + * @copyright 2008 Olli Savolainen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + // Initialise everything on the quiz edit/order and paging page. var quiz_edit = {}; -function quiz_edit_init() { +function quiz_edit_init(Y) { + M.core_scroll_manager.scroll_to_saved_pos(Y); + Y.on('submit', function(e) { + M.core_scroll_manager.save_scroll_pos(Y, 'id_existingcategory'); + }, '#mform1'); + Y.on('submit', function(e) { + M.core_scroll_manager.save_scroll_pos(Y, e.target.get('firstChild')); + }, '.quizsavegradesform'); // Add random question dialogue -------------------------------------------- - var randomquestiondialog = YAHOO.util.Dom.get('randomquestiondialog'); if (randomquestiondialog) { YAHOO.util.Dom.get(document.body).appendChild(randomquestiondialog); @@ -125,15 +154,3 @@ function quiz_settings_init() { }, 50); }); } - -// Depending on which page this is, do the appropriate initialisation. -function quiz_edit_generic_init() { - switch (document.body.id) { - case 'page-mod-quiz-edit': - quiz_edit_init(); - break; - case 'page-mod-quiz-mod': - quiz_settings_init(); - } -} -YAHOO.util.Event.onDOMReady(quiz_edit_generic_init); diff --git a/mod/quiz/edit.php b/mod/quiz/edit.php index 1f826c45a54..5207b4b87a9 100644 --- a/mod/quiz/edit.php +++ b/mod/quiz/edit.php @@ -1,27 +1,19 @@ . -/////////////////////////////////////////////////////////////////////////// -// // -// NOTICE OF COPYRIGHT // -// // -// Moodle - Modular Object-Oriented Dynamic Learning Environment // -// http://moodle.org // -// // -// Copyright (C) 1999 onwards Martin Dougiamas and others // -// // -// This program 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 2 of the License, or // -// (at your option) any later version. // -// // -// This program 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: // -// // -// http://www.gnu.org/copyleft/gpl.html // -// // -/////////////////////////////////////////////////////////////////////////// /** * Page to edit quizzes @@ -43,15 +35,19 @@ * delete Removes a question from the quiz * savechanges Saves the order and grades for questions in the quiz * - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package quiz - *//** */ + * @package mod + * @subpackage quiz + * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + require_once('../../config.php'); require_once($CFG->dirroot . '/mod/quiz/editlib.php'); require_once($CFG->dirroot . '/mod/quiz/addrandomform.php'); require_once($CFG->dirroot . '/question/category_class.php'); + /** * Callback function called from question_list() function * (which is called from showbank()) @@ -64,8 +60,8 @@ function module_specific_buttons($cmid, $cmoptions) { } else { $disabled = ''; } - $out = '\n"; + $out = '\n"; return $out; } @@ -74,7 +70,7 @@ function module_specific_buttons($cmid, $cmoptions) { * (which is called from showbank()) */ function module_specific_controls($totalnumber, $recurse, $category, $cmid, $cmoptions) { - global $QTYPES, $OUTPUT; + global $OUTPUT; $out = ''; $catcontext = get_context_instance_by_id($category->contextid); if (has_capability('moodle/question:useall', $catcontext)) { @@ -84,7 +80,8 @@ function module_specific_controls($totalnumber, $recurse, $category, $cmid, $cmo $disabled = ''; } $randomusablequestions = - $QTYPES['random']->get_usable_questions_from_category($category->id, $recurse, '0'); + question_bank::get_qtype('random')->get_available_questions_from_category( + $category->id, $recurse); $maxrand = count($randomusablequestions); if ($maxrand > 0) { for ($i = 1; $i <= min(10, $maxrand); $i++) { @@ -117,9 +114,11 @@ function module_specific_controls($totalnumber, $recurse, $category, $cmid, $cmo //this page otherwise they would go in question_edit_setup $quiz_reordertool = optional_param('reordertool', -1, PARAM_BOOL); $quiz_qbanktool = optional_param('qbanktool', -1, PARAM_BOOL); +$scrollpos = optional_param('scrollpos', '', PARAM_INT); list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) = question_edit_setup('editq', '/mod/quiz/edit.php', true); +$quiz->questions = quiz_clean_layout($quiz->questions); $defaultcategoryobj = question_make_default_categories($contexts->all()); $defaultcategory = $defaultcategoryobj->id . ',' . $defaultcategoryobj->contextid; @@ -154,7 +153,7 @@ if (!$course) { print_error('invalidcourseid', 'error'); } -$questionbank = new quiz_question_bank_view($contexts, $thispageurl, $course, $cm); +$questionbank = new quiz_question_bank_view($contexts, $thispageurl, $course, $cm, $quiz); $questionbank->set_quiz_has_attempts($quizhasattempts); // Log this visit. @@ -164,7 +163,7 @@ add_to_log($cm->course, 'quiz', 'editquestions', // You need mod/quiz:manage in addition to question capabilities to access this page. require_capability('mod/quiz:manage', $contexts->lowest()); -if (empty($quiz->grades)) { // Construct an array to hold all the grades. +if (empty($quiz->grades)) { $quiz->grades = quiz_get_all_question_grades($quiz); } @@ -184,53 +183,57 @@ foreach ($params as $key => $value) { } } +$afteractionurl = new moodle_url($thispageurl); +if ($scrollpos) { + $afteractionurl->param('scrollpos', $scrollpos); +} if (($up = optional_param('up', false, PARAM_INT)) && confirm_sesskey()) { $quiz->questions = quiz_move_question_up($quiz->questions, $up); - quiz_save_new_layout($quiz); - redirect($thispageurl); + $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id)); + quiz_delete_previews($quiz); + redirect($afteractionurl); } if (($down = optional_param('down', false, PARAM_INT)) && confirm_sesskey()) { $quiz->questions = quiz_move_question_down($quiz->questions, $down); - quiz_save_new_layout($quiz); - redirect($thispageurl); + $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id)); + quiz_delete_previews($quiz); + redirect($afteractionurl); } if (optional_param('repaginate', false, PARAM_BOOL) && confirm_sesskey()) { // Re-paginate the quiz $questionsperpage = optional_param('questionsperpage', $quiz->questionsperpage, PARAM_INT); $quiz->questions = quiz_repaginate($quiz->questions, $questionsperpage ); - quiz_save_new_layout($quiz); + $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id)); + quiz_delete_previews($quiz); + redirect($afteractionurl); } if (($addquestion = optional_param('addquestion', 0, PARAM_INT)) && confirm_sesskey()) { -/// Add a single question to the current quiz + // Add a single question to the current quiz $addonpage = optional_param('addonpage', 0, PARAM_INT); quiz_add_quiz_question($addquestion, $quiz, $addonpage); - quiz_update_sumgrades($quiz); quiz_delete_previews($quiz); + quiz_update_sumgrades($quiz); $thispageurl->param('lastchanged', $addquestion); - redirect($thispageurl); + redirect($afteractionurl); } if (optional_param('add', false, PARAM_BOOL) && confirm_sesskey()) { -/// Add selected questions to the current quiz + // Add selected questions to the current quiz $rawdata = (array) data_submitted(); - foreach ($rawdata as $key => $value) { // Parse input for question ids + foreach ($rawdata as $key => $value) { // Parse input for question ids if (preg_match('!^q([0-9]+)$!', $key, $matches)) { $key = $matches[1]; quiz_add_quiz_question($key, $quiz); } } - quiz_update_sumgrades($quiz); quiz_delete_previews($quiz); - redirect($thispageurl); + quiz_update_sumgrades($quiz); + redirect($afteractionurl); } -$qcobject = new question_category_object($pagevars['cpage'], $thispageurl, - $contexts->having_one_edit_tab_cap('categories'), $defaultcategoryobj->id, - $defaultcategory, null, $contexts->having_cap('moodle/question:add')); - if ((optional_param('addrandom', false, PARAM_BOOL)) && confirm_sesskey()) { // Add random questions to the quiz $recurse = optional_param('recurse', 0, PARAM_BOOL); @@ -239,31 +242,35 @@ if ((optional_param('addrandom', false, PARAM_BOOL)) && confirm_sesskey()) { $randomcount = required_param('randomcount', PARAM_INT); quiz_add_random_questions($quiz, $addonpage, $categoryid, $randomcount, $recurse); - quiz_update_sumgrades($quiz); quiz_delete_previews($quiz); - redirect($thispageurl); + quiz_update_sumgrades($quiz); + redirect($afteractionurl); } -if (optional_param('addnewpagesafterselected', null, PARAM_CLEAN) && !empty($selectedquestionids) && confirm_sesskey()) { +if (optional_param('addnewpagesafterselected', null, PARAM_CLEAN) && + !empty($selectedquestionids) && confirm_sesskey()) { foreach ($selectedquestionids as $questionid) { $quiz->questions = quiz_add_page_break_after($quiz->questions, $questionid); } - quiz_save_new_layout($quiz); - redirect($thispageurl); + $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id)); + quiz_delete_previews($quiz); + redirect($afteractionurl); } $addpage = optional_param('addpage', false, PARAM_INT); if ($addpage !== false && confirm_sesskey()) { $quiz->questions = quiz_add_page_break_at($quiz->questions, $addpage); - quiz_save_new_layout($quiz); - redirect($thispageurl); + $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id)); + quiz_delete_previews($quiz); + redirect($afteractionurl); } $deleteemptypage = optional_param('deleteemptypage', false, PARAM_INT); if (($deleteemptypage !== false) && confirm_sesskey()) { $quiz->questions = quiz_delete_empty_page($quiz->questions, $deleteemptypage); - quiz_save_new_layout($quiz); - redirect($thispageurl); + $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id)); + quiz_delete_previews($quiz); + redirect($afteractionurl); } $remove = optional_param('remove', false, PARAM_INT); @@ -271,19 +278,23 @@ if (($remove = optional_param('remove', false, PARAM_INT)) && confirm_sesskey()) quiz_remove_question($quiz, $remove); quiz_update_sumgrades($quiz); quiz_delete_previews($quiz); - redirect($thispageurl); + redirect($afteractionurl); } -if (optional_param('quizdeleteselected', false, PARAM_BOOL) && !empty($selectedquestionids) && confirm_sesskey()) { +if (optional_param('quizdeleteselected', false, PARAM_BOOL) && + !empty($selectedquestionids) && confirm_sesskey()) { foreach ($selectedquestionids as $questionid) { quiz_remove_question($quiz, $questionid); } - quiz_update_sumgrades($quiz); quiz_delete_previews($quiz); - redirect($thispageurl); + quiz_update_sumgrades($quiz); + redirect($afteractionurl); } if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey()) { + $deletepreviews = false; + $recomputesummarks = false; + $oldquestions = explode(',', $quiz->questions); // the questions in the old order $questions = array(); // for questions in the new order $rawdata = (array) data_submitted(); @@ -295,15 +306,15 @@ if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey()) { foreach ($rawdata as $key => $value) { if (preg_match('!^g([0-9]+)$!', $key, $matches)) { - /// Parse input for question -> grades + // Parse input for question -> grades $questionid = $matches[1]; $quiz->grades[$questionid] = clean_param($value, PARAM_FLOAT); - quiz_update_question_instance($quiz->grades[$questionid], $questionid, $quiz->id); - quiz_delete_previews($quiz); - quiz_update_sumgrades($quiz); + quiz_update_question_instance($quiz->grades[$questionid], $questionid, $quiz); + $deletepreviews = true; + $recomputesummarks = true; } else if (preg_match('!^o(pg)?([0-9]+)$!', $key, $matches)) { - /// Parse input for ordering info + // Parse input for ordering info $questionid = $matches[2]; // Make sure two questions don't overwrite each other. If we get a second // question with the same position, shift the second one along to the next gap. @@ -317,6 +328,7 @@ if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey()) { } else { $questions[$value] = $questionid; } + $deletepreviews = true; } } @@ -325,8 +337,8 @@ if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey()) { ksort($questions); $questions[] = 0; $quiz->questions = implode(',', $questions); - quiz_save_new_layout($quiz); - quiz_delete_previews($quiz); + $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id)); + $deletepreviews = true; } //get a list of questions to move, later to be added in the appropriate @@ -350,8 +362,8 @@ if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey()) { $moveselectedpos = $pagebreakpositions[$moveselectedonpage - 1]; array_splice($questions, $moveselectedpos, 0, $selectedquestionids); $quiz->questions = implode(',', $questions); - quiz_save_new_layout($quiz); - quiz_delete_previews($quiz); + $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id)); + $deletepreviews = true; } // If rescaling is required save the new maximum @@ -360,7 +372,16 @@ if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey()) { quiz_set_grade($maxgrade, $quiz); } - redirect($thispageurl); + if ($deletepreviews) { + quiz_delete_previews($quiz); + } + if ($recomputesummarks) { + quiz_update_sumgrades($quiz); + quiz_update_all_attempt_sumgrades($quiz); + quiz_update_all_final_grades($quiz); + quiz_update_grades($quiz, 0, true); + } + redirect($afteractionurl); } $questionbank->process_actions($thispageurl, $cm); @@ -369,8 +390,10 @@ $questionbank->process_actions($thispageurl, $cm); $PAGE->requires->yui2_lib('container'); $PAGE->requires->yui2_lib('dragdrop'); -$PAGE->requires->skip_link_to('questionbank', get_string('skipto', 'access', get_string('questionbank', 'question'))); -$PAGE->requires->skip_link_to('quizcontentsblock', get_string('skipto', 'access', get_string('questionsinthisquiz', 'quiz'))); +$PAGE->requires->skip_link_to('questionbank', + get_string('skipto', 'access', get_string('questionbank', 'question'))); +$PAGE->requires->skip_link_to('quizcontentsblock', + get_string('skipto', 'access', get_string('questionsinthisquiz', 'quiz'))); $PAGE->set_title($pagetitle); $PAGE->set_heading($course->fullname); $node = $PAGE->settingsnav->find('mod_quiz_edit', navigation_node::TYPE_SETTING); @@ -380,7 +403,7 @@ if ($node) { echo $OUTPUT->header(); // Initialise the JavaScript. -$quizeditconfig = new stdClass; +$quizeditconfig = new stdClass(); $quizeditconfig->url = $thispageurl->out(true, array('qbanktool' => '0')); $quizeditconfig->dialoglisteners = array(); $numberoflisteners = max(quiz_number_of_pages($quiz->questions), 1); @@ -388,7 +411,9 @@ for ($pageiter = 1; $pageiter <= $numberoflisteners; $pageiter++) { $quizeditconfig->dialoglisteners[] = 'addrandomdialoglaunch_' . $pageiter; } $PAGE->requires->data_for_js('quiz_edit_config', $quizeditconfig); +$PAGE->requires->js('/question/qengine.js'); $PAGE->requires->js('/mod/quiz/edit.js'); +$PAGE->requires->js_init_call('quiz_edit_init'); // Print the tabs to switch mode. if ($quiz_reordertool) { @@ -397,8 +422,10 @@ if ($quiz_reordertool) { $currenttab = 'edit'; } $tabs = array(array( - new tabobject('edit', new moodle_url($thispageurl, array('reordertool' => 0)), get_string('editingquiz', 'quiz')), - new tabobject('reorder', new moodle_url($thispageurl, array('reordertool' => 1)), get_string('orderingquiz', 'quiz')), + new tabobject('edit', new moodle_url($thispageurl, + array('reordertool' => 0)), get_string('editingquiz', 'quiz')), + new tabobject('reorder', new moodle_url($thispageurl, + array('reordertool' => 1)), get_string('orderingquiz', 'quiz')), )); print_tabs($tabs, $currenttab); @@ -447,7 +474,8 @@ if ($quiz->shufflequestions) { $repaginatingdisabled = false; } if ($quiz_reordertool) { - echo '
'; echo '
'; } @@ -501,10 +529,12 @@ if ($quiz_reordertool) { echo ''; $attributes = array(); $attributes['disabled'] = $repaginatingdisabledhtml ? 'disabled' : null; - $select = html_writer::select($perpage, 'questionsperpage', $quiz->questionsperpage, null, $attributes); + $select = html_writer::select( + $perpage, 'questionsperpage', $quiz->questionsperpage, null, $attributes); print_string('repaginate', 'quiz', $select); echo '
'; - echo ' '; + echo ' '; echo '
'; } @@ -529,16 +559,15 @@ if (!$quiz_reordertool) { 'cmid' => $cm->id, )); ?> -
-
name); ?> - - -
-
display(); -?>
-
+
+
name); ?> + + +
+
display(); + ?>
+
footer(); -?> diff --git a/mod/quiz/editlib.php b/mod/quiz/editlib.php index f93d9c0ef5a..41c5f27738c 100644 --- a/mod/quiz/editlib.php +++ b/mod/quiz/editlib.php @@ -1,34 +1,35 @@ . -/////////////////////////////////////////////////////////////////////////// -// // -// NOTICE OF COPYRIGHT // -// // -// Moodle - Modular Object-Oriented Dynamic Learning Environment // -// http://moodle.org // -// // -// Copyright (C) 1999 onwards Martin Dougiamas and others // -// // -// This program 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 2 of the License, or // -// (at your option) any later version. // -// // -// This program 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: // -// // -// http://www.gnu.org/copyleft/gpl.html // -// // -/////////////////////////////////////////////////////////////////////////// /** - * Functions used by edit.php to edit quizzes + * This contains functions that are called from within the quiz module only + * Functions that are also called by core Moodle are in {@link lib.php} + * This script also loads the code in {@link questionlib.php} which holds + * the module-indpendent code for handling questions and which in turn + * initialises all the questiontype classes. * - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package quiz - *//** */ + * @package mod + * @subpackage quiz + * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); @@ -51,13 +52,14 @@ function quiz_remove_question($quiz, $questionid) { unset($questionids[$key]); $quiz->questions = implode(',', $questionids); $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id)); - $DB->delete_records('quiz_question_instances', array('quiz' => $quiz->instance, 'question' => $questionid)); + $DB->delete_records('quiz_question_instances', + array('quiz' => $quiz->instance, 'question' => $questionid)); } /** * Remove an empty page from the quiz layout. If that is not possible, do nothing. * @param string $layout the existinng layout, $quiz->questions. - * @param integer $index the position into $layout where the empty page should be removed. + * @param int $index the position into $layout where the empty page should be removed. * @return the updated layout */ function quiz_delete_empty_page($layout, $index) { @@ -85,8 +87,9 @@ function quiz_delete_empty_page($layout, $index) { * @param int $id The id of the question to be added * @param object $quiz The extended quiz object as used by edit.php * This is updated by this function - * @param int $page Which page in quiz to add the question on. If 0 (default), add at the end - * @return boolean false if the question was already in the quiz + * @param int $page Which page in quiz to add the question on. If 0 (default), + * add at the end + * @return bool false if the question was already in the quiz */ function quiz_add_quiz_question($id, $quiz, $page = 0) { global $DB; @@ -141,15 +144,17 @@ function quiz_add_quiz_question($id, $quiz, $page = 0) { $quiz->questions = implode(',', $questions); $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id)); - // update question grades - $quiz->grades[$id] = $DB->get_field('question', 'defaultgrade', array('id' => $id)); - quiz_update_question_instance($quiz->grades[$id], $id, $quiz->instance); - - return true; + // Add the new question instance. + $instance = new stdClass(); + $instance->quiz = $quiz->id; + $instance->question = $id; + $instance->grade = $DB->get_field('question', 'defaultmark', array('id' => $id)); + $DB->insert_record('quiz_question_instances', $instance); } -function quiz_add_random_questions($quiz, $addonpage, $categoryid, $number, $includesubcategories) { - global $DB, $QTYPES; +function quiz_add_random_questions($quiz, $addonpage, $categoryid, $number, + $includesubcategories) { + global $DB; $category = $DB->get_record('question_categories', array('id' => $categoryid)); if (!$category) { @@ -162,11 +167,14 @@ function quiz_add_random_questions($quiz, $addonpage, $categoryid, $number, $inc // Find existing random questions in this category that are // not used by any quiz. if ($existingquestions = $DB->get_records_sql( - "SELECT q.id,q.qtype FROM {question} q - WHERE qtype = '" . RANDOM . "' + "SELECT q.id, q.qtype FROM {question} q + WHERE qtype = 'random' AND category = ? AND " . $DB->sql_compare_text('questiontext') . " = ? - AND NOT EXISTS (SELECT * FROM {quiz_question_instances} WHERE question = q.id) + AND NOT EXISTS ( + SELECT * + FROM {quiz_question_instances} + WHERE question = q.id) ORDER BY id", array($category->id, $includesubcategories))) { // Take as many of these as needed. while (($existingquestion = array_shift($existingquestions)) && $number > 0) { @@ -180,15 +188,16 @@ function quiz_add_random_questions($quiz, $addonpage, $categoryid, $number, $inc } // More random questions are needed, create them. - $form->questiontext = array('text' => $includesubcategories, 'format' => 0); - $form->defaultgrade = 1; - $form->hidden = 1; for ($i = 0; $i < $number; $i += 1) { + $form = new stdClass(); + $form->questiontext = array('text' => $includesubcategories, 'format' => 0); $form->category = $category->id . ',' . $category->contextid; + $form->defaultmark = 1; + $form->hidden = 1; $form->stamp = make_unique_id_code(); // Set the unique code (not to be changed) - $question = new stdClass; - $question->qtype = RANDOM; - $question = $QTYPES[RANDOM]->save_question($question, $form); + $question = new stdClass(); + $question->qtype = 'random'; + $question = question_bank::get_qtype('random')->save_question($question, $form); if (!isset($question->id)) { print_error('cannotinsertrandomquestion', 'quiz'); } @@ -199,7 +208,7 @@ function quiz_add_random_questions($quiz, $addonpage, $categoryid, $number, $inc /** * Add a page break after at particular position$. * @param string $layout the existinng layout, $quiz->questions. - * @param integer $index the position into $layout where the empty page should be removed. + * @param int $index the position into $layout where the empty page should be removed. * @return the updated layout */ function quiz_add_page_break_at($layout, $index) { @@ -216,7 +225,7 @@ function quiz_add_page_break_at($layout, $index) { /** * Add a page break after a particular question. * @param string $layout the existinng layout, $quiz->questions. - * @param integer $qustionid the question to add the page break after. + * @param int $qustionid the question to add the page break after. * @return the updated layout */ function quiz_add_page_break_after($layout, $questionid) { @@ -248,23 +257,30 @@ function quiz_save_new_layout($quiz) { * * Saves changes to the question grades in the quiz_question_instances table. * It does not update 'sumgrades' in the quiz table. - * @return boolean Indicates success or failure. - * @param integer grade The maximal grade for the question - * @param integer $questionid The id of the question - * @param integer $quizid The id of the quiz to update / add the instances for. + * + * @param int grade The maximal grade for the question + * @param int $questionid The id of the question + * @param int $quizid The id of the quiz to update / add the instances for. */ -function quiz_update_question_instance($grade, $questionid, $quizid) { +function quiz_update_question_instance($grade, $questionid, $quiz) { global $DB; - if ($instance = $DB->get_record('quiz_question_instances', array('quiz' => $quizid, 'question' => $questionid))) { - $instance->grade = $grade; - return $DB->update_record('quiz_question_instances', $instance); - } else { - unset($instance); - $instance->quiz = $quizid; - $instance->question = $questionid; - $instance->grade = $grade; - return $DB->insert_record('quiz_question_instances', $instance); + $instance = $DB->get_record('quiz_question_instances', array('quiz' => $quiz->id, + 'question' => $questionid)); + $slot = quiz_get_slot_for_question($quiz, $questionid); + + if (!$instance || !$slot) { + throw new coding_exception('Attempt to change the grade of a quesion not in the quiz.'); } + + if (abs($grade - $instance->grade) < 1e-7) { + // Grade has not changed. Nothing to do. + return; + } + + $instance->grade = $grade; + $DB->update_record('quiz_question_instances', $instance); + question_engine::set_max_mark_in_attempts(new qubaids_for_quiz($quiz->id), + $slot, $grade); } // Private function used by the following two. @@ -295,7 +311,7 @@ function _quiz_move_question($layout, $questionid, $shift) { * Move a particular question one space earlier in the $quiz->questions list. * If that is not possible, do nothing. * @param string $layout the existinng layout, $quiz->questions. - * @param integer $questionid the id of a question. + * @param int $questionid the id of a question. * @return the updated layout */ function quiz_move_question_up($layout, $questionid) { @@ -306,7 +322,7 @@ function quiz_move_question_up($layout, $questionid) { * Move a particular question one space later in the $quiz->questions list. * If that is not possible, do nothing. * @param string $layout the existinng layout, $quiz->questions. - * @param integer $questionid the id of a question. + * @param int $questionid the id of a question. * @return the updated layout */ function quiz_move_question_down($layout, $questionid) { @@ -323,14 +339,14 @@ function quiz_move_question_down($layout, $questionid) { * $quiz->grades * @param object $pageurl The url of the current page with the parameters required * for links returning to the current page, as a moodle_url object - * @param boolean $allowdelete Indicates whether the delete icons should be displayed - * @param boolean $reordertool Indicates whether the reorder tool should be displayed - * @param boolean $quiz_qbanktool Indicates whether the question bank should be displayed - * @param boolean $hasattempts Indicates whether the quiz has attempts + * @param bool $allowdelete Indicates whether the delete icons should be displayed + * @param bool $reordertool Indicates whether the reorder tool should be displayed + * @param bool $quiz_qbanktool Indicates whether the question bank should be displayed + * @param bool $hasattempts Indicates whether the quiz has attempts */ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool, $quiz_qbanktool, $hasattempts, $defaultcategoryobj) { - global $USER, $CFG, $QTYPES, $DB, $OUTPUT; + global $USER, $CFG, $DB, $OUTPUT; $strorder = get_string('order'); $strquestionname = get_string('questionname', 'quiz'); $strgrade = get_string('grade'); @@ -351,11 +367,12 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool, if ($quiz->questions) { list($usql, $params) = $DB->get_in_or_equal(explode(',', $quiz->questions)); - $questions = $DB->get_records_sql("SELECT q.*,c.contextid - FROM {question} q, - {question_categories} c - WHERE q.id $usql - AND q.category = c.id", $params); + $params[] = $quiz->id; + $questions = $DB->get_records_sql("SELECT q.*, qc.contextid, qqi.grade as maxmark + FROM {question} q + JOIN {question_categories} qc ON qc.id = q.category + JOIN {quiz_question_instances} qqi ON qqi.question = q.id + WHERE q.id $usql AND qqi.quiz = ?", $params); } else { $questions = array(); } @@ -429,20 +446,15 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool, $qno = 1; //the current question (includes questions and descriptions) $questioncount = 0; - //the ordinal of current element in the layout - //(includes page breaks, questions and descriptions) - $count = 0; //the current page number in iteration $pagecount = 0; - $sumgrade = 0; - $pageopen = false; $returnurl = str_replace($CFG->wwwroot, '', $pageurl->out(false)); $questiontotalcount = count($order); - foreach ($order as $i => $qnum) { + foreach ($order as $count => $qnum) { $reordercheckbox = ''; $reordercheckboxlabel = ''; @@ -451,10 +463,23 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool, if ($qnum && empty($questions[$qnum])) { continue; } + // If the questiontype is missing change the question type - if ($qnum && !array_key_exists($questions[$qnum]->qtype, $QTYPES)) { + if ($qnum && !array_key_exists($qnum, $questions)) { + $fakequestion = new stdClass(); + $fakequestion->id = 0; + $fakequestion->qtype = 'missingtype'; + $fakequestion->name = get_string('deletedquestion', 'qtype_missingtype'); + $fakequestion->questiontext = '

' . + get_string('deletedquestion', 'qtype_missing') . '

'; + $fakequestion->length = 0; + $questions[$qnum] = $fakequestion; + $quiz->grades[$qnum] = 0; + + } else if ($qnum && !question_bank::qtype_exists($questions[$qnum]->qtype)) { $questions[$qnum]->qtype = 'missingtype'; } + if ($qnum != 0 || ($qnum == 0 && !$pageopen)) { //this is either a question or a page break after another // (no page is currently open) @@ -466,17 +491,19 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool, '
'; $pageopen = true; } - if ($qnum == 0 && $i < $questiontotalcount) { + if ($qnum == 0 && $count < $questiontotalcount) { // This is the second successive page break. Tell the user the page is empty. echo '
'; print_string('noquestionsonpage', 'quiz'); echo '
'; if ($allowdelete) { echo '
'; - echo '' . $strremove . ''; + echo $OUTPUT->action_icon($pageurl->out(true, + array('deleteemptypage' => $count - 1, 'sesskey'=>sesskey())), + new pix_icon('t/delete', $strremove), + new component_action('click', + 'M.core_scroll_manager.save_scroll_action'), + array('title' => $strremove)); echo '
'; } } @@ -493,151 +520,159 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool, //this is an actual question /* Display question start */ -?> + ?>
- id . - '" id="s' . $question->id . '" />'; - $reordercheckboxlabel = ''; - } - if (!$quiz->shufflequestions) { - // Print and increment question number - $questioncountstring = ''; - if ($questioncount>999 || ($reordertool && $questioncount>99)) { - $questioncountstring = - "$reordercheckboxlabel$questioncount" . - $reordercheckboxlabelclose . $reordercheckbox; - } else { - $questioncountstring = $reordercheckboxlabel . $questioncount . - $reordercheckboxlabelclose . $reordercheckbox; + id . + '" id="s' . $question->id . '" />'; + $reordercheckboxlabel = ''; + } + if (!$quiz->shufflequestions) { + // Print and increment question number + $questioncountstring = ''; + if ($questioncount>999 || ($reordertool && $questioncount>99)) { + $questioncountstring = + "$reordercheckboxlabel$questioncount" . + $reordercheckboxlabelclose . $reordercheckbox; + } else { + $questioncountstring = $reordercheckboxlabel . $questioncount . + $reordercheckboxlabelclose . $reordercheckbox; + } + echo $questioncountstring; + $qno += $question->length; + } else { + echo "$reordercheckboxlabel ? $reordercheckboxlabelclose" . + " $reordercheckbox"; } - echo $questioncountstring; - $qno += $question->length; - } else { - echo "$reordercheckboxlabel ? $reordercheckboxlabelclose" . - " $reordercheckbox"; - } - ?> + ?>
= $lastindex - 1) { - $upbuttonclass = 'upwithoutdown'; + if ($count != 0) { + if (!$hasattempts) { + $upbuttonclass = ''; + if ($count >= $lastindex - 1) { + $upbuttonclass = 'upwithoutdown'; + } + echo $OUTPUT->action_icon($pageurl->out(true, + array('up' => $question->id, 'sesskey'=>sesskey())), + new pix_icon('t/up', $strmoveup), + new component_action('click', + 'M.core_scroll_manager.save_scroll_action'), + array('title' => $strmoveup)); } - echo "out(true, array('up' => $question->id, 'sesskey'=>sesskey())) . "\">pix_url('t/up') . "\" class=\"iconsmall - $upbuttonclass\" alt=\"$strmoveup\" />"; - } - } - if ($count < $lastindex - 1) { - if (!$hasattempts) { - echo "out(true, array('down' => $question->id, 'sesskey'=>sesskey())) . "\">pix_url('t/down') . "\" class=\"iconsmall\"" . - " alt=\"$strmovedown\" />"; } - } - if ($allowdelete && question_has_capability_on($question, 'use', $question->category)) { - // remove from quiz, not question delete. - if (!$hasattempts) { - echo "out(true, array('remove' => $question->id, 'sesskey'=>sesskey())) . "\"> - pix_url('t/delete') . "\" " . - "class=\"iconsmall\" alt=\"$strremove\" />"; + if ($count < $lastindex - 1) { + if (!$hasattempts) { + echo $OUTPUT->action_icon($pageurl->out(true, + array('down' => $question->id, 'sesskey'=>sesskey())), + new pix_icon('t/down', $strmovedown), + new component_action('click', + 'M.core_scroll_manager.save_scroll_action'), + array('title' => $strmovedown)); + } + } + if ($allowdelete && (empty($question->id) || + question_has_capability_on($question, 'use', $question->category))) { + // remove from quiz, not question delete. + if (!$hasattempts) { + echo $OUTPUT->action_icon($pageurl->out(true, + array('remove' => $question->id, 'sesskey'=>sesskey())), + new pix_icon('t/delete', $strremove), + new component_action('click', + 'M.core_scroll_manager.save_scroll_action'), + array('title' => $strremove)); + } } - } ?>
qtype != 'description' && !$reordertool) { - ?> + if ($question->qtype != 'description' && !$reordertool) { + ?>
-
+
:
- id . '" id="inputq' . $question->id . - '" size="' . ($quiz->decimalpoints + 2) . '" value="' . (0 + $quiz->grades[$qnum]) . - '" tabindex="' . ($lastindex + $qno) . '" />'; - ?> + id . + '" id="inputq' . $question->id . + '" size="' . ($quiz->decimalpoints + 2) . + '" value="' . (0 + $quiz->grades[$qnum]) . + '" tabindex="' . ($lastindex + $qno) . '" />'; + ?>
-qtype == 'random') { - echo '' . get_string("configurerandomquestion", "quiz") . ''; -} + qtype == 'random') { + echo '' . + get_string("configurerandomquestion", "quiz") . ''; + } -?> + ?>
- +
- id . '" size="2" value="' . - (10*$count + 10) . - '" tabindex="' . ($lastindex + $qno) . - '" />'; - ?> + id . + '" size="2" value="' . (10*$count + 10) . + '" tabindex="' . ($lastindex + $qno) . '" />'; + ?>
- + ?>
-qtype == 'random') { // it is a random question - if (!$reordertool) { - quiz_print_randomquestion($question, $pageurl, $quiz, $quiz_qbanktool); - } else { - quiz_print_randomquestion_reordertool($question, $pageurl, $quiz); + qtype == 'random') { // it is a random question + if (!$reordertool) { + quiz_print_randomquestion($question, $pageurl, $quiz, $quiz_qbanktool); + } else { + quiz_print_randomquestion_reordertool($question, $pageurl, $quiz); + } + } else { // it is a single question + if (!$reordertool) { + quiz_print_singlequestion($question, $returnurl, $quiz); + } else { + quiz_print_singlequestion_reordertool($question, $returnurl, $quiz); + } } - } else { // it is a single question - if (!$reordertool) { - quiz_print_singlequestion($question, $returnurl, $quiz); - } else { - quiz_print_singlequestion_reordertool($question, $returnurl, $quiz); - } - } ?>
- grades[$qnum]; - + shufflequestions && $i < $questiontotalcount - 1)) { + if (!$reordertool && !($quiz->shufflequestions && + $count < $questiontotalcount - 1)) { quiz_print_pagecontrols($quiz, $pageurl, $pagecount, $hasattempts, $defaultcategoryobj); - } else if ($i < $questiontotalcount - 1) { + } else if ($count < $questiontotalcount - 1) { //do not include the last page break for reordering //to avoid creating a new extra page in the end echo ' -
+
- + - - - /> + + + />
@@ -732,14 +778,14 @@ function quiz_print_pagecontrols($quiz, $pageurl, $page, $hasattempts, $defaultc * @param object $quiz The quiz in the context of which the question is being displayed */ function quiz_print_singlequestion($question, $returnurl, $quiz) { - global $QTYPES; echo '
'; - echo quiz_question_edit_button($quiz->cmid, $question, $returnurl, quiz_question_tostring($question) . ' '); + echo quiz_question_edit_button($quiz->cmid, $question, $returnurl, + quiz_question_tostring($question) . ' '); echo ''; - $namestr = $QTYPES[$question->qtype]->local_name(); print_question_icon($question); - echo " $namestr"; - echo '' . quiz_question_preview_button($quiz, $question, true) . ''; + echo ' ' . question_bank::get_qtype_name($question->qtype) . ''; + echo '' . + quiz_question_preview_button($quiz, $question, true) . ''; echo "
\n"; } /** @@ -749,13 +795,14 @@ function quiz_print_singlequestion($question, $returnurl, $quiz) { * @param object $question A question object from the database questions table * @param object $questionurl The url of the question editing page as a moodle_url object * @param object $quiz The quiz in the context of which the question is being displayed - * @param boolean $quiz_qbanktool Indicate to this function if the question bank window open + * @param bool $quiz_qbanktool Indicate to this function if the question bank window open */ function quiz_print_randomquestion(&$question, &$pageurl, &$quiz, $quiz_qbanktool) { - global $DB, $QTYPES, $OUTPUT; + global $DB, $OUTPUT; echo '
'; - if (!$category = $DB->get_record('question_categories', array('id' => $question->category))) { + if (!$category = $DB->get_record('question_categories', + array('id' => $question->category))) { echo $OUTPUT->notification('Random question category not found!'); return; } @@ -765,7 +812,7 @@ function quiz_print_randomquestion(&$question, &$pageurl, &$quiz, $quiz_qbanktoo print_random_option_icon($question); echo ' ' . get_string('randomfromcategory', 'quiz') . '
'; - $a = new stdClass; + $a = new stdClass(); $a->arrow = $OUTPUT->rarrow(); $strshowcategorycontents = get_string('showcategorycontents', 'quiz', $a); @@ -774,11 +821,13 @@ function quiz_print_randomquestion(&$question, &$pageurl, &$quiz, $quiz_qbanktoo $linkcategorycontents = ' ' . $strshowcategorycontents . ''; echo '
'; - echo '' . $category->name . ''; - echo '' . quiz_question_preview_button($quiz, $question, true) . ''; + echo '' . + $category->name . ''; + echo '' . + quiz_question_preview_button($quiz, $question, true) . ''; echo '
'; - $questionids = $QTYPES['random']->get_usable_questions_from_category( + $questionids = question_bank::get_qtype('random')->get_available_questions_from_category( $category->id, $question->questiontext == '1', '0'); $questioncount = count($questionids); @@ -791,7 +840,7 @@ function quiz_print_randomquestion(&$question, &$pageurl, &$quiz, $quiz_qbanktoo echo '
'; // Embed the link into the string with instructions - $a = new stdClass; + $a = new stdClass(); $a->catname = '' . $category->name . ''; $a->link = $linkcategorycontents; echo get_string('addnewquestionsqbank', 'quiz', $a); @@ -800,13 +849,9 @@ function quiz_print_randomquestion(&$question, &$pageurl, &$quiz, $quiz_qbanktoo // Category has questions // Get a sample from the database, - $toshow = array_slice($questionids, 0, NUM_QS_TO_SHOW_IN_RANDOM); - $questionidstoshow = array(); - foreach ($toshow as $a) { - $questionidstoshow[] = $a->id; - } + $questionidstoshow = array_slice($questionids, 0, NUM_QS_TO_SHOW_IN_RANDOM); $questionstoshow = $DB->get_records_list('question', 'id', $questionidstoshow, - '', 'id,qtype,name,questiontext,questiontextformat'); + '', 'id, qtype, name, questiontext, questiontextformat'); // list them, echo '
    '; @@ -829,7 +874,6 @@ function quiz_print_randomquestion(&$question, &$pageurl, &$quiz, $quiz_qbanktoo echo '
    '; echo '
    '; echo '
'; - } /** @@ -860,14 +904,15 @@ function quiz_print_singlequestion_reordertool($question, $returnurl, $quiz) { * @param object $quiz The quiz in the context of which the question is being displayed */ function quiz_print_randomquestion_reordertool(&$question, &$pageurl, &$quiz) { - global $DB, $QTYPES, $OUTPUT; + global $DB, $OUTPUT; // Load the category, and the number of available questions in it. if (!$category = $DB->get_record('question_categories', array('id' => $question->category))) { echo $OUTPUT->notification('Random question category not found!'); return; } - $questioncount = count($QTYPES['random']->get_usable_questions_from_category( + $questioncount = count(question_bank::get_qtype( + 'random')->get_available_questions_from_category( $category->id, $question->questiontext == '1', '0')); $reordercheckboxlabel = '