Browse files

Merge branch 'MOODLE_25_STABLE' into install_25_STABLE

  • Loading branch information...
2 parents d43d161 + dd99fdd commit 441678ecb469b9460107069c483c294d7e2e485c AMOS bot committed Aug 24, 2013
Showing with 1,000 additions and 141 deletions.
  1. +30 −15 backup/import.php
  2. +24 −18 backup/moodle2/restore_qtype_plugin.class.php
  3. +0 −1 badges/lib/awardlib.php
  4. +0 −1 badges/renderer.php
  5. +1 −0 cache/stores/file/lib.php
  6. +1 −7 calendar/lib.php
  7. +71 −0 calendar/tests/lib_test.php
  8. +2 −2 config-dist.php
  9. +3 −1 course/lib.php
  10. +2 −2 enrol/flatfile/lib.php
  11. +29 −0 enrol/tests/enrollib_test.php
  12. +2 −1 enrol/yui/rolemanager/assets/skins/sam/rolemanager.css
  13. +5 −1 enrol/yui/rolemanager/rolemanager.js
  14. +2 −1 lib/badgeslib.php
  15. +1 −1 lib/dml/mssql_native_moodle_database.php
  16. +1 −1 lib/dml/sqlsrv_native_moodle_database.php
  17. +32 −4 lib/dml/tests/dml_test.php
  18. +14 −4 lib/formslib.php
  19. +24 −3 lib/modinfolib.php
  20. +2 −2 lib/navigationlib.php
  21. +15 −0 lib/phpmailer/moodle_phpmailer.php
  22. +13 −0 lib/phpunit/classes/advanced_testcase.php
  23. +87 −0 lib/phpunit/classes/phpmailer_sink.php
  24. +55 −0 lib/phpunit/classes/util.php
  25. +1 −0 lib/phpunit/lib.php
  26. +1 −1 lib/pluginlib.php
  27. +1 −1 lib/simplepie/moodle_simplepie.php
  28. +1 −0 mod/assign/tests/generator/lib.php
  29. +6 −3 mod/assign/tests/lib_test.php
  30. +11 −10 mod/forum/lib.php
  31. +126 −0 mod/forum/tests/lib_test.php
  32. +3 −4 mod/quiz/renderer.php
  33. +13 −1 mod/scorm/datamodels/aicclib.php
  34. +17 −2 mod/scorm/db/upgrade.php
  35. +1 −1 mod/scorm/version.php
  36. +7 −7 question/behaviour/manualgraded/tests/walkthrough_test.php
  37. +6 −2 question/category.php
  38. +17 −12 question/category_class.php
  39. +16 −3 question/category_form.php
  40. +44 −10 question/engine/datalib.php
  41. +11 −4 question/engine/questionattempt.php
  42. +1 −1 question/engine/questionattemptstep.php
  43. +2 −2 question/type/essay/question.php
  44. +23 −0 question/type/essay/tests/helper.php
  45. +3 −3 question/type/essay/tests/question_test.php
  46. +218 −1 question/type/essay/tests/walkthrough_test.php
  47. +19 −0 question/type/match/db/upgrade.php
  48. +4 −2 question/type/multianswer/module.js
  49. +7 −0 question/type/multianswer/styles.css
  50. +0 −2 question/type/numerical/edit_numerical_form.php
  51. +2 −0 question/type/numerical/styles.css
  52. +1 −0 theme/base/style/admin.css
  53. +1 −0 theme/base/style/core.css
  54. +3 −0 theme/bootstrapbase/less/moodle/core.less
  55. +8 −0 theme/bootstrapbase/less/moodle/forms.less
  56. +6 −0 theme/bootstrapbase/less/moodle/question.less
  57. +2 −2 theme/bootstrapbase/style/moodle.css
  58. +2 −2 version.php
View
45 backup/import.php
@@ -113,31 +113,46 @@
// Mark the UI finished.
$rc->finish_ui();
// Execute prechecks
+ $warnings = false;
if (!$rc->execute_precheck()) {
$precheckresults = $rc->get_precheck_results();
- if (is_array($precheckresults) && !empty($precheckresults['errors'])) {
- fulldelete($tempdestination);
-
- echo $OUTPUT->header();
- echo $renderer->precheck_notices($precheckresults);
- echo $OUTPUT->continue_button(new moodle_url('/course/view.php', array('id'=>$course->id)));
- echo $OUTPUT->footer();
- die();
+ if (is_array($precheckresults)) {
+ if (!empty($precheckresults['errors'])) { // If errors are found, terminate the import.
+ fulldelete($tempdestination);
+
+ echo $OUTPUT->header();
+ echo $renderer->precheck_notices($precheckresults);
+ echo $OUTPUT->continue_button(new moodle_url('/course/view.php', array('id'=>$course->id)));
+ echo $OUTPUT->footer();
+ die();
+ }
+ if (!empty($precheckresults['warnings'])) { // If warnings are found, go ahead but display warnings later.
+ $warnings = $precheckresults['warnings'];
+ }
}
- } else {
- if ($restoretarget == backup::TARGET_CURRENT_DELETING || $restoretarget == backup::TARGET_EXISTING_DELETING) {
- restore_dbops::delete_course_content($course->id);
- }
- // Execute the restore
- $rc->execute_plan();
}
+ if ($restoretarget == backup::TARGET_CURRENT_DELETING || $restoretarget == backup::TARGET_EXISTING_DELETING) {
+ restore_dbops::delete_course_content($course->id);
+ }
+ // Execute the restore.
+ $rc->execute_plan();
// Delete the temp directory now
fulldelete($tempdestination);
// Display a notification and a continue button
echo $OUTPUT->header();
- echo $OUTPUT->notification(get_string('importsuccess', 'backup'),'notifysuccess');
+ if ($warnings) {
+ echo $OUTPUT->box_start();
+ echo $OUTPUT->notification(get_string('warning'), 'notifywarning');
+ echo html_writer::start_tag('ul', array('class'=>'list'));
+ foreach ($warnings as $warning) {
+ echo html_writer::tag('li', $warning);
+ }
+ echo html_writer::end_tag('ul');
+ echo $OUTPUT->box_end();
+ }
+ echo $OUTPUT->notification(get_string('importsuccess', 'backup'), 'notifysuccess');
echo $OUTPUT->continue_button(new moodle_url('/course/view.php', array('id'=>$course->id)));
echo $OUTPUT->footer();
View
42 backup/moodle2/restore_qtype_plugin.class.php
@@ -35,6 +35,18 @@
*/
abstract class restore_qtype_plugin extends restore_plugin {
+ /*
+ * A simple answer to id cache for a single questions answers.
+ * @var array
+ */
+ private $questionanswercache = array();
+
+ /*
+ * The id of the current question in the questionanswercache.
+ * @var int
+ */
+ private $questionanswercacheid = null;
+
/**
* Add to $paths the restore_path_elements needed
* to handle question_answers for a given question
@@ -147,38 +159,32 @@ public function process_question_answer($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_answers}
- WHERE question = ?
- AND ' . $DB->sql_compare_text('answer', 255) . ' = ' . $DB->sql_compare_text('?', 255);
- $params = array($newquestionid, $data->answertext);
- $newitemid = $DB->get_field_sql($sql, $params);
-
- // Not able to find the answer, let's try cleaning the answertext
- // of all the question answers in DB as slower fallback. MDL-30018.
- if (!$newitemid) {
+ // Have we cached the current question?
+ if ($this->questionanswercacheid !== $newquestionid) {
+ // The question changed, purge and start again!
+ $this->questionanswercache = array();
$params = array('question' => $newquestionid);
$answers = $DB->get_records('question_answers', $params, '', 'id, answer');
+ $this->questionanswercacheid = $newquestionid;
+ // Cache all cleaned answers for a simple text match.
foreach ($answers as $answer) {
- // Clean in the same way than {@link xml_writer::xml_safe_utf8()}.
+ // MDL-30018: Clean in the same way as {@link xml_writer::xml_safe_utf8()}.
$clean = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is','', $answer->answer); // Clean CTRL chars.
$clean = preg_replace("/\r\n|\r/", "\n", $clean); // Normalize line ending.
- if ($clean === $data->answertext) {
- $newitemid = $data->id;
- }
+ $this->questionanswercache[$clean] = $answer->id;
}
}
- // If we haven't found the newitemid, something has gone really wrong, question in DB
- // is missing answers, exception
- if (!$newitemid) {
+ if (!isset($this->questionanswercache[$data->answertext])) {
+ // If we haven't found the matching answer, something has gone really wrong, the question in the DB
+ // is missing answers, throw an exception.
$info = new stdClass();
$info->filequestionid = $oldquestionid;
$info->dbquestionid = $newquestionid;
$info->answer = $data->answertext;
throw new restore_step_exception('error_question_answers_missing_in_db', $info);
}
+ $newitemid = $this->questionanswercache[$data->answertext];
}
// Create mapping (we'll use this intensively when restoring question_states. And also answerfeedback files)
$this->set_mapping('question_answer', $oldid, $newitemid);
View
1 badges/lib/awardlib.php
@@ -26,7 +26,6 @@
defined('MOODLE_INTERNAL') || die();
-require_once(dirname(dirname(dirname(__FILE__))) . '/config.php');
require_once($CFG->libdir . '/badgeslib.php');
require_once($CFG->dirroot . '/user/selector/lib.php');
View
1 badges/renderer.php
@@ -26,7 +26,6 @@
require_once($CFG->libdir . '/badgeslib.php');
require_once($CFG->libdir . '/tablelib.php');
-require_once($CFG->dirroot . '/user/filters/lib.php');
/**
* Standard HTML output renderer for badges
View
1 cache/stores/file/lib.php
@@ -336,6 +336,7 @@ public function get($key) {
$filename = $key.'.cache';
$file = $this->file_path_for_key($key);
$ttl = $this->definition->get_ttl();
+ $maxtime = 0;
if ($ttl) {
$maxtime = cache::now() - $ttl;
}
View
8 calendar/lib.php
@@ -1391,14 +1391,8 @@ function calendar_get_module_cached(&$coursecache, $modulename, $instance) {
* @return stdClass $coursecache[$courseid] return the specific course cache
*/
function calendar_get_course_cached(&$coursecache, $courseid) {
- global $COURSE, $DB;
-
if (!isset($coursecache[$courseid])) {
- if ($courseid == $COURSE->id) {
- $coursecache[$courseid] = $COURSE;
- } else {
- $coursecache[$courseid] = $DB->get_record('course', array('id'=>$courseid));
- }
+ $coursecache[$courseid] = get_course($courseid);
}
return $coursecache[$courseid];
}
View
71 calendar/tests/lib_test.php
@@ -0,0 +1,71 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Calendar lib unit tests
+ *
+ * @package core_calendar
+ * @copyright 2013 Dan Poltawski <dan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+global $CFG;
+require_once($CFG->dirroot . '/calendar/lib.php');
+
+/**
+ * Unit tests for calendar lib
+ *
+ * @package core_calendar
+ * @copyright 2013 Dan Poltawski <dan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_calendar_lib_testcase extends advanced_testcase {
+
+ public function test_calendar_get_course_cached() {
+ $this->resetAfterTest(true);
+
+ // Setup some test courses.
+ $course1 = $this->getDataGenerator()->create_course();
+ $course2 = $this->getDataGenerator()->create_course();
+ $course3 = $this->getDataGenerator()->create_course();
+
+ // Load courses into cache.
+ $coursecache = null;
+ calendar_get_course_cached($coursecache, $course1->id);
+ calendar_get_course_cached($coursecache, $course2->id);
+ calendar_get_course_cached($coursecache, $course3->id);
+
+ // Verify the cache.
+ $this->assertArrayHasKey($course1->id, $coursecache);
+ $cachedcourse1 = $coursecache[$course1->id];
+ $this->assertEquals($course1->id, $cachedcourse1->id);
+ $this->assertEquals($course1->shortname, $cachedcourse1->shortname);
+ $this->assertEquals($course1->fullname, $cachedcourse1->fullname);
+
+ $this->assertArrayHasKey($course2->id, $coursecache);
+ $cachedcourse2 = $coursecache[$course2->id];
+ $this->assertEquals($course2->id, $cachedcourse2->id);
+ $this->assertEquals($course2->shortname, $cachedcourse2->shortname);
+ $this->assertEquals($course2->fullname, $cachedcourse2->fullname);
+
+ $this->assertArrayHasKey($course3->id, $coursecache);
+ $cachedcourse3 = $coursecache[$course3->id];
+ $this->assertEquals($course3->id, $cachedcourse3->id);
+ $this->assertEquals($course3->shortname, $cachedcourse3->shortname);
+ $this->assertEquals($course3->fullname, $cachedcourse3->fullname);
+ }
+}
View
4 config-dist.php
@@ -480,10 +480,10 @@
// $CFG->debugusers = '2';
//
// Prevent theme caching
-// $CFG->themerev = -1; // NOT FOR PRODUCTION SERVERS!
+// $CFG->themedesignermode = true; // NOT FOR PRODUCTION SERVERS!
//
// Prevent JS caching
-// $CFG->jsrev = -1; // NOT FOR PRODUCTION SERVERS!
+// $CFG->cachejs = false; // NOT FOR PRODUCTION SERVERS!
//
// Prevent core_string_manager on-disk cache
// $CFG->langstringcache = false; // NOT FOR PRODUCTION SERVERS!
View
4 course/lib.php
@@ -959,7 +959,9 @@ function get_array_of_activities($courseid) {
$mod[$seq]->extraclasses = $info->extraclasses;
}
if (!empty($info->iconurl)) {
- $mod[$seq]->iconurl = $info->iconurl;
+ // Convert URL to string as it's easier to store. Also serialized object contains \0 byte and can not be written to Postgres DB.
+ $url = new moodle_url($info->iconurl);
+ $mod[$seq]->iconurl = $url->out(false);
}
if (!empty($info->onclick)) {
$mod[$seq]->onclick = $info->onclick;
View
4 enrol/flatfile/lib.php
@@ -311,8 +311,8 @@ protected function process_file(progress_trace $trace) {
}
$roleid = $rolemap[$fields[1]];
- if (empty($fields[2]) or !$user = $DB->get_record("user", array("idnumber"=>$fields[2]))) {
- $trace->output("Unknown user idnumber in field 3 - ignoring line $line", 1);
+ if (empty($fields[2]) or !$user = $DB->get_record("user", array("idnumber"=>$fields[2], 'deleted'=>0))) {
+ $trace->output("Unknown user idnumber or deleted user in field 3 - ignoring line $line", 1);
continue;
}
View
29 enrol/tests/enrollib_test.php
@@ -250,4 +250,33 @@ public function test_enrol_user_sees_own_courses() {
$this->assertTrue(enrol_user_sees_own_courses());
$this->assertEquals($reads, $DB->perf_get_reads());
}
+
+ public function test_enrol_get_shared_courses() {
+ $this->resetAfterTest();
+
+ $user1 = $this->getDataGenerator()->create_user();
+ $user2 = $this->getDataGenerator()->create_user();
+ $user3 = $this->getDataGenerator()->create_user();
+
+ $course1 = $this->getDataGenerator()->create_course();
+ $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
+ $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
+
+ $course2 = $this->getDataGenerator()->create_course();
+ $this->getDataGenerator()->enrol_user($user1->id, $course2->id);
+
+ // Test that user1 and user2 have courses in common.
+ $this->assertTrue(enrol_get_shared_courses($user1, $user2, false, true));
+ // Test that user1 and user3 have no courses in common.
+ $this->assertFalse(enrol_get_shared_courses($user1, $user3, false, true));
+
+ // Test retrieving the courses in common.
+ $sharedcourses = enrol_get_shared_courses($user1, $user2, true);
+
+ // Only should be one shared course.
+ $this->assertCount(1, $sharedcourses);
+ $sharedcourse = array_shift($sharedcourses);
+ // It should be course 1.
+ $this->assertEquals($sharedcourse->id, $course1->id);
+ }
}
View
3 enrol/yui/rolemanager/assets/skins/sam/rolemanager.css
@@ -5,4 +5,5 @@
.enrolpanel .container .header h2 {font-size:90%;text-align:center;margin:5px;}
.enrolpanel .container .header .close {width:25px;height:15px;position:absolute;top:5px;right:1em;cursor:pointer;background:url("sprite.png") no-repeat scroll 0 0 transparent;}
.enrolpanel .container .content {}
-.enrolpanel .container .content input {margin:5px;font-size:10px;}
+.enrolpanel .container .content input {margin:5px;font-size:10px;}
+.enrolpanel.roleassign.visible .container {width:auto;}
View
6 enrol/yui/rolemanager/rolemanager.js
@@ -381,7 +381,11 @@ YUI.add('moodle-enrol-rolemanager', function(Y) {
var roles = this.user.get(CONTAINER).one('.col_role .roles');
var x = roles.getX() + 10;
var y = roles.getY() + this.user.get(CONTAINER).get('offsetHeight') - 10;
- this.get('elementNode').setStyle('left', x).setStyle('top', y);
+ if ( Y.one(document.body).hasClass('dir-rtl') ) {
+ this.get('elementNode').setStyle('right', x - 20).setStyle('top', y);
+ } else {
+ this.get('elementNode').setStyle('left', x).setStyle('top', y);
+ }
this.get('elementNode').addClass('visible');
this.escCloseEvent = Y.on('key', this.hide, document.body, 'down:27', this);
this.displayed = true;
View
3 lib/badgeslib.php
@@ -1106,7 +1106,8 @@ function print_badge_image(badge $badge, stdClass $context, $size = 'small') {
$imageurl = moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id, '/', $fsize, false);
// Appending a random parameter to image link to forse browser reload the image.
- $attributes = array('src' => $imageurl . '?' . rand(1, 10000), 'alt' => s($badge->name), 'class' => 'activatebadge');
+ $imageurl->param('refresh', rand(1, 10000));
+ $attributes = array('src' => $imageurl, 'alt' => s($badge->name), 'class' => 'activatebadge');
return html_writer::empty_tag('img', $attributes);
}
View
2 lib/dml/mssql_native_moodle_database.php
@@ -1227,7 +1227,7 @@ public function sql_length($fieldname) {
}
public function sql_order_by_text($fieldname, $numchars=32) {
- return ' CONVERT(varchar, ' . $fieldname . ', ' . $numchars . ')';
+ return " CONVERT(varchar({$numchars}), {$fieldname})";
}
/**
View
2 lib/dml/sqlsrv_native_moodle_database.php
@@ -1289,7 +1289,7 @@ public function sql_length($fieldname) {
}
public function sql_order_by_text($fieldname, $numchars = 32) {
- return ' CONVERT(varchar, '.$fieldname.', '.$numchars.')';
+ return " CONVERT(varchar({$numchars}), {$fieldname})";
}
/**
View
36 lib/dml/tests/dml_test.php
@@ -3573,7 +3573,7 @@ function test_cast_char2real() {
$this->assertEquals(next($records)->nametext, '91.10');
}
- function sql_compare_text() {
+ public function test_sql_compare_text() {
$DB = $this->tdb;
$dbman = $DB->get_manager();
@@ -3588,15 +3588,43 @@ function sql_compare_text() {
$DB->insert_record($tablename, array('name'=>'abcd', 'description'=>'abcd'));
$DB->insert_record($tablename, array('name'=>'abcdef', 'description'=>'bbcdef'));
- $DB->insert_record($tablename, array('name'=>'aaaabb', 'description'=>'aaaacccccccccccccccccc'));
+ $DB->insert_record($tablename, array('name'=>'aaaa', 'description'=>'aaaacccccccccccccccccc'));
+ $DB->insert_record($tablename, array('name'=>'xxxx', 'description'=>'123456789a123456789b123456789c123456789d'));
+ // Only some supported databases truncate TEXT fields for comparisons, currently MSSQL and Oracle.
+ $dbtruncatestextfields = ($DB->get_dbfamily() == 'mssql' || $DB->get_dbfamily() == 'oracle');
+
+ if ($dbtruncatestextfields) {
+ // Ensure truncation behaves as expected.
+
+ $sql = "SELECT " . $DB->sql_compare_text('description') . " AS field FROM {{$tablename}} WHERE name = ?";
+ $description = $DB->get_field_sql($sql, array('xxxx'));
+
+ // Should truncate to 32 chars (the default).
+ $this->assertEquals('123456789a123456789b123456789c12', $description);
+
+ $sql = "SELECT " . $DB->sql_compare_text('description', 35) . " AS field FROM {{$tablename}} WHERE name = ?";
+ $description = $DB->get_field_sql($sql, array('xxxx'));
+
+ // Should truncate to the specified number of chars.
+ $this->assertEquals('123456789a123456789b123456789c12345', $description);
+ }
+
+ // Ensure text field comparison is successful.
$sql = "SELECT * FROM {{$tablename}} WHERE name = ".$DB->sql_compare_text('description');
$records = $DB->get_records_sql($sql);
- $this->assertEquals(count($records), 1);
+ $this->assertCount(1, $records);
$sql = "SELECT * FROM {{$tablename}} WHERE name = ".$DB->sql_compare_text('description', 4);
$records = $DB->get_records_sql($sql);
- $this->assertEquals(count($records), 2);
+ if ($dbtruncatestextfields) {
+ // Should truncate description to 4 characters before comparing.
+ $this->assertCount(2, $records);
+ } else {
+ // Should leave untruncated, so one less match.
+ $this->assertCount(1, $records);
+ }
+
}
function test_unique_index_collation_trouble() {
View
18 lib/formslib.php
@@ -2136,25 +2136,31 @@ function qf_errorHandler(element, _qfMsg) {
return true;
}
+
if (_qfMsg != \'\') {
var errorSpan = document.getElementById(\'id_error_\'+element.name);
if (!errorSpan) {
errorSpan = document.createElement("span");
errorSpan.id = \'id_error_\'+element.name;
errorSpan.className = "error";
element.parentNode.insertBefore(errorSpan, element.parentNode.firstChild);
+ document.getElementById(errorSpan.id).setAttribute(\'TabIndex\', \'0\');
+ document.getElementById(errorSpan.id).focus();
}
while (errorSpan.firstChild) {
errorSpan.removeChild(errorSpan.firstChild);
}
errorSpan.appendChild(document.createTextNode(_qfMsg.substring(3)));
- errorSpan.appendChild(document.createElement("br"));
if (div.className.substr(div.className.length - 6, 6) != " error"
- && div.className != "error") {
- div.className += " error";
+ && div.className != "error") {
+ div.className += " error";
+ linebreak = document.createElement("br");
+ linebreak.className = "error";
+ linebreak.id = \'id_error_break_\'+element.name;
+ errorSpan.parentNode.insertBefore(linebreak, errorSpan.nextSibling);
}
return false;
@@ -2163,6 +2169,10 @@ function qf_errorHandler(element, _qfMsg) {
if (errorSpan) {
errorSpan.parentNode.removeChild(errorSpan);
}
+ var linebreak = document.getElementById(\'id_error_break_\'+element.name);
+ if (linebreak) {
+ linebreak.parentNode.removeChild(linebreak);
+ }
if (div.className.substr(div.className.length - 6, 6) == " error") {
div.className = div.className.substr(0, div.className.length - 6);
@@ -2210,7 +2220,7 @@ function validate_' . $this->_formName . '_' . $escapedElementName . '(element)
ret = validate_' . $this->_formName . '_' . $escapedElementName.'(frm.elements[\''.$elementName.'\']) && ret;
if (!ret && !first_focus) {
first_focus = true;
- frm.elements[\''.$elementName.'\'].focus();
+ document.getElementById(\'id_error_'.$elementName.'\').focus();
}
';
View
27 lib/modinfolib.php
@@ -242,7 +242,7 @@ public function get_section_info($sectionnumber, $strictness = IGNORE_MISSING) {
* @param int $userid User ID
*/
public function __construct($course, $userid) {
- global $CFG, $DB;
+ global $CFG, $DB, $COURSE, $SITE;
// Check modinfo field is set. If not, build and load it.
if (empty($course->modinfo) || empty($course->sectioncache)) {
@@ -287,9 +287,29 @@ public function __construct($course, $userid) {
}
}
- // If we haven't already preloaded contexts for the course, do it now
+ // If we haven't already preloaded contexts for the course, do it now.
+ // Modules are also cached here as long as it's the first time this course has been preloaded.
preload_course_contexts($course->id);
+ // Quick integrity check: as a result of race conditions modinfo may not be regenerated after the change.
+ // It is especially dangerous if modinfo contains the deleted course module, as it results in fatal error.
+ // We can check it very cheap by validating the existence of module context.
+ if ($course->id == $COURSE->id || $course->id == $SITE->id) {
+ // Only verify current course (or frontpage) as pages with many courses may not have module contexts cached.
+ // (Uncached modules will result in a very slow verification).
+ foreach ($info as $mod) {
+ if (!context_module::instance($mod->cm, IGNORE_MISSING)) {
+ debugging('Course cache integrity check failed: course module with id '. $mod->cm.
+ ' does not have context. Rebuilding cache for course '. $course->id);
+ rebuild_course_cache($course->id);
+ $this->course = $DB->get_record('course', array('id' => $course->id), '*', MUST_EXIST);
+ $info = unserialize($this->course->modinfo);
+ $sectioncache = unserialize($this->course->sectioncache);
+ break;
+ }
+ }
+ }
+
// Loop through each piece of module data, constructing it
$modexists = array();
foreach ($info as $mod) {
@@ -1079,7 +1099,8 @@ public function __construct(course_modinfo $modinfo, $course, $mod, $info) {
$this->indent = isset($mod->indent) ? $mod->indent : 0;
$this->extra = isset($mod->extra) ? $mod->extra : '';
$this->extraclasses = isset($mod->extraclasses) ? $mod->extraclasses : '';
- $this->iconurl = isset($mod->iconurl) ? $mod->iconurl : '';
+ // iconurl may be stored as either string or instance of moodle_url.
+ $this->iconurl = isset($mod->iconurl) ? new moodle_url($mod->iconurl) : '';
$this->onclick = isset($mod->onclick) ? $mod->onclick : '';
$this->content = isset($mod->content) ? $mod->content : '';
$this->icon = isset($mod->icon) ? $mod->icon : '';
View
4 lib/navigationlib.php
@@ -3737,7 +3737,7 @@ protected function load_module_settings() {
require_once($file);
}
- $modulenode = $this->add(get_string('pluginadministration', $this->page->activityname));
+ $modulenode = $this->add(get_string('pluginadministration', $this->page->activityname), null, self::TYPE_SETTING, null, 'modulesettings');
$modulenode->force_open();
// Settings for the module
@@ -4144,7 +4144,7 @@ protected function generate_user_settings($courseid, $userid, $gstitle='usercurr
protected function load_block_settings() {
global $CFG;
- $blocknode = $this->add(print_context_name($this->context));
+ $blocknode = $this->add(print_context_name($this->context), null, self::TYPE_SETTING, null, 'blocksettings');
$blocknode->force_open();
// Assign local roles
View
15 lib/phpmailer/moodle_phpmailer.php
@@ -125,4 +125,19 @@ public function EncodeQP($string, $line_max = 76, $space_conv = false) {
fclose($fp);
return $out;
}
+
+ protected function PostSend() {
+ // Now ask phpunit if it wants to catch this message.
+ if (PHPUNIT_TEST && phpunit_util::is_redirecting_messages()) {
+ $mail = new stdClass();
+ $mail->header = $this->MIMEHeader;
+ $mail->body = $this->MIMEBody;
+ $mail->subject = $this->Subject;
+ $mail->from = $this->From;
+ phpunit_util::phpmailer_sent($mail);
+ return true;
+ } else {
+ return parent::PostSend();
+ }
+ }
}
View
13 lib/phpunit/classes/advanced_testcase.php
@@ -320,6 +320,19 @@ public function redirectMessages() {
}
/**
+ * Starts email redirection.
+ *
+ * You can verify if email were sent or not by inspecting the email
+ * array in the returned phpmailer sink instance. The redirection
+ * can be stopped by calling $sink->close();
+ *
+ * @return phpunit_message_sink
+ */
+ public function redirectEmails() {
+ return phpunit_util::start_phpmailer_redirection();
+ }
+
+ /**
* Cleanup after all tests are executed.
*
* Note: do not forget to call this if overridden...
View
87 lib/phpunit/classes/phpmailer_sink.php
@@ -0,0 +1,87 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * phpmailer message sink.
+ *
+ * @package core
+ * @category phpunit
+ * @copyright 2013 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+/**
+ * phpmailer message sink.
+ *
+ * @package core
+ * @category phpunit
+ * @copyright 2013 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class phpunit_phpmailer_sink {
+ /**
+ * @var array of records which would have been sent by phpmailer.
+ */
+ protected $messages = array();
+
+ /**
+ * Stop message redirection.
+ *
+ * Use if you do not want message redirected any more.
+ */
+ public function close() {
+ phpunit_util::stop_phpmailer_redirection();
+ }
+
+ /**
+ * To be called from phpunit_util only!
+ *
+ * @param stdClass $message record from message_read table
+ */
+ public function add_message($message) {
+ /* Number messages from 0. */
+ $this->messages[] = $message;
+ }
+
+ /**
+ * Returns all redirected messages.
+ *
+ * The instances are records form the message_read table.
+ * The array indexes are numbered from 0 and the order is matching
+ * the creation of events.
+ *
+ * @return array
+ */
+ public function get_messages() {
+ return $this->messages;
+ }
+
+ /**
+ * Return number of messages redirected to this sink.
+ * @return int
+ */
+ public function count() {
+ return count($this->messages);
+ }
+
+ /**
+ * Removes all previously stored messages.
+ */
+ public function clear() {
+ $this->messages = array();
+ }
+}
View
55 lib/phpunit/classes/util.php
@@ -43,6 +43,9 @@ class phpunit_util extends testing_util {
/** @var phpunit_message_sink alternative target for moodle messaging */
protected static $messagesink = null;
+ /** @var phpunit_phpmailer_sink alternative target for phpmailer messaging */
+ protected static $phpmailersink = null;
+
/**
* @var array Files to skip when resetting dataroot folder
*/
@@ -95,6 +98,9 @@ public static function reset_all_data($detectchanges = false) {
// Stop any message redirection.
phpunit_util::stop_message_redirection();
+ // Stop any message redirection.
+ phpunit_util::stop_phpmailer_redirection();
+
// Release memory and indirectly call destroy() methods to release resource handles, etc.
gc_collect_cycles();
@@ -660,4 +666,53 @@ public static function message_sent($message) {
self::$messagesink->add_message($message);
}
}
+
+ /**
+ * Start phpmailer redirection.
+ *
+ * Note: Do not call directly from tests,
+ * use $sink = $this->redirectEmails() instead.
+ *
+ * @return phpunit_phpmailer_sink
+ */
+ public static function start_phpmailer_redirection() {
+ if (self::$phpmailersink) {
+ self::stop_phpmailer_redirection();
+ }
+ self::$phpmailersink = new phpunit_phpmailer_sink();
+ return self::$phpmailersink;
+ }
+
+ /**
+ * End phpmailer redirection.
+ *
+ * Note: Do not call directly from tests,
+ * use $sink->close() instead.
+ */
+ public static function stop_phpmailer_redirection() {
+ self::$phpmailersink = null;
+ }
+
+ /**
+ * Are messages for phpmailer redirected to some sink?
+ *
+ * Note: to be called from moodle_phpmailer.php only!
+ *
+ * @return bool
+ */
+ public static function is_redirecting_phpmailer() {
+ return !empty(self::$phpmailersink);
+ }
+
+ /**
+ * To be called from messagelib.php only!
+ *
+ * @param stdClass $message record from message_read table
+ * @return bool true means send message, false means message "sent" to sink.
+ */
+ public static function phpmailer_sent($message) {
+ if (self::$phpmailersink) {
+ self::$phpmailersink->add_message($message);
+ }
+ }
}
View
1 lib/phpunit/lib.php
@@ -30,6 +30,7 @@
require_once(__DIR__.'/classes/util.php');
require_once(__DIR__.'/classes/message_sink.php');
+require_once(__DIR__.'/classes/phpmailer_sink.php');
require_once(__DIR__.'/classes/basic_testcase.php');
require_once(__DIR__.'/classes/database_driver_testcase.php');
require_once(__DIR__.'/classes/arraydataset.php');
View
2 lib/pluginlib.php
@@ -3301,7 +3301,7 @@ public function init_display_name() {
*/
protected function load_version_php($disablecache=false) {
- $cache = cache::make('core', 'plugininfo_base');
+ $cache = cache::make('core', 'plugininfo_mod');
$versionsphp = $cache->get('versions_php');
View
2 lib/simplepie/moodle_simplepie.php
@@ -153,7 +153,7 @@ public function __construct($url, $timeout = 10, $redirects = 5, $headers = null
if ($parser->parse()) {
$this->headers = $parser->headers;
- $this->body = $parser->body;
+ $this->body = trim($parser->body);
$this->status_code = $parser->status_code;
View
1 mod/assign/tests/generator/lib.php
@@ -76,6 +76,7 @@ public function create_instance($record = null, array $options = null) {
$record->coursemodule = $this->precreate_course_module($record->course, $options);
$id = assign_add_instance($record, null);
+ rebuild_course_cache($record->course, true);
return $this->post_add_instance($id, $record->coursemodule);
}
}
View
9 mod/assign/tests/lib_test.php
@@ -40,23 +40,26 @@
class mod_assign_lib_testcase extends mod_assign_base_testcase {
public function test_assign_print_overview() {
+ global $DB;
$this->setUser($this->editingteachers[0]);
$this->create_instance();
$this->create_instance(array('duedate'=>time()));
+ $courses = $DB->get_records('course', array('id' => $this->course->id));
+
$this->setUser($this->students[0]);
$overview = array();
- assign_print_overview(array($this->course->id => $this->course), $overview);
+ assign_print_overview($courses, $overview);
$this->assertEquals(count($overview), 1);
$this->setUser($this->teachers[0]);
$overview = array();
- assign_print_overview(array($this->course->id => $this->course), $overview);
+ assign_print_overview($courses, $overview);
$this->assertEquals(count($overview), 1);
$this->setUser($this->editingteachers[0]);
$overview = array();
- assign_print_overview(array($this->course->id => $this->course), $overview);
+ assign_print_overview($courses, $overview);
$this->assertEquals(1, count($overview));
}
View
21 mod/forum/lib.php
@@ -8149,16 +8149,18 @@ function forum_get_courses_user_posted_in($user, $discussionsonly = false, $incl
// table and join to the userid there. If we are looking for posts then we need
// to join to the forum_posts table.
if (!$discussionsonly) {
- $joinsql = 'JOIN {forum_discussions} fd ON fd.course = c.id
- JOIN {forum_posts} fp ON fp.discussion = fd.id';
- $wheresql = 'fp.userid = :userid';
- $params = array('userid' => $user->id);
+ $subquery = "(SELECT DISTINCT fd.course
+ FROM {forum_discussions} fd
+ JOIN {forum_posts} fp ON fp.discussion = fd.id
+ WHERE fp.userid = :userid )";
} else {
- $joinsql = 'JOIN {forum_discussions} fd ON fd.course = c.id';
- $wheresql = 'fd.userid = :userid';
- $params = array('userid' => $user->id);
+ $subquery= "(SELECT DISTINCT fd.course
+ FROM {forum_discussions} fd
+ WHERE fd.userid = :userid )";
}
+ $params = array('userid' => $user->id);
+
// Join to the context table so that we can preload contexts if required.
if ($includecontexts) {
list($ctxselect, $ctxjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
@@ -8169,11 +8171,10 @@ function forum_get_courses_user_posted_in($user, $discussionsonly = false, $incl
// Now we need to get all of the courses to search.
// All courses where the user has posted within a forum will be returned.
- $sql = "SELECT DISTINCT c.* $ctxselect
+ $sql = "SELECT c.* $ctxselect
FROM {course} c
- $joinsql
$ctxjoin
- WHERE $wheresql";
+ WHERE c.id IN ($subquery)";
$courses = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
if ($includecontexts) {
array_map('context_instance_preload', $courses);
View
126 mod/forum/tests/lib_test.php
@@ -0,0 +1,126 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Forum library tests.
+ *
+ * @package mod_forum
+ * @copyright 2013 Dan Poltawski <dan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+require_once($CFG->dirroot . '/mod/forum/lib.php');
+
+class mod_forum_lib_testcase extends advanced_testcase {
+
+ public function test_forum_get_courses_user_posted_in() {
+ $this->resetAfterTest();
+
+ $user1 = $this->getDataGenerator()->create_user();
+ $user2 = $this->getDataGenerator()->create_user();
+ $user3 = $this->getDataGenerator()->create_user();
+
+ $course1 = $this->getDataGenerator()->create_course();
+ $course2 = $this->getDataGenerator()->create_course();
+ $course3 = $this->getDataGenerator()->create_course();
+
+ // Create 3 forums, one in each course.
+ $record = new stdClass();
+ $record->course = $course1->id;
+ $forum1 = $this->getDataGenerator()->create_module('forum', $record);
+
+ $record = new stdClass();
+ $record->course = $course2->id;
+ $forum2 = $this->getDataGenerator()->create_module('forum', $record);
+
+ $record = new stdClass();
+ $record->course = $course3->id;
+ $forum3 = $this->getDataGenerator()->create_module('forum', $record);
+
+ // Add a second forum in course 1.
+ $record = new stdClass();
+ $record->course = $course1->id;
+ $forum4 = $this->getDataGenerator()->create_module('forum', $record);
+
+ // Add discussions to course 1 started by user1.
+ $record = new stdClass();
+ $record->course = $course1->id;
+ $record->userid = $user1->id;
+ $record->forum = $forum1->id;
+ $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+ $record = new stdClass();
+ $record->course = $course1->id;
+ $record->userid = $user1->id;
+ $record->forum = $forum4->id;
+ $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+ // Add discussions to course2 started by user1.
+ $record = new stdClass();
+ $record->course = $course2->id;
+ $record->userid = $user1->id;
+ $record->forum = $forum2->id;
+ $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+ // Add discussions to course 3 started by user2.
+ $record = new stdClass();
+ $record->course = $course3->id;
+ $record->userid = $user2->id;
+ $record->forum = $forum3->id;
+ $discussion3 = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+ // Add post to course 3 by user1.
+ $record = new stdClass();
+ $record->course = $course3->id;
+ $record->userid = $user1->id;
+ $record->forum = $forum3->id;
+ $record->discussion = $discussion3->id;
+ $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+
+ // User 3 hasn't posted anything, so shouldn't get any results.
+ $user3courses = forum_get_courses_user_posted_in($user3);
+ $this->assertEmpty($user3courses);
+
+ // User 2 has only posted in course3.
+ $user2courses = forum_get_courses_user_posted_in($user2);
+ $this->assertCount(1, $user2courses);
+ $user2course = array_shift($user2courses);
+ $this->assertEquals($course3->id, $user2course->id);
+ $this->assertEquals($course3->shortname, $user2course->shortname);
+
+ // User 1 has posted in all 3 courses.
+ $user1courses = forum_get_courses_user_posted_in($user1);
+ $this->assertCount(3, $user1courses);
+ foreach ($user1courses as $course) {
+ $this->assertContains($course->id, array($course1->id, $course2->id, $course3->id));
+ $this->assertContains($course->shortname, array($course1->shortname, $course2->shortname,
+ $course3->shortname));
+
+ }
+
+ // User 1 has only started a discussion in course 1 and 2 though.
+ $user1courses = forum_get_courses_user_posted_in($user1, true);
+ $this->assertCount(2, $user1courses);
+ foreach ($user1courses as $course) {
+ $this->assertContains($course->id, array($course1->id, $course2->id));
+ $this->assertContains($course->shortname, array($course1->shortname, $course2->shortname));
+ }
+ }
+}
View
7 mod/quiz/renderer.php
@@ -1083,14 +1083,13 @@ public function view_result_info($quiz, $context, $cm, $viewobj) {
}
if ($viewobj->gradebookfeedback) {
$resultinfo .= $this->heading(get_string('comment', 'quiz'), 3, 'main');
- $resultinfo .= '<p class="quizteacherfeedback">'.$viewobj->gradebookfeedback.
- "</p>\n";
+ $resultinfo .= html_writer::div($viewobj->gradebookfeedback, 'quizteacherfeedback') . "\n";
}
if ($viewobj->feedbackcolumn) {
$resultinfo .= $this->heading(get_string('overallfeedback', 'quiz'), 3, 'main');
- $resultinfo .= html_writer::tag('p',
+ $resultinfo .= html_writer::div(
quiz_feedback_for_grade($viewobj->mygrade, $quiz, $context),
- array('class' => 'quizgradefeedback'))."\n";
+ 'quizgradefeedback') . "\n";
}
if ($resultinfo) {
View
14 mod/scorm/datamodels/aicclib.php
@@ -134,11 +134,17 @@ function scorm_parse_aicc($scorm) {
$extension = strtolower(substr($ext, 1));
if (in_array($extension, $extaiccfiles)) {
$id = strtolower(basename($filename, $ext));
+ if (!isset($ids[$id])) {
+ $ids[$id] = new stdClass();
+ }
$ids[$id]->$extension = $file;
}
}
foreach ($ids as $courseid => $id) {
+ if (!isset($courses[$courseid])) {
+ $courses[$courseid] = new stdClass();
+ }
if (isset($id->crs)) {
$contents = $id->crs->get_content();
$rows = explode("\r\n", $contents);
@@ -169,6 +175,9 @@ function scorm_parse_aicc($scorm) {
if (preg_match($regexp, $rows[$i], $matches)) {
for ($j=0; $j<count($columns->columns); $j++) {
$column = $columns->columns[$j];
+ if (!isset($courses[$courseid]->elements[substr(trim($matches[$columns->mastercol+1]), 1 , -1)])) {
+ $courses[$courseid]->elements[substr(trim($matches[$columns->mastercol+1]), 1 , -1)] = new stdClass();
+ }
$courses[$courseid]->elements[substr(trim($matches[$columns->mastercol+1]), 1 , -1)]->$column = substr(trim($matches[$j+1]), 1, -1);
}
}
@@ -268,13 +277,16 @@ function scorm_parse_aicc($scorm) {
if (isset($course->elements)) {
foreach ($course->elements as $element) {
unset($sco);
+ $sco = new stdClass();
$sco->identifier = $element->system_id;
$sco->scorm = $scorm->id;
$sco->organization = $course->id;
$sco->title = $element->title;
- if (!isset($element->parent) || strtolower($element->parent) == 'root') {
+ if (!isset($element->parent)) {
$sco->parent = '/';
+ } else if (strtolower($element->parent) == 'root') {
+ $sco->parent = $course->id;
} else {
$sco->parent = $element->parent;
}
View
19 mod/scorm/db/upgrade.php
@@ -77,12 +77,27 @@ function xmldb_scorm_upgrade($oldversion) {
// Moodle v2.4.0 release upgrade line
- // Put any upgrade step following this
-
+ // Put any upgrade step following this.
// Moodle v2.5.0 release upgrade line.
// Put any upgrade step following this.
+ // Fix AICC parent/child relationships (MDL-37394).
+ if ($oldversion < 2013050101) {
+ // Get all AICC packages.
+ $aiccpackages = $DB->get_recordset('scorm', array('version' => 'AICC'), '', 'id');
+ foreach ($aiccpackages as $aicc) {
+ $sql = "UPDATE {scorm_scoes}
+ SET parent = organization
+ WHERE scorm = ?
+ AND " . $DB->sql_isempty('scorm_scoes', 'manifest', false, false) . "
+ AND " . $DB->sql_isnotempty('scorm_scoes', 'organization', false, false) . "
+ AND parent = '/'";
+ $DB->execute($sql, array($aicc->id));
+ }
+ $aiccpackages->close();
+ upgrade_mod_savepoint(true, 2013050101, 'scorm');
+ }
return true;
}
View
2 mod/scorm/version.php
@@ -25,7 +25,7 @@
defined('MOODLE_INTERNAL') || die();
-$module->version = 2013050100; // The current module version (Date: YYYYMMDDXX)
+$module->version = 2013050101; // The current module version (Date: YYYYMMDDXX)
$module->requires = 2013050100; // Requires this Moodle version
$module->component = 'mod_scorm'; // Full name of the plugin (used for diagnostics)
$module->cron = 300;
View
14 question/behaviour/manualgraded/tests/walkthrough_test.php
@@ -38,7 +38,7 @@
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class qbehaviour_manualgraded_walkthrough_test extends qbehaviour_walkthrough_test_base {
+class qbehaviour_manualgraded_walkthrough_testcase extends qbehaviour_walkthrough_test_base {
public function test_manual_graded_essay() {
// Create an essay question.
@@ -56,7 +56,7 @@ public function test_manual_graded_essay() {
$this->get_does_not_contain_feedback_expectation());
// Simulate some data submitted by the student.
- $this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_PLAIN));
+ $this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
// Verify.
$this->check_current_state(question_state::$complete);
@@ -68,16 +68,16 @@ public function test_manual_graded_essay() {
// Process the same data again, check it does not create a new step.
$numsteps = $this->get_step_count();
- $this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_PLAIN));
+ $this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
$this->check_step_count($numsteps);
// Process different data, check it creates a new step.
- $this->process_submission(array('answer' => ''));
+ $this->process_submission(array('answer' => '', 'answerformat' => FORMAT_HTML));
$this->check_step_count($numsteps + 1);
$this->check_current_state(question_state::$todo);
// Change back, check it creates a new step.
- $this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_PLAIN));
+ $this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
$this->check_step_count($numsteps + 2);
// Finish the attempt.
@@ -206,7 +206,7 @@ public function test_manual_graded_ignore_repeat_sumbission() {
$this->check_current_mark(null);
// Simulate some data submitted by the student.
- $this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_PLAIN));
+ $this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
// Verify.
$this->check_current_state(question_state::$complete);
@@ -283,7 +283,7 @@ public function test_manual_graded_essay_can_grade_0() {
$this->get_does_not_contain_feedback_expectation());
// Simulate some data submitted by the student.
- $this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_PLAIN));
+ $this->process_submission(array('answer' => 'This is my wonderful essay!', 'answerformat' => FORMAT_HTML));
// Verify.
$this->check_current_state(question_state::$complete);
View
8 question/category.php
@@ -104,10 +104,14 @@
if ($qcobject->catform->is_cancelled()) {
redirect($thispageurl);
} else if ($catformdata = $qcobject->catform->get_data()) {
+ $catformdata->infoformat = $catformdata->info['format'];
+ $catformdata->info = $catformdata->info['text'];
if (!$catformdata->id) {//new category
- $qcobject->add_category($catformdata->parent, $catformdata->name, $catformdata->info);
+ $qcobject->add_category($catformdata->parent, $catformdata->name,
+ $catformdata->info, false, $catformdata->infoformat);
} else {
- $qcobject->update_category($catformdata->id, $catformdata->parent, $catformdata->name, $catformdata->info);
+ $qcobject->update_category($catformdata->id, $catformdata->parent,
+ $catformdata->name, $catformdata->info, $catformdata->infoformat);
}
redirect($thispageurl);
} else if ((!empty($param->delete) and (!$questionstomove) and confirm_sesskey())) {
View
29 question/category_class.php
@@ -134,25 +134,28 @@ public function item_html($extraargs = array()){
*/
class question_category_object {
- var $str;
/**
- * Nested lists to display categories.
- *
- * @var array
+ * @var array common language strings.
*/
- var $editlists = array();
- var $newtable;
- var $tab;
- var $tabsize = 3;
+ public $str;
+
+ /**
+ * @var array nested lists to display categories.
+ */
+ public $editlists = array();
+ public $newtable;
+ public $tab;
+ public $tabsize = 3;
/**
* @var moodle_url Object representing url for this page
*/
- var $pageurl;
+ public $pageurl;
+
/**
* @var question_category_edit_form Object representing form for adding / editing categories.
*/
- var $catform;
+ public $catform;
/**
* Constructor
@@ -376,7 +379,7 @@ public function move_questions($oldcat, $newcat){
/**
* Creates a new category with given params
*/
- public function add_category($newparent, $newcategory, $newinfo, $return = false) {
+ public function add_category($newparent, $newcategory, $newinfo, $return = false, $newinfoformat = FORMAT_HTML) {
global $DB;
if (empty($newcategory)) {
print_error('categorynamecantbeblank', 'question');
@@ -396,6 +399,7 @@ public function add_category($newparent, $newcategory, $newinfo, $return = false
$cat->contextid = $contextid;
$cat->name = $newcategory;
$cat->info = $newinfo;
+ $cat->infoformat = $newinfoformat;
$cat->sortorder = 999;
$cat->stamp = make_unique_id_code();
$categoryid = $DB->insert_record("question_categories", $cat);
@@ -409,7 +413,7 @@ public function add_category($newparent, $newcategory, $newinfo, $return = false
/**
* Updates an existing category with given params
*/
- public function update_category($updateid, $newparent, $newname, $newinfo) {
+ public function update_category($updateid, $newparent, $newname, $newinfo, $newinfoformat = FORMAT_HTML) {
global $CFG, $DB;
if (empty($newname)) {
print_error('categorynamecantbeblank', 'question');
@@ -441,6 +445,7 @@ public function update_category($updateid, $newparent, $newname, $newinfo) {
$cat->id = $updateid;
$cat->name = $newname;
$cat->info = $newinfo;
+ $cat->infoformat = $newinfoformat;
$cat->parent = $parentid;
$cat->contextid = $tocontextid;
$DB->update_record('question_categories', $cat);
View
19 question/category_form.php
@@ -59,14 +59,27 @@ protected function definition() {
$mform->addRule('name', get_string('categorynamecantbeblank', 'question'), 'required', null, 'client');
$mform->setType('name', PARAM_TEXT);
- $mform->addElement('textarea', 'info', get_string('categoryinfo', 'question'), array('rows'=> '10', 'cols'=>'45'));
+ $mform->addElement('editor', 'info', get_string('categoryinfo', 'question'),
+ array('rows' => 10), array('noclean' => 1));
$mform->setDefault('info', '');
- $mform->setType('info', PARAM_TEXT);
+ $mform->setType('info', PARAM_RAW);
$this->add_action_buttons(false, get_string('addcategory', 'question'));
$mform->addElement('hidden', 'id', 0);
$mform->setType('id', PARAM_INT);
}
-}
+ public function set_data($current) {
+ if (is_object($current)) {
+ $current = (array) $current;
+ }
+ if (!empty($current['info'])) {
+ $current['info'] = array('text' => $current['info'],
+ 'infoformat' => $current['infoformat']);
+ } else {
+ $current['info'] = array('text' => '', 'infoformat' => FORMAT_HTML);
+ }
+ parent::set_data($current);
+ }
+}
View
54 question/engine/datalib.php
@@ -1279,22 +1279,21 @@ protected function compute_value($draftitemid, $text) {
$string .= $file->get_filepath() . $file->get_filename() . '|' .
$file->get_contenthash() . '|';
}
-
- if ($string) {
- $hash = md5($string);
- } else {
- $hash = '';
- }
+ $hash = md5($string);
if (is_null($text)) {
- return $hash;
+ if ($string) {
+ return $hash;
+ } else {
+ return '';
+ }
}
// We add the file hash so a simple string comparison will say if the
// files have been changed. First strip off any existing file hash.
- $text = preg_replace('/\s*<!-- File hash: \w+ -->\s*$/', '', $text);
- $text = file_rewrite_urls_to_pluginfile($text, $draftitemid);
- if ($hash) {
+ if ($text !== '') {
+ $text = preg_replace('/\s*<!-- File hash: \w+ -->\s*$/', '', $text);
+ $text = file_rewrite_urls_to_pluginfile($text, $draftitemid);
$text .= '<!-- File hash: ' . $hash . ' -->';
}
return $text;
@@ -1379,6 +1378,41 @@ public function __toString() {
public function get_files() {
return $this->step->get_qt_files($this->name, $this->contextid);
}
+
+ /**
+ * Copy these files into a draft area, and return the corresponding
+ * {@link question_file_saver} that can save them again.
+ *
+ * This is used by {@link question_attempt::start_based_on()}, which is used
+ * (for example) by the quizzes 'Each attempt builds on last' feature.
+ *
+ * @return question_file_saver that can re-save these files again.
+ */
+ public function get_question_file_saver() {
+
+ // There are three possibilities here for what $value will look like:
+ // 1) some HTML content followed by an MD5 hash in a HTML comment;
+ // 2) a plain MD5 hash;
+ // 3) or some real content, without any hash.
+ // The problem is that 3) is ambiguous in the case where a student writes
+ // a response that looks exactly like an MD5 hash. For attempts made now,
+ // we avoid case 3) by always going for case 1) or 2) (except when the
+ // response is blank. However, there may be case 3) data in the database
+ // so we need to handle it as best we can.
+ if (preg_match('/\s*<!-- File hash: [0-9a-zA-Z]{32} -->\s*$/', $this->value)) {
+ $value = preg_replace('/\s*<!-- File hash: [0-9a-zA-Z]{32} -->\s*$/', '', $this->value);
+
+ } else if (preg_match('/^[0-9a-zA-Z]{32}$/', $this->value)) {
+ $value = null;
+
+ } else {
+ $value = $this->value;
+ }
+
+ list($draftid, $text) = $this->step->prepare_response_files_draft_itemid_with_text(
+ $this->name, $this->contextid, $value);
+ return new question_file_saver($draftid, 'question', 'response_' . $this->name, $text);
+ }
}
View
15 question/engine/questionattempt.php
@@ -923,7 +923,13 @@ public function start_based_on(question_attempt $oldqa) {
* @return array name => value pairs.
*/
protected function get_resume_data() {
- return $this->behaviour->get_resume_data();
+ $resumedata = $this->behaviour->get_resume_data();
+ foreach ($resumedata as $name => $value) {
+ if ($value instanceof question_file_loader) {
+ $resumedata[$name] = $value->get_question_file_saver();
+ }
+ }
+ return $resumedata;
}
/**
@@ -975,11 +981,12 @@ public function get_submitted_var($name, $type, $postdata = null) {
*/
protected function process_response_files($name, $draftidname, $postdata = null, $text = null) {
if ($postdata) {
- // There can be no files with test data (at the moment).
- return null;
+ // For simulated posts, get the draft itemid from there.
+ $draftitemid = $this->get_submitted_var($draftidname, PARAM_INT, $postdata);
+ } else {
+ $draftitemid = file_get_submitted_draft_itemid($draftidname);
}
- $draftitemid = file_get_submitted_draft_itemid($draftidname);
if (!$draftitemid) {
return null;
}
View
2 question/engine/questionattemptstep.php
@@ -106,7 +106,7 @@ public function __construct($data = array(), $timecreated = null, $userid = null
global $USER;
if (!is_array($data)) {
- echo format_backtrace(debug_backtrace());
+ throw new coding_exception('$data must be an array when constructing a question_attempt_step.');
}
$this->state = question_state::$unprocessed;
$this->data = $data;
View
4 question/type/essay/question.php
@@ -87,12 +87,12 @@ public function is_complete_response(array $response) {
public function is_same_response(array $prevresponse, array $newresponse) {
if (array_key_exists('answer', $prevresponse) && $prevresponse['answer'] !== $this->responsetemplate) {
- $value1 = $prevresponse['answer'];
+ $value1 = (string) $prevresponse['answer'];
} else {
$value1 = '';
}
if (array_key_exists('answer', $newresponse) && $newresponse['answer'] !== $this->responsetemplate) {
- $value2 = $newresponse['answer'];
+ $value2 = (string) $newresponse['answer'];
} else {
$value2 = '';
}
View
23 question/type/essay/tests/helper.php
@@ -79,6 +79,29 @@ public function make_essay_question_editorfilepicker() {
}
/**
+ * Make the data what would be received from the editing form for an essay
+ * question using the HTML editor allowing embedded files as input, and up
+ * to three attachments.
+ *
+ * @return stdClass the data that would be returned by $form->get_gata();
+ */
+ public function get_essay_question_form_data_editorfilepicker() {
+ $fromform = new stdClass();
+
+ $fromform->name = 'Essay question with filepicker and attachments';
+ $fromform->questiontext = array('text' => 'Please write a story about a frog.', 'format' => FORMAT_HTML);
+ $fromform->defaultmark = 1.0;
+ $fromform->generalfeedback = array('text' => 'I hope your story had a beginning, a middle and an end.', 'format' => FORMAT_HTML);
+ $fromform->responseformat = 'editorfilepicker';
+ $fromform->responsefieldlines = 10;
+ $fromform->attachments = 3;
+ $fromform->graderinfo = array('text' => '', 'format' => FORMAT_HTML);
+ $fromform->responsetemplate = array('text' => '', 'format' => FORMAT_HTML);
+
+ return $fromform;
+ }
+
+ /**
* Makes an essay question using plain text input.
* @return qtype_essay_question
*/
View
6 question/type/essay/tests/question_test.php
@@ -36,7 +36,7 @@
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class qtype_essay_question_test extends advanced_testcase {
+class qtype_essay_question_testcase extends advanced_testcase {
public function test_get_question_summary() {
$essay = test_question_maker::make_an_essay_question();
$essay->questiontext = 'Hello <img src="http://example.com/globe.png" alt="world" />';
@@ -46,8 +46,8 @@ public function test_get_question_summary() {
public function test_summarise_response() {
$longstring = str_repeat('0123456789', 50);
$essay = test_question_maker::make_an_essay_question();
- $this->assertEquals($longstring,
- $essay->summarise_response(array('answer' => $longstring, 'answerformat' => FORMAT_PLAIN)));
+ $this->assertEquals($longstring, $essay->summarise_response(
+ array('answer' => $longstring, 'answerformat' => FORMAT_HTML)));
}
public function test_is_same_response() {
View
219 question/type/essay/tests/walkthrough_test.php
@@ -35,7 +35,7 @@
* @copyright 2013 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class qtype_essay_walkthrough_test extends qbehaviour_walkthrough_test_base {
+class qtype_essay_walkthrough_testcase extends qbehaviour_walkthrough_test_base {
protected function check_contains_textarea($name, $content = '', $height = 10) {
$fieldname = $this->quba->get_field_prefix($this->slot) . $name;
@@ -50,6 +50,28 @@ protected function check_contains_textarea($name, $content = '', $height = 10) {
}
}
+ /**
+ * Helper method: Store a test file with a given name and contents in a
+ * draft file area.
+ *
+ * @param int $usercontextid user context id.
+ * @param int $draftitemid draft item id.
+ * @param string $filename filename.
+ * @param string $contents file contents.
+ */
+ protected function save_file_to_draft_area($usercontextid, $draftitemid, $filename, $contents) {
+ $fs = get_file_storage();
+
+ $filerecord = new stdClass();
+ $filerecord->contextid = $usercontextid;
+ $filerecord->component = 'user';
+ $filerecord->filearea = 'draft';
+ $filerecord->itemid = $draftitemid;
+ $filerecord->filepath = '/';
+ $filerecord->filename = $filename;
+ $fs->create_file_from_string($filerecord, $contents);
+ }
+
public function test_deferred_feedback_html_editor() {
// Create an essay question.
@@ -204,4 +226,199 @@ public function test_responsetemplate() {
$this->get_contains_question_text_expectation($q),
$this->get_contains_general_feedback_expectation($q));
}
+
+ public function test_deferred_feedback_html_editor_with_files_attempt_on_last() {
+ global $CFG, $USER;
+
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+ $usercontextid = context_user::instance($USER->id)->id;
+ $fs = get_file_storage();
+
+ // Create an essay question in the DB.
+ $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
+ $cat = $generator->create_question_category();
+ $question = $generator->create_question('essay', 'editorfilepicker', array('category' => $cat->id));
+
+ // Start attempt at the question.
+ $q = question_bank::load_question($question->id);
+ $this->start_attempt_at_question($q, 'deferredfeedback', 1);
+
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_step_count(1);
+
+ // Process a response and check the expected result.
+ // First we need to get the draft item ids.
+ $this->render();
+ if (!preg_match('/env=editor&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
+ throw new coding_exception('Editor draft item id not found.');
+ }
+ $editordraftid = $matches[1];
+ if (!preg_match('/env=filemanager&amp;action=browse&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
+ throw new coding_exception('File manager draft item id not found.');
+ }
+ $attachementsdraftid = $matches[1];
+
+ $this->save_file_to_draft_area($usercontextid, $editordraftid, 'smile.txt', ':-)');
+ $this->save_file_to_draft_area($usercontextid, $attachementsdraftid, 'greeting.txt', 'Hello world!');
+ $this->process_submission(array(
+ 'answer' => 'Here is a picture: <img src="' . $CFG->wwwroot .
+ "/draftfile.php/{$usercontextid}/user/draft/{$editordraftid}/smile.txt" .
+ '" alt="smile">.',
+ 'answerformat' => FORMAT_HTML,
+ 'answer:itemid' => $editordraftid,
+ 'attachments' => $attachementsdraftid));
+
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(null);
+ $this->check_step_count(2);
+ $this->save_quba();
+
+ // Save the same response again, and verify no new step is created.
+ $this->load_quba();
+
+ $this->render();
+ if (!preg_match('/env=editor&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
+ throw new coding_exception('Editor draft item id not found.');
+ }
+ $editordraftid = $matches[1];
+ if (!preg_match('/env=filemanager&amp;action=browse&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
+ throw new coding_exception('File manager draft item id not found.');
+ }
+ $attachementsdraftid = $matches[1];
+
+ $this->process_submission(array(
+ 'answer' => 'Here is a picture: <img src="' . $CFG->wwwroot .
+ "/draftfile.php/{$usercontextid}/user/draft/{$editordraftid}/smile.txt" .
+ '" alt="smile">.',
+ 'answerformat' => FORMAT_HTML,
+ 'answer:itemid' => $editordraftid,
+ 'attachments' => $attachementsdraftid));
+
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(null);
+ $this->check_step_count(2);
+
+ // Now submit all and finish.
+ $this->finish();
+ $this->check_current_state(question_state::$needsgrading);
+ $this->check_current_mark(null);
+ $this->check_step_count(3);
+ $this->save_quba();
+
+ // Now start a new attempt based on the old one.
+ $this->load_quba();
+ $oldqa = $this->get_question_attempt();
+
+ $q = question_bank::load_question($question->id);
+ $this->quba = question_engine::make_questions_usage_by_activity('unit_test',
+ context_system::instance());
+ $this->quba->set_preferred_behaviour('deferredfeedback');
+ $this->slot = $this->quba->add_question($q, 1);
+ $this->quba->start_question_based_on($this->slot, $oldqa);
+
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(null);
+ $this->check_step_count(1);
+ $this->save_quba();
+
+ // Now save the same response again, and ensure that a new step is not created.
+ $this->load_quba();
+
+ $this->render();
+ if (!preg_match('/env=editor&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
+ throw new coding_exception('Editor draft item id not found.');
+ }
+ $editordraftid = $matches[1];
+ if (!preg_match('/env=filemanager&amp;action=browse&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
+ throw new coding_exception('File manager draft item id not found.');
+ }
+ $attachementsdraftid = $matches[1];
+
+ $this->process_submission(array(
+ 'answer' => 'Here is a picture: <img src="' . $CFG->wwwroot .
+ "/draftfile.php/{$usercontextid}/user/draft/{$editordraftid}/smile.txt" .
+ '" alt="smile">.',
+ 'answerformat' => FORMAT_HTML,
+ 'answer:itemid' => $editordraftid,
+ 'attachments' => $attachementsdraftid));
+
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(null);
+ $this->check_step_count(1);
+ }
+
+ public function test_deferred_feedback_html_editor_with_files_attempt_on_last_no_files_uploaded() {
+ global $CFG, $USER;
+
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+ $usercontextid = context_user::instance($USER->id)->id;
+ $fs = get_file_storage();
+
+ // Create an essay question in the DB.
+ $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
+ $cat = $generator->create_question_category();
+ $question = $generator->create_question('essay', 'editorfilepicker', array('category' => $cat->id));
+
+ // Start attempt at the question.
+ $q = question_bank::load_question($question->id);
+ $this->start_attempt_at_question($q, 'deferredfeedback', 1);
+
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_step_count(1);
+
+ // Process a response and check the expected result.
+ // First we need to get the draft item ids.
+ $this->render();
+ if (!preg_match('/env=editor&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
+ throw new coding_exception('Editor draft item id not found.');
+ }
+ $editordraftid = $matches[1];
+ if (!preg_match('/env=filemanager&amp;action=browse&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
+ throw new coding_exception('File manager draft item id not found.');
+ }
+ $attachementsdraftid = $matches[1];
+
+ $this->process_submission(array(
+ 'answer' => 'I refuse to draw you a picture, so there!',
+ 'answerformat' => FORMAT_HTML,
+ 'answer:itemid' => $editordraftid,
+ 'attachments' => $attachementsdraftid));
+
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(null);
+ $this->check_step_count(2);
+ $this->save_quba();
+
+ // Now submit all and finish.
+ $this->finish();
+ $this->check_current_state(question_state::$needsgrading);
+ $this->check_current_mark(null);
+ $this->check_step_count(3);
+ $this->save_quba();
+
+ // Now start a new attempt based on the old one.
+ $this->load_quba();
+ $oldqa = $this->get_question_attempt();
+
+ $q = question_bank::load_question($question->id);
+ $this->quba = question_engine::make_questions_usage_by_activity('unit_test',
+ context_system::instance());
+ $this->quba->set_preferred_behaviour('deferredfeedback');
+ $this->slot = $this->quba->add_question($q, 1);
+ $this->quba->start_question_based_on($this->slot, $oldqa);
+
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(null);
+ $this->check_step_count(1);
+ $this->save_quba();
+
+ // Check the display.
+ $this->load_quba();
+ $this->render();
+ $this->assertRegExp('/I refuse to draw you a picture, so there!/', $this->currentoutput);
+ }
}
View
19 question/type/match/db/upgrade.php
@@ -44,6 +44,25 @@ function xmldb_qtype_match_upgrade($oldversion) {
// Moodle v2.4.0 release upgrade line.
// Put any upgrade step following this.
+ if ($oldversion < 2013012099) {
+ // Find duplicate rows before they break the 2013012103 step below.
+ $problemids = $DB->get_recordset_sql("
+ SELECT question, MIN(id) AS recordidtokeep
+ FROM {question_match}
+ GROUP BY question
+ HAVING COUNT(1) > 1
+ ");
+ foreach ($problemids as $problem) {
+ $DB->delete_records_select('question_match',
+ 'question = ? AND id > ?',
+ array($problem->question, $problem->recordidtokeep));
+ }
+ $problemids->close();
+
+ // Shortanswer savepoint reached.
+ upgrade_plugin_savepoint(true, 2013012099, 'qtype', 'match');
+ }
+
if ($oldversion < 2013012100) {
// Define table question_match to be renamed to qtype_match_options.
View
6 question/type/multianswer/module.js
@@ -27,7 +27,7 @@ M.qtype_multianswer = M.qtype_multianswer || {};
M.qtype_multianswer.init = function (Y, questiondiv) {
- Y.one(questiondiv).all('span.subquestion').each(function(subqspan, i) {
+ Y.one(questiondiv).all('span.subquestion').each(function(subqspan) {
var feedbackspan = subqspan.one('.feedbackspan');
if (!feedbackspan) {
return;
@@ -39,7 +39,9 @@ M.qtype_multianswer.init = function (Y, questiondiv) {
align: {
node: subqspan,
points: [Y.WidgetPositionAlign.TC, Y.WidgetPositionAlign.BC]
- }
+ },
+ constrain: subqspan.ancestor('div.que'),
+ preventOverlap: true
});
overlay.render();
View
7 question/type/multianswer/styles.css
@@ -1,10 +1,17 @@
.que.multianswer .feedbackspan {
display: block;
+ max-width: 70%;
background: #fff3bf;
padding: 0.5em;
margin-top: 1em;
box-shadow: 0.5em 0.5em 1em #000000;
}
+body.ie6 .que.multianswer .feedbackspan,
+body.ie7 .que.multianswer .feedbackspan,
+body.ie8 .que.multianswer .feedbackspan,
+body.ie9 .que.multianswer .feedbackspan {
+ width: 70%;
+}
.que.multianswer .answer .specificfeedback {
display: inline;
padding: 0 0.7em;
View
2 question/type/numerical/edit_numerical_form.php
@@ -186,8 +186,6 @@ protected function add_unit_fields($mform) {
protected function unit_group($mform) {
$grouparray = array();
$grouparray[] = $mform->createElement('text', 'unit', get_string('unit', 'quiz'), array('size'=>10));
- $grouparray[] = $mform->createElement('static', '', '', ' ' .
- get_string('multiplier', 'quiz').' ');
$grouparray[] = $mform->createElement('text', 'multiplier',
get_string('multiplier', 'quiz'), array('size'=>10));
View
2 question/type/numerical/styles.css
@@ -34,6 +34,8 @@ body#page-question-type-numerical div[id^=fgroup_id_][id*=answeroptions_] .fgrou
font-weight: bold;
}
+body.path-question-type div#fgroup_id_penaltygrp label[for^=id_unitpenalty],
+body.path-question-type div[id^=fgroup_id_units_] label[for^='id_unit_'],
body#page-question-type-numerical div[id^=fgroup_id_][id*=answeroptions_] label[for^='id_answer_']{
position: absolute;
left: -10000px;
View
1 theme/base/style/admin.css
@@ -8,6 +8,7 @@
.path-admin #assignrole {width: 60%;margin-left: auto;margin-right: auto;}
.path-admin .admintable .leftalign {text-align: left;}
+.dir-rtl.path-admin .admintable .leftalign {text-align: right;}
.path-admin .admintable .centeralign {text-align: center;}
.path-admin .admintable.environmenttable .name,
View
1 theme/base/style/core.css
@@ -263,6 +263,7 @@ a.skip:active {position: static;display: block;}
.mform .fitem fieldset.felement {margin-left:15%;padding-left:1%;margin-bottom:0}
.mform .error,
.mform .required {color:#A00;}
+.mform span.error {display: inline-block;padding: 4px;margin-bottom: 4px;background-color: #F2DEDE;border: 1px solid #EED3D7;}
.mform .required .fgroup span label {color:#000;}
.mform .fdescription.required {color:#A00;text-align:right;}
.dir-rtl .mform .fdescription.required {text-align:left;}
View
3 theme/bootstrapbase/less/moodle/core.less
@@ -1063,6 +1063,9 @@ body.tag .managelink {
#page-enrol-users .enrol_user_buttons {
float: right;
}
+#page-enrol-users.dir-rtl .enrol_user_buttons {
+ float: left;
+}
#page-enrol-users .enrol_user_buttons .enrolusersbutton {
margin-left: 1em;
display: inline;
View
8 theme/bootstrapbase/less/moodle/forms.less
@@ -23,6 +23,14 @@ form {
.mform fieldset.error {
border: 1px solid @errorText;<