From 941c251304385f4c63c0fedd67778179fcebb525 Mon Sep 17 00:00:00 2001 From: Adrian Greeve Date: Tue, 14 Aug 2012 13:25:36 +0800 Subject: [PATCH 01/90] MDL-34429 - lib - Changed the log downloads to use the new CSV class. --- course/lib.php | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/course/lib.php b/course/lib.php index 06fb241774c1d..3f2a2a54e45bd 100644 --- a/course/lib.php +++ b/course/lib.php @@ -534,10 +534,19 @@ function print_mnet_log($hostid, $course, $user=0, $date=0, $order="l.time ASC", function print_log_csv($course, $user, $date, $order='l.time DESC', $modname, $modid, $modaction, $groupid) { - global $DB; + global $DB, $CFG; + + require_once($CFG->libdir . '/csvlib.class.php'); - $text = get_string('course')."\t".get_string('time')."\t".get_string('ip_address')."\t". - get_string('fullnameuser')."\t".get_string('action')."\t".get_string('info'); + $csvexporter = new csv_export_writer('tab'); + + $header = array(); + $header[] = get_string('course'); + $header[] = get_string('time'); + $header[] = get_string('ip_address'); + $header[] = get_string('fullnameuser'); + $header[] = get_string('action'); + $header[] = get_string('info'); if (!$logs = build_logs_array($course, $user, $date, $order, '', '', $modname, $modid, $modaction, $groupid)) { @@ -564,16 +573,10 @@ function print_log_csv($course, $user, $date, $order='l.time DESC', $modname, $strftimedatetime = get_string("strftimedatetime"); - $filename = 'logs_'.userdate(time(),get_string('backupnameformat', 'langconfig'),99,false); - $filename .= '.txt'; - header("Content-Type: application/download\n"); - header("Content-Disposition: attachment; filename=\"$filename\""); - header("Expires: 0"); - header("Cache-Control: must-revalidate,post-check=0,pre-check=0"); - header("Pragma: public"); - - echo get_string('savedat').userdate(time(), $strftimedatetime)."\n"; - echo $text."\n"; + $csvexporter->set_filename('logs', '.txt'); + $title = array(get_string('savedat').userdate(time(), $strftimedatetime)); + $csvexporter->add_data($title); + $csvexporter->add_data($header); if (empty($logs['logs'])) { return true; @@ -603,9 +606,9 @@ function print_log_csv($course, $user, $date, $order='l.time DESC', $modname, $firstField = format_string($courses[$log->course], true, array('context' => $coursecontext)); $fullname = fullname($log, has_capability('moodle/site:viewfullnames', $coursecontext)); $row = array($firstField, userdate($log->time, $strftimedatetime), $log->ip, $fullname, $log->module.' '.$log->action, $log->info); - $text = implode("\t", $row); - echo $text." \n"; + $csvexporter->add_data($row); } + $csvexporter->download_file(); return true; } From 5a3f67af27245bc63435fd262a88165f7cec7dbf Mon Sep 17 00:00:00 2001 From: Adrian Greeve Date: Tue, 14 Aug 2012 15:09:02 +0800 Subject: [PATCH 02/90] MDL-32108 - navigation - After logging in the user is directed to mysite instead of site home. Thanks to lurii Kucherov for this patch. --- login/index.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/login/index.php b/login/index.php index 909a9faeefa54..6276a57899349 100644 --- a/login/index.php +++ b/login/index.php @@ -204,16 +204,16 @@ // no wantsurl stored or external - go to homepage $urltogo = $CFG->wwwroot.'/'; unset($SESSION->wantsurl); - } - /// Go to my-moodle page instead of site homepage if defaulthomepage set to homepage_my - if (!empty($CFG->defaulthomepage) && $CFG->defaulthomepage == HOMEPAGE_MY && !is_siteadmin() && !isguestuser()) { - if ($urltogo == $CFG->wwwroot or $urltogo == $CFG->wwwroot.'/' or $urltogo == $CFG->wwwroot.'/index.php') { - $urltogo = $CFG->wwwroot.'/my/'; + $home_page = get_home_page(); + // Go to my-moodle page instead of site homepage if defaulthomepage set to homepage_my + if ($home_page == HOMEPAGE_MY && !is_siteadmin() && !isguestuser()) { + if ($urltogo == $CFG->wwwroot or $urltogo == $CFG->wwwroot.'/' or $urltogo == $CFG->wwwroot.'/index.php') { + $urltogo = $CFG->wwwroot.'/my/'; + } } } - /// check if user password has expired /// Currently supported only for ldap-authentication module $userauth = get_auth_plugin($USER->auth); From 9ce8411317121756818ab6b37ab68c63dfb16f45 Mon Sep 17 00:00:00 2001 From: Juho Viitasalo Date: Fri, 9 Mar 2012 13:33:09 +0200 Subject: [PATCH 03/90] Changed two notification to output correctly --- user/profile.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user/profile.php b/user/profile.php index 979319dcd88e4..e06e502d33509 100644 --- a/user/profile.php +++ b/user/profile.php @@ -61,7 +61,7 @@ if ($user->deleted) { $PAGE->set_context(context_system::instance()); echo $OUTPUT->header(); - echo $OUTPUT->heading(get_string('userdeleted')); + echo $OUTPUT->notification(get_string('userdeleted')); echo $OUTPUT->footer(); die; } @@ -82,7 +82,7 @@ $PAGE->set_url('/user/profile.php', array('id'=>$userid)); $PAGE->navbar->add($struser); echo $OUTPUT->header(); - echo $OUTPUT->heading(get_string('usernotavailable', 'error')); + echo $OUTPUT->notification(get_string('usernotavailable', 'error')); echo $OUTPUT->footer(); exit; } From f518ff588da27e0dada1b3360d65c737913adfb4 Mon Sep 17 00:00:00 2001 From: Dan Marsden Date: Wed, 22 Aug 2012 09:01:54 +1200 Subject: [PATCH 04/90] MDL-34994 mod_choice: fix restore of user responses - use correct optionid --- mod/choice/backup/moodle2/restore_choice_stepslib.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mod/choice/backup/moodle2/restore_choice_stepslib.php b/mod/choice/backup/moodle2/restore_choice_stepslib.php index 75a56f80516d9..337eb7fe87800 100644 --- a/mod/choice/backup/moodle2/restore_choice_stepslib.php +++ b/mod/choice/backup/moodle2/restore_choice_stepslib.php @@ -80,10 +80,9 @@ protected function process_choice_answer($data) { global $DB; $data = (object)$data; - $oldid = $data->id; $data->choiceid = $this->get_new_parentid('choice'); - $data->optionid = $this->get_mappingid('choice_option', $oldid); + $data->optionid = $this->get_mappingid('choice_option', $data->optionid); $data->userid = $this->get_mappingid('user', $data->userid); $data->timemodified = $this->apply_date_offset($data->timemodified); From fb65ab00066a35756125781521719dcbfe0e71bb Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Wed, 22 Aug 2012 09:49:42 +0200 Subject: [PATCH 05/90] MDL-35010 availability: fix sometimes missing fieldset --- course/editsection_form.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/course/editsection_form.php b/course/editsection_form.php index f27ff2aacd715..73da8d3d2a721 100644 --- a/course/editsection_form.php +++ b/course/editsection_form.php @@ -38,6 +38,7 @@ public function definition_after_data() { $course = $this->_customdata['course']; if (!empty($CFG->enableavailability)) { + $mform->addElement('header', '', get_string('availabilityconditions', 'condition')); // String used by conditions more than once $strcondnone = get_string('none', 'condition'); // Grouping conditions - only if grouping is enabled at site level @@ -51,7 +52,6 @@ public function definition_after_data() { $grouping->name, true, array('context' => $context)); } } - $mform->addElement('header', '', get_string('availabilityconditions', 'condition')); $mform->addElement('select', 'groupingid', get_string('groupingsection', 'group'), $options); $mform->addHelpButton('groupingid', 'groupingsection', 'group'); } From 802f408f350e87beca3d40262a60db318b8acf89 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Thu, 23 Aug 2012 00:00:27 +0100 Subject: [PATCH 06/90] MDL-31244, MDL-25063 algebra filter: fix common false positives. There are two well-known cases where the algebra filter messes up input that is obviously not meant for the algebra filter: 1. Copy and paste of unified diffs. 2. @@PLUGINFILE@@ tokens in the HTML that are due to be replaced by the files API. This fix detects these two cases, and just stops the algebra filter from replacing them. --- filter/algebra/filter.php | 19 ++++-- filter/algebra/tests/filter_test.php | 90 ++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 filter/algebra/tests/filter_test.php diff --git a/filter/algebra/filter.php b/filter/algebra/filter.php index 9ba698a55c93f..61fe8e7017b9e 100644 --- a/filter/algebra/filter.php +++ b/filter/algebra/filter.php @@ -89,7 +89,7 @@ function filter_algebra_image($imagefile, $tex= "", $height="", $width="", $alig } class filter_algebra extends moodle_text_filter { - function filter($text, array $options = array()){ + public function filter($text, array $options = array()){ global $CFG, $DB; /// Do a quick check using stripos to avoid unnecessary wor @@ -114,9 +114,6 @@ function filter($text, array $options = array()){ # return $text; # } - - $text .= ' '; - preg_match_all('/@(@@+)([^@])/',$text,$matches); for ($i=0;$i(.+?)<\/algebra>|@@(.+?)@@/is', $text, $matches); for ($i=0; $i','',$algebra); $algebra = str_replace('','',$algebra); $algebra = str_replace('','',$algebra); @@ -159,7 +167,7 @@ function filter($text, array $options = array()){ $algebra = str_replace('upsilon','zupslon',$algebra); $algebra = preg_replace('!\r\n?!',' ',$algebra); $algebra = escapeshellarg($algebra); - if ( (PHP_OS == "WINNT") || (PHP_OS == "WIN32") || (PHP_OS == "Windows") ) { + if ( (PHP_OS == "WINNT") || (PHP_OS == "WIN32") || (PHP_OS == "Windows")) { $cmd = "cd $CFG->dirroot\\filter\\algebra & algebra2tex.pl $algebra"; } else { $cmd = "cd $CFG->dirroot/filter/algebra; ./algebra2tex.pl $algebra"; @@ -220,6 +228,7 @@ function filter($text, array $options = array()){ $texexp = preg_replace('/\\\int\\\left\((.+?d[a-z])\\\right\)/s','\int '. "\$1 ",$texexp); $texexp = preg_replace('/\\\lim\\\left\((.+?),(.+?),(.+?)\\\right\)/s','\lim_'. "{\$2\\to \$3}\$1 ",$texexp); $texexp = str_replace('\mbox', '', $texexp); // now blacklisted in tex, sorry + $texcache = new stdClass(); $texcache->filter = 'algebra'; $texcache->version = 1; $texcache->md5key = $md5; diff --git a/filter/algebra/tests/filter_test.php b/filter/algebra/tests/filter_test.php new file mode 100644 index 0000000000000..6c7db8fbdad0d --- /dev/null +++ b/filter/algebra/tests/filter_test.php @@ -0,0 +1,90 @@ +. + +/** + * Unit test for the filter_algebra + * + * @package filter_algebra + * @category phpunit + * @copyright 2012 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/filter/algebra/filter.php'); + + +/** + * Unit tests for filter_algebra. + * + * Note that this only tests some of the filter logic. It does not acutally test + * the normal case of the filter working, because I cannot make it work on my + * test server, and if it does not work here, it probably does not also work + * for other people. A failing test will be irritating noise. + * + * @copyright 2012 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class filter_algebra_testcase extends basic_testcase { + + protected $filter; + + protected function setUp() { + parent::setUp(); + $this->filter = new filter_algebra(context_system::instance(), array()); + } + + function test_algebra_filter_no_algebra() { + $this->assertEquals('

Look no algebra!

', + $this->filter->filter('

Look no algebra!

')); + } + + + function test_algebra_filter_pluginfile() { + $this->assertEquals('', + $this->filter->filter('')); + } + + function test_algebra_filter_draftfile() { + $this->assertEquals('', + $this->filter->filter('')); + } + + function test_algebra_filter_unified_diff() { + $diff = ' +diff -u -r1.1 Worksheet.php +--- Worksheet.php 26 Sep 2003 04:18:02 -0000 1.1 ++++ Worksheet.php 18 Nov 2009 03:58:50 -0000 +@@ -1264,10 +1264,10 @@ + } + + // Strip the = or @ sign at the beginning of the formula string +- if (ereg("^=",$formula)) { ++ if (preg_match("/^=/",$formula)) { + $formula = preg_replace("/(^=)/","",$formula); + } +- elseif(ereg("^@",$formula)) { ++ elseif(preg_match("/^@/",$formula)) { + $formula = preg_replace("/(^@)/","",$formula); + } + else { +'; + $this->assertEquals('
' . $diff . '
', + $this->filter->filter('
' . $diff . '
')); + } +} From 93266d0fe052fd0b3fc31fa4ab500f860f424f1a Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 20 Aug 2012 12:46:32 +0100 Subject: [PATCH 07/90] MDL-31837 numerical tolerance: better handling of very small tolerances. The changes between Moodle 1.9 and 2.1 made the marking of very small answers like 10^-20 almost impossible. This change fixes it. This fix is almost entirely due the the careful research of Pierre Pichet, who carefully testing various proposals, and worked out that this one seemed best. --- question/type/numerical/question.php | 8 ++-- question/type/numerical/tests/answer_test.php | 42 ++++++++++++++++--- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/question/type/numerical/question.php b/question/type/numerical/question.php index 93d276af9ce1f..110cfdc02a941 100644 --- a/question/type/numerical/question.php +++ b/question/type/numerical/question.php @@ -320,10 +320,13 @@ public function get_tolerance_interval() { throw new coding_exception('Cannot work out tolerance interval for answer *.'); } + // Smallest number that, when added to 1, is different from 1. + $epsilon = pow(10, -1 * ini_get('precision')); + // We need to add a tiny fraction depending on the set precision to make // the comparison work correctly, otherwise seemingly equal values can // yield false. See MDL-3225. - $tolerance = (float) $this->tolerance + pow(10, -1 * ini_get('precision')); + $tolerance = abs($this->tolerance) + $epsilon; switch ($this->tolerancetype) { case 1: case 'relative': @@ -331,8 +334,7 @@ public function get_tolerance_interval() { return array($this->answer - $range, $this->answer + $range); case 2: case 'nominal': - $tolerance = $this->tolerance + pow(10, -1 * ini_get('precision')) * - max(1, abs($this->answer)); + $tolerance = $this->tolerance + $epsilon * max(abs($this->tolerance), abs($this->answer), $epsilon); return array($this->answer - $tolerance, $this->answer + $tolerance); case 3: case 'geometric': diff --git a/question/type/numerical/tests/answer_test.php b/question/type/numerical/tests/answer_test.php index c3d276ae64143..a74fed3c9247d 100644 --- a/question/type/numerical/tests/answer_test.php +++ b/question/type/numerical/tests/answer_test.php @@ -25,8 +25,10 @@ */ global $CFG; +require_once($CFG->dirroot . '/question/engine/tests/helpers.php'); require_once($CFG->dirroot . '/question/type/numerical/question.php'); + class qtype_numerical_answer_test extends advanced_testcase { public function test_within_tolerance_nominal() { $answer = new qtype_numerical_answer(13, 7.0, 1.0, '', FORMAT_MOODLE, 1.0); @@ -38,16 +40,46 @@ public function test_within_tolerance_nominal() { $this->assertFalse($answer->within_tolerance(8.01)); } + public function test_within_tolerance_nominal_zero() { + // Either an answer or tolerance of 0 requires special care. We still + // don't want to end up comparing two floats for absolute equality. + + // Zero tol, non-zero answer. + $answer = new qtype_numerical_answer(13, 1e-20, 1.0, '', FORMAT_MOODLE, 0.0); + $this->assertFalse($answer->within_tolerance(0.9999999e-20)); + $this->assertTrue($answer->within_tolerance(1e-20)); + $this->assertFalse($answer->within_tolerance(1.0000001e-20)); + + // Non-zero tol, zero answer. + $answer = new qtype_numerical_answer(13, 0.0, 1.0, '', FORMAT_MOODLE, 1e-24); + $this->assertFalse($answer->within_tolerance(-2e-24)); + $this->assertTrue($answer->within_tolerance(-1e-24)); + $this->assertTrue($answer->within_tolerance(0)); + $this->assertTrue($answer->within_tolerance(1e-24)); + $this->assertFalse($answer->within_tolerance(2e-24)); + + // Zero tol, zero answer. + $answer = new qtype_numerical_answer(13, 0.0, 1.0, '', FORMAT_MOODLE, 1e-24); + $this->assertFalse($answer->within_tolerance(-1e-20)); + $this->assertTrue($answer->within_tolerance(-1e-35)); + $this->assertTrue($answer->within_tolerance(0)); + $this->assertTrue($answer->within_tolerance(1e-35)); + $this->assertFalse($answer->within_tolerance(1e-20)); + + // Non-zero tol, non-zero answer. + $answer = new qtype_numerical_answer(13, 1e-20, 1.0, '', FORMAT_MOODLE, 1e-24); + $this->assertFalse($answer->within_tolerance(1.0002e-20)); + $this->assertTrue($answer->within_tolerance(1.0001e-20)); + $this->assertTrue($answer->within_tolerance(1e-20)); + $this->assertTrue($answer->within_tolerance(1.0001e-20)); + $this->assertFalse($answer->within_tolerance(1.0002e-20)); + } + public function test_within_tolerance_blank() { $answer = new qtype_numerical_answer(13, 1234, 1.0, '', FORMAT_MOODLE, ''); $this->assertTrue($answer->within_tolerance(1234)); $this->assertFalse($answer->within_tolerance(1234.000001)); $this->assertFalse($answer->within_tolerance(0)); - - $answer = new qtype_numerical_answer(13, 0, 1.0, '', FORMAT_MOODLE, ''); - $this->assertTrue($answer->within_tolerance(0)); - $this->assertFalse($answer->within_tolerance(pow(10, -1 * ini_get('precision') + 1))); - $this->assertTrue($answer->within_tolerance(pow(10, -1 * ini_get('precision')))); } public function test_within_tolerance_relative() { From d2acbd1ad4e853a2824880f94ca09ced9b38a9d5 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Tue, 21 Aug 2012 14:11:13 +0100 Subject: [PATCH 08/90] MDL-34993 questions: convert numeric fields to float on load. NUMBER(X,Y) typically come back from the DB as strings. If you don't convert them to float, then when you display them, it appears as 1.0000000, which is not normally what you want. Also, increase the size of the field on the edit form, so if you question does have default mark 0.1234567, you can see that! --- lib/questionlib.php | 8 ++++++++ question/type/edit_question_form.php | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/questionlib.php b/lib/questionlib.php index 149f351446834..f738015befebd 100644 --- a/lib/questionlib.php +++ b/lib/questionlib.php @@ -780,14 +780,22 @@ function question_load_questions($questionids, $extrafields = '', $join = '') { */ function _tidy_question($question, $loadtags = false) { global $CFG; + + // Load question-type specific fields. if (!question_bank::is_qtype_installed($question->qtype)) { $question->questiontext = html_writer::tag('p', get_string('warningmissingtype', 'qtype_missingtype')) . $question->questiontext; } question_bank::get_qtype($question->qtype)->get_question_options($question); + + // Convert numeric fields to float. (Prevents these being displayed as 1.0000000.) + $question->defaultmark += 0; + $question->penalty += 0; + 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); diff --git a/question/type/edit_question_form.php b/question/type/edit_question_form.php index 7428254ebe826..714d6a87009e9 100644 --- a/question/type/edit_question_form.php +++ b/question/type/edit_question_form.php @@ -190,7 +190,7 @@ protected function definition() { $mform->setType('questiontext', PARAM_RAW); $mform->addElement('text', 'defaultmark', get_string('defaultmark', 'question'), - array('size' => 3)); + array('size' => 7)); $mform->setType('defaultmark', PARAM_FLOAT); $mform->setDefault('defaultmark', 1); $mform->addRule('defaultmark', null, 'required', null, 'client'); From 9c2e178e2fd435c52abaf781f58eb6aa9ff5bf9b Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Thu, 23 Aug 2012 11:53:47 +0100 Subject: [PATCH 09/90] MDL-35003 questions: remove stray full stop after correct answer. In a few situations, this full stop makes things a bit more grammatical, but there are many other situations where it causes problems. So, on balance we will remove it. --- question/type/match/lang/en/qtype_match.php | 2 +- question/type/multichoice/lang/en/qtype_multichoice.php | 2 +- question/type/shortanswer/lang/en/qtype_shortanswer.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/question/type/match/lang/en/qtype_match.php b/question/type/match/lang/en/qtype_match.php index f9f1f02829b60..a096607bbc8f7 100644 --- a/question/type/match/lang/en/qtype_match.php +++ b/question/type/match/lang/en/qtype_match.php @@ -25,7 +25,7 @@ $string['addmoreqblanks'] = '{no} More Sets of Blanks'; $string['availablechoices'] = 'Available choices'; -$string['correctansweris'] = 'The correct answer is: {$a}.'; +$string['correctansweris'] = 'The correct answer is: {$a}'; $string['filloutthreeqsandtwoas'] = 'You must provide at least two questions and three answers. You can provide extra wrong answers by giving an answer with a blank question. Entries where both the question and the answer are blank will be ignored.'; $string['nomatchinganswer'] = 'You must specify an answer matching the question \'{$a}\'.'; $string['nomatchinganswerforq'] = 'You must specify an answer for this question.'; diff --git a/question/type/multichoice/lang/en/qtype_multichoice.php b/question/type/multichoice/lang/en/qtype_multichoice.php index ba236be0da1d0..8f3c02edf8798 100644 --- a/question/type/multichoice/lang/en/qtype_multichoice.php +++ b/question/type/multichoice/lang/en/qtype_multichoice.php @@ -37,7 +37,7 @@ $string['choiceno'] = 'Choice {$a}'; $string['choices'] = 'Available choices'; $string['clozeaid'] = 'Enter missing word'; -$string['correctansweris'] = 'The correct answer is: {$a}.'; +$string['correctansweris'] = 'The correct answer is: {$a}'; $string['correctfeedback'] = 'For any correct response'; $string['errgradesetanswerblank'] = 'Grade set, but the Answer is blank'; $string['errfractionsaddwrong'] = 'The positive grades you have chosen do not add up to 100%
Instead, they add up to {$a}%'; diff --git a/question/type/shortanswer/lang/en/qtype_shortanswer.php b/question/type/shortanswer/lang/en/qtype_shortanswer.php index df9c127e4efce..0f42cf6a4ab9a 100644 --- a/question/type/shortanswer/lang/en/qtype_shortanswer.php +++ b/question/type/shortanswer/lang/en/qtype_shortanswer.php @@ -30,7 +30,7 @@ $string['caseno'] = 'No, case is unimportant'; $string['casesensitive'] = 'Case sensitivity'; $string['caseyes'] = 'Yes, case must match'; -$string['correctansweris'] = 'The correct answer is: {$a}.'; +$string['correctansweris'] = 'The correct answer is: {$a}'; $string['correctanswers'] = 'Correct answers'; $string['filloutoneanswer'] = 'You must provide at least one possible answer. Answers left blank will not be used. \'*\' can be used as a wildcard to match any characters. The first matching answer will be used to determine the score and feedback.'; $string['notenoughanswers'] = 'This type of question requires at least {$a} answers'; From 2b3f70dbaddf2d85686256a9a133a01e4a45eace Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Thu, 23 Aug 2012 11:16:24 +0100 Subject: [PATCH 10/90] MDL-35023 qtype calculated: fix strict syntax problem. --- question/type/calculated/questiontype.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/question/type/calculated/questiontype.php b/question/type/calculated/questiontype.php index 9083ae3d53b5c..2ffb7d5f8b62a 100644 --- a/question/type/calculated/questiontype.php +++ b/question/type/calculated/questiontype.php @@ -1701,17 +1701,19 @@ public function print_dataset_definitions_category_shared($question, $datasetdef WHERE i.id = d.datasetdefinition AND i.category = ?"; if ($records = $DB->get_records_sql($sql, array($category))) { foreach ($records as $r) { + $key = "$r->type-$r->category-$r->name"; $sql1 = "SELECT q.* FROM {question} q WHERE q.id = ?"; - if (!isset ($datasetdefs["$r->type-$r->category-$r->name"])) { - $datasetdefs["$r->type-$r->category-$r->name"]= $r; + if (!isset($datasetdefs[$key])) { + $datasetdefs[$key] = $r; } if ($questionb = $DB->get_records_sql($sql1, array($r->question))) { - $datasetdefs["$r->type-$r->category-$r->name"]->questions[ - $r->question]->name = $questionb[$r->question]->name; - $datasetdefs["$r->type-$r->category-$r->name"]->questions[ - $r->question]->id = $questionb[$r->question]->id; + $datasetdefs[$key]->questions[$r->question] = new stdClass(); + $datasetdefs[$key]->questions[$r->question]->name = + $questionb[$r->question]->name; + $datasetdefs[$key]->questions[$r->question]->id = + $questionb[$r->question]->id; } } } From e7a6779efbc1db177cca7dbe371361d1aecb814d Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Thu, 23 Aug 2012 11:37:51 +0100 Subject: [PATCH 11/90] MDL-35026 qtype multianswer: misnamed string. AMOS BEGIN MOV [questionnadded,qtype_multianswer],[questionsadded,qtype_multianswer] AMOS END --- question/type/multianswer/lang/en/qtype_multianswer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/question/type/multianswer/lang/en/qtype_multianswer.php b/question/type/multianswer/lang/en/qtype_multianswer.php index 192bb770604ce..ddc01555a6a3a 100644 --- a/question/type/multianswer/lang/en/qtype_multianswer.php +++ b/question/type/multianswer/lang/en/qtype_multianswer.php @@ -42,7 +42,6 @@ $string['pluginnameediting'] = 'Editing an Embedded answers (Cloze) question'; $string['pluginnamesummary'] = 'Questions of this type are very flexible, but can only be created by entering text containing special codes that create embedded multiple-choice, short answers and numerical questions.'; $string['qtypenotrecognized'] = 'questiontype {$a} not recognized'; -$string['questionnadded'] = 'Question added'; $string['questiondefinition'] = 'Question definition'; $string['questiondeleted'] = 'Question deleted'; $string['questioninquiz'] = ' @@ -52,6 +51,7 @@
  • change the questions order in the text,
  • change their question type (numerical, shortanswer, multiple choice).
  • '; +$string['questionsadded'] = 'Question added'; $string['questionsless'] = '{$a} question(s) less than in the multianswer question stored in the database'; $string['questionsmissing'] = 'The question text must include at least one embedded answer.'; $string['questionsmore'] = '{$a} question(s) more than in the multianswer question stored in the database'; From 940f7b9171276ef5776a576aed12e730e674dd4a Mon Sep 17 00:00:00 2001 From: M Kassaei Date: Thu, 23 Aug 2012 17:58:56 +0100 Subject: [PATCH 12/90] MDL-35038 quiz reports: document the API changes in 2.3. --- mod/quiz/report/upgrade.txt | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/mod/quiz/report/upgrade.txt b/mod/quiz/report/upgrade.txt index c12e7ff470b3c..0a3cdd7a58c41 100644 --- a/mod/quiz/report/upgrade.txt +++ b/mod/quiz/report/upgrade.txt @@ -20,3 +20,24 @@ frequency should be defined in version.php, not in the quiz_reports table. === 2.3 === * Support for the old way of doing cron in a separate cron.php file has been removed. +You need a lib.php file inside the pluginneme (quiz report name) and a cron function +with the name quiz_pluginname_cron(), where pluginnme is the report name (e.g.: +quiz_statistics_cron()). + +* Some globally defined constants with the prefix "QUIZ_REPORT_ATTEMPTS_" are put inside +the abstract class "quiz_attempts_report" in Moodle 2.3.and they associate as follows: + +withis the classes drived from "quiz_attempts_report": + +parent::ALL_WITH replaces QUIZ_REPORT_ATTEMPTS_ALL +parent::ENROLLED_ALL replaces QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS +parent::ENROLLED_WITH replaces QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH +parent::ENROLLED_WITHOUT replaces QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO + +anywhere else: +quiz_attempts_report::ALL_WITH replaces QUIZ_REPORT_ATTEMPTS_ALL +quiz_attempts_report::ENROLLED_ALL replaces QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS +quiz_attempts_report::ENROLLED_WITH replaces QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH +quiz_attempts_report::ENROLLED_WITHOUT replaces QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO + +* The clas "quiz_attempt_report" ahd been renbamed as "quiz_attempts_report" From f8d354282c9510de20332c875b05de37a01d8eee Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Thu, 23 Aug 2012 18:26:43 +0100 Subject: [PATCH 13/90] MDL-35038 quiz reports: clarify the API changes docs. Also re-organise the file to put the most recent changes at the top. --- mod/quiz/report/upgrade.txt | 54 ++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/mod/quiz/report/upgrade.txt b/mod/quiz/report/upgrade.txt index 0a3cdd7a58c41..d1821cac14832 100644 --- a/mod/quiz/report/upgrade.txt +++ b/mod/quiz/report/upgrade.txt @@ -3,9 +3,34 @@ This files describes API changes for quiz report plugins. Overview of this plugin type at http://docs.moodle.org/dev/Quiz_reports -=== earlier versions === +=== 2.3 === -* ... API changes were not documented properly. Sorry. (There weren't many!) +* Support for the old way of doing cron in a separate cron.php file has been removed. +Instead, you need a lib.php file inside the plugin with a cron function +called quiz_myreportname_cron(). The statistics report is an example of how +it should be done. + +* There was a big refactor of the quiz reports, in issues MDL-32300, MDL-32322 and MDL-3030. +It is difficult to explain the changes. Probably the best way to understand what +happened is to look at + git log mod/quiz/report/overview + git log mod/quiz/report/responses +and so on. Here are some notes on a few of the changes: + +The class quiz_attempt_report was renamed to quiz_attempts_report (with an extra s). + +Some globally defined constants with the prefix QUIZ_REPORT_ATTEMPTS_ moved into +the quiz_attempts_report class. Specifically + +quiz_attempts_report::ALL_WITH replaces QUIZ_REPORT_ATTEMPTS_ALL +quiz_attempts_report::ENROLLED_ALL replaces QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS +quiz_attempts_report::ENROLLED_WITH replaces QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH +quiz_attempts_report::ENROLLED_WITHOUT replaces QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO + +Your if you have a table class, it needs to be renamed like +quiz_report_myreportname_table -> quiz_myreportname_table. That is, all the +class names in your plugin should start with the frankenstyle plugin name +quiz_myreportname. === 2.2 === @@ -17,27 +42,6 @@ This replaces the old way of having a separate cron.php file. Also, the cron frequency should be defined in version.php, not in the quiz_reports table. -=== 2.3 === - -* Support for the old way of doing cron in a separate cron.php file has been removed. -You need a lib.php file inside the pluginneme (quiz report name) and a cron function -with the name quiz_pluginname_cron(), where pluginnme is the report name (e.g.: -quiz_statistics_cron()). - -* Some globally defined constants with the prefix "QUIZ_REPORT_ATTEMPTS_" are put inside -the abstract class "quiz_attempts_report" in Moodle 2.3.and they associate as follows: - -withis the classes drived from "quiz_attempts_report": - -parent::ALL_WITH replaces QUIZ_REPORT_ATTEMPTS_ALL -parent::ENROLLED_ALL replaces QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS -parent::ENROLLED_WITH replaces QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH -parent::ENROLLED_WITHOUT replaces QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO - -anywhere else: -quiz_attempts_report::ALL_WITH replaces QUIZ_REPORT_ATTEMPTS_ALL -quiz_attempts_report::ENROLLED_ALL replaces QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS -quiz_attempts_report::ENROLLED_WITH replaces QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH -quiz_attempts_report::ENROLLED_WITHOUT replaces QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO +=== earlier versions === -* The clas "quiz_attempt_report" ahd been renbamed as "quiz_attempts_report" +* ... API changes were not documented properly. Sorry. (There weren't many!) From ca9385682f2a0810a7194de7594624bac159e0a6 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Wed, 22 Aug 2012 22:02:48 +0100 Subject: [PATCH 14/90] MDL-34430 qtype essay: upgrade from MDL-31393 needs a progress bar. --- question/type/essay/db/upgrade.php | 68 +++++++++++++++++------------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/question/type/essay/db/upgrade.php b/question/type/essay/db/upgrade.php index 0441751c3bf90..762c0c0f28506 100644 --- a/question/type/essay/db/upgrade.php +++ b/question/type/essay/db/upgrade.php @@ -41,41 +41,51 @@ function xmldb_qtype_essay_upgrade($oldversion) { // Put any upgrade step following this if ($oldversion < 2011102701) { - // In Moodle <= 2.0 essay had both question.generalfeedback and question_answers.feedback. - // This was silly, and in Moodel >= 2.1 only question.generalfeedback. To avoid - // dataloss, we concatenate question_answers.feedback onto the end of question.generalfeedback. - $toupdate = $DB->get_recordset_sql(" - SELECT q.id, - q.generalfeedback, - q.generalfeedbackformat, - qa.feedback, - qa.feedbackformat - + $sql = " FROM {question} q JOIN {question_answers} qa ON qa.question = q.id WHERE q.qtype = 'essay' - AND " . $DB->sql_isnotempty('question_answers', 'feedback', false, true)); - - foreach ($toupdate as $data) { - upgrade_set_timeout(60); - if ($data->generalfeedbackformat == $data->feedbackformat) { - $DB->set_field('question', 'generalfeedback', - $data->generalfeedback . $data->feedback, - array('id' => $data->id)); - - } else { - $newdata = new stdClass(); - $newdata->id = $data->id; - $newdata->generalfeedback = - qtype_essay_convert_to_html($data->generalfeedback, $data->generalfeedbackformat) . - qtype_essay_convert_to_html($data->feedback, $data->feedbackformat); - $newdata->generalfeedbackformat = FORMAT_HTML; - $DB->update_record('question', $newdata); + AND " . $DB->sql_isnotempty('question_answers', 'feedback', false, true); + // In Moodle <= 2.0 essay had both question.generalfeedback and question_answers.feedback. + // This was silly, and in Moodel >= 2.1 only question.generalfeedback. To avoid + // dataloss, we concatenate question_answers.feedback onto the end of question.generalfeedback. + $count = $DB->count_records_sql(" + SELECT COUNT(1) $sql"); + if ($count) { + $progressbar = new progress_bar('essay23', 500, true); + $done = 0; + + $toupdate = $DB->get_recordset_sql(" + SELECT q.id, + q.generalfeedback, + q.generalfeedbackformat, + qa.feedback, + qa.feedbackformat + $sql"); + + foreach ($toupdate as $data) { + $progressbar->update($done, $count, "Updating essay feedback ($done/$count)."); + upgrade_set_timeout(60); + if ($data->generalfeedbackformat == $data->feedbackformat) { + $DB->set_field('question', 'generalfeedback', + $data->generalfeedback . $data->feedback, + array('id' => $data->id)); + + } else { + $newdata = new stdClass(); + $newdata->id = $data->id; + $newdata->generalfeedback = + qtype_essay_convert_to_html($data->generalfeedback, $data->generalfeedbackformat) . + qtype_essay_convert_to_html($data->feedback, $data->feedbackformat); + $newdata->generalfeedbackformat = FORMAT_HTML; + $DB->update_record('question', $newdata); + } } - } - $toupdate->close(); + $progressbar->update($count, $count, "Updating essay feedback complete!"); + $toupdate->close(); + } // Essay savepoint reached. upgrade_plugin_savepoint(true, 2011102701, 'qtype', 'essay'); From c6841df4dfc31c25316fd9d5ba57f7f8313362e4 Mon Sep 17 00:00:00 2001 From: Andrew Davis Date: Fri, 17 Aug 2012 11:28:56 +0800 Subject: [PATCH 15/90] MDL-34406 message: removed an unnecessary string creation --- message/edit.php | 1 - 1 file changed, 1 deletion(-) diff --git a/message/edit.php b/message/edit.php index 5d0aca4dcdd25..4c908db13b271 100644 --- a/message/edit.php +++ b/message/edit.php @@ -179,7 +179,6 @@ /// Display page header $streditmymessage = get_string('editmymessage', 'message'); $strparticipants = get_string('participants'); -$userfullname = fullname($user, true); $PAGE->set_title("$course->shortname: $streditmymessage"); if ($course->id != SITEID) { From 5bc8335c6faa14e3fe04692b02391045d0f01ac7 Mon Sep 17 00:00:00 2001 From: Justin Filip Date: Fri, 2 Dec 2011 14:55:27 -0500 Subject: [PATCH 16/90] MDL-29598 backup Check whether a grade_letters record exists before trying to insert a new record --- backup/moodle2/restore_stepslib.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index 3677d7bee02a8..255808f28fa8a 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -270,7 +270,15 @@ protected function process_grade_letter($data) { $data->contextid = context_course::instance($this->get_courseid())->id; - $newitemid = $DB->insert_record('grade_letters', $data); + // MDL-29598 - Don't insert a duplicate record if this grade letter already exists + $gltest = (array)$data; + unset($gltest['id']); + if (!$DB->record_exists('grade_letters', $gltest)) { + $newitemid = $DB->insert_record('grade_letters', $data); + } else { + $newitemid = $data->id; + } + $this->set_mapping('grade_letter', $oldid, $newitemid); } protected function process_grade_setting($data) { From 67d4424a6b85243f3cfa8c504e4641dc234c2bdf Mon Sep 17 00:00:00 2001 From: David Monllao Date: Fri, 17 Aug 2012 10:18:20 +0800 Subject: [PATCH 17/90] MDL-29598 backup Avoid possible future duplicate grade letters More info in restore_activity_grades_structure_step->process_grade_letter() comments --- backup/moodle2/restore_stepslib.php | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index 255808f28fa8a..6b398521774b0 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -270,10 +270,9 @@ protected function process_grade_letter($data) { $data->contextid = context_course::instance($this->get_courseid())->id; - // MDL-29598 - Don't insert a duplicate record if this grade letter already exists - $gltest = (array)$data; - unset($gltest['id']); - if (!$DB->record_exists('grade_letters', $gltest)) { + $gradeletter = (array)$data; + unset($gradeletter['id']); + if (!$DB->record_exists('grade_letters', $gradeletter)) { $newitemid = $DB->insert_record('grade_letters', $data); } else { $newitemid = $data->id; @@ -2407,17 +2406,21 @@ protected function process_grade_grade($data) { /** * process activity grade_letters. Note that, while these are possible, - * because grade_letters are contextid based, in proctice, only course + * because grade_letters are contextid based, in practice, only course * context letters can be defined. So we keep here this method knowing * it won't be executed ever. gradebook restore will restore course letters. */ protected function process_grade_letter($data) { global $DB; - $data = (object)$data; + $data['contextid'] = $this->task->get_contextid(); + $gradeletter = (object)$data; - $data->contextid = $this->task->get_contextid(); - $newitemid = $DB->insert_record('grade_letters', $data); + // Check if it exists before adding it + unset($data['id']); + if (!$DB->record_exists('grade_letters', $data)) { + $newitemid = $DB->insert_record('grade_letters', $gradeletter); + } // no need to save any grade_letter mapping } } From c517046e4d04c5222decb774fd4db79e50c38185 Mon Sep 17 00:00:00 2001 From: Rajesh Taneja Date: Fri, 17 Aug 2012 15:46:09 +0800 Subject: [PATCH 18/90] MDL-30121 Assignment 2.2: Draft submission files will not be included in zip, if activity is open --- mod/assignment/type/upload/assignment.class.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/mod/assignment/type/upload/assignment.class.php b/mod/assignment/type/upload/assignment.class.php index 8133c8f820497..5f76748053fb5 100644 --- a/mod/assignment/type/upload/assignment.class.php +++ b/mod/assignment/type/upload/assignment.class.php @@ -1150,7 +1150,7 @@ public function download_submissions() { require_once($CFG->libdir.'/filelib.php'); $submissions = $this->get_submissions('',''); if (empty($submissions)) { - print_error('errornosubmissions', 'assignment'); + print_error('errornosubmissions', 'assignment', new moodle_url('/mod/assignment/submissions.php', array('id'=>$this->cm->id))); } $filesforzipping = array(); $fs = get_file_storage(); @@ -1164,6 +1164,11 @@ public function download_submissions() { } $filename = str_replace(' ', '_', clean_filename($this->course->shortname.'-'.$this->assignment->name.'-'.$groupname.$this->assignment->id.".zip")); //name of new zip file. foreach ($submissions as $submission) { + // If assignment is open and submission is not finalized then don't add it to zip. + $submissionstatus = $this->is_finalized($submission); + if ($this->isopen() && empty($submissionstatus)) { + continue; + } $a_userid = $submission->userid; //get userid if ((groups_is_member($groupid,$a_userid)or !$groupmode or !$groupid)) { $a_assignid = $submission->assignment; //get name of this assignment for use in the file names. @@ -1180,6 +1185,12 @@ public function download_submissions() { } } } // end of foreach loop + + // Throw error if no files are added. + if (empty($filesforzipping)) { + print_error('errornosubmissions', 'assignment', new moodle_url('/mod/assignment/submissions.php', array('id'=>$this->cm->id))); + } + if ($zipfile = assignment_pack_files($filesforzipping)) { send_temp_file($zipfile, $filename); //send file and delete after sending. } From 055cc8353d6dc0b342b4b36af9dbaf8e73e206ec Mon Sep 17 00:00:00 2001 From: Aaron Barnes Date: Fri, 24 Aug 2012 12:05:52 +1200 Subject: [PATCH 19/90] MDL-35042 blocks: Allow HTML block advanced setting to be toggled --- blocks/html/block_html.php | 12 ++++++++++-- blocks/html/edit_form.php | 10 +++++++--- blocks/html/lang/en/block_html.php | 4 +++- blocks/html/settings.php | 10 ++++++++++ 4 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 blocks/html/settings.php diff --git a/blocks/html/block_html.php b/blocks/html/block_html.php index 39040ae47dd73..b616925d70dbf 100644 --- a/blocks/html/block_html.php +++ b/blocks/html/block_html.php @@ -29,6 +29,10 @@ function init() { $this->title = get_string('pluginname', 'block_html'); } + function has_config() { + return true; + } + function applicable_formats() { return array('all' => true); } @@ -138,10 +142,14 @@ public function instance_can_be_docked() { * @return array */ function html_attributes() { + global $CFG; + $attributes = parent::html_attributes(); - if (!empty($this->config->classes)) { - $attributes['class'] .= ' '.$this->config->classes; + if (!empty($CFG->block_html_allowcssclasses)) { + if (!empty($this->config->classes)) { + $attributes['class'] .= ' '.$this->config->classes; + } } return $attributes; diff --git a/blocks/html/edit_form.php b/blocks/html/edit_form.php index f544f98cee02d..edb57055fd01f 100644 --- a/blocks/html/edit_form.php +++ b/blocks/html/edit_form.php @@ -31,6 +31,8 @@ */ class block_html_edit_form extends block_edit_form { protected function specific_definition($mform) { + global $CFG; + // Fields for editing HTML block title and contents. $mform->addElement('header', 'configheader', get_string('blocksettings', 'block')); @@ -42,9 +44,11 @@ protected function specific_definition($mform) { $mform->addRule('config_text', null, 'required', null, 'client'); $mform->setType('config_text', PARAM_RAW); // XSS is prevented when printing the block contents and serving files - $mform->addElement('text', 'config_classes', get_string('configclasses', 'block_html')); - $mform->setType('config_classes', PARAM_TEXT); - $mform->addHelpButton('config_classes', 'configclasses', 'block_html'); + if (!empty($CFG->block_html_allowcssclasses)) { + $mform->addElement('text', 'config_classes', get_string('configclasses', 'block_html')); + $mform->setType('config_classes', PARAM_TEXT); + $mform->addHelpButton('config_classes', 'configclasses', 'block_html'); + } } function set_data($defaults) { diff --git a/blocks/html/lang/en/block_html.php b/blocks/html/lang/en/block_html.php index 6e82be8064081..d331059e3f7ae 100644 --- a/blocks/html/lang/en/block_html.php +++ b/blocks/html/lang/en/block_html.php @@ -23,7 +23,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -$string['configclasses'] = 'Additional HTML classes'; +$string['allowadditionalcssclasses'] = 'Allow additional CSS classes'; +$string['configallowadditionalcssclasses'] = 'Adds a configuration option to HTML block instances allowing additional CSS classes to be set.'; +$string['configclasses'] = 'Additional CSS classes'; $string['configclasses_help'] = 'The purpose of this configuration is to aid with theming by helping distinguish HTML blocks from each other. Any CSS classes entered here (space delimited) will be appended to the block\'s default classes.'; $string['configcontent'] = 'Content'; $string['configtitle'] = 'Block title'; diff --git a/blocks/html/settings.php b/blocks/html/settings.php new file mode 100644 index 0000000000000..59cb79760d53d --- /dev/null +++ b/blocks/html/settings.php @@ -0,0 +1,10 @@ +fulltree) { + $settings->add(new admin_setting_configcheckbox('block_html_allowcssclasses', get_string('allowadditionalcssclasses', 'block_html'), + get_string('configallowadditionalcssclasses', 'block_html'), 0)); +} + + From 303385394228ac13308d5c0148b3128d777b11aa Mon Sep 17 00:00:00 2001 From: Tim Lock Date: Mon, 20 Aug 2012 14:33:01 +0800 Subject: [PATCH 20/90] MDL-31623 Course: Course reset bypass modules, which are removed --- course/reset_form.php | 6 +++--- lib/moodlelib.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/course/reset_form.php b/course/reset_form.php index 751c0c74f8ed7..020edd0114d4f 100644 --- a/course/reset_form.php +++ b/course/reset_form.php @@ -61,13 +61,13 @@ function definition (){ if ($allmods = $DB->get_records('modules') ) { foreach ($allmods as $mod) { $modname = $mod->name; - if (!$DB->count_records($modname, array('course'=>$COURSE->id))) { - continue; // skip mods with no instances - } $modfile = $CFG->dirroot."/mod/$modname/lib.php"; $mod_reset_course_form_definition = $modname.'_reset_course_form_definition'; $mod_reset__userdata = $modname.'_reset_userdata'; if (file_exists($modfile)) { + if (!$DB->count_records($modname, array('course'=>$COURSE->id))) { + continue; // Skip mods with no instances + } include_once($modfile); if (function_exists($mod_reset_course_form_definition)) { $mod_reset_course_form_definition($mform); diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 55cfd1ffa16c9..aaeddaaf6e534 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -4982,12 +4982,12 @@ function reset_course_userdata($data) { if ($allmods = $DB->get_records('modules') ) { foreach ($allmods as $mod) { $modname = $mod->name; - if (!$DB->count_records($modname, array('course'=>$data->courseid))) { - continue; // skip mods with no instances - } $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php'; $moddeleteuserdata = $modname.'_reset_userdata'; // Function to delete user data if (file_exists($modfile)) { + if (!$DB->count_records($modname, array('course'=>$data->courseid))) { + continue; // Skip mods with no instances + } include_once($modfile); if (function_exists($moddeleteuserdata)) { $modstatus = $moddeleteuserdata($data); From d197ea4300083cfa6c4f09613f9fb1f98ce2e69a Mon Sep 17 00:00:00 2001 From: Ankit Agarwal Date: Tue, 21 Aug 2012 14:20:30 +0800 Subject: [PATCH 21/90] MDL-34549 libraries: Replace get_context_instance_by_id() by context::instance_by_id() --- admin/roles/lib.php | 8 ++++---- admin/user/user_bulk_cohortadd.php | 2 +- blocks/community/block_community.php | 4 ++-- blocks/edit_form.php | 2 +- blocks/html/block_html.php | 2 +- blocks/html/lib.php | 2 +- blocks/quiz_results/block_quiz_results.php | 2 +- cohort/assign.php | 2 +- cohort/edit.php | 4 ++-- cohort/edit_form.php | 2 +- cohort/index.php | 2 +- comment/lib.php | 2 +- comment/locallib.php | 2 +- draftfile.php | 2 +- enrol/category/locallib.php | 8 ++++---- enrol/cohort/addinstance_form.php | 2 +- enrol/cohort/lib.php | 2 +- enrol/cohort/locallib.php | 6 +++--- enrol/externallib.php | 4 ++-- files/externallib.php | 4 ++-- files/renderer.php | 2 +- grade/grading/lib.php | 2 +- grade/grading/manage.php | 2 +- lib/blocklib.php | 2 +- lib/externallib.php | 2 +- lib/filebrowser/file_info_context_course.php | 2 +- lib/filebrowser/file_info_context_coursecat.php | 2 +- lib/filebrowser/file_info_context_module.php | 2 +- lib/questionlib.php | 4 ++-- lib/setuplib.php | 2 +- lib/tests/accesslib_test.php | 2 +- lib/weblib.php | 4 ++-- mod/data/lib.php | 2 +- mod/forum/lib.php | 2 +- mod/glossary/lib.php | 2 +- mod/quiz/edit.php | 2 +- mod/quiz/editlib.php | 2 +- mod/wiki/edit_form.php | 2 +- question/addquestion.php | 2 +- question/category.php | 2 +- question/category_class.php | 8 ++++---- question/editlib.php | 8 ++++---- question/engine/questionusage.php | 2 +- question/format.php | 6 +++--- question/import.php | 2 +- question/preview.php | 2 +- question/question.php | 6 +++--- question/type/calculated/datasetdefinitions_form.php | 2 +- question/type/calculated/datasetitems_form.php | 2 +- question/type/edit_question_form.php | 4 ++-- question/type/questiontypebase.php | 2 +- report/security/locallib.php | 4 ++-- repository/coursefiles/lib.php | 4 ++-- repository/draftfiles_ajax.php | 2 +- repository/lib.php | 6 +++--- repository/local/lib.php | 2 +- repository/manage_instances.php | 4 ++-- repository/recent/lib.php | 2 +- repository/repository_ajax.php | 4 ++-- user/index.php | 2 +- webservice/lib.php | 2 +- 61 files changed, 92 insertions(+), 92 deletions(-) diff --git a/admin/roles/lib.php b/admin/roles/lib.php index 43eb7f530b856..df537f008da93 100644 --- a/admin/roles/lib.php +++ b/admin/roles/lib.php @@ -933,7 +933,7 @@ protected function load_parent_permissions() { global $DB; /// Get the capabilities from the parent context, so that can be shown in the interface. - $parentcontext = get_context_instance_by_id(get_parent_contextid($this->context)); + $parentcontext = context::instance_by_id(get_parent_contextid($this->context)); $this->parentpermissions = role_context_capabilities($this->roleid, $parentcontext); } @@ -996,7 +996,7 @@ public function __construct($name, $options) { if (isset($options['context'])) { $this->context = $options['context']; } else { - $this->context = get_context_instance_by_id($options['contextid']); + $this->context = context::instance_by_id($options['contextid']); } $options['accesscontext'] = $this->context; parent::__construct($name, $options); @@ -1230,7 +1230,7 @@ protected function this_con_group_name($search, $numusers) { } protected function parent_con_group_name($search, $contextid) { - $context = get_context_instance_by_id($contextid); + $context = context::instance_by_id($contextid); $contextname = print_context_name($context, true, true); if ($search) { $a = new stdClass; @@ -1477,7 +1477,7 @@ public function get_intro_text() { function roles_get_potential_user_selector($context, $name, $options) { $blockinsidecourse = false; if ($context->contextlevel == CONTEXT_BLOCK) { - $parentcontext = get_context_instance_by_id(get_parent_contextid($context)); + $parentcontext = context::instance_by_id(get_parent_contextid($context)); $blockinsidecourse = in_array($parentcontext->contextlevel, array(CONTEXT_MODULE, CONTEXT_COURSE)); } diff --git a/admin/user/user_bulk_cohortadd.php b/admin/user/user_bulk_cohortadd.php index 1e32564fe59e8..be72d51c26c85 100644 --- a/admin/user/user_bulk_cohortadd.php +++ b/admin/user/user_bulk_cohortadd.php @@ -45,7 +45,7 @@ // external cohorts can not be modified continue; } - $context = get_context_instance_by_id($c->contextid); + $context = context::instance_by_id($c->contextid); if (!has_capability('moodle/cohort:assign', $context)) { continue; } diff --git a/blocks/community/block_community.php b/blocks/community/block_community.php index 35e3f2e212d2d..c211342cc180f 100644 --- a/blocks/community/block_community.php +++ b/blocks/community/block_community.php @@ -43,7 +43,7 @@ function user_can_addto($page) { function user_can_edit() { // Don't allow people to edit the block if they can't even use it if (!has_capability('moodle/community:add', - get_context_instance_by_id($this->instance->parentcontextid))) { + context::instance_by_id($this->instance->parentcontextid))) { return false; } return parent::user_can_edit(); @@ -52,7 +52,7 @@ function user_can_edit() { function get_content() { global $CFG, $OUTPUT, $USER; - $coursecontext = get_context_instance_by_id($this->instance->parentcontextid); + $coursecontext = context::instance_by_id($this->instance->parentcontextid); if (!has_capability('moodle/community:add', $coursecontext) or $this->content !== NULL) { diff --git a/blocks/edit_form.php b/blocks/edit_form.php index 9380fa2be7270..169c54de4ea9b 100644 --- a/blocks/edit_form.php +++ b/blocks/edit_form.php @@ -87,7 +87,7 @@ function definition() { $regionoptions = $this->page->theme->get_all_block_regions(); - $parentcontext = get_context_instance_by_id($this->block->instance->parentcontextid); + $parentcontext = context::instance_by_id($this->block->instance->parentcontextid); $mform->addElement('hidden', 'bui_parentcontextid', $parentcontext->id); $mform->addElement('static', 'bui_homecontext', get_string('createdat', 'block'), print_context_name($parentcontext)); diff --git a/blocks/html/block_html.php b/blocks/html/block_html.php index 39040ae47dd73..6ca71b30f1b13 100644 --- a/blocks/html/block_html.php +++ b/blocks/html/block_html.php @@ -104,7 +104,7 @@ function instance_delete() { function content_is_trusted() { global $SCRIPT; - if (!$context = get_context_instance_by_id($this->instance->parentcontextid)) { + if (!$context = context::instance_by_id($this->instance->parentcontextid)) { return false; } //find out if this block is on the profile page diff --git a/blocks/html/lib.php b/blocks/html/lib.php index a2555c3f88e8a..93fdbb8c8e407 100644 --- a/blocks/html/lib.php +++ b/blocks/html/lib.php @@ -53,7 +53,7 @@ function block_html_pluginfile($course, $birecord_or_cm, $context, $filearea, $a send_file_not_found(); } - if ($parentcontext = get_context_instance_by_id($birecord_or_cm->parentcontextid)) { + if ($parentcontext = context::instance_by_id($birecord_or_cm->parentcontextid)) { if ($parentcontext->contextlevel == CONTEXT_USER) { // force download on all personal pages including /my/ //because we do not have reliable way to find out from where this is used diff --git a/blocks/quiz_results/block_quiz_results.php b/blocks/quiz_results/block_quiz_results.php index d18af5563c886..8795a687f3489 100644 --- a/blocks/quiz_results/block_quiz_results.php +++ b/blocks/quiz_results/block_quiz_results.php @@ -65,7 +65,7 @@ public function get_owning_quiz() { if (empty($this->instance->parentcontextid)) { return 0; } - $parentcontext = get_context_instance_by_id($this->instance->parentcontextid); + $parentcontext = context::instance_by_id($this->instance->parentcontextid); if ($parentcontext->contextlevel != CONTEXT_MODULE) { return 0; } diff --git a/cohort/assign.php b/cohort/assign.php index b7f65896a5f7b..5eaa2a704f511 100644 --- a/cohort/assign.php +++ b/cohort/assign.php @@ -32,7 +32,7 @@ require_login(); $cohort = $DB->get_record('cohort', array('id'=>$id), '*', MUST_EXIST); -$context = get_context_instance_by_id($cohort->contextid, MUST_EXIST); +$context = context::instance_by_id($cohort->contextid, MUST_EXIST); require_capability('moodle/cohort:assign', $context); diff --git a/cohort/edit.php b/cohort/edit.php index dc1bd08bb0418..721d790ed67bb 100644 --- a/cohort/edit.php +++ b/cohort/edit.php @@ -40,9 +40,9 @@ $category = null; if ($id) { $cohort = $DB->get_record('cohort', array('id'=>$id), '*', MUST_EXIST); - $context = get_context_instance_by_id($cohort->contextid, MUST_EXIST); + $context = context::instance_by_id($cohort->contextid, MUST_EXIST); } else { - $context = get_context_instance_by_id($contextid, MUST_EXIST); + $context = context::instance_by_id($contextid, MUST_EXIST); if ($context->contextlevel != CONTEXT_COURSECAT and $context->contextlevel != CONTEXT_SYSTEM) { print_error('invalidcontext'); } diff --git a/cohort/edit_form.php b/cohort/edit_form.php index 957607171c884..81e7a7cb6c089 100644 --- a/cohort/edit_form.php +++ b/cohort/edit_form.php @@ -103,7 +103,7 @@ protected function get_category_options($currentcontextid) { } // always add current - this is not likely, but if the logic gets changed it might be a problem if (!isset($options[$currentcontextid])) { - $context = get_context_instance_by_id($currentcontextid, MUST_EXIST); + $context = context::instance_by_id($currentcontextid, MUST_EXIST); $options[$context->id] = print_context_name($syscontext); } return $options; diff --git a/cohort/index.php b/cohort/index.php index 026bcb855e909..00aab668325ca 100644 --- a/cohort/index.php +++ b/cohort/index.php @@ -35,7 +35,7 @@ require_login(); if ($contextid) { - $context = get_context_instance_by_id($contextid, MUST_EXIST); + $context = context::instance_by_id($contextid, MUST_EXIST); } else { $context = context_system::instance(); } diff --git a/comment/lib.php b/comment/lib.php index 9bf2653a6a09b..8954244e8260b 100644 --- a/comment/lib.php +++ b/comment/lib.php @@ -120,7 +120,7 @@ public function __construct(stdClass $options) { $this->contextid = $this->context->id; } else if(!empty($options->contextid)) { $this->contextid = $options->contextid; - $this->context = get_context_instance_by_id($this->contextid); + $this->context = context::instance_by_id($this->contextid); } else { print_error('invalidcontext'); } diff --git a/comment/locallib.php b/comment/locallib.php index 5a8b0d7bf9919..25f907f4c16f5 100644 --- a/comment/locallib.php +++ b/comment/locallib.php @@ -114,7 +114,7 @@ private function setup_course($courseid) { */ private function setup_plugin($comment) { global $DB; - $this->context = get_context_instance_by_id($comment->contextid); + $this->context = context::instance_by_id($comment->contextid); if (!$this->context) { return false; } diff --git a/draftfile.php b/draftfile.php index ede5706913491..821ae57665022 100644 --- a/draftfile.php +++ b/draftfile.php @@ -61,7 +61,7 @@ send_file_not_found(); } -$context = get_context_instance_by_id($contextid); +$context = context::instance_by_id($contextid); if ($context->contextlevel != CONTEXT_USER) { send_file_not_found(); } diff --git a/enrol/category/locallib.php b/enrol/category/locallib.php index 2c16f7662034f..9e4ab0c35ee60 100644 --- a/enrol/category/locallib.php +++ b/enrol/category/locallib.php @@ -45,8 +45,8 @@ public static function role_assigned($ra) { return true; } - // Only category level roles are interesting. - $parentcontext = get_context_instance_by_id($ra->contextid); + //only category level roles are interesting + $parentcontext = context::instance_by_id($ra->contextid); if ($parentcontext->contextlevel != CONTEXT_COURSECAT) { return true; } @@ -102,8 +102,8 @@ public static function role_unassigned($ra) { return true; } - // Only category level roles are interesting. - $parentcontext = get_context_instance_by_id($ra->contextid); + // only category level roles are interesting + $parentcontext = context::instance_by_id($ra->contextid); if ($parentcontext->contextlevel != CONTEXT_COURSECAT) { return true; } diff --git a/enrol/cohort/addinstance_form.php b/enrol/cohort/addinstance_form.php index e9015a2883ca3..c2094937c0cd1 100644 --- a/enrol/cohort/addinstance_form.php +++ b/enrol/cohort/addinstance_form.php @@ -46,7 +46,7 @@ function definition() { ORDER BY name ASC"; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $c) { - $context = get_context_instance_by_id($c->contextid); + $context = context::instance_by_id($c->contextid); if (!has_capability('moodle/cohort:view', $context)) { continue; } diff --git a/enrol/cohort/lib.php b/enrol/cohort/lib.php index b70b89f2fb16b..4ad304eb17b52 100644 --- a/enrol/cohort/lib.php +++ b/enrol/cohort/lib.php @@ -92,7 +92,7 @@ protected function can_add_new_instances($courseid) { ORDER BY name ASC"; $cohorts = $DB->get_records_sql($sql, $params); foreach ($cohorts as $c) { - $context = get_context_instance_by_id($c->contextid); + $context = context::instance_by_id($c->contextid); if (has_capability('moodle/cohort:view', $context)) { return true; } diff --git a/enrol/cohort/locallib.php b/enrol/cohort/locallib.php index 5ca237a6c29f4..869e06c4aa264 100644 --- a/enrol/cohort/locallib.php +++ b/enrol/cohort/locallib.php @@ -368,7 +368,7 @@ function enrol_cohort_get_cohorts(course_enrolment_manager $manager) { ORDER BY name ASC"; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $c) { - $context = get_context_instance_by_id($c->contextid); + $context = context::instance_by_id($c->contextid); if (!has_capability('moodle/cohort:view', $context)) { continue; } @@ -394,7 +394,7 @@ function enrol_cohort_can_view_cohort($cohortid) { global $DB; $cohort = $DB->get_record('cohort', array('id' => $cohortid), 'id, contextid'); if ($cohort) { - $context = get_context_instance_by_id($cohort->contextid); + $context = context::instance_by_id($cohort->contextid); if (has_capability('moodle/cohort:view', $context)) { return true; } @@ -457,7 +457,7 @@ function enrol_cohort_search_cohorts(course_enrolment_manager $manager, $offset // Track offset $offset++; // Check capabilities - $context = get_context_instance_by_id($c->contextid); + $context = context::instance_by_id($c->contextid); if (!has_capability('moodle/cohort:view', $context)) { continue; } diff --git a/enrol/externallib.php b/enrol/externallib.php index 0fd4182d48844..e669ca747f704 100644 --- a/enrol/externallib.php +++ b/enrol/externallib.php @@ -381,7 +381,7 @@ public static function assign_roles($assignments) { foreach ($params['assignments'] as $assignment) { // Ensure the current user is allowed to run this function in the enrolment context - $context = get_context_instance_by_id($assignment['contextid']); + $context = context::instance_by_id($assignment['contextid']); self::validate_context($context); require_capability('moodle/role:assign', $context); @@ -445,7 +445,7 @@ public static function unassign_roles($unassignments) { foreach ($params['unassignments'] as $unassignment) { // Ensure the current user is allowed to run this function in the unassignment context - $context = get_context_instance_by_id($unassignment['contextid']); + $context = context::instance_by_id($unassignment['contextid']); self::validate_context($context); require_capability('moodle/role:assign', $context); diff --git a/files/externallib.php b/files/externallib.php index f6001d2e9e60d..4d8e3aeb99c85 100644 --- a/files/externallib.php +++ b/files/externallib.php @@ -82,7 +82,7 @@ public static function get_files($contextid, $component, $filearea, $itemid, $fi if (empty($fileinfo['contextid'])) { $context = get_system_context(); } else { - $context = get_context_instance_by_id($fileinfo['contextid']); + $context = context::instance_by_id($fileinfo['contextid']); } if (empty($fileinfo['component'])) { $fileinfo['component'] = null; @@ -272,7 +272,7 @@ public static function upload($contextid, $component, $filearea, $itemid, $filep } if (!empty($fileinfo['contextid'])) { - $context = get_context_instance_by_id($fileinfo['contextid']); + $context = context::instance_by_id($fileinfo['contextid']); } else { $context = get_system_context(); } diff --git a/files/renderer.php b/files/renderer.php index cfa14e08b899c..6a648b05792e8 100644 --- a/files/renderer.php +++ b/files/renderer.php @@ -965,7 +965,7 @@ public function __construct(file_info $file_info, array $options = null) { $this->path = array(); while ($level) { $params = $level->get_params(); - $context = get_context_instance_by_id($params['contextid']); + $context = context::instance_by_id($params['contextid']); // $this->context is current context if ($context->id != $this->context->id or empty($params['filearea'])) { break; diff --git a/grade/grading/lib.php b/grade/grading/lib.php index c6e81864f8a2b..0a3de68b30648 100644 --- a/grade/grading/lib.php +++ b/grade/grading/lib.php @@ -223,7 +223,7 @@ public function load($areaid) { global $DB; $this->areacache = $DB->get_record('grading_areas', array('id' => $areaid), '*', MUST_EXIST); - $this->context = get_context_instance_by_id($this->areacache->contextid, MUST_EXIST); + $this->context = context::instance_by_id($this->areacache->contextid, MUST_EXIST); $this->component = $this->areacache->component; $this->area = $this->areacache->areaname; } diff --git a/grade/grading/manage.php b/grade/grading/manage.php index 954f91f90a151..c5d90ada8ec13 100644 --- a/grade/grading/manage.php +++ b/grade/grading/manage.php @@ -57,7 +57,7 @@ if (is_null($contextid) or is_null($component) or is_null($area)) { throw new coding_exception('The caller script must identify the gradable area.'); } - $context = get_context_instance_by_id($contextid, MUST_EXIST); + $context = context::instance_by_id($contextid, MUST_EXIST); $manager = get_grading_manager($context, $component, $area); } diff --git a/lib/blocklib.php b/lib/blocklib.php index 79ec70d185611..76ff72884ff02 100644 --- a/lib/blocklib.php +++ b/lib/blocklib.php @@ -1238,7 +1238,7 @@ public function process_url_edit() { $systemcontext = context_system::instance(); $frontpagecontext = context_course::instance(SITEID); - $parentcontext = get_context_instance_by_id($data->bui_parentcontextid); + $parentcontext = context::instance_by_id($data->bui_parentcontextid); // Updating stickiness and contexts. See MDL-21375 for details. if (has_capability('moodle/site:manageblocks', $parentcontext)) { // Check permissions in destination diff --git a/lib/externallib.php b/lib/externallib.php index f547850e0f66c..0c6e44dbc04d1 100644 --- a/lib/externallib.php +++ b/lib/externallib.php @@ -520,7 +520,7 @@ function external_generate_token($tokentype, $serviceorid, $userid, $contextorid $service = $serviceorid; } if (!is_object($contextorid)){ - $context = get_context_instance_by_id($contextorid, MUST_EXIST); + $context = context::instance_by_id($contextorid, MUST_EXIST); } else { $context = $contextorid; } diff --git a/lib/filebrowser/file_info_context_course.php b/lib/filebrowser/file_info_context_course.php index 05b3489029049..d1e703b7e9d11 100644 --- a/lib/filebrowser/file_info_context_course.php +++ b/lib/filebrowser/file_info_context_course.php @@ -401,7 +401,7 @@ public function get_children() { public function get_parent() { //TODO: error checking if get_parent_contextid() returns false $pcid = get_parent_contextid($this->context); - $parent = get_context_instance_by_id($pcid); + $parent = context::instance_by_id($pcid); return $this->browser->get_file_info($parent); } } diff --git a/lib/filebrowser/file_info_context_coursecat.php b/lib/filebrowser/file_info_context_coursecat.php index 1cded8310f1df..47636e6ce060c 100644 --- a/lib/filebrowser/file_info_context_coursecat.php +++ b/lib/filebrowser/file_info_context_coursecat.php @@ -195,7 +195,7 @@ public function get_children() { */ public function get_parent() { $cid = get_parent_contextid($this->context); - $parent = get_context_instance_by_id($cid); + $parent = context::instance_by_id($cid); return $this->browser->get_file_info($parent); } } diff --git a/lib/filebrowser/file_info_context_module.php b/lib/filebrowser/file_info_context_module.php index 0357ee34cde17..363f6a0b82f27 100644 --- a/lib/filebrowser/file_info_context_module.php +++ b/lib/filebrowser/file_info_context_module.php @@ -283,7 +283,7 @@ public function get_children() { */ public function get_parent() { $pcid = get_parent_contextid($this->context); - $parent = get_context_instance_by_id($pcid); + $parent = context::instance_by_id($pcid); return $this->browser->get_file_info($parent); } } diff --git a/lib/questionlib.php b/lib/questionlib.php index 149f351446834..fbfab99ab8433 100644 --- a/lib/questionlib.php +++ b/lib/questionlib.php @@ -1106,7 +1106,7 @@ function question_category_options($contexts, $top = false, $currentcat = 0, $categoriesarray = array(); foreach ($pcontexts as $pcontext) { $contextstring = print_context_name( - get_context_instance_by_id($pcontext), true, true); + context::instance_by_id($pcontext), true, true); foreach ($categories as $category) { if ($category->contextid == $pcontext) { $cid = $category->id; @@ -1333,7 +1333,7 @@ function question_has_capability_on($question, $cap, $cachecat = -1) { } } $category = $categories[$question->category]; - $context = get_context_instance_by_id($category->contextid); + $context = context::instance_by_id($category->contextid); if (array_search($cap, $question_questioncaps)!== false) { if (!has_capability('moodle/question:' . $cap . 'all', $context)) { diff --git a/lib/setuplib.php b/lib/setuplib.php index 51f8d827f42b9..16b2aa3cd9b66 100644 --- a/lib/setuplib.php +++ b/lib/setuplib.php @@ -206,7 +206,7 @@ function __construct($context, $capability, $errormessage, $stringfile) { $capabilityname = get_capability_string($capability); if ($context->contextlevel == CONTEXT_MODULE and preg_match('/:view$/', $capability)) { // we can not go to mod/xx/view.php because we most probably do not have cap to view it, let's go to course instead - $paranetcontext = get_context_instance_by_id(get_parent_contextid($context)); + $paranetcontext = context::instance_by_id(get_parent_contextid($context)); $link = get_context_url($paranetcontext); } else { $link = get_context_url($context); diff --git a/lib/tests/accesslib_test.php b/lib/tests/accesslib_test.php index 52d700fea28ab..b8535fd7c98ce 100644 --- a/lib/tests/accesslib_test.php +++ b/lib/tests/accesslib_test.php @@ -2271,7 +2271,7 @@ public function test_permission_evaluation() { foreach ($DB->get_records('context') as $contextid=>$record) { $context = context::instance_by_id($contextid); - $this->assertSame(get_context_instance_by_id($contextid), $context); + $this->assertSame(context::instance_by_id($contextid), $context); $this->assertSame(get_context_instance($record->contextlevel, $record->instanceid), $context); $this->assertSame(get_parent_contexts($context), $context->get_parent_context_ids()); if ($context->id == SYSCONTEXTID) { diff --git a/lib/weblib.php b/lib/weblib.php index 1295b00182dac..b3d7339346bf0 100644 --- a/lib/weblib.php +++ b/lib/weblib.php @@ -1072,7 +1072,7 @@ function format_text($text, $format = FORMAT_MOODLE, $options = NULL, $courseid_ if (is_object($options['context'])) { $context = $options['context']; } else { - $context = get_context_instance_by_id($options['context']); + $context = context::instance_by_id($options['context']); } } else if ($courseid_do_not_use) { // legacy courseid @@ -1281,7 +1281,7 @@ function format_string($string, $striplinks = true, $options = NULL) { // fallback to $PAGE->context this may be problematic in CLI and other non-standard pages :-( $options['context'] = $PAGE->context; } else if (is_numeric($options['context'])) { - $options['context'] = get_context_instance_by_id($options['context']); + $options['context'] = context::instance_by_id($options['context']); } if (!$options['context']) { diff --git a/mod/data/lib.php b/mod/data/lib.php index fa660c0dc500b..0cc49b312358f 100644 --- a/mod/data/lib.php +++ b/mod/data/lib.php @@ -1325,7 +1325,7 @@ function data_print_template($template, $records, $data, $search='', $page=0, $r * @return array an associative array of the user's rating permissions */ function data_rating_permissions($contextid, $component, $ratingarea) { - $context = get_context_instance_by_id($contextid, MUST_EXIST); + $context = context::instance_by_id($contextid, MUST_EXIST); if ($component != 'mod_data' || $ratingarea != 'entry') { return null; } diff --git a/mod/forum/lib.php b/mod/forum/lib.php index 405bc95df6e56..b36bcd66d52a0 100644 --- a/mod/forum/lib.php +++ b/mod/forum/lib.php @@ -3527,7 +3527,7 @@ function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=fa * @return array an associative array of the user's rating permissions */ function forum_rating_permissions($contextid, $component, $ratingarea) { - $context = get_context_instance_by_id($contextid, MUST_EXIST); + $context = context::instance_by_id($contextid, MUST_EXIST); if ($component != 'mod_forum' || $ratingarea != 'post') { // We don't know about this component/ratingarea so just return null to get the // default restrictive permissions. diff --git a/mod/glossary/lib.php b/mod/glossary/lib.php index 1ddcc56a89baa..ab8657a8eef1b 100644 --- a/mod/glossary/lib.php +++ b/mod/glossary/lib.php @@ -637,7 +637,7 @@ function glossary_rating_permissions($contextid, $component, $ratingarea) { // default restrictive permissions. return null; } - $context = get_context_instance_by_id($contextid); + $context = context::instance_by_id($contextid); return array( 'view' => has_capability('mod/glossary:viewrating', $context), 'viewany' => has_capability('mod/glossary:viewanyrating', $context), diff --git a/mod/quiz/edit.php b/mod/quiz/edit.php index ef5e633ead601..1fce2b49eac41 100644 --- a/mod/quiz/edit.php +++ b/mod/quiz/edit.php @@ -73,7 +73,7 @@ function module_specific_buttons($cmid, $cmoptions) { function module_specific_controls($totalnumber, $recurse, $category, $cmid, $cmoptions) { global $OUTPUT; $out = ''; - $catcontext = get_context_instance_by_id($category->contextid); + $catcontext = context::instance_by_id($category->contextid); if (has_capability('moodle/question:useall', $catcontext)) { if ($cmoptions->hasattempts) { $disabled = ' disabled="disabled"'; diff --git a/mod/quiz/editlib.php b/mod/quiz/editlib.php index 44f00d6ec89e6..987a39b3f0a6b 100644 --- a/mod/quiz/editlib.php +++ b/mod/quiz/editlib.php @@ -183,7 +183,7 @@ function quiz_add_random_questions($quiz, $addonpage, $categoryid, $number, print_error('invalidcategoryid', 'error'); } - $catcontext = get_context_instance_by_id($category->contextid); + $catcontext = context::instance_by_id($category->contextid); require_capability('moodle/question:useall', $catcontext); // Find existing random questions in this category that are diff --git a/mod/wiki/edit_form.php b/mod/wiki/edit_form.php index 9992ddeeeac39..2d34a6cf737cb 100644 --- a/mod/wiki/edit_form.php +++ b/mod/wiki/edit_form.php @@ -59,7 +59,7 @@ protected function definition() { if (isset($this->_customdata['pagetitle'])) { // Page title must be formatted properly here as this is output and not an element. - $pagetitle = get_string('editingpage', 'wiki', format_string($this->_customdata['pagetitle'], true, array('context' => get_context_instance_by_id($contextid, MUST_EXIST)))); + $pagetitle = get_string('editingpage', 'wiki', format_string($this->_customdata['pagetitle'], true, array('context' => context::instance_by_id($contextid, MUST_EXIST)))); } else { $pagetitle = get_string('editing', 'wiki'); } diff --git a/question/addquestion.php b/question/addquestion.php index fa262cdadc9d9..02e6224280a30 100644 --- a/question/addquestion.php +++ b/question/addquestion.php @@ -60,7 +60,7 @@ } // Check permissions. -$categorycontext = get_context_instance_by_id($category->contextid); +$categorycontext = context::instance_by_id($category->contextid); require_capability('moodle/question:add', $categorycontext); // Ensure other optional params get passed on to question.php. diff --git a/question/category.php b/question/category.php index 5ad3de9404974..b208a469a4bf5 100644 --- a/question/category.php +++ b/question/category.php @@ -71,7 +71,7 @@ if (!$category = $DB->get_record("question_categories", array("id" => $param->delete))) { // security print_error('nocate', 'question', $thispageurl->out(), $param->delete); } - $categorycontext = get_context_instance_by_id($category->contextid); + $categorycontext = context::instance_by_id($category->contextid); $qcobject->moveform = new question_move_form($thispageurl, array('contexts'=>array($categorycontext), 'currentcat'=>$param->delete)); if ($qcobject->moveform->is_cancelled()){ diff --git a/question/category_class.php b/question/category_class.php index fd65dbac81fe4..79339f29e1cb4 100644 --- a/question/category_class.php +++ b/question/category_class.php @@ -246,7 +246,7 @@ public function output_edit_lists() { $listhtml = $list->to_html(0, array('str'=>$this->str)); if ($listhtml){ echo $OUTPUT->box_start('boxwidthwide boxaligncenter generalbox questioncategories contextlevel' . $list->context->contextlevel); - echo $OUTPUT->heading(get_string('questioncatsfor', 'question', print_context_name(get_context_instance_by_id($context))), 3); + echo $OUTPUT->heading(get_string('questioncatsfor', 'question', print_context_name(context::instance_by_id($context))), 3); echo $listhtml; echo $OUTPUT->box_end(); } @@ -374,7 +374,7 @@ public function add_category($newparent, $newcategory, $newinfo, $return = false } list($parentid, $contextid) = explode(',', $newparent); //moodle_form makes sure select element output is legal no need for further cleaning - require_capability('moodle/question:managecategory', get_context_instance_by_id($contextid)); + require_capability('moodle/question:managecategory', context::instance_by_id($contextid)); if ($parentid) { if(!($DB->get_field('question_categories', 'contextid', array('id' => $parentid)) == $contextid)) { @@ -418,12 +418,12 @@ public function update_category($updateid, $newparent, $newname, $newinfo) { } // Check permissions. - $fromcontext = get_context_instance_by_id($oldcat->contextid); + $fromcontext = context::instance_by_id($oldcat->contextid); require_capability('moodle/question:managecategory', $fromcontext); // If moving to another context, check permissions some more. if ($oldcat->contextid != $tocontextid) { - $tocontext = get_context_instance_by_id($tocontextid); + $tocontext = context::instance_by_id($tocontextid); require_capability('moodle/question:managecategory', $tocontext); } diff --git a/question/editlib.php b/question/editlib.php index f28b9c2fa6000..bb4633f4fd051 100644 --- a/question/editlib.php +++ b/question/editlib.php @@ -117,7 +117,7 @@ function question_can_delete_cat($todelete) { print_error('cannotdeletecate', 'question'); } else { $contextid = $DB->get_field('question_categories', 'contextid', array('id' => $todelete)); - require_capability('moodle/question:managecategory', get_context_instance_by_id($contextid)); + require_capability('moodle/question:managecategory', context::instance_by_id($contextid)); } } @@ -1323,7 +1323,7 @@ protected function display_question_list($contexts, $pageurl, $categoryandcontex $strdelete = get_string('delete'); list($categoryid, $contextid) = explode(',', $categoryandcontext); - $catcontext = get_context_instance_by_id($contextid); + $catcontext = context::instance_by_id($contextid); $canadd = has_capability('moodle/question:add', $catcontext); $caneditall =has_capability('moodle/question:editall', $catcontext); @@ -1467,7 +1467,7 @@ public function process_actions() { if (! $tocategory = $DB->get_record('question_categories', array('id' => $tocategoryid, 'contextid' => $contextid))) { print_error('cannotfindcate', 'question'); } - $tocontext = get_context_instance_by_id($contextid); + $tocontext = context::instance_by_id($contextid); require_capability('moodle/question:add', $tocontext); $rawdata = (array) data_submitted(); $questionids = array(); @@ -1717,7 +1717,7 @@ function question_get_display_preference($param, $default, $type, $thispageurl) function require_login_in_context($contextorid = null){ global $DB, $CFG; if (!is_object($contextorid)){ - $context = get_context_instance_by_id($contextorid); + $context = context::instance_by_id($contextorid); } else { $context = $contextorid; } diff --git a/question/engine/questionusage.php b/question/engine/questionusage.php index e4de1979502f2..d6fbae8d639d7 100644 --- a/question/engine/questionusage.php +++ b/question/engine/questionusage.php @@ -708,7 +708,7 @@ public static function load_from_records($records, $qubaid) { } $quba = new question_usage_by_activity($record->component, - get_context_instance_by_id($record->contextid)); + context::instance_by_id($record->contextid)); $quba->set_id_from_database($record->qubaid); $quba->set_preferred_behaviour($record->preferredbehaviour); diff --git a/question/format.php b/question/format.php index e1cc7db6ac0a2..bf9daca967151 100644 --- a/question/format.php +++ b/question/format.php @@ -128,7 +128,7 @@ public function setCategory($category) { debugging('You shouldn\'t call setCategory after setQuestions'); } $this->category = $category; - $this->importcontext = get_context_instance_by_id($this->category->contextid); + $this->importcontext = context::instance_by_id($this->category->contextid); } /** @@ -500,10 +500,10 @@ protected function create_category_path($catpath) { } if ($this->contextfromfile && $contextid !== false) { - $context = get_context_instance_by_id($contextid); + $context = context::instance_by_id($contextid); require_capability('moodle/question:add', $context); } else { - $context = get_context_instance_by_id($this->category->contextid); + $context = context::instance_by_id($this->category->contextid); } $this->importcontext = $context; diff --git a/question/import.php b/question/import.php index 20b6c9a4c7322..7110e531b73ed 100644 --- a/question/import.php +++ b/question/import.php @@ -42,7 +42,7 @@ print_error('nocategory', 'question'); } -$categorycontext = get_context_instance_by_id($category->contextid); +$categorycontext = context::instance_by_id($category->contextid); $category->context = $categorycontext; //this page can be called without courseid or cmid in which case //we get the context from the category object. diff --git a/question/preview.php b/question/preview.php index 293876260a461..b8a83ed5c4b92 100644 --- a/question/preview.php +++ b/question/preview.php @@ -60,7 +60,7 @@ require_login(); $category = $DB->get_record('question_categories', array('id' => $question->category), '*', MUST_EXIST); - $context = get_context_instance_by_id($category->contextid); + $context = context::instance_by_id($category->contextid); $PAGE->set_context($context); // Note that in the other cases, require_login will set the correct page context. } diff --git a/question/question.php b/question/question.php index 63f54ad613e5d..47a6893b162ab 100644 --- a/question/question.php +++ b/question/question.php @@ -154,7 +154,7 @@ // Check permissions $question->formoptions = new stdClass(); -$categorycontext = get_context_instance_by_id($category->contextid); +$categorycontext = context::instance_by_id($category->contextid); $addpermission = has_capability('moodle/question:add', $categorycontext); if ($id) { @@ -259,7 +259,7 @@ if ($movecontext) { // We are just moving the question to a different context. list($tocatid, $tocontextid) = explode(',', $fromform->categorymoveto); - require_capability('moodle/question:add', get_context_instance_by_id($tocontextid)); + require_capability('moodle/question:add', context::instance_by_id($tocontextid)); question_move_questions_to_category(array($question->id), $tocatid); } else { @@ -267,7 +267,7 @@ if (!empty($question->id)) { question_require_capability_on($question, 'edit'); } else { - require_capability('moodle/question:add', get_context_instance_by_id($newcontextid)); + require_capability('moodle/question:add', context::instance_by_id($newcontextid)); if (!empty($fromform->makecopy) && !$question->formoptions->cansaveasnew) { print_error('nopermissions', '', '', 'edit'); } diff --git a/question/type/calculated/datasetdefinitions_form.php b/question/type/calculated/datasetdefinitions_form.php index 23a15f30de14d..45efea3bcddc7 100644 --- a/question/type/calculated/datasetdefinitions_form.php +++ b/question/type/calculated/datasetdefinitions_form.php @@ -65,7 +65,7 @@ public function __construct($submiturl, $question) { print_error('categorydoesnotexist', 'question', $returnurl); } $this->category = $category; - $this->categorycontext = get_context_instance_by_id($category->contextid); + $this->categorycontext = context::instance_by_id($category->contextid); parent::__construct($submiturl); } diff --git a/question/type/calculated/datasetitems_form.php b/question/type/calculated/datasetitems_form.php index 3ff97505cc311..aa2156aad4462 100644 --- a/question/type/calculated/datasetitems_form.php +++ b/question/type/calculated/datasetitems_form.php @@ -79,7 +79,7 @@ public function __construct($submiturl, $question, $regenerate) { print_error('categorydoesnotexist', 'question', $returnurl); } $this->category = $category; - $this->categorycontext = get_context_instance_by_id($category->contextid); + $this->categorycontext = context::instance_by_id($category->contextid); //get the dataset defintions for this question if (empty($question->id)) { $this->datasetdefs = $this->qtypeobj->get_dataset_definitions( diff --git a/question/type/edit_question_form.php b/question/type/edit_question_form.php index 7428254ebe826..9a2ebeb41579b 100644 --- a/question/type/edit_question_form.php +++ b/question/type/edit_question_form.php @@ -101,14 +101,14 @@ public function __construct($submiturl, $question, $category, $contexts, $formed $record = $DB->get_record('question_categories', array('id' => $question->category), 'contextid'); - $this->context = get_context_instance_by_id($record->contextid); + $this->context = context::instance_by_id($record->contextid); $this->editoroptions = array('subdirs' => 1, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'context' => $this->context); $this->fileoptions = array('subdirs' => 1, 'maxfiles' => -1, 'maxbytes' => -1); $this->category = $category; - $this->categorycontext = get_context_instance_by_id($category->contextid); + $this->categorycontext = context::instance_by_id($category->contextid); parent::__construct($submiturl, null, 'post', '', null, $formeditable); } diff --git a/question/type/questiontypebase.php b/question/type/questiontypebase.php index 50bbe76ee1dce..22226da6e3ff1 100644 --- a/question/type/questiontypebase.php +++ b/question/type/questiontypebase.php @@ -1023,7 +1023,7 @@ public function generate_test($name, $courseid=null) { protected function get_context_by_category_id($category) { global $DB; $contextid = $DB->get_field('question_categories', 'contextid', array('id'=>$category)); - $context = get_context_instance_by_id($contextid); + $context = context::instance_by_id($contextid); return $context; } diff --git a/report/security/locallib.php b/report/security/locallib.php index 65736291ca97e..f6021fd2193b9 100644 --- a/report/security/locallib.php +++ b/report/security/locallib.php @@ -850,7 +850,7 @@ function report_security_check_riskbackup($detailed=false) { $links = array(); foreach ($overriddenroles as $role) { $role->name = $role->localname; - $context = get_context_instance_by_id($role->contextid); + $context = context::instance_by_id($role->contextid); $role->name = role_get_name($role, $context, ROLENAME_BOTH); $role->contextname = print_context_name($context); $role->url = "$CFG->wwwroot/$CFG->admin/roles/override.php?contextid=$role->contextid&roleid=$role->id"; @@ -867,7 +867,7 @@ function report_security_check_riskbackup($detailed=false) { $sqluserinfo ORDER BY u.lastname, u.firstname", $params); foreach ($rs as $user) { - $context = get_context_instance_by_id($user->contextid); + $context = context::instance_by_id($user->contextid); $url = "$CFG->wwwroot/$CFG->admin/roles/assign.php?contextid=$user->contextid&roleid=$user->roleid"; $a = (object)array('fullname'=>fullname($user), 'url'=>$url, 'email'=>$user->email, 'contextname'=>print_context_name($context)); diff --git a/repository/coursefiles/lib.php b/repository/coursefiles/lib.php index c25be53ca12d9..ba4891a3eddff 100644 --- a/repository/coursefiles/lib.php +++ b/repository/coursefiles/lib.php @@ -67,7 +67,7 @@ public function get_listing($encodedpath = '', $page = '') { if (is_array($params)) { $filepath = is_null($params['filepath']) ? NULL : clean_param($params['filepath'], PARAM_PATH);; $filename = is_null($params['filename']) ? NULL : clean_param($params['filename'], PARAM_FILE); - $context = get_context_instance_by_id(clean_param($params['contextid'], PARAM_INT)); + $context = context::instance_by_id(clean_param($params['contextid'], PARAM_INT)); } } else { $filename = null; @@ -158,7 +158,7 @@ public function get_link($encoded) { $filepath = clean_param($params['filepath'], PARAM_PATH);; $filearea = clean_param($params['filearea'], PARAM_AREA); $component = clean_param($params['component'], PARAM_COMPONENT); - $context = get_context_instance_by_id($contextid); + $context = context::instance_by_id($contextid); $file_info = $browser->get_file_info($context, $component, $filearea, $fileitemid, $filepath, $filename); return $file_info->get_url(); diff --git a/repository/draftfiles_ajax.php b/repository/draftfiles_ajax.php index a3ea3aa00b809..43ff97d678c2f 100644 --- a/repository/draftfiles_ajax.php +++ b/repository/draftfiles_ajax.php @@ -309,7 +309,7 @@ if (isset($source->original)) { $reffiles = $fs->search_references($source->original); foreach ($reffiles as $reffile) { - $refcontext = get_context_instance_by_id($reffile->get_contextid()); + $refcontext = context::instance_by_id($reffile->get_contextid()); $fileinfo = $browser->get_file_info($refcontext, $reffile->get_component(), $reffile->get_filearea(), $reffile->get_itemid(), $reffile->get_filepath(), $reffile->get_filename()); if (empty($fileinfo)) { $return['references'][] = get_string('undisclosedreference', 'repository'); diff --git a/repository/lib.php b/repository/lib.php index e8a60f4b72e06..1a51e8eee1d40 100644 --- a/repository/lib.php +++ b/repository/lib.php @@ -496,7 +496,7 @@ public function __construct($repositoryid, $context = SYSCONTEXTID, $options = a if (is_object($context)) { $this->context = $context; } else { - $this->context = get_context_instance_by_id($context); + $this->context = context::instance_by_id($context); } $this->instance = $DB->get_record('repository_instances', array('id'=>$this->id)); $this->readonly = $readonly; @@ -1175,7 +1175,7 @@ public function get_reference_details($reference, $filestatus = 0) { $fileinfo = null; $params = file_storage::unpack_reference($reference, true); if (is_array($params)) { - $context = get_context_instance_by_id($params['contextid']); + $context = context::instance_by_id($params['contextid']); if ($context) { $browser = get_file_browser(); $fileinfo = $browser->get_file_info($context, $params['component'], $params['filearea'], $params['itemid'], $params['filepath'], $params['filename']); @@ -1622,7 +1622,7 @@ public function get_file_size($source) { $filepath = clean_param($params['filepath'], PARAM_PATH); $filearea = clean_param($params['filearea'], PARAM_AREA); $component = clean_param($params['component'], PARAM_COMPONENT); - $context = get_context_instance_by_id($contextid); + $context = context::instance_by_id($contextid); $file_info = $browser->get_file_info($context, $component, $filearea, $fileitemid, $filepath, $filename); if (!empty($file_info)) { $filesize = $file_info->get_filesize(); diff --git a/repository/local/lib.php b/repository/local/lib.php index a4c511dcbee87..37d214a57a8cf 100644 --- a/repository/local/lib.php +++ b/repository/local/lib.php @@ -63,7 +63,7 @@ public function get_listing($encodedpath = '', $page = '') { $itemid = is_null($params['itemid']) ? NULL : clean_param($params['itemid'], PARAM_INT); $filepath = is_null($params['filepath']) ? NULL : clean_param($params['filepath'], PARAM_PATH);; $filename = is_null($params['filename']) ? NULL : clean_param($params['filename'], PARAM_FILE); - $context = get_context_instance_by_id(clean_param($params['contextid'], PARAM_INT)); + $context = context::instance_by_id(clean_param($params['contextid'], PARAM_INT)); } } else { $itemid = null; diff --git a/repository/manage_instances.php b/repository/manage_instances.php index a6b1363798d13..43efe2efb53ed 100644 --- a/repository/manage_instances.php +++ b/repository/manage_instances.php @@ -65,7 +65,7 @@ $url->param('usercourseid', $usercourseid); } -$context = get_context_instance_by_id($contextid); +$context = context::instance_by_id($contextid); $PAGE->set_url($url); $PAGE->set_context($context); @@ -177,7 +177,7 @@ } $success = $instance->set_option($settings); } else { - $success = repository::static_function($plugin, 'create', $plugin, 0, get_context_instance_by_id($contextid), $fromform); + $success = repository::static_function($plugin, 'create', $plugin, 0, context::instance_by_id($contextid), $fromform); $data = data_submitted(); } if ($success) { diff --git a/repository/recent/lib.php b/repository/recent/lib.php index f1265c84add3f..76cdb69e2a8b8 100644 --- a/repository/recent/lib.php +++ b/repository/recent/lib.php @@ -122,7 +122,7 @@ public function get_listing($encodedpath = '', $page = '') { foreach ($files as $file) { // Check that file exists and accessible, retrieve size/date info $browser = get_file_browser(); - $context = get_context_instance_by_id($file['contextid']); + $context = context::instance_by_id($file['contextid']); $fileinfo = $browser->get_file_info($context, $file['component'], $file['filearea'], $file['itemid'], $file['filepath'], $file['filename']); if ($fileinfo) { diff --git a/repository/repository_ajax.php b/repository/repository_ajax.php index d9f3cea305fd7..f19738be1b9a9 100644 --- a/repository/repository_ajax.php +++ b/repository/repository_ajax.php @@ -94,8 +94,8 @@ // global search case 'gsearch': $params = array(); - $params['context'] = array(get_context_instance_by_id($contextid), get_system_context()); - $params['currentcontext'] = get_context_instance_by_id($contextid); + $params['context'] = array(context::instance_by_id($contextid), get_system_context()); + $params['currentcontext'] = context::instance_by_id($contextid); $repos = repository::get_instances($params); $list = array(); foreach($repos as $repo){ diff --git a/user/index.php b/user/index.php index b30a10b0408b5..73fe0ee667fca 100644 --- a/user/index.php +++ b/user/index.php @@ -34,7 +34,7 @@ 'id' => $courseid)); if ($contextid) { - $context = get_context_instance_by_id($contextid, MUST_EXIST); + $context = context::instance_by_id($contextid, MUST_EXIST); if ($context->contextlevel != CONTEXT_COURSE) { print_error('invalidcontext'); } diff --git a/webservice/lib.php b/webservice/lib.php index 66ed91f8f8ea3..31e78b9a5cf42 100644 --- a/webservice/lib.php +++ b/webservice/lib.php @@ -949,7 +949,7 @@ protected function authenticate_by_token($tokentype){ . ' is not supported - check this allowed user'); } - $this->restricted_context = get_context_instance_by_id($token->contextid); + $this->restricted_context = context::instance_by_id($token->contextid); $this->restricted_serviceid = $token->externalserviceid; $user = $DB->get_record('user', array('id'=>$token->userid), '*', MUST_EXIST); From 5fbe2118bc60ee7611684e4029a50a71b2bbc27a Mon Sep 17 00:00:00 2001 From: Ankit Agarwal Date: Tue, 21 Aug 2012 15:02:40 +0800 Subject: [PATCH 22/90] MDL-34549 libraries: Changing strictness of context::instance_by_id() when required --- blocks/html/block_html.php | 2 +- blocks/html/lib.php | 2 +- comment/locallib.php | 2 +- enrol/externallib.php | 4 ++-- lib/filebrowser/file_info_context_course.php | 2 +- lib/filebrowser/file_info_context_coursecat.php | 2 +- lib/filebrowser/file_info_context_module.php | 2 +- lib/tests/accesslib_test.php | 2 +- question/editlib.php | 2 +- question/type/questiontypebase.php | 2 +- repository/lib.php | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/blocks/html/block_html.php b/blocks/html/block_html.php index 6ca71b30f1b13..c60c118e9e3a8 100644 --- a/blocks/html/block_html.php +++ b/blocks/html/block_html.php @@ -104,7 +104,7 @@ function instance_delete() { function content_is_trusted() { global $SCRIPT; - if (!$context = context::instance_by_id($this->instance->parentcontextid)) { + if (!$context = context::instance_by_id($this->instance->parentcontextid, IGNORE_MISSING)) { return false; } //find out if this block is on the profile page diff --git a/blocks/html/lib.php b/blocks/html/lib.php index 93fdbb8c8e407..03d58ca27ceee 100644 --- a/blocks/html/lib.php +++ b/blocks/html/lib.php @@ -53,7 +53,7 @@ function block_html_pluginfile($course, $birecord_or_cm, $context, $filearea, $a send_file_not_found(); } - if ($parentcontext = context::instance_by_id($birecord_or_cm->parentcontextid)) { + if ($parentcontext = context::instance_by_id($birecord_or_cm->parentcontextid, IGNORE_MISSING)) { if ($parentcontext->contextlevel == CONTEXT_USER) { // force download on all personal pages including /my/ //because we do not have reliable way to find out from where this is used diff --git a/comment/locallib.php b/comment/locallib.php index 25f907f4c16f5..c53fa801a2016 100644 --- a/comment/locallib.php +++ b/comment/locallib.php @@ -114,7 +114,7 @@ private function setup_course($courseid) { */ private function setup_plugin($comment) { global $DB; - $this->context = context::instance_by_id($comment->contextid); + $this->context = context::instance_by_id($comment->contextid, IGNORE_MISSING); if (!$this->context) { return false; } diff --git a/enrol/externallib.php b/enrol/externallib.php index e669ca747f704..757362aa6942e 100644 --- a/enrol/externallib.php +++ b/enrol/externallib.php @@ -381,7 +381,7 @@ public static function assign_roles($assignments) { foreach ($params['assignments'] as $assignment) { // Ensure the current user is allowed to run this function in the enrolment context - $context = context::instance_by_id($assignment['contextid']); + $context = context::instance_by_id($assignment['contextid'], IGNORE_MISSING); self::validate_context($context); require_capability('moodle/role:assign', $context); @@ -445,7 +445,7 @@ public static function unassign_roles($unassignments) { foreach ($params['unassignments'] as $unassignment) { // Ensure the current user is allowed to run this function in the unassignment context - $context = context::instance_by_id($unassignment['contextid']); + $context = context::instance_by_id($unassignment['contextid'], IGNORE_MISSING); self::validate_context($context); require_capability('moodle/role:assign', $context); diff --git a/lib/filebrowser/file_info_context_course.php b/lib/filebrowser/file_info_context_course.php index d1e703b7e9d11..0f0816b15e942 100644 --- a/lib/filebrowser/file_info_context_course.php +++ b/lib/filebrowser/file_info_context_course.php @@ -401,7 +401,7 @@ public function get_children() { public function get_parent() { //TODO: error checking if get_parent_contextid() returns false $pcid = get_parent_contextid($this->context); - $parent = context::instance_by_id($pcid); + $parent = context::instance_by_id($pcid, IGNORE_MISSING); return $this->browser->get_file_info($parent); } } diff --git a/lib/filebrowser/file_info_context_coursecat.php b/lib/filebrowser/file_info_context_coursecat.php index 47636e6ce060c..9cab4502f25e6 100644 --- a/lib/filebrowser/file_info_context_coursecat.php +++ b/lib/filebrowser/file_info_context_coursecat.php @@ -195,7 +195,7 @@ public function get_children() { */ public function get_parent() { $cid = get_parent_contextid($this->context); - $parent = context::instance_by_id($cid); + $parent = context::instance_by_id($cid, IGNORE_MISSING); return $this->browser->get_file_info($parent); } } diff --git a/lib/filebrowser/file_info_context_module.php b/lib/filebrowser/file_info_context_module.php index 363f6a0b82f27..7e8ac7722acda 100644 --- a/lib/filebrowser/file_info_context_module.php +++ b/lib/filebrowser/file_info_context_module.php @@ -283,7 +283,7 @@ public function get_children() { */ public function get_parent() { $pcid = get_parent_contextid($this->context); - $parent = context::instance_by_id($pcid); + $parent = context::instance_by_id($pcid, IGNORE_MISSING); return $this->browser->get_file_info($parent); } } diff --git a/lib/tests/accesslib_test.php b/lib/tests/accesslib_test.php index b8535fd7c98ce..4542513ba4910 100644 --- a/lib/tests/accesslib_test.php +++ b/lib/tests/accesslib_test.php @@ -2271,7 +2271,7 @@ public function test_permission_evaluation() { foreach ($DB->get_records('context') as $contextid=>$record) { $context = context::instance_by_id($contextid); - $this->assertSame(context::instance_by_id($contextid), $context); + $this->assertSame(context::instance_by_id($contextid, IGNORE_MISSING), $context); $this->assertSame(get_context_instance($record->contextlevel, $record->instanceid), $context); $this->assertSame(get_parent_contexts($context), $context->get_parent_context_ids()); if ($context->id == SYSCONTEXTID) { diff --git a/question/editlib.php b/question/editlib.php index bb4633f4fd051..bb563f718f332 100644 --- a/question/editlib.php +++ b/question/editlib.php @@ -1717,7 +1717,7 @@ function question_get_display_preference($param, $default, $type, $thispageurl) function require_login_in_context($contextorid = null){ global $DB, $CFG; if (!is_object($contextorid)){ - $context = context::instance_by_id($contextorid); + $context = context::instance_by_id($contextorid, IGNORE_MISSING); } else { $context = $contextorid; } diff --git a/question/type/questiontypebase.php b/question/type/questiontypebase.php index 22226da6e3ff1..9b51a8b00437f 100644 --- a/question/type/questiontypebase.php +++ b/question/type/questiontypebase.php @@ -1023,7 +1023,7 @@ public function generate_test($name, $courseid=null) { protected function get_context_by_category_id($category) { global $DB; $contextid = $DB->get_field('question_categories', 'contextid', array('id'=>$category)); - $context = context::instance_by_id($contextid); + $context = context::instance_by_id($contextid, IGNORE_MISSING); return $context; } diff --git a/repository/lib.php b/repository/lib.php index 1a51e8eee1d40..10a8d1c65734e 100644 --- a/repository/lib.php +++ b/repository/lib.php @@ -1175,7 +1175,7 @@ public function get_reference_details($reference, $filestatus = 0) { $fileinfo = null; $params = file_storage::unpack_reference($reference, true); if (is_array($params)) { - $context = context::instance_by_id($params['contextid']); + $context = context::instance_by_id($params['contextid'], IGNORE_MISSING); if ($context) { $browser = get_file_browser(); $fileinfo = $browser->get_file_info($context, $params['component'], $params['filearea'], $params['itemid'], $params['filepath'], $params['filename']); From 42b1867410ef9a11310d6629198dca5fc4e01d9f Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Fri, 24 Aug 2012 11:51:20 +0800 Subject: [PATCH 23/90] MDL-34310 display grading method description inside render_preview This way we can use the function gradingform_controller::render_preview() to display a preview for students in different modules without worrying on gradingform-specific options on whether to display description for students or not --- grade/grading/form/guide/lib.php | 8 ++++++++ grade/grading/form/guide/preview.php | 3 --- grade/grading/form/rubric/lib.php | 8 ++++++++ grade/grading/form/rubric/preview.php | 3 --- grade/grading/manage.php | 1 - 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/grade/grading/form/guide/lib.php b/grade/grading/form/guide/lib.php index 1990064071dd9..976b0c5fe4ada 100644 --- a/grade/grading/form/guide/lib.php +++ b/grade/grading/form/guide/lib.php @@ -506,6 +506,14 @@ public function render_preview(moodle_page $page) { $comments = $this->definition->guide_comment; $options = $this->get_options(); $guide = ''; + if (has_capability('moodle/grade:managegradingforms', $page->context)) { + $showdescription = true; + } else { + $showdescription = $options['showdescriptionstudent']; + } + if ($showdescription) { + $guide .= $output->box($this->get_formatted_description(), 'gradingform_guide-description'); + } if (has_capability('moodle/grade:managegradingforms', $page->context)) { $guide .= $output->display_guide_mapping_explained($this->get_min_max_score()); $guide .= $output->display_guide($criteria, $comments, $options, self::DISPLAY_PREVIEW, 'guide'); diff --git a/grade/grading/form/guide/preview.php b/grade/grading/form/guide/preview.php index ba8aa5d56e3a4..a2c6501779858 100644 --- a/grade/grading/form/guide/preview.php +++ b/grade/grading/form/guide/preview.php @@ -49,8 +49,5 @@ echo $OUTPUT->header(); echo $OUTPUT->heading($title); -if (!empty($options['showdescriptionstudent'])) { - echo $OUTPUT->box($controller->get_formatted_description(), 'gradingform_guide-description'); -} echo $controller->render_preview($PAGE); echo $OUTPUT->footer(); diff --git a/grade/grading/form/rubric/lib.php b/grade/grading/form/rubric/lib.php index ae97eeedb0b24..247724d3daae5 100644 --- a/grade/grading/form/rubric/lib.php +++ b/grade/grading/form/rubric/lib.php @@ -508,6 +508,14 @@ public function render_preview(moodle_page $page) { $criteria = $this->definition->rubric_criteria; $options = $this->get_options(); $rubric = ''; + if (has_capability('moodle/grade:managegradingforms', $page->context)) { + $showdescription = true; + } else { + $showdescription = $options['showdescriptionstudent']; + } + if ($showdescription) { + $rubric .= $output->box($this->get_formatted_description(), 'gradingform_rubric-description'); + } if (has_capability('moodle/grade:managegradingforms', $page->context)) { $rubric .= $output->display_rubric_mapping_explained($this->get_min_max_score()); $rubric .= $output->display_rubric($criteria, $options, self::DISPLAY_PREVIEW, 'rubric'); diff --git a/grade/grading/form/rubric/preview.php b/grade/grading/form/rubric/preview.php index 1a7145819351d..babdef4f48bea 100644 --- a/grade/grading/form/rubric/preview.php +++ b/grade/grading/form/rubric/preview.php @@ -49,8 +49,5 @@ echo $OUTPUT->header(); echo $OUTPUT->heading($title); -if (!empty($options['showdescriptionstudent'])) { - echo $OUTPUT->box($controller->get_formatted_description(), 'gradingform_rubric-description'); -} echo $controller->render_preview($PAGE); echo $OUTPUT->footer(); diff --git a/grade/grading/manage.php b/grade/grading/manage.php index 954f91f90a151..a8149c8870a73 100644 --- a/grade/grading/manage.php +++ b/grade/grading/manage.php @@ -233,7 +233,6 @@ $tag = html_writer::tag('span', get_string('statusdraft', 'core_grading'), array('class' => 'status draft')); } echo $output->heading(s($definition->name) . ' ' . $tag, 3, 'definition-name'); - echo $output->box($controller->get_formatted_description()); echo $output->box($controller->render_preview($PAGE), 'definition-preview'); } } From 77b3e35105ac24139353c25ebda50595b7a7e35f Mon Sep 17 00:00:00 2001 From: Raymond Wijaya Date: Mon, 20 Aug 2012 14:04:29 +0800 Subject: [PATCH 24/90] MDL-34907: Add a missing string that triggers Javascript error in assignment upgrade tool --- admin/tool/assignmentupgrade/lang/en/tool_assignmentupgrade.php | 1 + admin/tool/assignmentupgrade/module.js | 2 +- admin/tool/assignmentupgrade/renderer.php | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/admin/tool/assignmentupgrade/lang/en/tool_assignmentupgrade.php b/admin/tool/assignmentupgrade/lang/en/tool_assignmentupgrade.php index e9a61dcb38123..9dc8c6ece9fca 100644 --- a/admin/tool/assignmentupgrade/lang/en/tool_assignmentupgrade.php +++ b/admin/tool/assignmentupgrade/lang/en/tool_assignmentupgrade.php @@ -36,6 +36,7 @@ $string['conversionfailed'] = 'The assignment conversion was not successful. The log from the upgrade was:
    {$a}'; $string['listnotupgraded'] = 'List assignments that have not been upgraded'; $string['listnotupgraded_desc'] = 'You can upgrade individual assignments from here'; +$string['noassignmentsselected'] = 'No assignments selected'; $string['noassignmentstoupgrade'] = 'There are no assignments that require upgrading'; $string['notsupported'] = ''; $string['notupgradedintro'] = 'This page lists the assignments created with an older version of Moodle that have not been upgraded to the new assignment module in Moodle 2.3. Not all assignments can be upgraded - if they were created with a custom assignment subtype, then that subtype will need to be upgraded to the new assignment plugin format in order to complete the upgrade.'; diff --git a/admin/tool/assignmentupgrade/module.js b/admin/tool/assignmentupgrade/module.js index edee839835fff..6370e706807ed 100644 --- a/admin/tool/assignmentupgrade/module.js +++ b/admin/tool/assignmentupgrade/module.js @@ -56,7 +56,7 @@ M.tool_assignmentupgrade = { assignmentsinput = Y.one('input.selectedassignments'); assignmentsinput.set('value', selectedassignments.join(',')); if (selectedassignments.length == 0) { - alert(M.str.assign.noassignmentsselected); + alert(M.str.tool_assignmentupgrade.noassignmentsselected); e.preventDefault(); } }); diff --git a/admin/tool/assignmentupgrade/renderer.php b/admin/tool/assignmentupgrade/renderer.php index ddb8cfabad4aa..29192d76e6c1e 100644 --- a/admin/tool/assignmentupgrade/renderer.php +++ b/admin/tool/assignmentupgrade/renderer.php @@ -119,7 +119,7 @@ public function assignment_list_page(tool_assignmentupgrade_assignments_table $a $output = ''; $output .= $this->header(); $this->page->requires->js_init_call('M.tool_assignmentupgrade.init_upgrade_table', array()); - + $this->page->requires->string_for_js('noassignmentsselected', 'tool_assignmentupgrade'); $output .= $this->heading(get_string('notupgradedtitle', 'tool_assignmentupgrade')); $output .= $this->box(get_string('notupgradedintro', 'tool_assignmentupgrade')); From 694b11ab4a0a5c91e8b8c9bbb83623b56db82503 Mon Sep 17 00:00:00 2001 From: Raymond Wijaya Date: Wed, 22 Aug 2012 12:47:53 +0800 Subject: [PATCH 25/90] MDL-35004: Fix 'can not create a new instance of the assignment module with no completion setting' --- mod/assign/locallib.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php index 60a3f9251c1a8..455afd1fadf85 100644 --- a/mod/assign/locallib.php +++ b/mod/assign/locallib.php @@ -417,7 +417,7 @@ public function add_instance(stdClass $formdata, $callplugins) { $update->duedate = $formdata->duedate; $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate; $update->grade = $formdata->grade; - $update->completionsubmit = $formdata->completionsubmit; + $update->completionsubmit = !empty($formdata->completionsubmit); $returnid = $DB->insert_record('assign', $update); $this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST); // cache the course record @@ -637,7 +637,7 @@ public function update_instance($formdata) { $update->duedate = $formdata->duedate; $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate; $update->grade = $formdata->grade; - $update->completionsubmit = $formdata->completionsubmit; + $update->completionsubmit = !empty($formdata->completionsubmit); $result = $DB->update_record('assign', $update); $this->instance = $DB->get_record('assign', array('id'=>$update->id), '*', MUST_EXIST); From a8dfc4837e0bf86baa676db42e36ac0cbb893bb7 Mon Sep 17 00:00:00 2001 From: Ankit Agarwal Date: Tue, 21 Aug 2012 11:28:27 +0800 Subject: [PATCH 26/90] MDL-31633 grades: Fixing incorrect redirect when trying to edit grade letters from admin settings --- grade/lib.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/grade/lib.php b/grade/lib.php index 7765f82aca679..37629ebf297ec 100644 --- a/grade/lib.php +++ b/grade/lib.php @@ -2610,6 +2610,7 @@ public static function get_info_edit_structure($courseid) { * @return array */ public static function get_info_letters($courseid) { + global $SITE; if (self::$letterinfo !== null) { return self::$letterinfo; } @@ -2617,9 +2618,15 @@ public static function get_info_letters($courseid) { $canmanage = has_capability('moodle/grade:manage', $context); $canmanageletters = has_capability('moodle/grade:manageletters', $context); if ($canmanage || $canmanageletters) { + // Redirect to system context when report is accessed from admin settings MDL-31633 + if ($context->instanceid == $SITE->id) { + $param = array('edit' => 1); + } else { + $param = array('edit' => 1,'id' => $context->id); + } self::$letterinfo = array( 'view' => new grade_plugin_info('view', new moodle_url('/grade/edit/letter/index.php', array('id'=>$context->id)), get_string('view')), - 'edit' => new grade_plugin_info('edit', new moodle_url('/grade/edit/letter/index.php', array('edit'=>1,'id'=>$context->id)), get_string('edit')) + 'edit' => new grade_plugin_info('edit', new moodle_url('/grade/edit/letter/index.php', $param), get_string('edit')) ); } else { self::$letterinfo = false; From 7ace84e069346041ff4f47321b3f63b751f05f93 Mon Sep 17 00:00:00 2001 From: Jean-Michel Vedrine Date: Sat, 11 Aug 2012 21:00:54 +0200 Subject: [PATCH 27/90] MDL-25492 Blackboard V6+ question import is broken. --- question/format.php | 68 +- question/format/blackboard/format.php | 27 - question/format/blackboard_six/format.php | 1029 ++-------------- question/format/blackboard_six/formatbase.php | 163 +++ question/format/blackboard_six/formatpool.php | 467 ++++++++ question/format/blackboard_six/formatqti.php | 894 ++++++++++++++ .../lang/en/qformat_blackboard_six.php | 13 +- .../tests/blackboardformatpool_test.php | 330 +++++ .../tests/blackboardsixformatqti_test.php | 330 +++++ .../tests/fixtures/sample_blackboard_pool.dat | 142 +++ .../tests/fixtures/sample_blackboard_qti.dat | 1058 +++++++++++++++++ question/format/blackboard_six/version.php | 5 +- question/format/examview/format.php | 35 - question/format/gift/format.php | 13 - 14 files changed, 3593 insertions(+), 981 deletions(-) create mode 100644 question/format/blackboard_six/formatbase.php create mode 100644 question/format/blackboard_six/formatpool.php create mode 100644 question/format/blackboard_six/formatqti.php create mode 100644 question/format/blackboard_six/tests/blackboardformatpool_test.php create mode 100644 question/format/blackboard_six/tests/blackboardsixformatqti_test.php create mode 100644 question/format/blackboard_six/tests/fixtures/sample_blackboard_pool.dat create mode 100644 question/format/blackboard_six/tests/fixtures/sample_blackboard_qti.dat diff --git a/question/format.php b/question/format.php index e1cc7db6ac0a2..9b46a1f8aa42d 100644 --- a/question/format.php +++ b/question/format.php @@ -408,20 +408,44 @@ public function importprocess($category) { $question->timecreated = time(); $question->modifiedby = $USER->id; $question->timemodified = time(); + $fileoptions = array( + 'subdirs' => false, + 'maxfiles' => -1, + 'maxbytes' => 0, + ); + if (is_array($question->questiontext)) { + // Importing images from draftfile. + $questiontext = $question->questiontext; + $question->questiontext = $questiontext['text']; + } + if (is_array($question->generalfeedback)) { + $generalfeedback = $question->generalfeedback; + $question->generalfeedback = $generalfeedback['text']; + } $question->id = $DB->insert_record('question', $question); - if (isset($question->questiontextfiles)) { + + if (!empty($questiontext['itemid'])) { + $question->questiontext = file_save_draft_area_files($questiontext['itemid'], + $this->importcontext->id, 'question', 'questiontext', $question->id, + $fileoptions, $question->questiontext); + } else if (isset($question->questiontextfiles)) { foreach ($question->questiontextfiles as $file) { question_bank::get_qtype($question->qtype)->import_file( $this->importcontext, 'question', 'questiontext', $question->id, $file); } } - if (isset($question->generalfeedbackfiles)) { + if (!empty($generalfeedback['itemid'])) { + $question->generalfeedback = file_save_draft_area_files($generalfeedback['itemid'], + $this->importcontext->id, 'question', 'generalfeedback', $question->id, + $fileoptions, $question->generalfeedback); + } else if (isset($question->generalfeedbackfiles)) { foreach ($question->generalfeedbackfiles as $file) { question_bank::get_qtype($question->qtype)->import_file( $this->importcontext, 'question', 'generalfeedback', $question->id, $file); } } + $DB->update_record('question', $question); $this->questionids[] = $question->id; @@ -636,6 +660,24 @@ protected function defaultquestion() { return $question; } + /** + * Add a blank combined feedback to a question object. + * @param object question + * @return object question + */ + protected function add_blank_combined_feedback($question) { + $question->correctfeedback['text'] = ''; + $question->correctfeedback['format'] = $question->questiontextformat; + $question->correctfeedback['files'] = array(); + $question->partiallycorrectfeedback['text'] = ''; + $question->partiallycorrectfeedback['format'] = $question->questiontextformat; + $question->partiallycorrectfeedback['files'] = array(); + $question->incorrectfeedback['text'] = ''; + $question->incorrectfeedback['format'] = $question->questiontextformat; + $question->incorrectfeedback['files'] = array(); + return $question; + } + /** * Given the data known to define a question in * this format, this function converts it into a question @@ -901,6 +943,28 @@ protected function format_question_text($question) { class qformat_based_on_xml extends qformat_default { + /** + * A lot of imported files contain unwanted entities. + * This method tries to clean up all known problems. + * @param string str string to correct + * @return string the corrected string + */ + public function cleaninput($str) { + + $html_code_list = array( + "'" => "'", + "’" => "'", + "“" => "\"", + "”" => "\"", + "–" => "-", + "—" => "-", + ); + $str = strtr($str, $html_code_list); + // Use textlib entities_to_utf8 function to convert only numerical entities. + $str = textlib::entities_to_utf8($str, false); + return $str; + } + /** * Return the array moodle is expecting * for an HTML text. No processing is done on $text. diff --git a/question/format/blackboard/format.php b/question/format/blackboard/format.php index 88e01306dca83..1e7fa0dd779b2 100644 --- a/question/format/blackboard/format.php +++ b/question/format/blackboard/format.php @@ -59,33 +59,6 @@ public function mime_type() { return mimeinfo('type', '.dat'); } - /** - * Some softwares put entities in exported files. - * This method try to clean up known problems. - * @param string str string to correct - * @return string the corrected string - */ - public function cleaninput($str) { - if (!$this->ishtml) { - return $str; - } - $html_code_list = array( - "'" => "'", - "’" => "'", - "[" => "[", - "“" => "\"", - "”" => "\"", - "]" => "]", - "'" => "'", - "–" => "-", - "—" => "-", - ); - $str = strtr($str, $html_code_list); - // Use textlib entities_to_utf8 function to convert only numerical entities. - $str = textlib::entities_to_utf8($str, false); - return $str; - } - /** * Parse the array of lines into an array of questions * this *could* burn memory - but it won't happen that much diff --git a/question/format/blackboard_six/format.php b/question/format/blackboard_six/format.php index 047bfa94f0e3f..32652d131ff79 100644 --- a/question/format/blackboard_six/format.php +++ b/question/format/blackboard_six/format.php @@ -15,936 +15,169 @@ // along with Moodle. If not, see . /** - * Blackboard 6.0 question importer. + * Blackboard V5 and V6 question importer. * - * @package qformat - * @subpackage blackboard_six + * @package qformat_blackboard_six * @copyright 2005 Michael Penney * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - defined('MOODLE_INTERNAL') || die(); -require_once ($CFG->libdir . '/xmlize.php'); - - -/** - * Blackboard 6.0 question importer. - * - * @copyright 2005 Michael Penney - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class qformat_blackboard_six extends qformat_default { - function provide_import() { - return true; - } - - public function can_import_file($file) { - $mimetypes = array( - mimeinfo('type', '.dat'), - mimeinfo('type', '.zip') - ); - return in_array($file->get_mimetype(), $mimetypes); - } - - - //Function to check and create the needed dir to unzip file to - function check_and_create_import_dir($unique_code) { - - global $CFG; - - $status = $this->check_dir_exists($CFG->tempdir."",true); - if ($status) { - $status = $this->check_dir_exists($CFG->tempdir."/bbquiz_import",true); - } - if ($status) { - $status = $this->check_dir_exists($CFG->tempdir."/bbquiz_import/".$unique_code,true); - } - - return $status; - } - - function clean_temp_dir($dir='') { +require_once($CFG->libdir . '/xmlize.php'); +require_once($CFG->dirroot . '/question/format/blackboard_six/formatbase.php'); +require_once($CFG->dirroot . '/question/format/blackboard_six/formatqti.php'); +require_once($CFG->dirroot . '/question/format/blackboard_six/formatpool.php'); + +class qformat_blackboard_six extends qformat_blackboard_six_base { + /** @var int Blackboard assessment qti files were always imported by the blackboard_six plugin. */ + const FILETYPE_QTI = 1; + /** @var int Blackboard question pool files were previously handled by the blackboard plugin. */ + const FILETYPE_POOL = 2; + /** @var int type of file being imported, one of the constants FILETYPE_QTI or FILETYPE_POOL. */ + public $filetype; + + public function get_filecontent($path) { + $fullpath = $this->tempdir . '/' . $path; + if (is_file($fullpath) && is_readable($fullpath)) { + return file_get_contents($fullpath); + } + return false; + } + + /** + * Set the file type being imported + * @param int $type the imported file's type + */ + public function set_filetype($type) { + $this->filetype = $type; + } + + /** + * Return content of all files containing questions, + * as an array one element for each file found, + * For each file, the corresponding element is an array of lines. + * @param string filename name of file + * @return mixed contents array or false on failure + */ + public function readdata($filename) { global $CFG; - // for now we will just say everything happened okay note - // that a mess may be piling up in $CFG->tempdir/bbquiz_import - // TODO return true at top of the function renders all the following code useless - return true; - - if ($dir == '') { - $dir = $this->temp_dir; - } - $slash = "/"; - - // Create arrays to store files and directories - $dir_files = array(); - $dir_subdirs = array(); - - // Make sure we can delete it - chmod($dir, $CFG->directorypermissions); - - if ((($handle = opendir($dir))) == FALSE) { - // The directory could not be opened - return false; - } - - // Loop through all directory entries, and construct two temporary arrays containing files and sub directories - while(false !== ($entry = readdir($handle))) { - if (is_dir($dir. $slash .$entry) && $entry != ".." && $entry != ".") { - $dir_subdirs[] = $dir. $slash .$entry; - } - else if ($entry != ".." && $entry != ".") { - $dir_files[] = $dir. $slash .$entry; - } - } - - // Delete all files in the curent directory return false and halt if a file cannot be removed - $countdir_files = count($dir_files); - for($i=0; $i<$countdir_files; $i++) { - chmod($dir_files[$i], $CFG->directorypermissions); - if (((unlink($dir_files[$i]))) == FALSE) { + // Find if we are importing a .dat file. + if (strtolower(pathinfo($filename, PATHINFO_EXTENSION)) == 'dat') { + if (!is_readable($filename)) { + $this->error(get_string('filenotreadable', 'error')); return false; } - } - - // Empty sub directories and then remove the directory - $countdir_subdirs = count($dir_subdirs); - for($i=0; $i<$countdir_subdirs; $i++) { - chmod($dir_subdirs[$i], $CFG->directorypermissions); - if ($this->clean_temp_dir($dir_subdirs[$i]) == FALSE) { - return false; + // As we are not importing a .zip file, + // there is no imsmanifest, and it is not possible + // to parse it to find the file type. + // So we need to guess the file type by looking at the content. + // For now we will do that searching for a required tag. + // This is certainly not bullet-proof but works for all usual files. + $text = file_get_contents($filename); + if (strpos($text, '')) { + $this->set_filetype(self::FILETYPE_QTI); } - else { - if (rmdir($dir_subdirs[$i]) == FALSE) { - return false; - } + if (strpos($text, '')) { + $this->set_filetype(self::FILETYPE_POOL); } - } + // In all other cases we are not able to handle this question file. - // Close directory - closedir($handle); - if (rmdir($this->temp_dir) == FALSE) { - return false; + // Readquestions is now expecting an array of strings. + return array($text); } - // Success, every thing is gone return true - return true; - } - - //Function to check if a directory exists and, optionally, create it - function check_dir_exists($dir,$create=false) { - - global $CFG; - - $status = true; - if(!is_dir($dir)) { - if (!$create) { - $status = false; - } else { - umask(0000); - $status = mkdir ($dir,$CFG->directorypermissions); + // We are importing a zip file. + // Create name for temporary directory. + $unique_code = time(); + $this->tempdir = make_temp_directory('bbquiz_import/' . $unique_code); + if (is_readable($filename)) { + if (!copy($filename, $this->tempdir . '/bboard.zip')) { + $this->error(get_string('cannotcopybackup', 'question')); + fulldelete($this->tempdir); + return false; } - } - return $status; - } + if (unzip_file($this->tempdir . '/bboard.zip', '', false)) { + $dom = new DomDocument(); - function importpostprocess() { - /// Does any post-processing that may be desired - /// Argument is a simple array of question ids that - /// have just been added. - - // need to clean up temporary directory - return $this->clean_temp_dir(); - } - - function copy_file_to_course($filename) { - global $CFG, $COURSE; - $filename = str_replace('\\','/',$filename); - $fullpath = $this->temp_dir.'/res00001/'.$filename; - $basename = basename($filename); - - $copy_to = $CFG->dataroot.'/'.$COURSE->id.'/bb_import'; - - if ($this->check_dir_exists($copy_to,true)) { - if(is_readable($fullpath)) { - $copy_to.= '/'.$basename; - if (!copy($fullpath, $copy_to)) { + if (!$dom->load($this->tempdir . '/imsmanifest.xml')) { + $this->error(get_string('errormanifest', 'qformat_blackboard_six')); + fulldelete($this->tempdir); return false; } - else { - return $copy_to; - } - } - } - else { - return false; - } - } - function readdata($filename) { - /// Returns complete file with an array, one item per line - global $CFG; + $xpath = new DOMXPath($dom); - // if the extension is .dat we just return that, - // if .zip we unzip the file and get the data - $ext = substr($this->realfilename, strpos($this->realfilename,'.'), strlen($this->realfilename)-1); - if ($ext=='.dat') { - if (!is_readable($filename)) { - print_error('filenotreadable', 'error'); - } - return file($filename); - } + // We starts from the root element. + $query = '//resources/resource'; + $this->filebase = $this->tempdir; + $q_file = array(); - $unique_code = time(); - $temp_dir = $CFG->tempdir."/bbquiz_import/".$unique_code; - $this->temp_dir = $temp_dir; - if ($this->check_and_create_import_dir($unique_code)) { - if(is_readable($filename)) { - if (!copy($filename, "$temp_dir/bboard.zip")) { - print_error('cannotcopybackup', 'question'); - } - if(unzip_file("$temp_dir/bboard.zip", '', false)) { - // assuming that the information is in res0001.dat - // after looking at 6 examples this was always the case - $q_file = "$temp_dir/res00001.dat"; - if (is_file($q_file)) { - if (is_readable($q_file)) { - $filearray = file($q_file); - /// Check for Macintosh OS line returns (ie file on one line), and fix - if (preg_match("~\r~", $filearray[0]) AND !preg_match("~\n~", $filearray[0])) { - return explode("\r", $filearray[0]); - } else { - return $filearray; - } + $examfiles = $xpath->query($query); + foreach ($examfiles as $examfile) { + if ($examfile->getAttribute('type') == 'assessment/x-bb-qti-test' + || $examfile->getAttribute('type') == 'assessment/x-bb-qti-pool') { + + if ($content = $this->get_filecontent($examfile->getAttribute('bb:file'))) { + $this->set_filetype(self::FILETYPE_QTI); + $q_file[] = $content; } } - else { - print_error('cannotfindquestionfile', 'questioni'); - } - } - else { - print "filename: $filename
    tempdir: $temp_dir
    "; - print_error('cannotunzip', 'question'); - } - } - else { - print_error('cannotreaduploadfile'); - } - } - else { - print_error('cannotcreatetempdir'); - } - } - - function save_question_options($question) { - return true; - } - - - - protected function readquestions($lines) { - /// Parses an array of lines into an array of questions, - /// where each item is a question object as defined by - /// readquestion(). - - $text = implode($lines, " "); - $xml = xmlize($text, 0); - - $raw_questions = $xml['questestinterop']['#']['assessment'][0]['#']['section'][0]['#']['item']; - $questions = array(); - - foreach($raw_questions as $quest) { - $question = $this->create_raw_question($quest); - - switch($question->qtype) { - case "Matching": - $this->process_matching($question, $questions); - break; - case "Multiple Choice": - $this->process_mc($question, $questions); - break; - case "Essay": - $this->process_essay($question, $questions); - break; - case "Multiple Answer": - $this->process_ma($question, $questions); - break; - case "True/False": - $this->process_tf($question, $questions); - break; - case 'Fill in the Blank': - $this->process_fblank($question, $questions); - break; - case 'Short Response': - $this->process_essay($question, $questions); - break; - default: - print "Unknown or unhandled question type: \"$question->qtype\"
    "; - break; - } - - } - return $questions; - } - - -// creates a cleaner object to deal with for processing into moodle -// the object created is NOT a moodle question object -function create_raw_question($quest) { - - $question = new stdClass(); - $question->qtype = $quest['#']['itemmetadata'][0]['#']['bbmd_questiontype'][0]['#']; - $question->id = $quest['#']['itemmetadata'][0]['#']['bbmd_asi_object_id'][0]['#']; - $presentation->blocks = $quest['#']['presentation'][0]['#']['flow'][0]['#']['flow']; - - foreach($presentation->blocks as $pblock) { - - $block = NULL; - $block->type = $pblock['@']['class']; - - switch($block->type) { - case 'QUESTION_BLOCK': - $sub_blocks = $pblock['#']['flow']; - foreach($sub_blocks as $sblock) { - //echo "Calling process_block from line 263
    "; - $this->process_block($sblock, $block); - } - break; - - case 'RESPONSE_BLOCK': - $choices = NULL; - switch($question->qtype) { - case 'Matching': - $bb_subquestions = $pblock['#']['flow']; - $sub_questions = array(); - foreach($bb_subquestions as $bb_subquestion) { - $sub_question = NULL; - $sub_question->ident = $bb_subquestion['#']['response_lid'][0]['@']['ident']; - $this->process_block($bb_subquestion['#']['flow'][0], $sub_question); - $bb_choices = $bb_subquestion['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label'][0]['#']['response_label']; - $choices = array(); - $this->process_choices($bb_choices, $choices); - $sub_question->choices = $choices; - if (!isset($block->subquestions)) { - $block->subquestions = array(); - } - $block->subquestions[] = $sub_question; + if ($examfile->getAttribute('type') == 'assessment/x-bb-pool') { + if ($examfile->getAttribute('baseurl')) { + $this->filebase = $this->tempdir. '/' . $examfile->getAttribute('baseurl'); } - break; - case 'Multiple Answer': - $bb_choices = $pblock['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label']; - $choices = array(); - $this->process_choices($bb_choices, $choices); - $block->choices = $choices; - break; - case 'Essay': - // Doesn't apply since the user responds with text input - break; - case 'Multiple Choice': - $mc_choices = $pblock['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label']; - foreach($mc_choices as $mc_choice) { - $choices = NULL; - $choices = $this->process_block($mc_choice, $choices); - $block->choices[] = $choices; + if ($content = $this->get_filecontent($examfile->getAttribute('file'))) { + $this->set_filetype(self::FILETYPE_POOL); + $q_file[] = $content; } - break; - case 'Short Response': - // do nothing? - break; - case 'Fill in the Blank': - // do nothing? - break; - default: - $bb_choices = $pblock['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label'][0]['#']['response_label']; - $choices = array(); - $this->process_choices($bb_choices, $choices); - $block->choices = $choices; - } - break; - case 'RIGHT_MATCH_BLOCK': - $matching_answerset = $pblock['#']['flow']; - - $answerset = array(); - foreach($matching_answerset as $answer) { - // $answerset[] = $this->process_block($answer, $bb_answer); - $bb_answer = null; - $bb_answer->text = $answer['#']['flow'][0]['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#']; - $answerset[] = $bb_answer; - } - $block->matching_answerset = $answerset; - break; - default: - print "UNHANDLED PRESENTATION BLOCK"; - break; - } - $question->{$block->type} = $block; - } - - // determine response processing - // there is a section called 'outcomes' that I don't know what to do with - $resprocessing = $quest['#']['resprocessing']; - $respconditions = $resprocessing[0]['#']['respcondition']; - $reponses = array(); - if ($question->qtype == 'Matching') { - $this->process_matching_responses($respconditions, $responses); - } - else { - $this->process_responses($respconditions, $responses); - } - $question->responses = $responses; - $feedbackset = $quest['#']['itemfeedback']; - $feedbacks = array(); - $this->process_feedback($feedbackset, $feedbacks); - $question->feedback = $feedbacks; - return $question; -} - -function process_block($cur_block, &$block) { - global $COURSE, $CFG; - - $cur_type = $cur_block['@']['class']; - switch($cur_type) { - case 'FORMATTED_TEXT_BLOCK': - $block->text = $this->strip_applet_tags_get_mathml($cur_block['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#']); - break; - case 'FILE_BLOCK': - //revisit this to make sure it is working correctly - // Commented out ['matapplication']..., etc. because I - // noticed that when I imported a new Blackboard 6 file - // and printed out the block, the tree did not extend past ['material'][0]['#'] - CT 8/3/06 - $block->file = $cur_block['#']['material'][0]['#'];//['matapplication'][0]['@']['uri']; - if ($block->file != '') { - // if we have a file copy it to the course dir and adjust its name to be visible over the web. - $block->file = $this->copy_file_to_course($block->file); - $block->file = $CFG->wwwroot.'/file.php/'.$COURSE->id.'/bb_import/'.basename($block->file); - } - break; - case 'Block': - if (isset($cur_block['#']['material'][0]['#']['mattext'][0]['#'])) { - $block->text = $cur_block['#']['material'][0]['#']['mattext'][0]['#']; - } - else if (isset($cur_block['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#'])) { - $block->text = $cur_block['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#']; - } - else if (isset($cur_block['#']['response_label'])) { - // this is a response label block - $sub_blocks = $cur_block['#']['response_label'][0]; - if(!isset($block->ident)) { - if(isset($sub_blocks['@']['ident'])) { - $block->ident = $sub_blocks['@']['ident']; } } - foreach($sub_blocks['#']['flow_mat'] as $sub_block) { - $this->process_block($sub_block, $block); - } - } - else { - if (isset($cur_block['#']['flow_mat']) || isset($cur_block['#']['flow'])) { - if (isset($cur_block['#']['flow_mat'])) { - $sub_blocks = $cur_block['#']['flow_mat']; - } - elseif (isset($cur_block['#']['flow'])) { - $sub_blocks = $cur_block['#']['flow']; - } - foreach ($sub_blocks as $sblock) { - // this will recursively grab the sub blocks which should be of one of the other types - $this->process_block($sblock, $block); - } - } - } - break; - case 'LINK_BLOCK': - // not sure how this should be included - if (!empty($cur_block['#']['material'][0]['#']['mattext'][0]['@']['uri'])) { - $block->link = $cur_block['#']['material'][0]['#']['mattext'][0]['@']['uri']; - } - else { - $block->link = ''; - } - break; - } - return $block; -} -function process_choices($bb_choices, &$choices) { - foreach($bb_choices as $choice) { - if (isset($choice['@']['ident'])) { - $cur_choice = $choice['@']['ident']; - } - else { //for multiple answer - $cur_choice = $choice['#']['response_label'][0];//['@']['ident']; - } - if (isset($choice['#']['flow_mat'][0])) { //for multiple answer - $cur_block = $choice['#']['flow_mat'][0]; - // Reset $cur_choice to NULL because process_block is expecting an object - // for the second argument and not a string, which is what is was set as - // originally - CT 8/7/06 - $cur_choice = null; - $this->process_block($cur_block, $cur_choice); - } - elseif (isset($choice['#']['response_label'])) { - // Reset $cur_choice to NULL because process_block is expecting an object - // for the second argument and not a string, which is what is was set as - // originally - CT 8/7/06 - $cur_choice = null; - $this->process_block($choice, $cur_choice); - } - $choices[] = $cur_choice; - } -} - -function process_matching_responses($bb_responses, &$responses) { - foreach($bb_responses as $bb_response) { - $response = NULL; - if (isset($bb_response['#']['conditionvar'][0]['#']['varequal'])) { - $response->correct = $bb_response['#']['conditionvar'][0]['#']['varequal'][0]['#']; - $response->ident = $bb_response['#']['conditionvar'][0]['#']['varequal'][0]['@']['respident']; - } - else { - $response->correct = 'Broken Question?'; - $response->ident = 'Broken Question?'; - } - $response->feedback = $bb_response['#']['displayfeedback'][0]['@']['linkrefid']; - $responses[] = $response; - } -} - -function process_responses($bb_responses, &$responses) { - foreach($bb_responses as $bb_response) { - //Added this line to instantiate $response. - // Without instantiating the $response variable, the same object - // gets added to the array - $response = new stdClass(); - if (isset($bb_response['@']['title'])) { - $response->title = $bb_response['@']['title']; - } - else { - $reponse->title = $bb_response['#']['displayfeedback'][0]['@']['linkrefid']; - } - $reponse->ident = array(); - if (isset($bb_response['#']['conditionvar'][0]['#'])){//['varequal'][0]['#'])) { - $response->ident[0] = $bb_response['#']['conditionvar'][0]['#'];//['varequal'][0]['#']; - } - else if (isset($bb_response['#']['conditionvar'][0]['#']['other'][0]['#'])) { - $response->ident[0] = $bb_response['#']['conditionvar'][0]['#']['other'][0]['#']; - } - - if (isset($bb_response['#']['conditionvar'][0]['#']['and'])){//[0]['#'])) { - $responseset = $bb_response['#']['conditionvar'][0]['#']['and'];//[0]['#']['varequal']; - foreach($responseset as $rs) { - $response->ident[] = $rs['#']; - if(!isset($response->feedback) and isset( $rs['@'] ) ) { - $response->feedback = $rs['@']['respident']; - } - } - } - else { - $response->feedback = $bb_response['#']['displayfeedback'][0]['@']['linkrefid']; - } - - // determine what point value to give response - if (isset($bb_response['#']['setvar'])) { - switch ($bb_response['#']['setvar'][0]['#']) { - case "SCORE.max": - $response->fraction = 1; - break; - default: - // I have only seen this being 0 or unset - // there are probably fractional values of SCORE.max, but I'm not sure what they look like - $response->fraction = 0; - break; - } - } - else { - // just going to assume this is the case this is probably not correct. - $response->fraction = 0; - } - - $responses[] = $response; - } -} - -function process_feedback($feedbackset, &$feedbacks) { - foreach($feedbackset as $bb_feedback) { - // Added line $feedback=null so that $feedback does not get reused in the loop - // and added the the $feedbacks[] array multiple times - $feedback = null; - $feedback->ident = $bb_feedback['@']['ident']; - if (isset($bb_feedback['#']['flow_mat'][0])) { - $this->process_block($bb_feedback['#']['flow_mat'][0], $feedback); - } - elseif (isset($bb_feedback['#']['solution'][0]['#']['solutionmaterial'][0]['#']['flow_mat'][0])) { - $this->process_block($bb_feedback['#']['solution'][0]['#']['solutionmaterial'][0]['#']['flow_mat'][0], $feedback); - } - $feedbacks[] = $feedback; - } -} - -/** - * Create common parts of question - */ -function process_common( $quest ) { - $question = $this->defaultquestion(); - $question->questiontext = $quest->QUESTION_BLOCK->text; - $question->name = shorten_text( $quest->id, 250 ); - - return $question; -} - -//---------------------------------------- -// Process True / False Questions -//---------------------------------------- -function process_tf($quest, &$questions) { - $question = $this->process_common( $quest ); - - $question->qtype = TRUEFALSE; - $question->single = 1; // Only one answer is allowed - // 0th [response] is the correct answer. - $responses = $quest->responses; - $correctresponse = $responses[0]->ident[0]['varequal'][0]['#']; - if ($correctresponse != 'false') { - $correct = true; - } - else { - $correct = false; - } - - foreach($quest->feedback as $fb) { - $fback->{$fb->ident} = $fb->text; - } - - if ($correct) { // true is correct - $question->answer = 1; - $question->feedbacktrue = $fback->correct; - $question->feedbackfalse = $fback->incorrect; - } else { // false is correct - $question->answer = 0; - $question->feedbacktrue = $fback->incorrect; - $question->feedbackfalse = $fback->correct; - } - $question->correctanswer = $question->answer; - $questions[] = $question; -} - - -//---------------------------------------- -// Process Fill in the Blank -//---------------------------------------- -function process_fblank($quest, &$questions) { - $question = $this->process_common( $quest ); - $question->qtype = SHORTANSWER; - $question->single = 1; - - $answers = array(); - $fractions = array(); - $feedbacks = array(); - - // extract the feedback - $feedback = array(); - foreach($quest->feedback as $fback) { - if (isset($fback->ident)) { - if ($fback->ident == 'correct' || $fback->ident == 'incorrect') { - $feedback[$fback->ident] = $fback->text; - } - } - } - - foreach($quest->responses as $response) { - if(isset($response->title)) { - if (isset($response->ident[0]['varequal'][0]['#'])) { - //for BB Fill in the Blank, only interested in correct answers - if ($response->feedback = 'correct') { - $answers[] = $response->ident[0]['varequal'][0]['#']; - $fractions[] = 1; - if (isset($feedback['correct'])) { - $feedbacks[] = $feedback['correct']; - } - else { - $feedbacks[] = ''; - } + if ($q_file) { + return $q_file; + } else { + $this->error(get_string('cannotfindquestionfile', 'question')); + fulldelete($this->tempdir); } - } - - } - } - - //Adding catchall to so that students can see feedback for incorrect answers when they enter something the - //instructor did not enter - $answers[] = '*'; - $fractions[] = 0; - if (isset($feedback['incorrect'])) { - $feedbacks[] = $feedback['incorrect']; - } - else { - $feedbacks[] = ''; - } - - $question->answer = $answers; - $question->fraction = $fractions; - $question->feedback = $feedbacks; // Changed to assign $feedbacks to $question->feedback instead of - - if (!empty($question)) { - $questions[] = $question; - } - -} - -//---------------------------------------- -// Process Multiple Choice Questions -//---------------------------------------- -function process_mc($quest, &$questions) { - $question = $this->process_common( $quest ); - $question->qtype = MULTICHOICE; - $question->single = 1; - - $feedback = array(); - foreach($quest->feedback as $fback) { - $feedback[$fback->ident] = $fback->text; - } - - foreach($quest->responses as $response) { - if (isset($response->title)) { - if ($response->title == 'correct') { - // only one answer possible for this qtype so first index is correct answer - $correct = $response->ident[0]['varequal'][0]['#']; - } - } - else { - // fallback method for when the title is not set - if ($response->feedback == 'correct') { - // only one answer possible for this qtype so first index is correct answer - $correct = $response->ident[0]['varequal'][0]['#']; // added [0]['varequal'][0]['#'] to $response->ident - CT 8/9/06 - } - } - } - - $i = 0; - foreach($quest->RESPONSE_BLOCK->choices as $response) { - $question->answer[$i] = $response->text; - if ($correct == $response->ident) { - $question->fraction[$i] = 1; - // this is a bit of a hack to catch the feedback... first we see if a 'correct' feedback exists - // then specific feedback for this question (maybe this should be switched?, but from my example - // question pools I have not seen response specific feedback, only correct or incorrect feedback - if (!empty($feedback['correct'])) { - $question->feedback[$i] = $feedback['correct']; - } - elseif (!empty($feedback[$i])) { - $question->feedback[$i] = $feedback[$i]; - } - else { - // failsafe feedback (should be '' instead?) - $question->feedback[$i] = "correct"; - } - } - else { - $question->fraction[$i] = 0; - if (!empty($feedback['incorrect'])) { - $question->feedback[$i] = $feedback['incorrect']; - } - elseif (!empty($feedback[$i])) { - $question->feedback[$i] = $feedback[$i]; - } - else { - // failsafe feedback (should be '' instead?) - $question->feedback[$i] = 'incorrect'; - } - } - $i++; - } - - if (!empty($question)) { - $questions[] = $question; - } -} - -//---------------------------------------- -// Process Multiple Choice Questions With Multiple Answers -//---------------------------------------- -function process_ma($quest, &$questions) { - $question = $this->process_common( $quest ); // copied this from process_mc - $question->qtype = MULTICHOICE; - $question->single = 0; // More than one answer allowed - - $answers = $quest->responses; - $correct_answers = array(); - foreach($answers as $answer) { - if($answer->title == 'correct') { - $answerset = $answer->ident[0]['and'][0]['#']['varequal']; - foreach($answerset as $ans) { - $correct_answers[] = $ans['#']; - } - } - } - - foreach ($quest->feedback as $fb) { - $feedback->{$fb->ident} = trim($fb->text); - } - - $correct_answer_count = count($correct_answers); - $choiceset = $quest->RESPONSE_BLOCK->choices; - $i = 0; - foreach($choiceset as $choice) { - $question->answer[$i] = trim($choice->text); - if (in_array($choice->ident, $correct_answers)) { - // correct answer - $question->fraction[$i] = floor(100000/$correct_answer_count)/100000; // strange behavior if we have more than 5 decimal places - $question->feedback[$i] = $feedback->correct; - } - else { - // wrong answer - $question->fraction[$i] = 0; - $question->feedback[$i] = $feedback->incorrect; - } - $i++; - } - - $questions[] = $question; -} - -//---------------------------------------- -// Process Essay Questions -//---------------------------------------- -function process_essay($quest, &$questions) { -// this should be rewritten to accomodate moodle 1.6 essay question type eventually - - if (defined("ESSAY")) { - // treat as short answer - $question = $this->process_common( $quest ); // copied this from process_mc - $question->qtype = ESSAY; - - $question->feedback = array(); - // not sure where to get the correct answer from - foreach($quest->feedback as $feedback) { - // Added this code to put the possible solution that the - // instructor gives as the Moodle answer for an essay question - if ($feedback->ident == 'solution') { - $question->feedback = $feedback->text; - } - } - //Added because essay/questiontype.php:save_question_option is expecting a - //fraction property - CT 8/10/06 - $question->fraction[] = 1; - if (!empty($question)) { - $questions[]=$question; - } - } - else { - print "Essay question types are not handled because the quiz question type 'Essay' does not exist in this installation of Moodle
    "; - print "    Omitted Question: ".$quest->QUESTION_BLOCK->text.'

    '; - } -} - -//---------------------------------------- -// Process Matching Questions -//---------------------------------------- -function process_matching($quest, &$questions) { - // renderedmatch is an optional plugin, so we need to check if it is defined - if (question_bank::is_qtype_installed('renderedmatch')) { - $question = $this->process_common($quest); - $question->valid = true; - $question->qtype = 'renderedmatch'; - - foreach($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) { - foreach($quest->responses as $rid => $resp) { - if ($resp->ident == $subq->ident) { - $correct = $resp->correct; - $feedback = $resp->feedback; - } - } - - foreach($subq->choices as $cid => $choice) { - if ($choice == $correct) { - $question->subquestions[] = $subq->text; - $question->subanswers[] = $quest->RIGHT_MATCH_BLOCK->matching_answerset[$cid]->text; - } - } - } - - // check format - $status = true; - if ( count($quest->RESPONSE_BLOCK->subquestions) > count($quest->RIGHT_MATCH_BLOCK->matching_answerset) || count($question->subquestions) < 2) { - $status = false; - } - else { - // need to redo to make sure that no two questions have the same answer (rudimentary now) - foreach($question->subanswers as $qstn) { - if(isset($previous)) { - if ($qstn == $previous) { - $status = false; - } - } - $previous = $qstn; - if ($qstn == '') { - $status = false; - } - } - } - - if ($status) { - $questions[] = $question; - } - else { - global $COURSE, $CFG; - print ''; - print ''; - - print ""; - print "'; - - print '
    This matching question is malformed. Please ensure there are no blank answers, no two questions have the same answer, and/or there are correct answers for each question. There must be at least as many subanswers as subquestions, and at least one subquestion.
    Question:".$quest->QUESTION_BLOCK->text; - if (isset($quest->QUESTION_BLOCK->file)) { - print '
    There is a subfile contained in the zipfile that has been copied to course files: bb_import/'.basename($quest->QUESTION_BLOCK->file).''; - if (preg_match('/(gif|jpg|jpeg|png)$/i', $quest->QUESTION_BLOCK->file)) { - print ''; - } - } - print "
    Subquestions:
      "; - foreach($quest->responses as $rs) { - $correct_responses->{$rs->ident} = $rs->correct; - } - foreach($quest->RESPONSE_BLOCK->subquestions as $subq) { - print '
    • '.$subq->text.'
        '; - foreach($subq->choices as $id=>$choice) { - print '
      • '; - if ($choice == $correct_responses->{$subq->ident}) { - print ''; - } - else { - print ''; - } - print $quest->RIGHT_MATCH_BLOCK->matching_answerset[$id]->text.'
      • '; - } - print '
      '; - } - print '
    Feedback:
      '; - foreach($quest->feedback as $fb) { - print '
    • '.$fb->ident.': '.$fb->text.'
    • '; - } - print '
    '; + } else { + $this->error(get_string('cannotunzip', 'question')); + fulldelete($this->temp_dir); + } + } else { + $this->error(get_string('cannotreaduploadfile', 'error')); + fulldelete($this->tempdir); + } + return false; + } + + /** + * Parse the array of strings into an array of questions. + * Each string is the content of a .dat questions file. + * This *could* burn memory - but it won't happen that much + * so fingers crossed! + * @param array of strings from the input file. + * @param stdClass $context + * @return array (of objects) question objects. + */ + public function readquestions($lines) { + + // Set up array to hold all our questions. + $questions = array(); + if ($this->filetype == self::FILETYPE_QTI) { + $importer = new qformat_blackboard_six_qti(); + } else if ($this->filetype == self::FILETYPE_POOL) { + $importer = new qformat_blackboard_six_pool(); + } else { + // In all other cases we are not able to import the file. + return false; } - } - else { - print "Matching question types are not handled because the quiz question type 'Rendered Matching' does not exist in this installation of Moodle
    "; - print "    Omitted Question: ".$quest->QUESTION_BLOCK->text.'

    '; - } -} + $importer->set_filebase($this->filebase); - -function strip_applet_tags_get_mathml($string) { - if(stristr($string, '') === FALSE) { - return $string; - } - else { - // strip all applet tags keeping stuff before/after and inbetween (if mathml) them - while (stristr($string, '') !== FALSE) { - preg_match("/(.*)\.*\<\/math\>)\".*\<\/applet\>(.*)/i",$string, $mathmls); - $string = $mathmls[1].$mathmls[2].$mathmls[3]; + // Each element of $lines is a string containing a complete xml document. + foreach ($lines as $text) { + $questions = array_merge($questions, $importer->readquestions($text)); } - return $string; + return $questions; } } - -} // close object - diff --git a/question/format/blackboard_six/formatbase.php b/question/format/blackboard_six/formatbase.php new file mode 100644 index 0000000000000..da08e913551f1 --- /dev/null +++ b/question/format/blackboard_six/formatbase.php @@ -0,0 +1,163 @@ +. + +/** + * Blackboard V5 and V6 question importer. + * + * @package qformat_blackboard_six + * @copyright 2012 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Base class question import format for zip files with images + * + */ + +class qformat_blackboard_six_base extends qformat_based_on_xml { + /** @var string path to path to root of image tree in unzipped archive. */ + public $filebase = ''; + /** @var string path to the temporary directory. */ + public $tempdir = ''; + /** + * This plugin provide import + * @return bool true + */ + public function provide_import() { + return true; + } + + /** + * Check if the given file is capable of being imported by this plugin. + * As {@link file_storage::mimetype()} now uses finfo PHP extension if available, + * the value returned by $file->get_mimetype for a .dat file is not the same on all servers. + * So we must made 2 checks to verify if the plugin can import the file. + * @param stored_file $file the file to check + * @return bool whether this plugin can import the file + */ + public function can_import_file($file) { + $mimetypes = array( + mimeinfo('type', '.dat'), + mimeinfo('type', '.zip') + ); + return in_array($file->get_mimetype(), $mimetypes) || in_array(mimeinfo('type', $file->get_filename()), $mimetypes); + } + + public function mime_type() { + return mimeinfo('type', '.zip'); + } + + /** + * Does any post-processing that may be desired + * Clean the temporary directory if a zip file was imported + * @return bool success + */ + public function importpostprocess() { + if ($this->tempdir != '') { + fulldelete($this->tempdir); + } + return true; + } + /** + * Set the path to the root of images tree + * @param string $path path to images root + */ + public function set_filebase($path) { + $this->filebase = $path; + } + + /** + * Store an image file in a draft filearea + * @param array $text, if itemid element don't exists it will be created + * @param string tempdir path to root of image tree + * @param string filepathinsidetempdir path to image in the tree + * @param string filename image's name + * @return string new name of the image as it was stored + */ + protected function store_file_for_text_field(&$text, $tempdir, $filepathinsidetempdir, $filename) { + global $USER; + $fs = get_file_storage(); + if (empty($text['itemid'])) { + $text['itemid'] = file_get_unused_draft_itemid(); + } + // As question file areas don't support subdirs, + // convert path to filename. + // So that images with same name can be imported. + $newfilename = clean_param(str_replace('/', '__', $filepathinsidetempdir . '__' . $filename), PARAM_FILE); + $filerecord = array( + 'contextid' => context_user::instance($USER->id)->id, + 'component' => 'user', + 'filearea' => 'draft', + 'itemid' => $text['itemid'], + 'filepath' => '/', + 'filename' => $newfilename, + ); + $fs->create_file_from_pathname($filerecord, $tempdir . '/' . $filepathinsidetempdir . '/' . $filename); + return $newfilename; + } + + /** + * Given an HTML text with references to images files, + * store all images in a draft filearea, + * and return an array with all urls in text recoded, + * format set to FORMAT_HTML, and itemid set to filearea itemid + * @param string text text to parse and recode + * @return array with keys text, format, itemid. + */ + public function text_field($text) { + $data = array(); + // Step one, find all file refs then add to array. + preg_match_all('|]+src="([^"]*)"|i', $text, $out); // Find all src refs. + + foreach ($out[1] as $path) { + $fullpath = $this->filebase . '/' . $path; + + if (is_readable($fullpath)) { + $dirpath = dirname($path); + $filename = basename($path); + $newfilename = $this->store_file_for_text_field($data, $this->filebase, $dirpath, $filename); + $text = preg_replace("|$path|", "@@PLUGINFILE@@/" . $newfilename, $text); + } + + } + $data['text'] = $text; + $data['format'] = FORMAT_HTML; + return $data; + } + + /** + * Same as text_field but text is cleaned. + * @param string text text to parse and recode + * @return array with keys text, format, itemid. + */ + public function cleaned_text_field($text) { + return $this->text_field($this->cleaninput($text)); + } + + /** + * Convert the question text to plain text. + * We need to overwrite this function because questiontext is an array. + */ + protected function format_question_text($question) { + global $DB; + $formatoptions = new stdClass(); + $formatoptions->noclean = true; + return html_to_text(format_text($question->questiontext['text'], + $question->questiontext['format'], $formatoptions), 0, false); + } +} diff --git a/question/format/blackboard_six/formatpool.php b/question/format/blackboard_six/formatpool.php new file mode 100644 index 0000000000000..80867f34b85f6 --- /dev/null +++ b/question/format/blackboard_six/formatpool.php @@ -0,0 +1,467 @@ +. + +/** + * Blackboard V5 and V6 question importer. + * + * @package qformat_blackboard_six + * @copyright 2003 Scott Elliott + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/xmlize.php'); + +/** + * Blackboard pool question importer. + * + * @copyright 2003 Scott Elliott + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +class qformat_blackboard_six_pool extends qformat_blackboard_six_base { + // Is the current question's question text escaped HTML (true for most if not all Blackboard files). + public $ishtml = true; + + /** + * Parse the xml document into an array of questions + * this *could* burn memory - but it won't happen that much + * so fingers crossed! + * @param array of lines from the input file. + * @param stdClass $context + * @return array (of objects) questions objects. + */ + protected function readquestions($text) { + + // This converts xml to big nasty data structure, + // the 0 means keep white space as it is. + try { + $xml = xmlize($text, 0, 'UTF-8', true); + } catch (xml_format_exception $e) { + $this->error($e->getMessage(), ''); + return false; + } + + $questions = array(); + + $this->process_tf($xml, $questions); + $this->process_mc($xml, $questions); + $this->process_ma($xml, $questions); + $this->process_fib($xml, $questions); + $this->process_matching($xml, $questions); + $this->process_essay($xml, $questions); + + return $questions; + } + + /** + * Do question import processing common to every qtype. + * @param array $questiondata the xml tree related to the current question + * @return object initialized question object. + */ + public function process_common($questiondata) { + + // This routine initialises the question object. + $question = $this->defaultquestion(); + + // Determine if the question is already escaped html. + $this->ishtml = $this->getpath($questiondata, + array('#', 'BODY', 0, '#', 'FLAGS', 0, '#', 'ISHTML', 0, '@', 'value'), + false, false); + + // Put questiontext in question object. + $text = $this->getpath($questiondata, + array('#', 'BODY', 0, '#', 'TEXT', 0, '#'), + '', true, get_string('importnotext', 'qformat_blackboard_six')); + + $question->questiontext = $this->cleaned_text_field($text); + $question->questiontextformat = FORMAT_HTML; // Needed because add_blank_combined_feedback uses it. + + // Put name in question object. We must ensure it is not empty and it is less than 250 chars. + $question->name = shorten_text(strip_tags($question->questiontext['text']), 200); + $question->name = substr($question->name, 0, 250); + if (!$question->name) { + $id = $this->getpath($questiondata, + array('@', 'id'), '', true); + $question->name = get_string('defaultname', 'qformat_blackboard_six' , $id); + } + + $question->generalfeedback = ''; + $question->generalfeedbackformat = FORMAT_HTML; + $question->generalfeedbackfiles = array(); + + // TODO : read the mark from the POOL TITLE QUESTIONLIST section. + $question->defaultmark = 1; + return $question; + } + + /** + * Process Essay Questions + * @param array xml the xml tree + * @param array questions the questions already parsed + */ + public function process_essay($xml, &$questions) { + + if ($this->getpath($xml, array('POOL', '#', 'QUESTION_ESSAY'), false, false)) { + $essayquestions = $this->getpath($xml, + array('POOL', '#', 'QUESTION_ESSAY'), false, false); + } else { + return; + } + + foreach ($essayquestions as $thisquestion) { + + $question = $this->process_common($thisquestion); + + $question->qtype = 'essay'; + + $question->answer = ''; + $answer = $this->getpath($thisquestion, + array('#', 'ANSWER', 0, '#', 'TEXT', 0, '#'), '', true); + $question->graderinfo = $this->cleaned_text_field($answer); + $question->feedback = ''; + $question->responseformat = 'editor'; + $question->responsefieldlines = 15; + $question->attachments = 0; + $question->fraction = 0; + + $questions[] = $question; + } + } + + /** + * Process True / False Questions + * @param array xml the xml tree + * @param array questions the questions already parsed + */ + public function process_tf($xml, &$questions) { + + if ($this->getpath($xml, array('POOL', '#', 'QUESTION_TRUEFALSE'), false, false)) { + $tfquestions = $this->getpath($xml, + array('POOL', '#', 'QUESTION_TRUEFALSE'), false, false); + } else { + return; + } + + foreach ($tfquestions as $thisquestion) { + + $question = $this->process_common($thisquestion); + + $question->qtype = 'truefalse'; + $question->single = 1; // Only one answer is allowed. + + $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), array(), false); + + $correctanswer = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER', 0, '@', 'answer_id'), + '', true); + + // First choice is true, second is false. + $id = $this->getpath($choices[0], array('@', 'id'), '', true); + $correctfeedback = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'), + '', true); + $incorrectfeedback = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'), + '', true); + if (strcmp($id, $correctanswer) == 0) { // True is correct. + $question->answer = 1; + $question->feedbacktrue = $this->cleaned_text_field($correctfeedback); + $question->feedbackfalse = $this->cleaned_text_field($incorrectfeedback); + } else { // False is correct. + $question->answer = 0; + $question->feedbacktrue = $this->cleaned_text_field($incorrectfeedback); + $question->feedbackfalse = $this->cleaned_text_field($correctfeedback); + } + $question->correctanswer = $question->answer; + $questions[] = $question; + } + } + + /** + * Process Multiple Choice Questions with single answer + * @param array xml the xml tree + * @param array questions the questions already parsed + */ + public function process_mc($xml, &$questions) { + + if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MULTIPLECHOICE'), false, false)) { + $mcquestions = $this->getpath($xml, + array('POOL', '#', 'QUESTION_MULTIPLECHOICE'), false, false); + } else { + return; + } + + foreach ($mcquestions as $thisquestion) { + + $question = $this->process_common($thisquestion); + + $correctfeedback = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'), + '', true); + $incorrectfeedback = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'), + '', true); + $question->correctfeedback = $this->cleaned_text_field($correctfeedback); + $question->partiallycorrectfeedback = $this->text_field(''); + $question->incorrectfeedback = $this->cleaned_text_field($incorrectfeedback); + + $question->qtype = 'multichoice'; + $question->single = 1; // Only one answer is allowed. + + $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false); + $correctanswerid = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER', 0, '@', 'answer_id'), + '', true); + foreach ($choices as $choice) { + $choicetext = $this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true); + // Put this choice in the question object. + $question->answer[] = $this->cleaned_text_field($choicetext); + + $choiceid = $this->getpath($choice, array('@', 'id'), '', true); + // If choice is the right answer, give 100% mark, otherwise give 0%. + if (strcmp ($choiceid, $correctanswerid) == 0) { + $question->fraction[] = 1; + } else { + $question->fraction[] = 0; + } + // There is never feedback specific to each choice. + $question->feedback[] = $this->text_field(''); + } + $questions[] = $question; + } + } + + /** + * Process Multiple Choice Questions With Multiple Answers + * @param array xml the xml tree + * @param array questions the questions already parsed + */ + public function process_ma($xml, &$questions) { + if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MULTIPLEANSWER'), false, false)) { + $maquestions = $this->getpath($xml, + array('POOL', '#', 'QUESTION_MULTIPLEANSWER'), false, false); + } else { + return; + } + + foreach ($maquestions as $thisquestion) { + $question = $this->process_common($thisquestion); + + $correctfeedback = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'), + '', true); + $incorrectfeedback = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'), + '', true); + $question->correctfeedback = $this->cleaned_text_field($correctfeedback); + // As there is no partially correct feedback we use incorrect one. + $question->partiallycorrectfeedback = $this->cleaned_text_field($incorrectfeedback); + $question->incorrectfeedback = $this->cleaned_text_field($incorrectfeedback); + + $question->qtype = 'multichoice'; + $question->defaultmark = 1; + $question->single = 0; // More than one answers allowed. + + $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false); + $correctanswerids = array(); + foreach ($this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER'), false, false) as $correctanswer) { + if ($correctanswer) { + $correctanswerids[] = $this->getpath($correctanswer, + array('@', 'answer_id'), + '', true); + } + } + $fraction = 1/count($correctanswerids); + + foreach ($choices as $choice) { + $choicetext = $this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true); + // Put this choice in the question object. + $question->answer[] = $this->cleaned_text_field($choicetext); + + $choiceid = $this->getpath($choice, array('@', 'id'), '', true); + + $iscorrect = in_array($choiceid, $correctanswerids); + + if ($iscorrect) { + $question->fraction[] = $fraction; + } else { + $question->fraction[] = 0; + } + // There is never feedback specific to each choice. + $question->feedback[] = $this->text_field(''); + } + $questions[] = $question; + } + } + + /** + * Process Fill in the Blank Questions + * @param array xml the xml tree + * @param array questions the questions already parsed + */ + public function process_fib($xml, &$questions) { + if ($this->getpath($xml, array('POOL', '#', 'QUESTION_FILLINBLANK'), false, false)) { + $fibquestions = $this->getpath($xml, + array('POOL', '#', 'QUESTION_FILLINBLANK'), false, false); + } else { + return; + } + + foreach ($fibquestions as $thisquestion) { + + $question = $this->process_common($thisquestion); + + $question->qtype = 'shortanswer'; + $question->usecase = 0; // Ignore case. + + $correctfeedback = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'), + '', true); + $incorrectfeedback = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'), + '', true); + $answers = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false); + foreach ($answers as $answer) { + $question->answer[] = $this->getpath($answer, + array('#', 'TEXT', 0, '#'), '', true); + $question->fraction[] = 1; + $question->feedback[] = $this->cleaned_text_field($correctfeedback); + } + $question->answer[] = '*'; + $question->fraction[] = 0; + $question->feedback[] = $this->cleaned_text_field($incorrectfeedback); + + $questions[] = $question; + } + } + + /** + * Process Matching Questions + * @param array xml the xml tree + * @param array questions the questions already parsed + */ + public function process_matching($xml, &$questions) { + if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MATCH'), false, false)) { + $matchquestions = $this->getpath($xml, + array('POOL', '#', 'QUESTION_MATCH'), false, false); + } else { + return; + } + // Blackboard questions can't be imported in core Moodle without a loss in data, + // as core match question don't allow HTML in subanswers. The contributed ddmatch + // question type support HTML in subanswers. + // The ddmatch question type is not part of core, so we need to check if it is defined. + $ddmatchisinstalled = question_bank::is_qtype_installed('ddmatch'); + + foreach ($matchquestions as $thisquestion) { + + $question = $this->process_common($thisquestion); + if ($ddmatchisinstalled) { + $question->qtype = 'ddmatch'; + } else { + $question->qtype = 'match'; + } + + $correctfeedback = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'), + '', true); + $incorrectfeedback = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'), + '', true); + $question->correctfeedback = $this->cleaned_text_field($correctfeedback); + // As there is no partially correct feedback we use incorrect one. + $question->partiallycorrectfeedback = $this->cleaned_text_field($incorrectfeedback); + $question->incorrectfeedback = $this->cleaned_text_field($incorrectfeedback); + + $choices = $this->getpath($thisquestion, + array('#', 'CHOICE'), false, false); // Blackboard "choices" are Moodle subanswers. + $answers = $this->getpath($thisquestion, + array('#', 'ANSWER'), false, false); // Blackboard "answers" are Moodle subquestions. + $correctanswers = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER'), false, false); // Mapping between choices and answers. + $mappings = array(); + foreach ($correctanswers as $correctanswer) { + if ($correctanswer) { + $correctchoiceid = $this->getpath($correctanswer, + array('@', 'choice_id'), '', true); + $correctanswerid = $this->getpath($correctanswer, + array('@', 'answer_id'), + '', true); + $mappings[$correctanswerid] = $correctchoiceid; + } + } + + foreach ($choices as $choice) { + if ($ddmatchisinstalled) { + $choicetext = $this->cleaned_text_field($this->getpath($choice, + array('#', 'TEXT', 0, '#'), '', true)); + } else { + $choicetext = trim(strip_tags($this->getpath($choice, + array('#', 'TEXT', 0, '#'), '', true))); + } + + if ($choicetext != '') { // Only import non empty subanswers. + $subquestion = ''; + $choiceid = $this->getpath($choice, + array('@', 'id'), '', true); + $fiber = array_search($choiceid, $mappings); + $fiber = array_keys ($mappings, $choiceid); + foreach ($fiber as $correctanswerid) { + // We have found a correspondance for this choice so we need to take the associated answer. + foreach ($answers as $answer) { + $currentanswerid = $this->getpath($answer, + array('@', 'id'), '', true); + if (strcmp ($currentanswerid, $correctanswerid) == 0) { + $subquestion = $this->getpath($answer, + array('#', 'TEXT', 0, '#'), '', true); + break; + } + } + $question->subquestions[] = $this->cleaned_text_field($subquestion); + $question->subanswers[] = $choicetext; + } + + if ($subquestion == '') { // Then in this case, $choice is a distractor. + $question->subquestions[] = $this->text_field(''); + $question->subanswers[] = $choicetext; + } + } + } + + // Verify that this matching question has enough subquestions and subanswers. + $subquestioncount = 0; + $subanswercount = 0; + $subanswers = $question->subanswers; + foreach ($question->subquestions as $key => $subquestion) { + $subquestion = $subquestion['text']; + $subanswer = $subanswers[$key]; + if ($subquestion != '') { + $subquestioncount++; + } + $subanswercount++; + } + if ($subquestioncount < 2 || $subanswercount < 3) { + $this->error(get_string('notenoughtsubans', 'qformat_blackboard_six', $question->questiontext['text'])); + } else { + $questions[] = $question; + } + + } + } +} diff --git a/question/format/blackboard_six/formatqti.php b/question/format/blackboard_six/formatqti.php new file mode 100644 index 0000000000000..223d9f608ca26 --- /dev/null +++ b/question/format/blackboard_six/formatqti.php @@ -0,0 +1,894 @@ +. + +/** + * Blackboard V5 and V6 question importer. + * + * @package qformat_blackboard_six + * @copyright 2005 Michael Penney + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/xmlize.php'); + +/** + * Blackboard 6.0 question importer. + * + * @copyright 2005 Michael Penney + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qformat_blackboard_six_qti extends qformat_blackboard_six_base { + /** + * Parse the xml document into an array of questions + * this *could* burn memory - but it won't happen that much + * so fingers crossed! + * @param array of lines from the input file. + * @param stdClass $context + * @return array (of objects) questions objects. + */ + protected function readquestions($text) { + + // This converts xml to big nasty data structure, + // the 0 means keep white space as it is. + try { + $xml = xmlize($text, 0, 'UTF-8', true); + } catch (xml_format_exception $e) { + $this->error($e->getMessage(), ''); + return false; + } + + $questions = array(); + // First step : we are only interested in the tags. + $rawquestions = $this->getpath($xml, + array('questestinterop', '#', 'assessment', 0, '#', 'section', 0, '#', 'item'), + array(), false); + // Each tag contains data related to a single question. + foreach ($rawquestions as $quest) { + // Second step : parse each question data into the intermediate + // rawquestion structure array. + // Warning : rawquestions are not Moodle questions. + $question = $this->create_raw_question($quest); + // Third step : convert a rawquestion into a Moodle question. + switch($question->qtype) { + case "Matching": + $this->process_matching($question, $questions); + break; + case "Multiple Choice": + $this->process_mc($question, $questions); + break; + case "Essay": + $this->process_essay($question, $questions); + break; + case "Multiple Answer": + $this->process_ma($question, $questions); + break; + case "True/False": + $this->process_tf($question, $questions); + break; + case 'Fill in the Blank': + $this->process_fblank($question, $questions); + break; + case 'Short Response': + $this->process_essay($question, $questions); + break; + default: + $this->error(get_string('unknownorunhandledtype', 'qformat_blackboard_six', $question->qtype)); + break; + } + } + return $questions; + } + + /** + * Creates a cleaner object to deal with for processing into Moodle. + * The object returned is NOT a moodle question object. + * @param array $quest XML question data + * @return object rawquestion + */ + public function create_raw_question($quest) { + + $rawquestion = new stdClass(); + $rawquestion->qtype = $this->getpath($quest, + array('#', 'itemmetadata', 0, '#', 'bbmd_questiontype', 0, '#'), + '', true); + $rawquestion->id = $this->getpath($quest, + array('#', 'itemmetadata', 0, '#', 'bbmd_asi_object_id', 0, '#'), + '', true); + $presentation = new stdClass(); + $presentation->blocks = $this->getpath($quest, + array('#', 'presentation', 0, '#', 'flow', 0, '#', 'flow'), + array(), false); + + foreach ($presentation->blocks as $pblock) { + $block = new stdClass(); + $block->type = $this->getpath($pblock, + array('@', 'class'), + '', true); + + switch($block->type) { + case 'QUESTION_BLOCK': + $subblocks = $this->getpath($pblock, + array('#', 'flow'), + array(), false); + foreach ($subblocks as $sblock) { + $this->process_block($sblock, $block); + } + break; + + case 'RESPONSE_BLOCK': + $choices = null; + switch($rawquestion->qtype) { + case 'Matching': + $bbsubquestions = $this->getpath($pblock, + array('#', 'flow'), + array(), false); + $sub_questions = array(); + foreach ($bbsubquestions as $bbsubquestion) { + $sub_question = new stdClass(); + $sub_question->ident = $this->getpath($bbsubquestion, + array('#', 'response_lid', 0, '@', 'ident'), + '', true); + $this->process_block($this->getpath($bbsubquestion, + array('#', 'flow', 0), + false, false), $sub_question); + $bbchoices = $this->getpath($bbsubquestion, + array('#', 'response_lid', 0, '#', 'render_choice', 0, + '#', 'flow_label', 0, '#', 'response_label'), + array(), false); + $choices = array(); + $this->process_choices($bbchoices, $choices); + $sub_question->choices = $choices; + if (!isset($block->subquestions)) { + $block->subquestions = array(); + } + $block->subquestions[] = $sub_question; + } + break; + case 'Multiple Answer': + $bbchoices = $this->getpath($pblock, + array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label'), + array(), false); + $choices = array(); + $this->process_choices($bbchoices, $choices); + $block->choices = $choices; + break; + case 'Essay': + // Doesn't apply since the user responds with text input. + break; + case 'Multiple Choice': + $mcchoices = $this->getpath($pblock, + array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label'), + array(), false); + foreach ($mcchoices as $mcchoice) { + $choices = new stdClass(); + $choices = $this->process_block($mcchoice, $choices); + $block->choices[] = $choices; + } + break; + case 'Short Response': + // Do nothing? + break; + case 'Fill in the Blank': + // Do nothing? + break; + default: + $bbchoices = $this->getpath($pblock, + array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', + 'flow_label', 0, '#', 'response_label'), + array(), false); + $choices = array(); + $this->process_choices($bbchoices, $choices); + $block->choices = $choices; + } + break; + case 'RIGHT_MATCH_BLOCK': + $matchinganswerset = $this->getpath($pblock, + array('#', 'flow'), + false, false); + + $answerset = array(); + foreach ($matchinganswerset as $answer) { + $bbanswer = new stdClass; + $bbanswer->text = $this->getpath($answer, + array('#', 'flow', 0, '#', 'material', 0, '#', 'mat_extension', + 0, '#', 'mat_formattedtext', 0, '#'), + false, false); + $answerset[] = $bbanswer; + } + $block->matchinganswerset = $answerset; + break; + default: + $this->error(get_string('unhandledpresblock', 'qformat_blackboard_six')); + break; + } + $rawquestion->{$block->type} = $block; + } + + // Determine response processing. + // There is a section called 'outcomes' that I don't know what to do with. + $resprocessing = $this->getpath($quest, + array('#', 'resprocessing'), + array(), false); + + $respconditions = $this->getpath($resprocessing[0], + array('#', 'respcondition'), + array(), false); + $responses = array(); + if ($rawquestion->qtype == 'Matching') { + $this->process_matching_responses($respconditions, $responses); + } else { + $this->process_responses($respconditions, $responses); + } + $rawquestion->responses = $responses; + $feedbackset = $this->getpath($quest, + array('#', 'itemfeedback'), + array(), false); + + $feedbacks = array(); + $this->process_feedback($feedbackset, $feedbacks); + $rawquestion->feedback = $feedbacks; + return $rawquestion; + } + + /** + * Helper function to process an XML block into an object. + * Can call himself recursively if necessary to parse this branch of the XML tree. + * @param array $curblock XML block to parse + * @return object $block parsed + */ + public function process_block($curblock, $block) { + + $curtype = $this->getpath($curblock, + array('@', 'class'), + '', true); + + switch($curtype) { + case 'FORMATTED_TEXT_BLOCK': + $text = $this->getpath($curblock, + array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext', 0, '#'), + '', true); + $block->text = $this->strip_applet_tags_get_mathml($text); + break; + case 'FILE_BLOCK': + $block->filename = $this->getpath($curblock, + array('#', 'material', 0, '#'), + '', true); + if ($block->filename != '') { + // TODO : determine what to do with the file's content. + $this->error(get_string('filenothandled', 'qformat_blackboard_six', $block->filename)); + } + break; + case 'Block': + if ($this->getpath($curblock, + array('#', 'material', 0, '#', 'mattext'), + false, false)) { + $block->text = $this->getpath($curblock, + array('#', 'material', 0, '#', 'mattext', 0, '#'), + '', true); + } else if ($this->getpath($curblock, + array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext'), + false, false)) { + $block->text = $this->getpath($curblock, + array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext', 0, '#'), + '', true); + } else if ($this->getpath($curblock, + array('#', 'response_label'), + false, false)) { + // This is a response label block. + $subblocks = $this->getpath($curblock, + array('#', 'response_label', 0), + array(), false); + if (!isset($block->ident)) { + + if ($this->getpath($subblocks, + array('@', 'ident'), '', true)) { + $block->ident = $this->getpath($subblocks, + array('@', 'ident'), '', true); + } + } + foreach ($this->getpath($subblocks, + array('#', 'flow_mat'), array(), false) as $subblock) { + $this->process_block($subblock, $block); + } + } else { + if ($this->getpath($curblock, + array('#', 'flow_mat'), false, false) + || $this->getpath($curblock, + array('#', 'flow'), false, false)) { + if ($this->getpath($curblock, + array('#', 'flow_mat'), false, false)) { + $subblocks = $this->getpath($curblock, + array('#', 'flow_mat'), array(), false); + } else if ($this->getpath($curblock, + array('#', 'flow'), false, false)) { + $subblocks = $this->getpath($curblock, + array('#', 'flow'), array(), false); + } + foreach ($subblocks as $sblock) { + // This will recursively grab the sub blocks which should be of one of the other types. + $this->process_block($sblock, $block); + } + } + } + break; + case 'LINK_BLOCK': + // Not sure how this should be included? + $link = $this->getpath($curblock, + array('#', 'material', 0, '#', 'mattext', 0, '@', 'uri'), '', true); + if (!empty($link)) { + $block->link = $link; + } else { + $block->link = ''; + } + break; + } + return $block; + } + + /** + * Preprocess XML blocks containing data for questions' choices. + * Called by {@link create_raw_question()} + * for matching, multichoice and fill in the blank questions. + * @param array $bbchoices XML block to parse + * @param array $choices array of choices suitable for a rawquestion. + */ + protected function process_choices($bbchoices, &$choices) { + foreach ($bbchoices as $choice) { + if ($this->getpath($choice, + array('@', 'ident'), '', true)) { + $curchoice = $this->getpath($choice, + array('@', 'ident'), '', true); + } else { // For multiple answers. + $curchoice = $this->getpath($choice, + array('#', 'response_label', 0), array(), false); + } + if ($this->getpath($choice, + array('#', 'flow_mat', 0), false, false)) { // For multiple answers. + $curblock = $this->getpath($choice, + array('#', 'flow_mat', 0), false, false); + // Reset $curchoice to new stdClass because process_block is expecting an object + // for the second argument and not a string, + // which is what is was set as originally - CT 8/7/06. + $curchoice = new stdClass(); + $this->process_block($curblock, $curchoice); + } else if ($this->getpath($choice, + array('#', 'response_label'), false, false)) { + // Reset $curchoice to new stdClass because process_block is expecting an object + // for the second argument and not a string, + // which is what is was set as originally - CT 8/7/06. + $curchoice = new stdClass(); + $this->process_block($choice, $curchoice); + } + $choices[] = $curchoice; + } + } + + /** + * Preprocess XML blocks containing data for subanswers + * Called by {@link create_raw_question()} + * for matching questions only. + * @param array $bbresponses XML block to parse + * @param array $responses array of responses suitable for a matching rawquestion. + */ + protected function process_matching_responses($bbresponses, &$responses) { + foreach ($bbresponses as $bbresponse) { + $response = new stdClass; + if ($this->getpath($bbresponse, + array('#', 'conditionvar', 0, '#', 'varequal'), false, false)) { + $response->correct = $this->getpath($bbresponse, + array('#', 'conditionvar', 0, '#', 'varequal', 0, '#'), '', true); + $response->ident = $this->getpath($bbresponse, + array('#', 'conditionvar', 0, '#', 'varequal', 0, '@', 'respident'), '', true); + } + // Suppressed an else block because if the above if condition is false, + // the question is not necessary a broken one, most of the time it's an tag. + + $response->feedback = $this->getpath($bbresponse, + array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true); + $responses[] = $response; + } + } + + /** + * Preprocess XML blocks containing data for responses processing. + * Called by {@link create_raw_question()} + * for all questions types. + * @param array $bbresponses XML block to parse + * @param array $responses array of responses suitable for a rawquestion. + */ + protected function process_responses($bbresponses, &$responses) { + foreach ($bbresponses as $bbresponse) { + $response = new stdClass(); + if ($this->getpath($bbresponse, + array('@', 'title'), '', true)) { + $response->title = $this->getpath($bbresponse, + array('@', 'title'), '', true); + } else { + $response->title = $this->getpath($bbresponse, + array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true); + } + $response->ident = array(); + if ($this->getpath($bbresponse, + array('#', 'conditionvar', 0, '#'), false, false)) { + $response->ident[0] = $this->getpath($bbresponse, + array('#', 'conditionvar', 0, '#'), array(), false); + } else if ($this->getpath($bbresponse, + array('#', 'conditionvar', 0, '#', 'other', 0, '#'), false, false)) { + $response->ident[0] = $this->getpath($bbresponse, + array('#', 'conditionvar', 0, '#', 'other', 0, '#'), array(), false); + } + if ($this->getpath($bbresponse, + array('#', 'conditionvar', 0, '#', 'and'), false, false)) { + $responseset = $this->getpath($bbresponse, + array('#', 'conditionvar', 0, '#', 'and'), array(), false); + foreach ($responseset as $rs) { + $response->ident[] = $this->getpath($rs, array('#'), array(), false); + if (!isset($response->feedback) and $this->getpath($rs, array('@'), false, false)) { + $response->feedback = $this->getpath($rs, + array('@', 'respident'), '', true); + } + } + } else { + $response->feedback = $this->getpath($bbresponse, + array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true); + } + + // Determine what fraction to give response. + if ($this->getpath($bbresponse, + array('#', 'setvar'), false, false)) { + switch ($this->getpath($bbresponse, + array('#', 'setvar', 0, '#'), false, false)) { + case "SCORE.max": + $response->fraction = 1; + break; + default: + // I have only seen this being 0 or unset. + // There are probably fractional values of SCORE.max, but I'm not sure what they look like. + $response->fraction = 0; + break; + } + } else { + // Just going to assume this is the case this is probably not correct. + $response->fraction = 0; + } + + $responses[] = $response; + } + } + + /** + * Preprocess XML blocks containing data for responses feedbacks. + * Called by {@link create_raw_question()} + * for all questions types. + * @param array $feedbackset XML block to parse + * @param array $feedbacks array of feedbacks suitable for a rawquestion. + */ + public function process_feedback($feedbackset, &$feedbacks) { + foreach ($feedbackset as $bb_feedback) { + $feedback = new stdClass(); + $feedback->ident = $this->getpath($bb_feedback, + array('@', 'ident'), '', true); + $feedback->text = ''; + if ($this->getpath($bb_feedback, + array('#', 'flow_mat', 0), false, false)) { + $this->process_block($this->getpath($bb_feedback, + array('#', 'flow_mat', 0), false, false), $feedback); + } else if ($this->getpath($bb_feedback, + array('#', 'solution', 0, '#', 'solutionmaterial', 0, '#', 'flow_mat', 0), false, false)) { + $this->process_block($this->getpath($bb_feedback, + array('#', 'solution', 0, '#', 'solutionmaterial', 0, '#', 'flow_mat', 0), false, false), $feedback); + } + + $feedbacks[$feedback->ident] = $feedback; + } + } + + /** + * Create common parts of question + * @param object $quest rawquestion + * @return object Moodle question. + */ + public function process_common($quest) { + $question = $this->defaultquestion(); + $text = $quest->QUESTION_BLOCK->text; + + $question->questiontext = $this->cleaned_text_field($text); + $question->questiontextformat = FORMAT_HTML; // Needed because add_blank_combined_feedback uses it. + + $question->name = shorten_text(strip_tags($question->questiontext['text']), 200); + $question->name = substr($question->name, 0, 250); + if (!$question->name) { + $question->name = get_string('defaultname', 'qformat_blackboard_six' , $quest->id); + } + $question->generalfeedback = ''; + $question->generalfeedbackformat = FORMAT_HTML; + $question->generalfeedbackfiles = array(); + + return $question; + } + + /** + * Process True / False Questions + * Parse a truefalse rawquestion and add the result + * to the array of questions already parsed. + * @param object $quest rawquestion + * @param $questions array of Moodle questions already done. + */ + protected function process_tf($quest, &$questions) { + $question = $this->process_common($quest); + + $question->qtype = 'truefalse'; + $question->single = 1; // Only one answer is allowed. + $question->penalty = 1; // Penalty = 1 for truefalse questions. + // 0th [response] is the correct answer. + $responses = $quest->responses; + $correctresponse = $this->getpath($responses[0]->ident[0], + array('varequal', 0, '#'), '', true); + if ($correctresponse != 'false') { + $correct = true; + } else { + $correct = false; + } + $fback = new stdClass(); + + foreach ($quest->feedback as $fb) { + $fback->{$fb->ident} = $fb->text; + } + + if ($correct) { // True is correct. + $question->answer = 1; + $question->feedbacktrue = $this->cleaned_text_field($fback->correct); + $question->feedbackfalse = $this->cleaned_text_field($fback->incorrect); + } else { // False is correct. + $question->answer = 0; + $question->feedbacktrue = $this->cleaned_text_field($fback->incorrect); + $question->feedbackfalse = $this->cleaned_text_field($fback->correct); + } + $question->correctanswer = $question->answer; + $questions[] = $question; + } + + /** + * Process Fill in the Blank Questions + * Parse a fillintheblank rawquestion and add the result + * to the array of questions already parsed. + * @param object $quest rawquestion + * @param $questions array of Moodle questions already done. + */ + protected function process_fblank($quest, &$questions) { + $question = $this->process_common($quest); + $question->qtype = 'shortanswer'; + $question->usecase = 0; // Ignore case. + + $answers = array(); + $fractions = array(); + $feedbacks = array(); + + // Extract the feedback. + $feedback = array(); + foreach ($quest->feedback as $fback) { + if (isset($fback->ident)) { + if ($fback->ident == 'correct' || $fback->ident == 'incorrect') { + $feedback[$fback->ident] = $fback->text; + } + } + } + + foreach ($quest->responses as $response) { + if (isset($response->title)) { + if ($this->getpath($response->ident[0], + array('varequal', 0, '#'), false, false)) { + // For BB Fill in the Blank, only interested in correct answers. + if ($response->feedback = 'correct') { + $answers[] = $this->getpath($response->ident[0], + array('varequal', 0, '#'), '', true); + $fractions[] = 1; + if (isset($feedback['correct'])) { + $feedbacks[] = $this->cleaned_text_field($feedback['correct']); + } else { + $feedbacks[] = $this->text_field(''); + } + } + } + + } + } + + // Adding catchall to so that students can see feedback for incorrect answers when they enter something, + // the instructor did not enter. + $answers[] = '*'; + $fractions[] = 0; + if (isset($feedback['incorrect'])) { + $feedbacks[] = $this->cleaned_text_field($feedback['incorrect']); + } else { + $feedbacks[] = $this->text_field(''); + } + + $question->answer = $answers; + $question->fraction = $fractions; + $question->feedback = $feedbacks; // Changed to assign $feedbacks to $question->feedback instead of. + + if (!empty($question)) { + $questions[] = $question; + } + + } + + /** + * Process Multichoice Questions + * Parse a multichoice single answer rawquestion and add the result + * to the array of questions already parsed. + * @param object $quest rawquestion + * @param $questions array of Moodle questions already done. + */ + protected function process_mc($quest, &$questions) { + $question = $this->process_common($quest); + $question->qtype = 'multichoice'; + $question = $this->add_blank_combined_feedback($question); + $question->single = 1; + $feedback = array(); + foreach ($quest->feedback as $fback) { + $feedback[$fback->ident] = $fback->text; + } + + foreach ($quest->responses as $response) { + if (isset($response->title)) { + if ($response->title == 'correct') { + // Only one answer possible for this qtype so first index is correct answer. + $correct = $this->getpath($response->ident[0], + array('varequal', 0, '#'), '', true); + } + } else { + // Fallback method for when the title is not set. + if ($response->feedback == 'correct') { + // Only one answer possible for this qtype so first index is correct answer. + $correct = $this->getpath($response->ident[0], + array('varequal', 0, '#'), '', true); + } + } + } + + $i = 0; + foreach ($quest->RESPONSE_BLOCK->choices as $response) { + $question->answer[$i] = $this->cleaned_text_field($response->text); + if ($correct == $response->ident) { + $question->fraction[$i] = 1; + // This is a bit of a hack to catch the feedback... first we see if a 'specific' + // feedback for this response exists, then if a 'correct' feedback exists. + + if (!empty($feedback[$response->ident]) ) { + $question->feedback[$i] = $this->cleaned_text_field($feedback[$response->ident]); + } else if (!empty($feedback['correct'])) { + $question->feedback[$i] = $this->cleaned_text_field($feedback['correct']); + } else if (!empty($feedback[$i])) { + $question->feedback[$i] = $this->cleaned_text_field($feedback[$i]); + } else { + $question->feedback[$i] = $this->cleaned_text_field(get_string('correct', 'question')); + } + } else { + $question->fraction[$i] = 0; + if (!empty($feedback[$response->ident]) ) { + $question->feedback[$i] = $this->cleaned_text_field($feedback[$response->ident]); + } else if (!empty($feedback['incorrect'])) { + $question->feedback[$i] = $this->cleaned_text_field($feedback['incorrect']); + } else if (!empty($feedback[$i])) { + $question->feedback[$i] = $this->cleaned_text_field($feedback[$i]); + } else { + $question->feedback[$i] = $this->cleaned_text_field(get_string('incorrect', 'question')); + } + } + $i++; + } + + if (!empty($question)) { + $questions[] = $question; + } + } + + /** + * Process Multiple Choice Questions With Multiple Answers. + * Parse a multichoice multianswer rawquestion and add the result + * to the array of questions already parsed. + * @param object $quest rawquestion + * @param $questions array of Moodle questions already done. + */ + public function process_ma($quest, &$questions) { + $question = $this->process_common($quest); + $question->qtype = 'multichoice'; + $question = $this->add_blank_combined_feedback($question); + $question->single = 0; // More than one answer allowed. + + $answers = $quest->responses; + $correctanswers = array(); + foreach ($answers as $answer) { + if ($answer->title == 'correct') { + $answerset = $this->getpath($answer->ident[0], + array('and', 0, '#', 'varequal'), array(), false); + foreach ($answerset as $ans) { + $correctanswers[] = $ans['#']; + } + } + } + $feedback = new stdClass(); + foreach ($quest->feedback as $fb) { + $feedback->{$fb->ident} = trim($fb->text); + } + + $correctanswercount = count($correctanswers); + $fraction = 1/$correctanswercount; + $choiceset = $quest->RESPONSE_BLOCK->choices; + $i = 0; + foreach ($choiceset as $choice) { + $question->answer[$i] = $this->cleaned_text_field(trim($choice->text)); + if (in_array($choice->ident, $correctanswers)) { + // Correct answer. + $question->fraction[$i] = $fraction; + $question->feedback[$i] = $this->cleaned_text_field($feedback->correct); + } else { + // Wrong answer. + $question->fraction[$i] = 0; + $question->feedback[$i] = $this->cleaned_text_field($feedback->incorrect); + } + $i++; + } + + $questions[] = $question; + } + + /** + * Process Essay Questions + * Parse an essay rawquestion and add the result + * to the array of questions already parsed. + * @param object $quest rawquestion + * @param $questions array of Moodle questions already done. + */ + public function process_essay($quest, &$questions) { + + $question = $this->process_common($quest); + $question->qtype = 'essay'; + + $question->feedback = array(); + // Not sure where to get the correct answer from? + foreach ($quest->feedback as $feedback) { + // Added this code to put the possible solution that the + // instructor gives as the Moodle answer for an essay question. + if ($feedback->ident == 'solution') { + $question->graderinfo = $this->cleaned_text_field($feedback->text); + } + } + // Added because essay/questiontype.php:save_question_option is expecting a + // fraction property - CT 8/10/06. + $question->fraction[] = 1; + $question->defaultmark = 1; + $question->responseformat = 'editor'; + $question->responsefieldlines = 15; + $question->attachments = 0; + + $questions[]=$question; + } + + /** + * Process Matching Questions + * Parse a matching rawquestion and add the result + * to the array of questions already parsed. + * @param object $quest rawquestion + * @param $questions array of Moodle questions already done. + */ + public function process_matching($quest, &$questions) { + + // Blackboard matching questions can't be imported in core Moodle without a loss in data, + // as core match question don't allow HTML in subanswers. The contributed ddmatch + // question type support HTML in subanswers. + // The ddmatch question type is not part of core, so we need to check if it is defined. + $ddmatchisinstalled = question_bank::is_qtype_installed('ddmatch'); + + $question = $this->process_common($quest); + $question = $this->add_blank_combined_feedback($question); + $question->valid = true; + if ($ddmatchisinstalled) { + $question->qtype = 'ddmatch'; + } else { + $question->qtype = 'match'; + } + // Construction of the array holding mappings between subanswers and subquestions. + foreach ($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) { + foreach ($quest->responses as $rid => $resp) { + if (isset($resp->ident) && $resp->ident == $subq->ident) { + $correct = $resp->correct; + } + } + + foreach ($subq->choices as $cid => $choice) { + if ($choice == $correct) { + $mappings[$subq->ident] = $cid; + } + } + } + + foreach ($subq->choices as $choiceid => $choice) { + $subanswertext = $quest->RIGHT_MATCH_BLOCK->matchinganswerset[$choiceid]->text; + if ($ddmatchisinstalled) { + $subanswer = $this->cleaned_text_field($subanswertext); + } else { + $subanswertext = html_to_text($this->cleaninput($subanswertext), 0); + $subanswer = $subanswertext; + } + + if ($subanswertext != '') { // Only import non empty subanswers. + $subquestion = ''; + + $fiber = array_keys ($mappings, $choiceid); + foreach ($fiber as $correctanswerid) { + // We have found a correspondance for this subanswer so we need to take the associated subquestion. + foreach ($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) { + $currentsubqid = $subq->ident; + if (strcmp ($currentsubqid, $correctanswerid) == 0) { + $subquestion = $subq->text; + break; + } + } + $question->subquestions[] = $this->cleaned_text_field($subquestion); + $question->subanswers[] = $subanswer; + } + + if ($subquestion == '') { // Then in this case, $choice is a distractor. + $question->subquestions[] = $this->text_field(''); + $question->subanswers[] = $subanswer; + } + } + } + + // Verify that this matching question has enough subquestions and subanswers. + $subquestioncount = 0; + $subanswercount = 0; + $subanswers = $question->subanswers; + foreach ($question->subquestions as $key => $subquestion) { + $subquestion = $subquestion['text']; + $subanswer = $subanswers[$key]; + if ($subquestion != '') { + $subquestioncount++; + } + $subanswercount++; + } + if ($subquestioncount < 2 || $subanswercount < 3) { + $this->error(get_string('notenoughtsubans', 'qformat_blackboard_six', $question->questiontext['text'])); + } else { + $questions[] = $question; + } + } + + /** + * Strip the applet tag used by Blackboard to render mathml formulas, + * keeping the mathml tag. + * @param string $string + * @return string + */ + public function strip_applet_tags_get_mathml($string) { + if (stristr($string, '') === false) { + return $string; + } else { + // Strip all applet tags keeping stuff before/after and inbetween (if mathml) them. + while (stristr($string, '') !== false) { + preg_match("/(.*)\.*\<\/math\>)\".*\<\/applet\>(.*)/i", $string, $mathmls); + $string = $mathmls[1].$mathmls[2].$mathmls[3]; + } + return $string; + } + } + +} diff --git a/question/format/blackboard_six/lang/en/qformat_blackboard_six.php b/question/format/blackboard_six/lang/en/qformat_blackboard_six.php index 75ff1e1e19021..020c7a25e8e4c 100644 --- a/question/format/blackboard_six/lang/en/qformat_blackboard_six.php +++ b/question/format/blackboard_six/lang/en/qformat_blackboard_six.php @@ -17,11 +17,18 @@ /** * Strings for component 'qformat_blackboard_six', language 'en', branch 'MOODLE_20_STABLE' * - * @package qformat - * @subpackage blackboard_six + * @package qformat_blackboard_six * @copyright 2010 Helen Foster * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['defaultname'] = 'Imported question {$a}'; +$string['errormanifest'] = 'Error while parsing the IMS manifest document'; +$string['importnotext'] = 'Missing question text in XML file'; +$string['filenothandled'] = 'This archive contains reference to a file material {$a} wich is not currently handled by import'; +$string['imagenotfound'] = 'Image file with path {$a} was not found in the import.'; +$string['notenoughtsubans'] = 'Unable to import matching question \'{$a}\' because a matching question must comprise at least two questions and three answers.'; $string['pluginname'] = 'Blackboard V6+'; -$string['pluginname_help'] = 'Blackboard V6+ format enables questions saved in Blackboard\'s export format to be imported via zip file. It provides limited support for Blackboard Version 6 and 7.'; +$string['pluginname_help'] = 'Blackboard V6+ format enables questions saved in all Blackboard export formats to be imported via a dat or zip file. For zip files, images import is supported.'; +$string['unhandledpresblock'] = 'Unhandled presentation bloc'; +$string['unknownorunhandledtype'] = 'Unknown or unhandled question type: {$a}'; diff --git a/question/format/blackboard_six/tests/blackboardformatpool_test.php b/question/format/blackboard_six/tests/blackboardformatpool_test.php new file mode 100644 index 0000000000000..22cadb73589d9 --- /dev/null +++ b/question/format/blackboard_six/tests/blackboardformatpool_test.php @@ -0,0 +1,330 @@ +. + +/** + * Unit tests for the Moodle Blackboard V6+ format. + * + * @package qformat_blackboard_six + * @copyright 2012 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->dirroot . '/question/format.php'); +require_once($CFG->dirroot . '/question/format/blackboard_six/format.php'); +require_once($CFG->dirroot . '/question/engine/tests/helpers.php'); + + +/** + * Unit tests for the blackboard question import format. + * + * @copyright 2012 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qformat_blackboard_six_pool_test extends question_testcase { + + public function make_test_xml() { + $xml = file_get_contents(__DIR__ . '/fixtures/sample_blackboard_pool.dat'); + return array(0=>$xml); + } + + public function test_import_match() { + + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(2); + $questions = $importer->readquestions($xml); + + $q = $questions[4]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'match'; + $expectedq->name = 'Classify the animals.'; + $expectedq->questiontext = array( + 'text' => 'Classify the animals.', + 'format' => FORMAT_HTML, + ); + $expectedq->questiontextformat = FORMAT_HTML; + $expectedq->correctfeedback = array('text' => '', + 'format' => FORMAT_HTML); + $expectedq->partiallycorrectfeedback = array('text' => '', + 'format' => FORMAT_HTML); + $expectedq->incorrectfeedback = array('text' => '', + 'format' => FORMAT_HTML); + $expectedq->generalfeedback = ''; + $expectedq->generalfeedbackformat = FORMAT_HTML; + $expectedq->defaultmark = 1; + $expectedq->length = 1; + $expectedq->penalty = 0.3333333; + $expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers'); + $expectedq->subquestions = array( + array('text' => 'cat', 'format' => FORMAT_HTML), + array('text' => '', 'format' => FORMAT_HTML), + array('text' => 'frog', 'format' => FORMAT_HTML), + array('text' => 'newt', 'format' => FORMAT_HTML)); + $expectedq->subanswers = array('mammal', 'insect', 'amphibian', 'amphibian'); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_multichoice_single() { + + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(2); + $questions = $importer->readquestions($xml); + $q = $questions[1]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'multichoice'; + $expectedq->single = 1; + $expectedq->name = 'What\'s between orange and green in the spectrum?'; + $expectedq->questiontext = array( + 'text' =>'What\'s between orange and green in the spectrum?', + 'format' => FORMAT_HTML, + ); + $expectedq->questiontextformat = FORMAT_HTML; + $expectedq->correctfeedback = array('text' => 'You gave the right answer.', + 'format' => FORMAT_HTML); + $expectedq->partiallycorrectfeedback = array('text' => '', + 'format' => FORMAT_HTML); + $expectedq->incorrectfeedback = array('text' => 'Only yellow is between orange and green in the spectrum.', + 'format' => FORMAT_HTML); + $expectedq->generalfeedback = ''; + $expectedq->generalfeedbackformat = FORMAT_HTML; + $expectedq->defaultmark = 1; + $expectedq->length = 1; + $expectedq->penalty = 0.3333333; + $expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers'); + $expectedq->answer = array( + 0 => array( + 'text' => 'red', + 'format' => FORMAT_HTML, + ), + 1 => array( + 'text' => 'yellow', + 'format' => FORMAT_HTML, + ), + 2 => array( + 'text' => 'blue', + 'format' => FORMAT_HTML, + ) + ); + $expectedq->fraction = array(0, 1, 0); + $expectedq->feedback = array( + 0 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ), + 1 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ), + 2 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ) + ); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_multichoice_multi() { + + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(2); + $questions = $importer->readquestions($xml); + $q = $questions[2]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'multichoice'; + $expectedq->single = 0; + $expectedq->name = 'What\'s between orange and green in the spectrum?'; + $expectedq->questiontext = array( + 'text' => 'What\'s between orange and green in the spectrum?', + 'format' => FORMAT_HTML, + ); + $expectedq->questiontextformat = FORMAT_HTML; + $expectedq->correctfeedback = array( + 'text' => 'You gave the right answer.', + 'format' => FORMAT_HTML, + ); + $expectedq->partiallycorrectfeedback = array( + 'text' => 'Only yellow and off-beige are between orange and green in the spectrum.', + 'format' => FORMAT_HTML, + ); + $expectedq->incorrectfeedback = array( + 'text' => 'Only yellow and off-beige are between orange and green in the spectrum.', + 'format' => FORMAT_HTML, + ); + $expectedq->generalfeedback = ''; + $expectedq->generalfeedbackformat = FORMAT_HTML; + $expectedq->defaultmark = 1; + $expectedq->length = 1; + $expectedq->penalty = 0.3333333; + $expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers'); + $expectedq->answer = array( + 0 => array( + 'text' => 'yellow', + 'format' => FORMAT_HTML, + ), + 1 => array( + 'text' => 'red', + 'format' => FORMAT_HTML, + ), + 2 => array( + 'text' => 'off-beige', + 'format' => FORMAT_HTML, + ), + 3 => array( + 'text' => 'blue', + 'format' => FORMAT_HTML, + ) + ); + $expectedq->fraction = array(0.5, 0, 0.5, 0); + $expectedq->feedback = array( + 0 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ), + 1 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ), + 2 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ), + 3 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ) + ); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_truefalse() { + + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(2); + $questions = $importer->readquestions($xml); + $q = $questions[0]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'truefalse'; + $expectedq->name = '42 is the Absolute Answer to everything.'; + $expectedq->questiontext = array( + 'text' => '42 is the Absolute Answer to everything.', + 'format' => FORMAT_HTML, + ); + $expectedq->questiontextformat = FORMAT_HTML; + $expectedq->generalfeedback = ''; + $expectedq->generalfeedbackformat = FORMAT_HTML; + $expectedq->defaultmark = 1; + $expectedq->length = 1; + $expectedq->correctanswer = 0; + $expectedq->feedbacktrue = array( + 'text' => '42 is the Ultimate Answer.', + 'format' => FORMAT_HTML, + ); + $expectedq->feedbackfalse = array( + 'text' => 'You gave the right answer.', + 'format' => FORMAT_HTML, + ); + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_fill_in_the_blank() { + + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(2); + $questions = $importer->readquestions($xml); + $q = $questions[3]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'shortanswer'; + $expectedq->name = 'Name an amphibian: __________.'; + $expectedq->questiontext = array( + 'text' => 'Name an amphibian: __________.', + 'format' => FORMAT_HTML, + ); + $expectedq->questiontextformat = FORMAT_HTML; + $expectedq->generalfeedback = ''; + $expectedq->generalfeedbackformat = FORMAT_HTML; + $expectedq->defaultmark = 1; + $expectedq->length = 1; + $expectedq->usecase = 0; + $expectedq->answer = array('frog', '*'); + $expectedq->fraction = array(1, 0); + $expectedq->feedback = array( + 0 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ), + 1 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ) + ); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_essay() { + + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(2); + $questions = $importer->readquestions($xml); + $q = $questions[5]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'essay'; + $expectedq->name = 'How are you?'; + $expectedq->questiontext = array( + 'text' => 'How are you?', + 'format' => FORMAT_HTML, + ); + $expectedq->questiontextformat = FORMAT_HTML; + $expectedq->generalfeedback = ''; + $expectedq->generalfeedbackformat = FORMAT_HTML; + $expectedq->defaultmark = 1; + $expectedq->length = 1; + $expectedq->responseformat = 'editor'; + $expectedq->responsefieldlines = 15; + $expectedq->attachments = 0; + $expectedq->graderinfo = array( + 'text' => 'Blackboard answer for essay questions will be imported as informations for graders.', + 'format' => FORMAT_HTML, + ); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } +} diff --git a/question/format/blackboard_six/tests/blackboardsixformatqti_test.php b/question/format/blackboard_six/tests/blackboardsixformatqti_test.php new file mode 100644 index 0000000000000..68df181bba3d9 --- /dev/null +++ b/question/format/blackboard_six/tests/blackboardsixformatqti_test.php @@ -0,0 +1,330 @@ +. + +/** + * Unit tests for the Moodle Blackboard V6+ format. + * + * @package qformat_blackboard_six + * @copyright 2012 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->dirroot . '/question/format.php'); +require_once($CFG->dirroot . '/question/format/blackboard_six/format.php'); +require_once($CFG->dirroot . '/question/engine/tests/helpers.php'); + + +/** + * Unit tests for the blackboard question import format. + * + * @copyright 2012 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qformat_blackboard_six_qti_test extends question_testcase { + + public function make_test_xml() { + $xml = file_get_contents(__DIR__ . '/fixtures/sample_blackboard_qti.dat'); + return array(0=>$xml); + } + public function test_import_match() { + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(1); + $questions = $importer->readquestions($xml); + $q = $questions[3]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'match'; + $expectedq->name = 'Classify the animals.'; + $expectedq->questiontext = array( + 'text' => 'Classify the animals.', + 'format' => FORMAT_HTML, + ); + $expectedq->questiontextformat = FORMAT_HTML; + $expectedq->correctfeedback = array('text' => '', + 'format' => FORMAT_HTML, 'files' => array()); + $expectedq->partiallycorrectfeedback = array('text' => '', + 'format' => FORMAT_HTML, 'files' => array()); + $expectedq->incorrectfeedback = array('text' => '', + 'format' => FORMAT_HTML, 'files' => array()); + $expectedq->generalfeedback = ''; + $expectedq->generalfeedbackformat = FORMAT_HTML; + $expectedq->defaultmark = 1; + $expectedq->length = 1; + $expectedq->penalty = 0.3333333; + $expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers'); + $expectedq->subquestions = array( + array('text' => '', 'format' => FORMAT_HTML), + array('text' => 'cat', 'format' => FORMAT_HTML), + array('text' => 'frog', 'format' => FORMAT_HTML), + array('text' => 'newt', 'format' => FORMAT_HTML)); + $expectedq->subanswers = array('insect', 'mammal', 'amphibian', 'amphibian'); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_multichoice_single() { + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(1); + $questions = $importer->readquestions($xml); + $q = $questions[1]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'multichoice'; + $expectedq->single = 1; + $expectedq->name = 'What\'s between orange and green in the spectrum?'; + $expectedq->questiontext = array( + 'text' => 'What\'s between orange and green in the spectrum?', + 'format' => FORMAT_HTML, + ); + $expectedq->questiontextformat = FORMAT_HTML; + $expectedq->correctfeedback = array('text' => '', + 'format' => FORMAT_HTML, 'files' => array()); + $expectedq->partiallycorrectfeedback = array('text' => '', + 'format' => FORMAT_HTML, 'files' => array()); + $expectedq->incorrectfeedback = array('text' => '', + 'format' => FORMAT_HTML, 'files' => array()); + $expectedq->generalfeedback = ''; + $expectedq->generalfeedbackformat = FORMAT_HTML; + $expectedq->defaultmark = 1; + $expectedq->length = 1; + $expectedq->penalty = 0.3333333; + $expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers'); + $expectedq->answer = array( + 0 => array( + 'text' => 'red', + 'format' => FORMAT_HTML, + ), + 1 => array( + 'text' => 'yellow', + 'format' => FORMAT_HTML, + ), + 2 => array( + 'text' => 'blue', + 'format' => FORMAT_HTML, + ) + ); + $expectedq->fraction = array(0, 1, 0); + $expectedq->feedback = array( + 0 => array( + 'text' => 'Red is not between orange and green in the spectrum but yellow is.', + 'format' => FORMAT_HTML, + ), + 1 => array( + 'text' => 'You gave the right answer.', + 'format' => FORMAT_HTML, + ), + 2 => array( + 'text' => 'Blue is not between orange and green in the spectrum but yellow is.', + 'format' => FORMAT_HTML, + ) + ); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_multichoice_multi() { + + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(1); + $questions = $importer->readquestions($xml); + $q = $questions[2]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'multichoice'; + $expectedq->single = 0; + $expectedq->name = 'What\'s between orange and green in the spectrum?'; + $expectedq->questiontext = array( + 'text' => 'What\'s between orange and green in the spectrum?', + 'format' => FORMAT_HTML, + ); + $expectedq->questiontextformat = FORMAT_HTML; + $expectedq->correctfeedback = array( + 'text' => '', + 'format' => FORMAT_HTML, + 'files' => array(), + ); + $expectedq->partiallycorrectfeedback = array( + 'text' => '', + 'format' => FORMAT_HTML, + 'files' => array(), + ); + $expectedq->incorrectfeedback = array( + 'text' => '', + 'format' => FORMAT_HTML, + 'files' => array(), + ); + $expectedq->generalfeedback = ''; + $expectedq->generalfeedbackformat = FORMAT_HTML; + $expectedq->defaultmark = 1; + $expectedq->length = 1; + $expectedq->penalty = 0.3333333; + $expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers'); + $expectedq->answer = array( + 0 => array( + 'text' => 'yellow', + 'format' => FORMAT_HTML, + ), + 1 => array( + 'text' => 'red', + 'format' => FORMAT_HTML, + ), + 2 => array( + 'text' => 'off-beige', + 'format' => FORMAT_HTML, + ), + 3 => array( + 'text' => 'blue', + 'format' => FORMAT_HTML, + ) + ); + $expectedq->fraction = array(0.5, 0, 0.5, 0); + $expectedq->feedback = array( + 0 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ), + 1 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ), + 2 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ), + 3 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ) + ); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_truefalse() { + + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(1); + $questions = $importer->readquestions($xml); + $q = $questions[0]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'truefalse'; + $expectedq->name = '42 is the Absolute Answer to everything.'; + $expectedq->questiontext = array( + 'text' => '42 is the Absolute Answer to everything.', + 'format' => FORMAT_HTML, + ); + $expectedq->questiontextformat = FORMAT_HTML; + $expectedq->generalfeedback = ''; + $expectedq->generalfeedbackformat = FORMAT_HTML; + $expectedq->defaultmark = 1; + $expectedq->length = 1; + $expectedq->correctanswer = 0; + $expectedq->feedbacktrue = array( + 'text' => '42 is the Ultimate Answer.', + 'format' => FORMAT_HTML, + ); + $expectedq->feedbackfalse = array( + 'text' => 'You gave the right answer.', + 'format' => FORMAT_HTML, + ); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_fill_in_the_blank() { + + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(1); + $questions = $importer->readquestions($xml); + $q = $questions[4]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'shortanswer'; + $expectedq->name = 'Name an amphibian: __________.'; + $expectedq->questiontext = array( + 'text' => 'Name an amphibian: __________.', + 'format' => FORMAT_HTML, + ); + $expectedq->questiontextformat = FORMAT_HTML; + $expectedq->generalfeedback = ''; + $expectedq->generalfeedbackformat = FORMAT_HTML; + $expectedq->defaultmark = 1; + $expectedq->length = 1; + $expectedq->usecase = 0; + $expectedq->answer = array('frog', '*'); + $expectedq->fraction = array(1, 0); + $expectedq->feedback = array( + 0 => array( + 'text' => 'A frog is an amphibian.', + 'format' => FORMAT_HTML, + ), + 1 => array( + 'text' => 'A frog is an amphibian.', + 'format' => FORMAT_HTML, + ) + ); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_essay() { + + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(1); + $questions = $importer->readquestions($xml); + $q = $questions[5]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'essay'; + $expectedq->name = 'How are you?'; + $expectedq->questiontext = array( + 'text' => 'How are you?', + 'format' => FORMAT_HTML + ); + $expectedq->questiontextformat = FORMAT_HTML; + $expectedq->generalfeedback = ''; + $expectedq->generalfeedbackformat = FORMAT_HTML; + $expectedq->defaultmark = 1; + $expectedq->length = 1; + $expectedq->responseformat = 'editor'; + $expectedq->responsefieldlines = 15; + $expectedq->attachments = 0; + $expectedq->graderinfo = array( + 'text' => 'Blackboard answer for essay questions will be imported as informations for graders.', + 'format' => FORMAT_HTML, + ); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } +} diff --git a/question/format/blackboard_six/tests/fixtures/sample_blackboard_pool.dat b/question/format/blackboard_six/tests/fixtures/sample_blackboard_pool.dat new file mode 100644 index 0000000000000..93bb583a3a447 --- /dev/null +++ b/question/format/blackboard_six/tests/fixtures/sample_blackboard_pool.dat @@ -0,0 +1,142 @@ + + + + <QUESTIONLIST> + <QUESTION id='q1' class='QUESTION_TRUEFALSE' points='1'/> + <QUESTION id='q7' class='QUESTION_MULTIPLECHOICE' points='1'/> + <QUESTION id='q8' class='QUESTION_MULTIPLEANSWER' points='1'/> + <QUESTION id='q39-44' class='QUESTION_MATCH' points='1'/> + <QUESTION id='q9' class='QUESTION_ESSAY' points='1'/> + <QUESTION id='q27' class='QUESTION_FILLINBLANK' points='1'/> + </QUESTIONLIST> + <QUESTION_TRUEFALSE id='q1'> + <BODY> + <TEXT><![CDATA[<span style="font-size:12pt">42 is the Absolute Answer to everything.</span>]]></TEXT> + <FLAGS> + <ISHTML value='true'/> + <ISNEWLINELITERAL value='false'/> + </FLAGS> + </BODY> + <ANSWER id='q1_a1'> + <TEXT>False</TEXT> + </ANSWER> + <ANSWER id='q1_a2'> + <TEXT>True</TEXT> + </ANSWER> + <GRADABLE> + <CORRECTANSWER answer_id='q1_a2'/> + <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT> + <FEEDBACK_WHEN_INCORRECT><![CDATA[42 is the Ultimate Answer.]]></FEEDBACK_WHEN_INCORRECT> + </GRADABLE> + </QUESTION_TRUEFALSE> + <QUESTION_MULTIPLECHOICE id='q7'> + <BODY> + <TEXT><![CDATA[<span style="font-size:12pt">What's between orange and green in the spectrum?</span>]]></TEXT> + <FLAGS> + <ISHTML value='true'/> + <ISNEWLINELITERAL value='false'/> + </FLAGS> + </BODY> + <ANSWER id='q7_a1' position='1'> + <TEXT><![CDATA[<span style="font-size:12pt">red</span>]]></TEXT> + </ANSWER> + <ANSWER id='q7_a2' position='2'> + <TEXT><![CDATA[<span style="font-size:12pt">yellow</span>]]></TEXT> + </ANSWER> + <ANSWER id='q7_a3' position='3'> + <TEXT><![CDATA[<span style="font-size:12pt">blue</span>]]></TEXT> + </ANSWER> + <GRADABLE> + <CORRECTANSWER answer_id='q7_a2'/> + <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT> + <FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow is between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT> + </GRADABLE> + </QUESTION_MULTIPLECHOICE> + <QUESTION_MULTIPLEANSWER id='q8'> + <BODY> + <TEXT><![CDATA[<span style="font-size:12pt">What's between orange and green in the spectrum?</span>]]></TEXT> + <FLAGS> + <ISHTML value='true'/> + <ISNEWLINELITERAL value='false'/> + </FLAGS> + </BODY> + <ANSWER id='q8_a1' position='1'> + <TEXT><![CDATA[<span style="font-size:12pt">yellow</span>]]></TEXT> + </ANSWER> + <ANSWER id='q8_a2' position='2'> + <TEXT><![CDATA[<span style="font-size:12pt">red</span>]]></TEXT> + </ANSWER> + <ANSWER id='q8_a3' position='3'> + <TEXT><![CDATA[<span style="font-size:12pt">off-beige</span>]]></TEXT> + </ANSWER> + <ANSWER id='q8_a4' position='4'> + <TEXT><![CDATA[<span style="font-size:12pt">blue</span>]]></TEXT> + </ANSWER> + <GRADABLE> + <CORRECTANSWER answer_id='q8_a1'/> + <CORRECTANSWER answer_id='q8_a3'/> + <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT> + <FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow and off-beige are between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT> + </GRADABLE> + </QUESTION_MULTIPLEANSWER> + <QUESTION_MATCH id='q39-44'> + <BODY> + <TEXT><![CDATA[<i>Classify the animals.</i>]]></TEXT> + <FLAGS> + <ISHTML value='true'/> + <ISNEWLINELITERAL value='false'/> + </FLAGS> + </BODY> + <ANSWER id='q39-44_a1' position='1'> + <TEXT><![CDATA[frog]]></TEXT> + </ANSWER> + <ANSWER id='q39-44_a2' position='2'> + <TEXT><![CDATA[cat]]></TEXT> + </ANSWER> + <ANSWER id='q39-44_a3' position='3'> + <TEXT><![CDATA[newt]]></TEXT> + </ANSWER> + <CHOICE id='q39-44_c1' position='1'> + <TEXT><![CDATA[mammal]]></TEXT> + </CHOICE> + <CHOICE id='q39-44_c2' position='2'> + <TEXT><![CDATA[insect]]></TEXT> + </CHOICE> + <CHOICE id='q39-44_c3' position='3'> + <TEXT><![CDATA[amphibian]]></TEXT> + </CHOICE> + <GRADABLE> + <CORRECTANSWER answer_id='q39-44_a1' choice_id='q39-44_c3'/> + <CORRECTANSWER answer_id='q39-44_a2' choice_id='q39-44_c1'/> + <CORRECTANSWER answer_id='q39-44_a3' choice_id='q39-44_c3'/> + </GRADABLE> + </QUESTION_MATCH> + <QUESTION_ESSAY id='q9'> + <BODY> + <TEXT><![CDATA[How are you?]]></TEXT> + <FLAGS> + <ISHTML value='true'/> + <ISNEWLINELITERAL value='false'/> + </FLAGS> + </BODY> + <ANSWER id='q9_a1'> + <TEXT><![CDATA[Blackboard answer for essay questions will be imported as informations for graders.]]></TEXT> + </ANSWER> + <GRADABLE> + </GRADABLE> + </QUESTION_ESSAY> + <QUESTION_FILLINBLANK id='q27'> + <BODY> + <TEXT><![CDATA[<span style="font-size:12pt">Name an amphibian: __________.</span>]]></TEXT> + <FLAGS> + <ISHTML value='true'/> + <ISNEWLINELITERAL value='false'/> + </FLAGS> + </BODY> + <ANSWER id='q27_a1' position='1'> + <TEXT>frog</TEXT> + </ANSWER> + <GRADABLE> + </GRADABLE> + </QUESTION_FILLINBLANK> +</POOL> diff --git a/question/format/blackboard_six/tests/fixtures/sample_blackboard_qti.dat b/question/format/blackboard_six/tests/fixtures/sample_blackboard_qti.dat new file mode 100644 index 0000000000000..4705c2b856730 --- /dev/null +++ b/question/format/blackboard_six/tests/fixtures/sample_blackboard_qti.dat @@ -0,0 +1,1058 @@ +<?xml version="1.0" encoding="UTF-8"?> +<questestinterop> + <assessment title="sample_blackboard_six"> + <section> + <item maxattempts="0"> + <itemmetadata> + <bbmd_asi_object_id>E03EE0034702442C9306629CBF618049</bbmd_asi_object_id> + <bbmd_asitype>Item</bbmd_asitype> + <bbmd_assessmenttype>Pool</bbmd_assessmenttype> + <bbmd_sectiontype>Subsection</bbmd_sectiontype> + <bbmd_questiontype>True/False</bbmd_questiontype> + <bbmd_is_from_cartridge>false</bbmd_is_from_cartridge> + <qmd_absolutescore>0.0,1.0</qmd_absolutescore> + <qmd_absolutescore_min>0.0</qmd_absolutescore_min> + <qmd_absolutescore_max>1.0</qmd_absolutescore_max> + <qmd_assessmenttype>Proprietary</qmd_assessmenttype> + <qmd_itemtype>Logical Identifier</qmd_itemtype> + <qmd_levelofdifficulty>School</qmd_levelofdifficulty> + <qmd_maximumscore>0.0</qmd_maximumscore> + <qmd_numberofitems>0</qmd_numberofitems> + <qmd_renderingtype>Proprietary</qmd_renderingtype> + <qmd_responsetype>Single</qmd_responsetype> + <qmd_scoretype>Absolute</qmd_scoretype> + <qmd_status>Normal</qmd_status> + <qmd_timelimit>0</qmd_timelimit> + <qmd_weighting>0.0</qmd_weighting> + <qmd_typeofsolution>Complete</qmd_typeofsolution> + </itemmetadata> + <presentation> + <flow class="Block"> + <flow class="QUESTION_BLOCK"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><span style="font-size:12pt">42 is the Absolute Answer to everything.</span></mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + </flow> + <flow class="RESPONSE_BLOCK"> + <response_lid ident="response" rcardinality="Single" rtiming="No"> + <render_choice maxnumber="0" minnumber="0" shuffle="No"> + <flow_label class="Block"> + <response_label ident="true" rarea="Ellipse" rrange="Exact" shuffle="Yes"> + <flow_mat class="Block"> + <material> + <mattext charset="us-ascii" texttype="text/plain" xml:space="default">true</mattext> + </material> + </flow_mat> + </response_label> + <response_label ident="false" rarea="Ellipse" rrange="Exact" shuffle="Yes"> + <flow_mat class="Block"> + <material> + <mattext charset="us-ascii" texttype="text/plain" xml:space="default">false</mattext> + </material> + </flow_mat> + </response_label> + </flow_label> + </render_choice> + </response_lid> + </flow> + </flow> + </presentation> + <resprocessing scoremodel="SumOfScores"> + <outcomes> + <decvar defaultval="0.0" maxvalue="1.0" minvalue="0.0" varname="SCORE" vartype="Decimal"/> + </outcomes> + <respcondition title="correct"> + <conditionvar> + <varequal case="No" respident="response">false</varequal> + </conditionvar> + <setvar action="Set" variablename="SCORE">SCORE.max</setvar> + <displayfeedback feedbacktype="Response" linkrefid="correct"/> + </respcondition> + <respcondition title="incorrect"> + <conditionvar> + <other/> + </conditionvar> + <setvar action="Set" variablename="SCORE">0.0</setvar> + <displayfeedback feedbacktype="Response" linkrefid="incorrect"/> + </respcondition> + </resprocessing> + <itemfeedback ident="correct" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">You gave the right answer.</mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + <itemfeedback ident="incorrect" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">42 is the <b>Ultimate</b> Answer.</mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + </item> + <item maxattempts="0"> + <itemmetadata> + <bbmd_asi_object_id>C74698725FFD4F85A692662108608D53</bbmd_asi_object_id> + <bbmd_asitype>Item</bbmd_asitype> + <bbmd_assessmenttype>Pool</bbmd_assessmenttype> + <bbmd_sectiontype>Subsection</bbmd_sectiontype> + <bbmd_questiontype>Multiple Choice</bbmd_questiontype> + <bbmd_is_from_cartridge>false</bbmd_is_from_cartridge> + <qmd_absolutescore>0.0,1.0</qmd_absolutescore> + <qmd_absolutescore_min>0.0</qmd_absolutescore_min> + <qmd_absolutescore_max>1.0</qmd_absolutescore_max> + <qmd_assessmenttype>Proprietary</qmd_assessmenttype> + <qmd_itemtype>Logical Identifier</qmd_itemtype> + <qmd_levelofdifficulty>School</qmd_levelofdifficulty> + <qmd_maximumscore>0.0</qmd_maximumscore> + <qmd_numberofitems>0</qmd_numberofitems> + <qmd_renderingtype>Proprietary</qmd_renderingtype> + <qmd_responsetype>Single</qmd_responsetype> + <qmd_scoretype>Absolute</qmd_scoretype> + <qmd_status>Normal</qmd_status> + <qmd_timelimit>0</qmd_timelimit> + <qmd_weighting>0.0</qmd_weighting> + <qmd_typeofsolution>Complete</qmd_typeofsolution> + </itemmetadata> + <presentation> + <flow class="Block"> + <flow class="QUESTION_BLOCK"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><span style="font-size:12pt">What's between orange and green in the spectrum?</span></mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + </flow> + <flow class="RESPONSE_BLOCK"> + <response_lid ident="response" rcardinality="Single" rtiming="No"> + <render_choice maxnumber="0" minnumber="0" shuffle="Yes"> + <flow_label class="Block"> + <response_label ident="7C2A0246CE8D46599FC0120BAE9FC92D" rarea="Ellipse" rrange="Exact" shuffle="Yes"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><span style="font-size:12pt">red</span></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </response_label> + </flow_label> + <flow_label class="Block"> + <response_label ident="2CBE1E044DE54F8395BDE7877A57837A" rarea="Ellipse" rrange="Exact" shuffle="Yes"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><span style="font-size:12pt">yellow</span></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </response_label> + </flow_label> + <flow_label class="Block"> + <response_label ident="67A8748A0883467FB45328E922C31D29" rarea="Ellipse" rrange="Exact" shuffle="Yes"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><span style="font-size:12pt">blue</span></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </response_label> + </flow_label> + </render_choice> + </response_lid> + </flow> + </flow> + </presentation> + <resprocessing scoremodel="SumOfScores"> + <outcomes> + <decvar defaultval="0.0" maxvalue="1.0" minvalue="0.0" varname="SCORE" vartype="Decimal"/> + </outcomes> + <respcondition title="correct"> + <conditionvar> + <varequal case="No" respident="response">2CBE1E044DE54F8395BDE7877A57837A</varequal> + </conditionvar> + <setvar action="Set" variablename="SCORE">SCORE.max</setvar> + <displayfeedback feedbacktype="Response" linkrefid="correct"/> + </respcondition> + <respcondition title="incorrect"> + <conditionvar> + <other/> + </conditionvar> + <setvar action="Set" variablename="SCORE">0.0</setvar> + <displayfeedback feedbacktype="Response" linkrefid="incorrect"/> + </respcondition> + </resprocessing> + <itemfeedback ident="correct" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + <itemfeedback ident="incorrect" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + <itemfeedback ident="7C2A0246CE8D46599FC0120BAE9FC92D" view="All"> + <solution feedbackstyle="Complete" view="All"> + <solutionmaterial> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">Red is not between orange and green in the spectrum but yellow is.</mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </solutionmaterial> + </solution> + </itemfeedback> + <itemfeedback ident="2CBE1E044DE54F8395BDE7877A57837A" view="All"> + <solution feedbackstyle="Complete" view="All"> + <solutionmaterial> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">You gave the right answer.</mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </solutionmaterial> + </solution> + </itemfeedback> + <itemfeedback ident="67A8748A0883467FB45328E922C31D29" view="All"> + <solution feedbackstyle="Complete" view="All"> + <solutionmaterial> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">Blue is not between orange and green in the spectrum but yellow is.</mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </solutionmaterial> + </solution> + </itemfeedback> + </item> + <item maxattempts="0"> + <itemmetadata> + <bbmd_asi_object_id>C18C56154AA04D56A4AE2FE430F4F49D</bbmd_asi_object_id> + <bbmd_asitype>Item</bbmd_asitype> + <bbmd_assessmenttype>Pool</bbmd_assessmenttype> + <bbmd_sectiontype>Subsection</bbmd_sectiontype> + <bbmd_questiontype>Multiple Answer</bbmd_questiontype> + <bbmd_is_from_cartridge>false</bbmd_is_from_cartridge> + <qmd_absolutescore>0.0,1.0</qmd_absolutescore> + <qmd_absolutescore_min>0.0</qmd_absolutescore_min> + <qmd_absolutescore_max>1.0</qmd_absolutescore_max> + <qmd_assessmenttype>Proprietary</qmd_assessmenttype> + <qmd_itemtype>Logical Identifier</qmd_itemtype> + <qmd_levelofdifficulty>School</qmd_levelofdifficulty> + <qmd_maximumscore>0.0</qmd_maximumscore> + <qmd_numberofitems>0</qmd_numberofitems> + <qmd_renderingtype>Proprietary</qmd_renderingtype> + <qmd_responsetype>Single</qmd_responsetype> + <qmd_scoretype>Absolute</qmd_scoretype> + <qmd_status>Normal</qmd_status> + <qmd_timelimit>0</qmd_timelimit> + <qmd_weighting>0.0</qmd_weighting> + <qmd_typeofsolution>Complete</qmd_typeofsolution> + </itemmetadata> + <presentation> + <flow class="Block"> + <flow class="QUESTION_BLOCK"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><i>What's between orange and green in the spectrum?</i></mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + </flow> + <flow class="RESPONSE_BLOCK"> + <response_lid ident="response" rcardinality="Multiple" rtiming="No"> + <render_choice maxnumber="0" minnumber="0" shuffle="Yes"> + <flow_label class="Block"> + <response_label ident="76CA08C366984445AC94B0244D1DBF4A" rarea="Ellipse" rrange="Exact" shuffle="Yes"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><span style="font-size:12pt">yellow</span></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </response_label> + </flow_label> + <flow_label class="Block"> + <response_label ident="FEC2A9886C8B498787A573C9181C9698" rarea="Ellipse" rrange="Exact" shuffle="Yes"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><span style="font-size:12pt">red</span></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </response_label> + </flow_label> + <flow_label class="Block"> + <response_label ident="7F66D24D2CAA472EA728773D46706DF3" rarea="Ellipse" rrange="Exact" shuffle="Yes"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><span style="font-size:12pt">off-beige</span></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </response_label> + </flow_label> + <flow_label class="Block"> + <response_label ident="547B16C1D788446396618EDD0A41D623" rarea="Ellipse" rrange="Exact" shuffle="Yes"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><span style="font-size:12pt">blue</span></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </response_label> + </flow_label> + </render_choice> + </response_lid> + </flow> + </flow> + </presentation> + <resprocessing scoremodel="SumOfScores"> + <outcomes> + <decvar defaultval="0.0" maxvalue="1.0" minvalue="0.0" varname="SCORE" vartype="Decimal"/> + </outcomes> + <respcondition title="correct"> + <conditionvar> + <and> + <varequal case="No" respident="response">76CA08C366984445AC94B0244D1DBF4A</varequal> + <not> + <varequal case="No" respident="response">FEC2A9886C8B498787A573C9181C9698</varequal> + </not> + <varequal case="No" respident="response">7F66D24D2CAA472EA728773D46706DF3</varequal> + <not> + <varequal case="No" respident="response">547B16C1D788446396618EDD0A41D623</varequal> + </not> + </and> + </conditionvar> + <setvar action="Set" variablename="SCORE">SCORE.max</setvar> + <displayfeedback feedbacktype="Response" linkrefid="correct"/> + </respcondition> + <respcondition title="incorrect"> + <conditionvar> + <other/> + </conditionvar> + <setvar action="Set" variablename="SCORE">0.0</setvar> + <displayfeedback feedbacktype="Response" linkrefid="incorrect"/> + </respcondition> + </resprocessing> + <itemfeedback ident="correct" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + <itemfeedback ident="incorrect" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + </item> + <item maxattempts="0"> + <itemmetadata> + <bbmd_asi_object_id>9C5DBA6A142A4B5887C61D333CFFEDA9</bbmd_asi_object_id> + <bbmd_asitype>Item</bbmd_asitype> + <bbmd_assessmenttype>Pool</bbmd_assessmenttype> + <bbmd_sectiontype>Subsection</bbmd_sectiontype> + <bbmd_questiontype>Matching</bbmd_questiontype> + <bbmd_is_from_cartridge>false</bbmd_is_from_cartridge> + <qmd_absolutescore>0.0,3</qmd_absolutescore> + <qmd_absolutescore_min>0.0</qmd_absolutescore_min> + <qmd_absolutescore_max>3</qmd_absolutescore_max> + <qmd_assessmenttype>Proprietary</qmd_assessmenttype> + <qmd_itemtype>Logical Identifier</qmd_itemtype> + <qmd_levelofdifficulty>School</qmd_levelofdifficulty> + <qmd_maximumscore>0.0</qmd_maximumscore> + <qmd_numberofitems>0</qmd_numberofitems> + <qmd_renderingtype>Proprietary</qmd_renderingtype> + <qmd_responsetype>Single</qmd_responsetype> + <qmd_scoretype>Absolute</qmd_scoretype> + <qmd_status>Normal</qmd_status> + <qmd_timelimit>0</qmd_timelimit> + <qmd_weighting>0.0</qmd_weighting> + <qmd_typeofsolution>Complete</qmd_typeofsolution> + </itemmetadata> + <presentation> + <flow class="Block"> + <flow class="QUESTION_BLOCK"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">Classify the animals.</mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + </flow> + <flow class="RESPONSE_BLOCK"> + <flow class="Block"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">cat</mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + <response_lid ident="6D3235200B3F43DFA8FA13E2B31BB40B" rcardinality="Single" rtiming="No"> + <render_choice maxnumber="0" minnumber="0" shuffle="Yes"> + <flow_label class="Block"> + <response_label ident="2F591AA030B240EF869FD56392FC41BC" rarea="Ellipse" rrange="Exact" shuffle="Yes"/> + <response_label ident="D75FEB705DCE41D59106659A2F94D819" rarea="Ellipse" rrange="Exact" shuffle="Yes"/> + <response_label ident="207B18A11C4B42BF87882A4BAF3CC805" rarea="Ellipse" rrange="Exact" shuffle="Yes"/> + </flow_label> + </render_choice> + </response_lid> + </flow> + <flow class="Block"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">frog</mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + <response_lid ident="0933892218204F5AB561E62A27701447" rcardinality="Single" rtiming="No"> + <render_choice maxnumber="0" minnumber="0" shuffle="Yes"> + <flow_label class="Block"> + <response_label ident="2C1FB19B5F9A4F7A85E798B9C46B8BF8" rarea="Ellipse" rrange="Exact" shuffle="Yes"/> + <response_label ident="0EEF502254D2496FB21FFD82B0A7F2B9" rarea="Ellipse" rrange="Exact" shuffle="Yes"/> + <response_label ident="8A9B69A93B0943AFAB890702199AB290" rarea="Ellipse" rrange="Exact" shuffle="Yes"/> + </flow_label> + </render_choice> + </response_lid> + </flow> + <flow class="Block"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">newt</mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + <response_lid ident="80F56B540A44490B8E94EC71C4584722" rcardinality="Single" rtiming="No"> + <render_choice maxnumber="0" minnumber="0" shuffle="Yes"> + <flow_label class="Block"> + <response_label ident="1D066C2DAEF349EB8E7845B339B0A4A9" rarea="Ellipse" rrange="Exact" shuffle="Yes"/> + <response_label ident="F2FB88DFE0D04DBEBD42B961728CA022" rarea="Ellipse" rrange="Exact" shuffle="Yes"/> + <response_label ident="4D1F5B3DB0EB4C41A0012625750DF86C" rarea="Ellipse" rrange="Exact" shuffle="Yes"/> + </flow_label> + </render_choice> + </response_lid> + </flow> + </flow> + <flow class="RIGHT_MATCH_BLOCK"> + <flow class="Block"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">insect</mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + </flow> + <flow class="Block"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">mammal</mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + </flow> + <flow class="Block"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">amphibian</mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + </flow> + </flow> + </flow> + </presentation> + <resprocessing scoremodel="SumOfScores"> + <outcomes> + <decvar defaultval="0.0" maxvalue="3.0" minvalue="0.0" varname="SCORE" vartype="Decimal"/> + </outcomes> + <respcondition> + <conditionvar> + <varequal case="No" respident="6D3235200B3F43DFA8FA13E2B31BB40B">D75FEB705DCE41D59106659A2F94D819</varequal> + </conditionvar> + <displayfeedback feedbacktype="Response" linkrefid="correct"/> + </respcondition> + <respcondition> + <conditionvar> + <varequal case="No" respident="0933892218204F5AB561E62A27701447">8A9B69A93B0943AFAB890702199AB290</varequal> + </conditionvar> + <displayfeedback feedbacktype="Response" linkrefid="correct"/> + </respcondition> + <respcondition> + <conditionvar> + <varequal case="No" respident="80F56B540A44490B8E94EC71C4584722">4D1F5B3DB0EB4C41A0012625750DF86C</varequal> + </conditionvar> + <displayfeedback feedbacktype="Response" linkrefid="correct"/> + </respcondition> + <respcondition title="incorrect"> + <conditionvar> + <other/> + </conditionvar> + <setvar action="Set" variablename="SCORE">0.0</setvar> + <displayfeedback feedbacktype="Response" linkrefid="incorrect"/> + </respcondition> + </resprocessing> + <itemfeedback ident="correct" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + <itemfeedback ident="incorrect" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + </item> + <item maxattempts="0"> + <itemmetadata> + <bbmd_asi_object_id>DD76E663D4244C598FC91CFC433F6D5B</bbmd_asi_object_id> + <bbmd_asitype>Item</bbmd_asitype> + <bbmd_assessmenttype>Pool</bbmd_assessmenttype> + <bbmd_sectiontype>Subsection</bbmd_sectiontype> + <bbmd_questiontype>Fill in the Blank</bbmd_questiontype> + <bbmd_is_from_cartridge>false</bbmd_is_from_cartridge> + <qmd_absolutescore>0.0,1.0</qmd_absolutescore> + <qmd_absolutescore_min>0.0</qmd_absolutescore_min> + <qmd_absolutescore_max>1.0</qmd_absolutescore_max> + <qmd_assessmenttype>Proprietary</qmd_assessmenttype> + <qmd_itemtype>Logical Identifier</qmd_itemtype> + <qmd_levelofdifficulty>School</qmd_levelofdifficulty> + <qmd_maximumscore>0.0</qmd_maximumscore> + <qmd_numberofitems>0</qmd_numberofitems> + <qmd_renderingtype>Proprietary</qmd_renderingtype> + <qmd_responsetype>Single</qmd_responsetype> + <qmd_scoretype>Absolute</qmd_scoretype> + <qmd_status>Normal</qmd_status> + <qmd_timelimit>0</qmd_timelimit> + <qmd_weighting>0.0</qmd_weighting> + <qmd_typeofsolution>Complete</qmd_typeofsolution> + </itemmetadata> + <presentation> + <flow class="Block"> + <flow class="QUESTION_BLOCK"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><span style="font-size:12pt">Name an amphibian&#58; __________.</span></mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + </flow> + <flow class="RESPONSE_BLOCK"> + <response_str ident="response" rcardinality="Single" rtiming="No"> + <render_fib charset="us-ascii" columns="127" encoding="UTF_8" fibtype="String" maxchars="0" maxnumber="0" minnumber="0" prompt="Box" rows="1"/> + </response_str> + </flow> + </flow> + </presentation> + <resprocessing scoremodel="SumOfScores"> + <outcomes> + <decvar defaultval="0.0" maxvalue="1.0" minvalue="0.0" varname="SCORE" vartype="Decimal"/> + </outcomes> + <respcondition title="1CE934E53BDB437B8FD315E68063DA47"> + <conditionvar> + <varequal case="No" respident="response">frog</varequal> + </conditionvar> + <displayfeedback feedbacktype="Response" linkrefid="correct"/> + <displayfeedback feedbacktype="Response" linkrefid="1CE934E53BDB437B8FD315E68063DA47"/> + </respcondition> + <respcondition title="incorrect"> + <conditionvar> + <other/> + </conditionvar> + <setvar action="Set" variablename="SCORE">0.0</setvar> + <displayfeedback feedbacktype="Response" linkrefid="incorrect"/> + </respcondition> + </resprocessing> + <itemfeedback ident="correct" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">A frog is an amphibian.</mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + <itemfeedback ident="incorrect" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">A frog is an amphibian.</mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + <itemfeedback ident="1CE934E53BDB437B8FD315E68063DA47" view="All"> + <solution feedbackstyle="Complete" view="All"> + <solutionmaterial> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">A frog is an amphibian.</mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </solutionmaterial> + </solution> + </itemfeedback> + </item> + <item maxattempts="0"> + <itemmetadata> + <bbmd_asi_object_id>39970AD8D5AE425A82338E17D42B7845</bbmd_asi_object_id> + <bbmd_asitype>Item</bbmd_asitype> + <bbmd_assessmenttype>Pool</bbmd_assessmenttype> + <bbmd_sectiontype>Subsection</bbmd_sectiontype> + <bbmd_questiontype>Essay</bbmd_questiontype> + <bbmd_is_from_cartridge>false</bbmd_is_from_cartridge> + <qmd_absolutescore>0.0,1.0</qmd_absolutescore> + <qmd_absolutescore_min>0.0</qmd_absolutescore_min> + <qmd_absolutescore_max>1.0</qmd_absolutescore_max> + <qmd_assessmenttype>Proprietary</qmd_assessmenttype> + <qmd_itemtype>Logical Identifier</qmd_itemtype> + <qmd_levelofdifficulty>School</qmd_levelofdifficulty> + <qmd_maximumscore>0.0</qmd_maximumscore> + <qmd_numberofitems>0</qmd_numberofitems> + <qmd_renderingtype>Proprietary</qmd_renderingtype> + <qmd_responsetype>Single</qmd_responsetype> + <qmd_scoretype>Absolute</qmd_scoretype> + <qmd_status>Normal</qmd_status> + <qmd_timelimit>0</qmd_timelimit> + <qmd_weighting>0.0</qmd_weighting> + <qmd_typeofsolution>Complete</qmd_typeofsolution> + </itemmetadata> + <presentation> + <flow class="Block"> + <flow class="QUESTION_BLOCK"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">How are you?</mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + </flow> + <flow class="RESPONSE_BLOCK"> + <response_str ident="response" rcardinality="Single" rtiming="No"> + <render_fib charset="us-ascii" columns="127" encoding="UTF_8" fibtype="String" maxchars="0" maxnumber="0" minnumber="0" prompt="Box" rows="8"/> + </response_str> + </flow> + </flow> + </presentation> + <resprocessing scoremodel="SumOfScores"> + <outcomes> + <decvar defaultval="0.0" maxvalue="1.0" minvalue="0.0" varname="SCORE" vartype="Decimal"/> + </outcomes> + <respcondition title="correct"> + <conditionvar/> + <setvar action="Set" variablename="SCORE">SCORE.max</setvar> + <displayfeedback feedbacktype="Response" linkrefid="correct"/> + </respcondition> + <respcondition title="incorrect"> + <conditionvar> + <other/> + </conditionvar> + <setvar action="Set" variablename="SCORE">0.0</setvar> + <displayfeedback feedbacktype="Response" linkrefid="incorrect"/> + </respcondition> + </resprocessing> + <itemfeedback ident="correct" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + <itemfeedback ident="incorrect" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + <itemfeedback ident="solution" view="All"> + <solution feedbackstyle="Complete" view="All"> + <solutionmaterial> + <flow_mat class="Block"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">Blackboard answer for essay questions will be imported as informations for graders.</mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + </solutionmaterial> + </solution> + </itemfeedback> + </item> + </section> + </assessment> +</questestinterop> diff --git a/question/format/blackboard_six/version.php b/question/format/blackboard_six/version.php index c037b03cf0729..c413d77ef4218 100644 --- a/question/format/blackboard_six/version.php +++ b/question/format/blackboard_six/version.php @@ -15,10 +15,9 @@ // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** - * Version information for the calculated question type. + * Version information for the blackboard_six question import format. * - * @package qformat - * @subpackage blackboard_six + * @package qformat_blackboard_six * @copyright 2011 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ diff --git a/question/format/examview/format.php b/question/format/examview/format.php index 7cea482d0343a..9cb9bb5c55309 100644 --- a/question/format/examview/format.php +++ b/question/format/examview/format.php @@ -61,28 +61,6 @@ public function mime_type() { return 'application/xml'; } - /** - * Some softwares put entities in exported files. - * This method try to clean up known problems. - * @param string str string to correct - * @return string the corrected string - */ - public function cleaninput($str) { - - $html_code_list = array( - "'" => "'", - "’" => "'", - "“" => "\"", - "”" => "\"", - "–" => "-", - "—" => "-", - ); - $str = strtr($str, $html_code_list); - // Use textlib entities_to_utf8 function to convert only numerical entities. - $str = textlib::entities_to_utf8( $str, false); - return $str; - } - /** * unxmlise reconstructs part of the xml data structure in order * to identify the actual data therein @@ -109,19 +87,6 @@ protected function unxmlise( $xml ) { return $text; } - protected function add_blank_combined_feedback($question) { - $question->correctfeedback['text'] = ''; - $question->correctfeedback['format'] = $question->questiontextformat; - $question->correctfeedback['files'] = array(); - $question->partiallycorrectfeedback['text'] = ''; - $question->partiallycorrectfeedback['format'] = $question->questiontextformat; - $question->partiallycorrectfeedback['files'] = array(); - $question->incorrectfeedback['text'] = ''; - $question->incorrectfeedback['format'] = $question->questiontextformat; - $question->incorrectfeedback['files'] = array(); - return $question; - } - public function parse_matching_groups($matching_groups) { if (empty($matching_groups)) { return; diff --git a/question/format/gift/format.php b/question/format/gift/format.php index 06bcebc564a17..1cb0e25157bfc 100644 --- a/question/format/gift/format.php +++ b/question/format/gift/format.php @@ -537,19 +537,6 @@ public function readquestion($lines) { } } - protected function add_blank_combined_feedback($question) { - $question->correctfeedback['text'] = ''; - $question->correctfeedback['format'] = $question->questiontextformat; - $question->correctfeedback['files'] = array(); - $question->partiallycorrectfeedback['text'] = ''; - $question->partiallycorrectfeedback['format'] = $question->questiontextformat; - $question->partiallycorrectfeedback['files'] = array(); - $question->incorrectfeedback['text'] = ''; - $question->incorrectfeedback['format'] = $question->questiontextformat; - $question->incorrectfeedback['files'] = array(); - return $question; - } - protected function repchar($text, $notused = 0) { // Escapes 'reserved' characters # = ~ {) : // Removes new lines From 116ad39b7ac217c8f9c8bff4b5a4c8a8f83b5b58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= <commits@skodak.org> Date: Sat, 18 Aug 2012 15:21:37 +0200 Subject: [PATCH 28/90] MDL-34877 add tinymce subplugin setting support Includes migration of spell related settings to spellchecker plugin. --- lib/editor/tinymce/adminlib.php | 9 ++++ lib/editor/tinymce/classes/plugin.php | 52 +++++++++++++++++++ lib/editor/tinymce/lang/en/editor_tinymce.php | 1 + .../tinymce/plugins/spellchecker/config.php | 7 ++- .../plugins/spellchecker/db/install.php | 32 ++++++++++++ .../plugins/spellchecker/db/upgrade.php | 40 ++++++++++++++ .../plugins/spellchecker/db/upgradelib.php | 41 +++++++++++++++ .../tinymce/plugins/spellchecker/lib.php | 3 +- .../tinymce/plugins/spellchecker/settings.php | 38 ++++++++++++++ .../tinymce/plugins/spellchecker/version.php | 2 +- lib/editor/tinymce/settings.php | 33 ++++++++---- 11 files changed, 243 insertions(+), 15 deletions(-) create mode 100644 lib/editor/tinymce/plugins/spellchecker/db/install.php create mode 100644 lib/editor/tinymce/plugins/spellchecker/db/upgrade.php create mode 100644 lib/editor/tinymce/plugins/spellchecker/db/upgradelib.php create mode 100644 lib/editor/tinymce/plugins/spellchecker/settings.php diff --git a/lib/editor/tinymce/adminlib.php b/lib/editor/tinymce/adminlib.php index 8e599d63b91c3..4d2dba2887659 100644 --- a/lib/editor/tinymce/adminlib.php +++ b/lib/editor/tinymce/adminlib.php @@ -34,4 +34,13 @@ class plugininfo_tinymce extends plugininfo_base { public function get_uninstall_url() { return new moodle_url('/lib/editor/tinymce/subplugins.php', array('delete' => $this->name, 'sesskey' => sesskey())); } + + public function get_settings_url() { + global $CFG; + if (file_exists("$CFG->dirroot/lib/editor/tinymce/plugins/$this->name/settings.php")) { + return new moodle_url('/admin/settings.php', array('section'=>'tinymce'.$this->name.'settings')); + } else { + return null; + } + } } diff --git a/lib/editor/tinymce/classes/plugin.php b/lib/editor/tinymce/classes/plugin.php index 9179031e46d11..8dca9b6a00f59 100644 --- a/lib/editor/tinymce/classes/plugin.php +++ b/lib/editor/tinymce/classes/plugin.php @@ -37,6 +37,9 @@ abstract class editor_tinymce_plugin { /** @var string Plugin folder */ protected $plugin; + /** @var array Plugin settings */ + protected $config = null; + /** * @param string $plugin Name of folder */ @@ -44,6 +47,55 @@ public function __construct($plugin) { $this->plugin = $plugin; } + /** + * Makes sure config is loaded and cached. + * @return void + */ + protected function load_config() { + if (!isset($this->config)) { + $name = $this->get_name(); + $this->config = get_config("tinymce_$name"); + } + } + + /** + * Returns plugin config value. + * @param string $name + * @param string $default value if config does not exist yet + * @return string value or default + */ + public function get_config($name, $default = null) { + $this->load_config(); + return isset($this->config->$name) ? $this->config->$name : $default; + } + + /** + * Sets plugin config value. + * @param string $name name of config + * @param string $value string config value, null means delete + * @return string value + */ + public function set_config($name, $value) { + $pluginname = $this->get_name(); + $this->load_config(); + if ($value === null) { + unset($this->config->$name); + } else { + $this->config->$name = $value; + } + set_config($name, $value, "tinymce_$pluginname"); + } + + /** + * Returns name of this tinymce plugin. + * @return string + */ + public function get_name() { + // All class names start with "tinymce_". + $words = explode('_', get_class($this), 2); + return $words[1]; + } + /** * Adjusts TinyMCE init parameters for this plugin. * diff --git a/lib/editor/tinymce/lang/en/editor_tinymce.php b/lib/editor/tinymce/lang/en/editor_tinymce.php index c52236fa1539f..1563b2ee746a2 100644 --- a/lib/editor/tinymce/lang/en/editor_tinymce.php +++ b/lib/editor/tinymce/lang/en/editor_tinymce.php @@ -29,6 +29,7 @@ $string['fontselectlist'] = 'Available fonts list'; $string['media_dlg:filename'] = 'Filename'; $string['pluginname'] = 'TinyMCE HTML editor'; +$string['settings'] = 'General settings'; $string['subplugindeleteconfirm'] = 'You are about to completely delete TinyMCE subplugin \'{$a}\'. This will completely delete everything in the database associated with this subplugin. Are you SURE you want to continue?'; diff --git a/lib/editor/tinymce/plugins/spellchecker/config.php b/lib/editor/tinymce/plugins/spellchecker/config.php index 6e36f41338407..4f1ac23e6e47b 100644 --- a/lib/editor/tinymce/plugins/spellchecker/config.php +++ b/lib/editor/tinymce/plugins/spellchecker/config.php @@ -27,8 +27,11 @@ @error_reporting(E_ALL ^ E_NOTICE); // Hide notices even if Moodle is configured to show them. // General settings -$config['general.engine'] = get_config('editor_tinymce', 'spellengine') ? - get_config('editor_tinymce', 'spellengine') : 'GoogleSpell'; +$engine = get_config('tinymce_spellchecker', 'spellengine'); +if (!$engine) { + $engine = 'GoogleSpell'; +} +$config['general.engine'] = $engine; // GoogleSpell settings $config['GoogleSpell.proxyhost'] = isset($CFG->proxyhost) ? $CFG->proxyhost : ''; diff --git a/lib/editor/tinymce/plugins/spellchecker/db/install.php b/lib/editor/tinymce/plugins/spellchecker/db/install.php new file mode 100644 index 0000000000000..dc659b5fdcc4e --- /dev/null +++ b/lib/editor/tinymce/plugins/spellchecker/db/install.php @@ -0,0 +1,32 @@ +<?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/>. + +/** + * Spellchecker post install script. + * + * @package tinymce_spellchecker + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +function xmldb_tinymce_spellchecker_install() { + global $CFG, $DB; + require_once(__DIR__.'/upgradelib.php'); + + tinymce_spellchecker_migrate_settings(); +} diff --git a/lib/editor/tinymce/plugins/spellchecker/db/upgrade.php b/lib/editor/tinymce/plugins/spellchecker/db/upgrade.php new file mode 100644 index 0000000000000..4012b1f43507e --- /dev/null +++ b/lib/editor/tinymce/plugins/spellchecker/db/upgrade.php @@ -0,0 +1,40 @@ +<?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/>. + +/** + * Spellchecker upgrade script. + * + * @package tinymce_spellchecker + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +function xmldb_tinymce_spellchecker_upgrade($oldversion) { + global $CFG, $DB; + require_once(__DIR__.'/upgradelib.php'); + + $dbman = $DB->get_manager(); + + if ($oldversion < 2012051800) { + tinymce_spellchecker_migrate_settings(); + upgrade_plugin_savepoint(true, 2012051800, 'tinymce', 'spellchecker'); + } + + + return true; +} diff --git a/lib/editor/tinymce/plugins/spellchecker/db/upgradelib.php b/lib/editor/tinymce/plugins/spellchecker/db/upgradelib.php new file mode 100644 index 0000000000000..0a99c1dc5b395 --- /dev/null +++ b/lib/editor/tinymce/plugins/spellchecker/db/upgradelib.php @@ -0,0 +1,41 @@ +<?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/>. + +/** + * Spellchecker upgrade script. + * + * @package tinymce_spellchecker + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Migrate spell related settings from tinymce. + */ +function tinymce_spellchecker_migrate_settings() { + $engine = get_config('editor_tinymce', 'spellengine'); + if ($engine !== false) { + set_config('spellengine', $engine, 'tinymce_spellchecker'); + unset_config('spellengine', 'editor_tinymce'); + } + $list = get_config('editor_tinymce', 'spelllanguagelist'); + if ($list !== false) { + set_config('spelllanguagelist', $list, 'tinymce_spellchecker'); + unset_config('spelllanguagelist', 'editor_tinymce'); + } +} diff --git a/lib/editor/tinymce/plugins/spellchecker/lib.php b/lib/editor/tinymce/plugins/spellchecker/lib.php index 2755b438f2e98..2a4cfde6788d7 100644 --- a/lib/editor/tinymce/plugins/spellchecker/lib.php +++ b/lib/editor/tinymce/plugins/spellchecker/lib.php @@ -30,8 +30,7 @@ protected function update_init_params(array &$params, context $context, global $CFG; // Check at least one language is supported. - $config = $params['moodle_config']; - $spelllanguagelist = empty($config->spelllanguagelist) ? '' : $config->spelllanguagelist; + $spelllanguagelist = $this->get_config('spelllanguagelist', ''); if ($spelllanguagelist !== '') { // Add button after code button in advancedbuttons3. $added = $this->add_button_after($params, 3, 'spellchecker', 'code', false); diff --git a/lib/editor/tinymce/plugins/spellchecker/settings.php b/lib/editor/tinymce/plugins/spellchecker/settings.php new file mode 100644 index 0000000000000..c4081e3a2ea64 --- /dev/null +++ b/lib/editor/tinymce/plugins/spellchecker/settings.php @@ -0,0 +1,38 @@ +<?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/>. + +/** + * Spellchecker settings. + * + * @package tinymce_spellchecker + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +if ($ADMIN->fulltree) { + $options = array( + 'PSpell'=>'PSpell', + 'GoogleSpell'=>'Google Spell', + 'PSpellShell'=>'PSpellShell'); + $settings->add(new admin_setting_configselect('tinymce_spellchecker/spellengine', + get_string('spellengine', 'admin'), '', 'GoogleSpell', $options)); + $settings->add(new admin_setting_configtextarea('tinymce_spellchecker/spelllanguagelist', + get_string('spelllanguagelist', 'admin'), '', + '+English=en,Danish=da,Dutch=nl,Finnish=fi,French=fr,German=de,Italian=it,Polish=pl,' . + 'Portuguese=pt,Spanish=es,Swedish=sv', PARAM_RAW)); +} diff --git a/lib/editor/tinymce/plugins/spellchecker/version.php b/lib/editor/tinymce/plugins/spellchecker/version.php index 83aac6d199e61..0ba52d01eed1b 100644 --- a/lib/editor/tinymce/plugins/spellchecker/version.php +++ b/lib/editor/tinymce/plugins/spellchecker/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); // The current plugin version (Date: YYYYMMDDXX). -$plugin->version = 2012051701; +$plugin->version = 2012051800; // Required Moodle version. $plugin->requires = 2011112900; // Full name of the plugin (used for diagnostics). diff --git a/lib/editor/tinymce/settings.php b/lib/editor/tinymce/settings.php index 3d973e1788e14..461e62f6296b3 100644 --- a/lib/editor/tinymce/settings.php +++ b/lib/editor/tinymce/settings.php @@ -24,18 +24,31 @@ defined('MOODLE_INTERNAL') || die; +$ADMIN->add('editorsettings', new admin_category('editortinymce', new lang_string('pluginname', 'editor_tinymce'))); + +$settings = new admin_settingpage('editorsettingstinymce', new lang_string('settings', 'editor_tinymce')); if ($ADMIN->fulltree) { - $options = array( - 'PSpell'=>'PSpell', - 'GoogleSpell'=>'Google Spell', - 'PSpellShell'=>'PSpellShell'); - $settings->add(new admin_setting_configselect('editor_tinymce/spellengine', - get_string('spellengine', 'admin'), '', 'GoogleSpell', $options)); - $settings->add(new admin_setting_configtextarea('editor_tinymce/spelllanguagelist', - get_string('spelllanguagelist', 'admin'), '', - '+English=en,Danish=da,Dutch=nl,Finnish=fi,French=fr,German=de,Italian=it,Polish=pl,' . - 'Portuguese=pt,Spanish=es,Swedish=sv', PARAM_RAW)); $settings->add(new admin_setting_configtextarea('editor_tinymce/fontselectlist', get_string('fontselectlist', 'editor_tinymce'), '', 'Trebuchet=Trebuchet MS,Verdana,Arial,Helvetica,sans-serif;Arial=arial,helvetica,sans-serif;Courier New=courier new,courier,monospace;Georgia=georgia,times new roman,times,serif;Tahoma=tahoma,arial,helvetica,sans-serif;Times New Roman=times new roman,times,serif;Verdana=verdana,arial,helvetica,sans-serif;Impact=impact;Wingdings=wingdings', PARAM_RAW)); } +$ADMIN->add('editortinymce', $settings); +unset($settings); + +$subplugins = get_plugin_list('tinymce'); +$disabled = array(); // Disabling of subplugins to be implemented later. +foreach ($subplugins as $name=>$dir) { + if (file_exists("$dir/settings.php")) { + $settings = new admin_settingpage('tinymce'.$name.'settings', new lang_string('pluginname', 'tinymce_'.$name), 'moodle/site:config', in_array($name, $disabled)); + // settings.php may create a subcategory or unset the settings completely. + include("$dir/settings.php"); + if ($settings) { + $ADMIN->add('editortinymce', $settings); + } + } +} +unset($subplugins); +unset($disabled); + +// TinyMCE does not have standard settings page. +$settings = null; From caaccae5da86e7b10495c468b7fcb4b14aca4501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= <commits@skodak.org> Date: Sat, 18 Aug 2012 16:26:27 +0200 Subject: [PATCH 29/90] MDL-34878 add custom TinyMCE toolbar setting --- lib/editor/tinymce/lang/en/editor_tinymce.php | 2 ++ lib/editor/tinymce/lib.php | 19 +++++++++++++++++++ lib/editor/tinymce/settings.php | 2 ++ 3 files changed, 23 insertions(+) diff --git a/lib/editor/tinymce/lang/en/editor_tinymce.php b/lib/editor/tinymce/lang/en/editor_tinymce.php index 1563b2ee746a2..2fbb5124d8109 100644 --- a/lib/editor/tinymce/lang/en/editor_tinymce.php +++ b/lib/editor/tinymce/lang/en/editor_tinymce.php @@ -26,6 +26,8 @@ //== Custom Moodle strings that are not part of upstream TinyMCE == $string['common:browseimage'] = 'Find or upload an image...'; $string['common:browsemedia'] = 'Find or upload a sound, video or applet...'; +$string['customtoolbar'] = 'Custom editor toolbar'; +$string['customtoolbar_desc'] = 'Each line contains a list of comma separated button names, use "|" as a group separator. Leave empty if you want standard toolbar.'; $string['fontselectlist'] = 'Available fonts list'; $string['media_dlg:filename'] = 'Filename'; $string['pluginname'] = 'TinyMCE HTML editor'; diff --git a/lib/editor/tinymce/lib.php b/lib/editor/tinymce/lib.php index 2ef3db13c4224..ac25047e0316f 100644 --- a/lib/editor/tinymce/lib.php +++ b/lib/editor/tinymce/lib.php @@ -190,6 +190,25 @@ protected function get_init_params($elementid, array $options=null) { // Allow plugins to adjust parameters. editor_tinymce_plugin::all_update_init_params($params, $context, $options); + // Should we override the default toolbar layout unconditionally? + $customtoolbar = trim($config->customtoolbar); + if ($customtoolbar) { + unset($params['theme_advanced_buttons1']); + unset($params['theme_advanced_buttons2']); + unset($params['theme_advanced_buttons3']); + unset($params['theme_advanced_buttons4']); + $customtoolbar = str_replace("\r", "\n", $customtoolbar); + $i = 1; + foreach (explode("\n", $customtoolbar) as $line) { + $line = preg_replace('/\s/', '', $line); + if ($line === '') { + continue; + } + $params['theme_advanced_buttons'.$i] = $line; + $i++; + } + } + // Remove temporary parameters. unset($params['moodle_config']); diff --git a/lib/editor/tinymce/settings.php b/lib/editor/tinymce/settings.php index 461e62f6296b3..45c1bc84eeb44 100644 --- a/lib/editor/tinymce/settings.php +++ b/lib/editor/tinymce/settings.php @@ -28,6 +28,8 @@ $settings = new admin_settingpage('editorsettingstinymce', new lang_string('settings', 'editor_tinymce')); if ($ADMIN->fulltree) { + $settings->add(new admin_setting_configtextarea('editor_tinymce/customtoolbar', + get_string('customtoolbar', 'editor_tinymce'), get_string('customtoolbar_desc', 'editor_tinymce'), '', PARAM_RAW, 100, 6)); $settings->add(new admin_setting_configtextarea('editor_tinymce/fontselectlist', get_string('fontselectlist', 'editor_tinymce'), '', 'Trebuchet=Trebuchet MS,Verdana,Arial,Helvetica,sans-serif;Arial=arial,helvetica,sans-serif;Courier New=courier new,courier,monospace;Georgia=georgia,times new roman,times,serif;Tahoma=tahoma,arial,helvetica,sans-serif;Times New Roman=times new roman,times,serif;Verdana=verdana,arial,helvetica,sans-serif;Impact=impact;Wingdings=wingdings', PARAM_RAW)); From 0b7858221c3e3f6445e8d2d93e0efac6129b135d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= <commits@skodak.org> Date: Sat, 18 Aug 2012 18:55:33 +0200 Subject: [PATCH 30/90] MDL-34955 add full TinyMCE subplugin management --- lib/editor/tinymce/adminlib.php | 186 +++++++++++++++++- lib/editor/tinymce/classes/plugin.php | 26 +++ lib/editor/tinymce/lang/en/editor_tinymce.php | 4 +- lib/editor/tinymce/lib.php | 5 - lib/editor/tinymce/plugins/dragmath/lib.php | 3 + .../tinymce/plugins/moodleemoticon/lib.php | 3 + .../tinymce/plugins/moodleimage/lib.php | 10 + .../tinymce/plugins/moodlemedia/lib.php | 10 + .../tinymce/plugins/moodlenolink/lib.php | 3 + .../tinymce/plugins/spellchecker/lib.php | 3 + lib/editor/tinymce/settings.php | 5 +- lib/editor/tinymce/subplugins.php | 23 +++ 12 files changed, 273 insertions(+), 8 deletions(-) diff --git a/lib/editor/tinymce/adminlib.php b/lib/editor/tinymce/adminlib.php index 4d2dba2887659..3c923d4936faa 100644 --- a/lib/editor/tinymce/adminlib.php +++ b/lib/editor/tinymce/adminlib.php @@ -26,11 +26,15 @@ require_once("$CFG->libdir/pluginlib.php"); + /** * Editor subplugin info class. + * + * @package editor_tinymce + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class plugininfo_tinymce extends plugininfo_base { - public function get_uninstall_url() { return new moodle_url('/lib/editor/tinymce/subplugins.php', array('delete' => $this->name, 'sesskey' => sesskey())); } @@ -43,4 +47,184 @@ public function get_settings_url() { return null; } } + + public function is_enabled() { + static $disabledsubplugins = null; // TODO: remove this once get_config() is cached via MUC! + + if (is_null($disabledsubplugins)) { + $disabledsubplugins = array(); + $config = get_config('editor_tinymce', 'disabledsubplugins'); + if ($config) { + $config = explode(',', $config); + foreach ($config as $sp) { + $sp = trim($sp); + if ($sp !== '') { + $disabledsubplugins[$sp] = $sp; + } + } + } + } + + return !isset($disabledsubplugins[$this->name]); + } +} + + +/** + * Special class for TinyMCE subplugin administration. + * + * @package editor_tinymce + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tiynce_subplugins_settings extends admin_setting { + public function __construct() { + $this->nosave = true; + parent::__construct('tinymcesubplugins', get_string('subplugintype_tinymce_plural', 'editor_tinymce'), '', ''); + } + + /** + * Always returns true, does nothing. + * + * @return true + */ + public function get_setting() { + return true; + } + + /** + * Always returns true, does nothing. + * + * @return true + */ + public function get_defaultsetting() { + return true; + } + + /** + * Always returns '', does not write anything. + * + * @param string $data + * @return string Always returns '' + */ + public function write_setting($data) { + // Do not write any setting. + return ''; + } + + /** + * Checks if $query is one of the available subplugins. + * + * @param string $query The string to search for + * @return bool Returns true if found, false if not + */ + public function is_related($query) { + if (parent::is_related($query)) { + return true; + } + + $subplugins = get_plugin_list('tinymce'); + foreach ($subplugins as $name=>$dir) { + if (stripos($name, $query) !== false) { + return true; + } + + $namestr = get_string('pluginname', 'tinymce_'.$name); + if (strpos(textlib::strtolower($namestr), textlib::strtolower($query)) !== false) { + return true; + } + } + return false; + } + + /** + * Builds the XHTML to display the control. + * + * @param string $data Unused + * @param string $query + * @return string + */ + public function output_html($data, $query='') { + global $CFG, $OUTPUT; + require_once("$CFG->libdir/editorlib.php"); + require_once("$CFG->libdir/pluginlib.php"); + require_once(__DIR__.'/lib.php'); + $tinymce = new tinymce_texteditor(); + $pluginmanager = plugin_manager::instance(); + + // display strings + $strbuttons = get_string('availablebuttons', 'editor_tinymce'); + $strdisable = get_string('disable'); + $strenable = get_string('enable'); + $strname = get_string('name'); + $strsettings = get_string('settings'); + $struninstall = get_string('uninstallplugin', 'admin'); + $strversion = get_string('version'); + + $subplugins = get_plugin_list('tinymce'); + + $return = $OUTPUT->heading(get_string('subplugins', 'editor_tinymce'), 3, 'main', true); + $return .= $OUTPUT->box_start('generalbox tinymcesubplugins'); + + $table = new html_table(); + $table->head = array($strname, $strbuttons, $strversion, $strenable, $strsettings, $struninstall); + $table->align = array('left', 'left', 'center', 'center', 'center', 'center'); + $table->data = array(); + $table->width = '100%'; + + // Iterate through subplugins. + foreach ($subplugins as $name => $dir) { + $namestr = get_string('pluginname', 'tinymce_'.$name); + $version = get_config('tinymce_'.$name, 'version'); + if ($version === false) { + $version = ''; + } + $plugin = $tinymce->get_plugin($name); + $plugininfo = $pluginmanager->get_plugin_info('tinymce_'.$name); + + // Add hide/show link. + if (!$version) { + $hideshow = ''; + $displayname = html_writer::tag('span', $name, array('class'=>'error')); + } else if ($plugininfo->is_enabled()) { + $url = new moodle_url('/lib/editor/tinymce/subplugins.php', array('sesskey'=>sesskey(), 'return'=>'settings', 'disable'=>$name)); + $hideshow = html_writer::empty_tag('img', array('src'=>$OUTPUT->pix_url('i/hide'), 'class'=>'icon', 'alt'=>$strdisable)); + $hideshow = html_writer::link($url, $hideshow); + $displayname = html_writer::tag('span', $namestr); + } else { + $url = new moodle_url('/lib/editor/tinymce/subplugins.php', array('sesskey'=>sesskey(), 'return'=>'settings', 'enable'=>$name)); + $hideshow = html_writer::empty_tag('img', array('src'=>$OUTPUT->pix_url('i/show'), 'class'=>'icon', 'alt'=>$strenable)); + $hideshow = html_writer::link($url, $hideshow); + $displayname = html_writer::tag('span', $namestr, array('class'=>'dimmed_text')); + } + + // Add available buttons. + $buttons = implode(', ', $plugin->get_buttons()); + $buttons = html_writer::tag('span', $buttons, array('class'=>'tinamcebuttons')); + + // Add settings link. + if (!$version) { + $settings = ''; + } else if ($url = $plugininfo->get_settings_url()) { + $settings = html_writer::link($url, $strsettings); + } else { + $settings = ''; + } + + // Add uninstall info. + if ($version) { + $url = new moodle_url($plugininfo->get_uninstall_url(), array('return'=>'settings')); + $uninstall = html_writer::link($url, $struninstall); + } else { + $uninstall = ''; + } + + // Add a row to the table. + $table->data[] = array($displayname, $buttons, $version, $hideshow, $settings, $uninstall); + } + $return .= html_writer::table($table); + $return .= html_writer::tag('p', get_string('tablenosave', 'admin')); + $return .= $OUTPUT->box_end(); + return highlight($query, $return); + } } diff --git a/lib/editor/tinymce/classes/plugin.php b/lib/editor/tinymce/classes/plugin.php index 8dca9b6a00f59..305f8275c7b15 100644 --- a/lib/editor/tinymce/classes/plugin.php +++ b/lib/editor/tinymce/classes/plugin.php @@ -40,6 +40,9 @@ abstract class editor_tinymce_plugin { /** @var array Plugin settings */ protected $config = null; + /** @var array list of buttons defined by this plugin */ + protected $buttons = array(); + /** * @param string $plugin Name of folder */ @@ -47,6 +50,15 @@ public function __construct($plugin) { $this->plugin = $plugin; } + /** + * Returns list of buttons defined by this plugin. + * useful mostly as information when setting custom toolbar. + * + * @return array + */ + public function get_buttons() { + return $this->buttons; + } /** * Makes sure config is loaded and cached. * @return void @@ -326,9 +338,23 @@ public static function all_update_init_params(array &$params, // Get list of plugin directories. $plugins = get_plugin_list('tinymce'); + // Get list of disabled subplugins. + $disabled = array(); + if ($params['moodle_config']->disabledsubplugins) { + foreach (explode(',', $params['moodle_config']->disabledsubplugins) as $sp) { + $sp = trim($sp); + if ($sp !== '') { + $disabled[$sp] = $sp; + } + } + } + // Construct all the plugins. $pluginobjects = array(); foreach ($plugins as $plugin => $dir) { + if (isset($disabled[$plugin])) { + continue; + } require_once($dir . '/lib.php'); $classname = 'tinymce_' . $plugin; $pluginobjects[] = new $classname($plugin); diff --git a/lib/editor/tinymce/lang/en/editor_tinymce.php b/lib/editor/tinymce/lang/en/editor_tinymce.php index 2fbb5124d8109..d08d0f1174567 100644 --- a/lib/editor/tinymce/lang/en/editor_tinymce.php +++ b/lib/editor/tinymce/lang/en/editor_tinymce.php @@ -24,15 +24,17 @@ //== Custom Moodle strings that are not part of upstream TinyMCE == +$string['availablebuttons'] = 'Available buttons'; $string['common:browseimage'] = 'Find or upload an image...'; $string['common:browsemedia'] = 'Find or upload a sound, video or applet...'; $string['customtoolbar'] = 'Custom editor toolbar'; -$string['customtoolbar_desc'] = 'Each line contains a list of comma separated button names, use "|" as a group separator. Leave empty if you want standard toolbar.'; +$string['customtoolbar_desc'] = 'Each line contains a list of comma separated button names, use "|" as a group separator. Leave empty if you want standard toolbar. See <a href="{$a}" target="_blank">{$a}</a> for the list of default TinyMCE buttons.'; $string['fontselectlist'] = 'Available fonts list'; $string['media_dlg:filename'] = 'Filename'; $string['pluginname'] = 'TinyMCE HTML editor'; $string['settings'] = 'General settings'; $string['subplugindeleteconfirm'] = 'You are about to completely delete TinyMCE subplugin \'{$a}\'. This will completely delete everything in the database associated with this subplugin. Are you SURE you want to continue?'; +$string['subplugintype_tinymce_plural'] = 'Plugins'; // == TinyMCE upstream lang strings from all standard upstream plugins == diff --git a/lib/editor/tinymce/lib.php b/lib/editor/tinymce/lib.php index ac25047e0316f..6ffef3927adf3 100644 --- a/lib/editor/tinymce/lib.php +++ b/lib/editor/tinymce/lib.php @@ -177,11 +177,6 @@ protected function get_init_params($elementid, array $options=null) { $params['extended_valid_elements'] = 'nolink,tex,algebra,lang[lang]'; $params['custom_elements'] = 'nolink,~tex,~algebra,lang'; - if (empty($options['legacy'])) { - if (isset($options['maxfiles']) and $options['maxfiles'] != 0) { - $params['file_browser_callback'] = "M.editor_tinymce.filepicker"; - } - } //Add onblur event for client side text validation if (!empty($options['required'])) { $params['init_instance_callback'] = 'M.editor_tinymce.onblur_event'; diff --git a/lib/editor/tinymce/plugins/dragmath/lib.php b/lib/editor/tinymce/plugins/dragmath/lib.php index 6532313bcb7d1..1cacacf12534e 100644 --- a/lib/editor/tinymce/plugins/dragmath/lib.php +++ b/lib/editor/tinymce/plugins/dragmath/lib.php @@ -24,6 +24,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tinymce_dragmath extends editor_tinymce_plugin { + /** @var array list of buttons defined by this plugin */ + protected $buttons = array('dragmath'); + protected function update_init_params(array &$params, context $context, array $options = null) { diff --git a/lib/editor/tinymce/plugins/moodleemoticon/lib.php b/lib/editor/tinymce/plugins/moodleemoticon/lib.php index 09f23c5d503ed..910cd5693542d 100644 --- a/lib/editor/tinymce/plugins/moodleemoticon/lib.php +++ b/lib/editor/tinymce/plugins/moodleemoticon/lib.php @@ -24,6 +24,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tinymce_moodleemoticon extends editor_tinymce_plugin { + /** @var array list of buttons defined by this plugin */ + protected $buttons = array('moodleemoticon'); + protected function update_init_params(array &$params, context $context, array $options = null) { global $OUTPUT; diff --git a/lib/editor/tinymce/plugins/moodleimage/lib.php b/lib/editor/tinymce/plugins/moodleimage/lib.php index afec3435d0c91..f284b7a8b782f 100644 --- a/lib/editor/tinymce/plugins/moodleimage/lib.php +++ b/lib/editor/tinymce/plugins/moodleimage/lib.php @@ -24,9 +24,19 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tinymce_moodleimage extends editor_tinymce_plugin { + /** @var array list of buttons defined by this plugin */ + protected $buttons = array('image'); + protected function update_init_params(array &$params, context $context, array $options = null) { + // Add file picker callback. + if (empty($options['legacy'])) { + if (isset($options['maxfiles']) and $options['maxfiles'] != 0) { + $params['file_browser_callback'] = "M.editor_tinymce.filepicker"; + } + } + // This plugin overrides standard 'image' button, no need to insert new button. // Add JS file, which uses default name. diff --git a/lib/editor/tinymce/plugins/moodlemedia/lib.php b/lib/editor/tinymce/plugins/moodlemedia/lib.php index 5669f477a14eb..048b1f5ef0de7 100644 --- a/lib/editor/tinymce/plugins/moodlemedia/lib.php +++ b/lib/editor/tinymce/plugins/moodlemedia/lib.php @@ -24,9 +24,19 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tinymce_moodlemedia extends editor_tinymce_plugin { + /** @var array list of buttons defined by this plugin */ + protected $buttons = array('moodlemedia'); + protected function update_init_params(array &$params, context $context, array $options = null) { + // Add file picker callback. + if (empty($options['legacy'])) { + if (isset($options['maxfiles']) and $options['maxfiles'] != 0) { + $params['file_browser_callback'] = "M.editor_tinymce.filepicker"; + } + } + // Add button after emoticon button in advancedbuttons3. $added = $this->add_button_after($params, 3, 'moodlemedia', 'moodleemoticon', false); diff --git a/lib/editor/tinymce/plugins/moodlenolink/lib.php b/lib/editor/tinymce/plugins/moodlenolink/lib.php index fb12c77c7351d..fedb3cc00e292 100644 --- a/lib/editor/tinymce/plugins/moodlenolink/lib.php +++ b/lib/editor/tinymce/plugins/moodlenolink/lib.php @@ -24,6 +24,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tinymce_moodlenolink extends editor_tinymce_plugin { + /** @var array list of buttons defined by this plugin */ + protected $buttons = array('moodlenolink'); + protected function update_init_params(array &$params, context $context, array $options = null) { diff --git a/lib/editor/tinymce/plugins/spellchecker/lib.php b/lib/editor/tinymce/plugins/spellchecker/lib.php index 2a4cfde6788d7..b2aee6d4bff03 100644 --- a/lib/editor/tinymce/plugins/spellchecker/lib.php +++ b/lib/editor/tinymce/plugins/spellchecker/lib.php @@ -25,6 +25,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tinymce_spellchecker extends editor_tinymce_plugin { + /** @var array list of buttons defined by this plugin */ + protected $buttons = array('spellchecker'); + protected function update_init_params(array &$params, context $context, array $options = null) { global $CFG; diff --git a/lib/editor/tinymce/settings.php b/lib/editor/tinymce/settings.php index 45c1bc84eeb44..d7ffb88332d67 100644 --- a/lib/editor/tinymce/settings.php +++ b/lib/editor/tinymce/settings.php @@ -28,8 +28,11 @@ $settings = new admin_settingpage('editorsettingstinymce', new lang_string('settings', 'editor_tinymce')); if ($ADMIN->fulltree) { + require_once(__DIR__.'/adminlib.php'); + $settings->add(new tiynce_subplugins_settings()); + $settings->add(new admin_setting_heading('tinymcegeneralheader', new lang_string('settings'), '')); $settings->add(new admin_setting_configtextarea('editor_tinymce/customtoolbar', - get_string('customtoolbar', 'editor_tinymce'), get_string('customtoolbar_desc', 'editor_tinymce'), '', PARAM_RAW, 100, 6)); + get_string('customtoolbar', 'editor_tinymce'), get_string('customtoolbar_desc', 'editor_tinymce', 'http://www.tinymce.com/wiki.php/Buttons/controls'), '', PARAM_RAW, 100, 6)); $settings->add(new admin_setting_configtextarea('editor_tinymce/fontselectlist', get_string('fontselectlist', 'editor_tinymce'), '', 'Trebuchet=Trebuchet MS,Verdana,Arial,Helvetica,sans-serif;Arial=arial,helvetica,sans-serif;Courier New=courier new,courier,monospace;Georgia=georgia,times new roman,times,serif;Tahoma=tahoma,arial,helvetica,sans-serif;Times New Roman=times new roman,times,serif;Verdana=verdana,arial,helvetica,sans-serif;Impact=impact;Wingdings=wingdings', PARAM_RAW)); diff --git a/lib/editor/tinymce/subplugins.php b/lib/editor/tinymce/subplugins.php index beba8f432ef19..57c153a7e36c7 100644 --- a/lib/editor/tinymce/subplugins.php +++ b/lib/editor/tinymce/subplugins.php @@ -27,6 +27,8 @@ $delete = optional_param('delete', '', PARAM_PLUGIN); $confirm = optional_param('confirm', '', PARAM_BOOL); +$disable = optional_param('disable', '', PARAM_PLUGIN); +$enable = optional_param('enable', '', PARAM_PLUGIN); $return = optional_param('return', 'overview', PARAM_ALPHA); $PAGE->set_context(context_system::instance()); @@ -69,6 +71,27 @@ echo $OUTPUT->footer(); die(); } + +} else { + $disabled = array(); + $disabledsubplugins = get_config('editor_tinymce', 'disabledsubplugins'); + if ($disabledsubplugins) { + $disabledsubplugins = explode(',', $disabledsubplugins); + foreach ($disabledsubplugins as $sp) { + $sp = trim($sp); + if ($sp !== '') { + $disabled[$sp] = $sp; + } + } + } + + if ($disable) { + $disabled[$disable] = $disable; + } else if ($enable) { + unset($disabled[$enable]); + } + + set_config('disabledsubplugins', implode(',', $disabled), 'editor_tinymce'); } redirect($returnurl); From 883ecce0e933f5b424441866a899a1c9389c8caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= <commits@skodak.org> Date: Tue, 21 Aug 2012 09:35:26 +0200 Subject: [PATCH 31/90] MDL-34955 fix missing string --- lib/editor/tinymce/adminlib.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/editor/tinymce/adminlib.php b/lib/editor/tinymce/adminlib.php index 3c923d4936faa..d8497f96fdcac 100644 --- a/lib/editor/tinymce/adminlib.php +++ b/lib/editor/tinymce/adminlib.php @@ -163,7 +163,7 @@ public function output_html($data, $query='') { $subplugins = get_plugin_list('tinymce'); - $return = $OUTPUT->heading(get_string('subplugins', 'editor_tinymce'), 3, 'main', true); + $return = $OUTPUT->heading(get_string('subplugintype_tinymce_plural', 'editor_tinymce'), 3, 'main', true); $return .= $OUTPUT->box_start('generalbox tinymcesubplugins'); $table = new html_table(); From 0bc9b897f90074ec5ade3bcc49344d6484d7d7b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= <commits@skodak.org> Date: Sat, 18 Aug 2012 22:48:52 +0200 Subject: [PATCH 32/90] MDL-34879 add upgrade.txt notes to editor_tinymce --- lib/editor/tinymce/upgrade.txt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 lib/editor/tinymce/upgrade.txt diff --git a/lib/editor/tinymce/upgrade.txt b/lib/editor/tinymce/upgrade.txt new file mode 100644 index 0000000000000..7374a19c99f89 --- /dev/null +++ b/lib/editor/tinymce/upgrade.txt @@ -0,0 +1,9 @@ +This files describes API changes in /lib/editor/tinymce/* - TinyMCE editor, +information provided here is intended especially for developers. + + +=== 2.4 === + +new features: + +* subplugin support - see http://docs.moodle.org/dev/TinyMCE_plugins \ No newline at end of file From b3aefe3cc85bc9c941449c1f10ff9fce02b57a5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= <commits@skodak.org> Date: Tue, 21 Aug 2012 10:07:03 +0200 Subject: [PATCH 33/90] MDL-34990 improve custom toolbar setting parsing It is probably better to parse the setting every time because somebody may put unsupported values directly into config.php, performance should not be an issue because we do not have editors on every page. --- lib/editor/tinymce/lib.php | 35 ++++++++++++---- lib/editor/tinymce/tests/editor_test.php | 53 ++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 lib/editor/tinymce/tests/editor_test.php diff --git a/lib/editor/tinymce/lib.php b/lib/editor/tinymce/lib.php index 6ffef3927adf3..73f01f774b1cc 100644 --- a/lib/editor/tinymce/lib.php +++ b/lib/editor/tinymce/lib.php @@ -186,19 +186,14 @@ protected function get_init_params($elementid, array $options=null) { editor_tinymce_plugin::all_update_init_params($params, $context, $options); // Should we override the default toolbar layout unconditionally? - $customtoolbar = trim($config->customtoolbar); + $customtoolbar = self::parse_toolbar_setting($config->customtoolbar); if ($customtoolbar) { unset($params['theme_advanced_buttons1']); unset($params['theme_advanced_buttons2']); unset($params['theme_advanced_buttons3']); unset($params['theme_advanced_buttons4']); - $customtoolbar = str_replace("\r", "\n", $customtoolbar); $i = 1; - foreach (explode("\n", $customtoolbar) as $line) { - $line = preg_replace('/\s/', '', $line); - if ($line === '') { - continue; - } + foreach ($customtoolbar as $line) { $params['theme_advanced_buttons'.$i] = $line; $i++; } @@ -210,6 +205,32 @@ protected function get_init_params($elementid, array $options=null) { return $params; } + /** + * Parse the custom toolbar setting. + * @param string $customtoolbar + * @return array csv toolbar lines + */ + public static function parse_toolbar_setting($customtoolbar) { + $result = array(); + $customtoolbar = trim($customtoolbar); + if ($customtoolbar === '') { + return $result; + } + $customtoolbar = str_replace("\r", "\n", $customtoolbar); + $customtoolbar = strtolower($customtoolbar); + foreach (explode("\n", $customtoolbar) as $line) { + $line = preg_replace('/[^a-z0-9_,\|\-]/', ',', $line); + $line = str_replace('|', ',|,', $line); + $line = preg_replace('/,,+/', ',', $line); + $line = trim($line, ',|'); + if ($line === '') { + continue; + } + $result[] = $line; + } + return $result; + } + /** * Gets a named plugin object. Will cause fatal error if plugin doesn't * exist. This is intended for use by plugin files themselves. diff --git a/lib/editor/tinymce/tests/editor_test.php b/lib/editor/tinymce/tests/editor_test.php new file mode 100644 index 0000000000000..27db61bb07581 --- /dev/null +++ b/lib/editor/tinymce/tests/editor_test.php @@ -0,0 +1,53 @@ +<?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/>. + +/** + * TinyMCE tests. + * + * @package editor_tinymce + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + + +/** + * TinyMCE tests. + * + * @package editor_tinymce + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class editor_tinymce_testcase extends advanced_testcase { + + public function test_toolbar_parsing() { + global $CFG; + require_once("$CFG->dirroot/lib/editorlib.php"); + require_once("$CFG->dirroot/lib/editor/tinymce/lib.php"); + + $result = tinymce_texteditor::parse_toolbar_setting("bold,italic\npreview"); + $this->assertSame(array('bold,italic', 'preview'), $result); + + $result = tinymce_texteditor::parse_toolbar_setting("| bold,|italic*blink\rpreview\n\n| \n paste STYLE | "); + $this->assertSame(array('bold,|,italic,blink', 'preview', 'paste,style'), $result); + + $result = tinymce_texteditor::parse_toolbar_setting("| \n\n| \n \r"); + $this->assertSame(array(), $result); + } +} From 1d1917aeaa4380479494d7f08aa2e55c1277ef4b Mon Sep 17 00:00:00 2001 From: sam marshall <s.marshall@open.ac.uk> Date: Tue, 20 Mar 2012 17:41:33 +0000 Subject: [PATCH 34/90] MDL-31973 Groups: groups_members table should have 'component', 'itemid' fields --- backup/moodle2/backup_stepslib.php | 2 +- backup/moodle2/restore_stepslib.php | 11 +++++ enrol/imsenterprise/lib.php | 16 ++++++- group/externallib.php | 3 ++ group/lib.php | 74 ++++++++++++++++++++++++++++- group/members.php | 4 ++ lang/en/group.php | 2 + lib/db/install.xml | 6 ++- lib/db/upgrade.php | 21 ++++++++ mod/page/lib.php | 4 ++ theme/base/style/user.css | 3 ++ user/selector/lib.php | 14 +++++- user/selector/module.js | 5 ++ user/selector/search.php | 3 ++ version.php | 2 +- 15 files changed, 162 insertions(+), 8 deletions(-) diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index b4399f2434464..a64864860f33b 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -1002,7 +1002,7 @@ protected function define_structure() { $members = new backup_nested_element('group_members'); $member = new backup_nested_element('group_member', array('id'), array( - 'userid', 'timeadded')); + 'userid', 'timeadded', 'component', 'itemid')); $groupings = new backup_nested_element('groupings'); diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index 3677d7bee02a8..f4a6d54604184 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -773,6 +773,17 @@ public function process_member($data) { // map user newitemid and insert if not member already if ($data->userid = $this->get_mappingid('user', $data->userid)) { if (!$DB->record_exists('groups_members', array('groupid' => $data->groupid, 'userid' => $data->userid))) { + // Check the componment, if any, exists + if (!empty($data->component)) { + $dir = get_component_directory($data->component); + if (!$dir || !is_dir($dir)) { + // Component does not exist on restored system; clear + // component and itemid + unset($data->component); + unset($data->itemid); + } + } + $DB->insert_record('groups_members', $data); } } diff --git a/enrol/imsenterprise/lib.php b/enrol/imsenterprise/lib.php index 81f9e4cf98033..0e0092563e38b 100644 --- a/enrol/imsenterprise/lib.php +++ b/enrol/imsenterprise/lib.php @@ -702,7 +702,8 @@ function process_membership_tag($tagcontents){ } // Add the user-to-group association if it doesn't already exist if($member->groupid) { - groups_add_member($member->groupid, $memberstoreobj->userid); + groups_add_member($member->groupid, $memberstoreobj->userid, + 'enrol_imsenterprise', $einstance->id); } } // End of group-enrolment (from member.role.extension.cohort tag) @@ -793,6 +794,19 @@ function load_role_mappings() { } } + /** + * Called whenever anybody tries (from the normal interface) to remove a group + * member which is registered as being created by this component. (Not called + * when deleting an entire group or course at once.) + * @param int $itemid Item ID that was stored in the group_members entry + * @param int $groupid Group ID + * @param int $userid User ID being removed from group + * @return bool True if the remove is permitted, false to give an error + */ + function enrol_imsenterprise_allow_group_member_remove($itemid, $groupid, $userid) { + return false; + } + } // end of class diff --git a/group/externallib.php b/group/externallib.php index 0c17c81a57b04..f7599d739ba0e 100644 --- a/group/externallib.php +++ b/group/externallib.php @@ -545,6 +545,9 @@ public static function delete_group_members($members) { } require_capability('moodle/course:managegroups', $context); + if (!groups_remove_member_allowed($group, $user)) { + throw new moodle_exception('errorremovenotpermitted', 'group', '', fullname($user)); + } groups_remove_member($group, $user); } diff --git a/group/lib.php b/group/lib.php index 68d3385c7dd78..e16bc01e46dba 100644 --- a/group/lib.php +++ b/group/lib.php @@ -33,10 +33,12 @@ * * @param mixed $grouporid The group id or group object * @param mixed $userorid The user id or user object + * @param string $component Optional component name e.g. 'enrol_imsenterprise' + * @param int $itemid Optional itemid associated with component * @return bool True if user added successfully or the user is already a * member of the group, false otherwise. */ -function groups_add_member($grouporid, $userorid) { +function groups_add_member($grouporid, $userorid, $component=null, $itemid=0) { global $DB; if (is_object($userorid)) { @@ -68,6 +70,25 @@ function groups_add_member($grouporid, $userorid) { $member->groupid = $groupid; $member->userid = $userid; $member->timeadded = time(); + $member->component = ''; + $member->itemid = 0; + + // Check the component exists if specified + if (!empty($component)) { + $dir = get_component_directory($component); + if ($dir && is_dir($dir)) { + // Component exists and can be used + $member->component = $component; + $member->itemid = $itemid; + } else { + throw new coding_exception('Invalid call to groups_add_member(). An invalid component was specified'); + } + } + + if ($itemid !== 0 && empty($member->component)) { + // An itemid can only be specified if a valid component was found + throw new coding_exception('Invalid call to groups_add_member(). A component must be specified if an itemid is given'); + } $DB->insert_record('groups_members', $member); @@ -78,11 +99,62 @@ function groups_add_member($grouporid, $userorid) { $eventdata = new stdClass(); $eventdata->groupid = $groupid; $eventdata->userid = $userid; + $eventdata->component = $member->component; + $eventdata->itemid = $member->itemid; events_trigger('groups_member_added', $eventdata); return true; } +/** + * Checks whether the current user is permitted (using the normal UI) to + * remove a specific group member, assuming that they have access to remove + * group members in general. + * + * For automatically-created group member entries, this checks with the + * relevant plugin to see whether it is permitted. The default, if the plugin + * doesn't provide a function, is true. + * + * For other entries (and any which have already been deleted/don't exist) it + * just returns true. + * + * @param mixed $grouporid The group id or group object + * @param mixed $userorid The user id or user object + * @return bool True if permitted, false otherwise + */ +function groups_remove_member_allowed($grouporid, $userorid) { + global $DB; + + if (is_object($userorid)) { + $userid = $userorid->id; + } else { + $userid = $userorid; + } + if (is_object($grouporid)) { + $groupid = $grouporid->id; + } else { + $groupid = $grouporid; + } + + // Get entry + if (!($entry = $DB->get_record('groups_members', + array('groupid' => $groupid, 'userid' => $userid), '*', IGNORE_MISSING))) { + // If the entry does not exist, they are allowed to remove it (this + // is consistent with groups_remove_member below). + return true; + } + + // If the entry does not have a component value, they can remove it + if (empty($entry->component)) { + return true; + } + + // It has a component value, so we need to call a plugin function (if it + // exists); the default is to allow removal + return component_callback($entry->component, 'allow_group_member_remove', + array($entry->itemid, $entry->groupid, $entry->userid), true); +} + /** * Deletes the link between the specified user and group. * diff --git a/group/members.php b/group/members.php index 421d1d34e1641..a8b74ef25145a 100644 --- a/group/members.php +++ b/group/members.php @@ -67,6 +67,10 @@ $userstoremove = $groupmembersselector->get_selected_users(); if (!empty($userstoremove)) { foreach ($userstoremove as $user) { + if (!groups_remove_member_allowed($groupid, $user->id)) { + print_error('errorremovenotpermitted', 'group', $returnurl, + $user->fullname); + } if (!groups_remove_member($groupid, $user->id)) { print_error('erroraddremoveuser', 'group', $returnurl); } diff --git a/lang/en/group.php b/lang/en/group.php index 150598651421d..807a806eb4d70 100644 --- a/lang/en/group.php +++ b/lang/en/group.php @@ -24,6 +24,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['addedby'] = 'Added by {$a}'; $string['addgroup'] = 'Add user into group'; $string['addgroupstogrouping'] = 'Add group to grouping'; $string['addgroupstogroupings'] = 'Add/remove groups'; @@ -62,6 +63,7 @@ $string['erroreditgroup'] = 'Error creating/updating group {$a}'; $string['erroreditgrouping'] = 'Error creating/updating grouping {$a}'; $string['errorinvalidgroup'] = 'Error, invalid group {$a}'; +$string['errorremovenotpermitted'] = 'You do not have permission to remove automatically-added group member {$a}'; $string['errorselectone'] = 'Please select a single group before choosing this option'; $string['errorselectsome'] = 'Please select one or more groups before choosing this option'; $string['evenallocation'] = 'Note: To keep group allocation even, the actual number of members per group differs from the number you specified.'; diff --git a/lib/db/install.xml b/lib/db/install.xml index e54cf890e9730..eb05d6e832104 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -2090,7 +2090,9 @@ <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" NEXT="groupid"/> <FIELD NAME="groupid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="id" NEXT="userid"/> <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="groupid" NEXT="timeadded"/> - <FIELD NAME="timeadded" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="userid"/> + <FIELD NAME="timeadded" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="userid" NEXT="component"/> + <FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false" COMMENT="Defines the Moodle component which added this group membership (e.g. 'auth_myplugin'), or blank if it was added manually. (Entries which are created by a Moodle component cannot be removed in the normal user interface.)" PREVIOUS="timeadded" NEXT="itemid"/> + <FIELD NAME="itemid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="If the 'component' field is set, this can be used to define the instance of the component that created the entry. Otherwise should be left as default (0)." PREVIOUS="component"/> </FIELDS> <KEYS> <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="groupid"/> @@ -2857,4 +2859,4 @@ </KEYS> </TABLE> </TABLES> -</XMLDB> \ No newline at end of file +</XMLDB> diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index d5b98563d2c9d..4eeeba1c81e5a 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -1119,6 +1119,27 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2012081600.01); } + if ($oldversion < 2012082300.01) { + // Define field component to be added to groups_members + $table = new xmldb_table('groups_members'); + $field = new xmldb_field('component', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null, 'timeadded'); + + // Conditionally launch add field component + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Define field itemid to be added to groups_members + $field = new xmldb_field('itemid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'component'); + + // Conditionally launch add field itemid + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Main savepoint reached + upgrade_main_savepoint(true, 2012082300.01); + } return true; } diff --git a/mod/page/lib.php b/mod/page/lib.php index 0f03e2abcfb26..ea2133f2e08a5 100644 --- a/mod/page/lib.php +++ b/mod/page/lib.php @@ -515,3 +515,7 @@ function page_dndupload_handle($uploadinfo) { return page_add_instance($data, null); } + +function mod_page_allow_group_member_remove($itemid, $groupid, $userid) { + return true; +} diff --git a/theme/base/style/user.css b/theme/base/style/user.css index 64f88e0a0d283..32102d32e5a13 100644 --- a/theme/base/style/user.css +++ b/theme/base/style/user.css @@ -40,6 +40,9 @@ .userselector select {width: 100%;} .userselector div {margin-top: 0.2em;} .userselector div label {margin-right: 0.3em;} +/* Next style does not work in all browsers but looks nicer when it does */ +.userselector .userselector-infobelow {font-size: 0.8em;} + #userselector_options {padding:0.3em 0;} #userselector_options .collapsibleregioncaption {font-weight: bold;} #userselector_options p {margin:0.2em 0;text-align:left;} diff --git a/user/selector/lib.php b/user/selector/lib.php index daecbf1e1a0c4..c1bdb2a7794a0 100644 --- a/user/selector/lib.php +++ b/user/selector/lib.php @@ -578,6 +578,12 @@ protected function output_optgroup($groupname, $users, $select) { unset($this->selected[$user->id]); $output .= ' <option' . $attributes . ' value="' . $user->id . '">' . $this->output_user($user) . "</option>\n"; + if (!empty($user->infobelow)) { + // 'Poor man's indent' here is because CSS styles do not work + // in select options, except in Firefox. + $output .= ' <option disabled="disabled" class="userselector-infobelow">' . + '    ' . s($user->infobelow) . '</option>'; + } } } else { $output = ' <optgroup label="' . htmlspecialchars($groupname) . '">' . "\n"; @@ -712,6 +718,10 @@ protected function convert_array_format($roles, $search) { foreach ($groupedusers[$groupname] as &$user) { unset($user->roles); $user->fullname = fullname($user); + if (!empty($user->component)) { + $user->infobelow = get_string('addedby', 'group', + get_string('pluginname', $user->component)); + } } } return $groupedusers; @@ -726,8 +736,8 @@ class group_members_selector extends groups_user_selector_base { public function find_users($search) { list($wherecondition, $params) = $this->search_sql($search, 'u'); $roles = groups_get_members_by_role($this->groupid, $this->courseid, - $this->required_fields_sql('u'), 'u.lastname, u.firstname', - $wherecondition, $params); + $this->required_fields_sql('u') . ', gm.component', + 'u.lastname, u.firstname', $wherecondition, $params); return $this->convert_array_format($roles, $search); } } diff --git a/user/selector/module.js b/user/selector/module.js index e6818c88f7cc1..80e37521c758b 100644 --- a/user/selector/module.js +++ b/user/selector/module.js @@ -240,6 +240,11 @@ M.core_user.init_user_selector = function (Y, name, hash, extrafields, lastsearc option.set('selected', false); } optgroup.append(option); + if (user.infobelow) { + extraoption = Y.Node.create('<option disabled="disabled" class="userselector-infobelow"/>'); + extraoption.appendChild(document.createTextNode(user.infobelow)); + optgroup.append(extraoption); + } count++; } diff --git a/user/selector/search.php b/user/selector/search.php index 341ebf5b22669..a4f45e4db9290 100644 --- a/user/selector/search.php +++ b/user/selector/search.php @@ -87,6 +87,9 @@ if (!empty($user->disabled)) { $output->disabled = true; } + if (!empty($user->infobelow)) { + $output->infobelow = $user->infobelow; + } $group[$user->id] = $output; } } diff --git a/version.php b/version.php index 528c92696df61..9c9909f1d3162 100644 --- a/version.php +++ b/version.php @@ -30,7 +30,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2012082300.00; // YYYYMMDD = weekly release date of this DEV branch +$version = 2012082300.01; // YYYYMMDD = weekly release date of this DEV branch // RR = release increments - 00 in DEV branches // .XX = incremental changes From aa5f05110f7a3a00f53cc670ec1885658ebd474c Mon Sep 17 00:00:00 2001 From: Tim Hunt <T.J.Hunt@open.ac.uk> Date: Fri, 24 Aug 2012 15:34:20 +0100 Subject: [PATCH 35/90] MDL-35055 question import: slight error with the Match grades option. Even in the 'Error if grade not listed case', it was applying a small tolerance. In the case of a fuzzy match, it was returning the inexact grade from the import file, rather than the precise grade that Moodle was expecting. That causes problems when the editing form is displayed, because the value from the database does not match any of the available options, so the grade is changed to 0%. --- lib/questionlib.php | 41 +++++++++++++++++++--------------- lib/tests/questionlib_test.php | 15 +++++++++++++ 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/lib/questionlib.php b/lib/questionlib.php index 149f351446834..a4792056ced89 100644 --- a/lib/questionlib.php +++ b/lib/questionlib.php @@ -209,39 +209,44 @@ function get_grade_options() { } /** - * match grade options - * if no match return error or match nearest + * Check whether a given grade is one of a list of allowed options. If not, + * depending on $matchgrades, either return the nearest match, or return false + * to signal an error. * @param array $gradeoptionsfull list of valid options * @param int $grade grade to be tested * @param string $matchgrades 'error' or 'nearest' - * @return mixed either 'fixed' value or false if erro + * @return mixed either 'fixed' value or false if error. */ -function match_grade_options($gradeoptionsfull, $grade, $matchgrades='error') { +function match_grade_options($gradeoptionsfull, $grade, $matchgrades = 'error') { + if ($matchgrades == 'error') { - // if we just need an error... + // (Almost) exact match, or an error. foreach ($gradeoptionsfull as $value => $option) { - // slightly fuzzy test, never check floats for equality :-) + // Slightly fuzzy test, never check floats for equality. if (abs($grade - $value) < 0.00001) { - return $grade; + return $value; // Be sure the return the proper value. } } - // didn't find a match so that's an error + // Didn't find a match so that's an error. return false; + } else if ($matchgrades == 'nearest') { - // work out nearest value - $hownear = array(); + // Work out nearest value + $best = false; + $bestmismatch = 2; foreach ($gradeoptionsfull as $value => $option) { - if ($grade==$value) { - return $grade; + $newmismatch = abs($grade - $value); + if ($newmismatch < $bestmismatch) { + $best = $value; + $bestmismatch = $newmismatch; } - $hownear[ $value ] = abs( $grade - $value ); } - // reverse sort list of deltas and grab the last (smallest) - asort( $hownear, SORT_NUMERIC ); - reset( $hownear ); - return key( $hownear ); + return $best; + } else { - return false; + // Unknow option passed. + throw new coding_exception('Unknown $matchgrades ' . $matchgrades . + ' passed to match_grade_options'); } } diff --git a/lib/tests/questionlib_test.php b/lib/tests/questionlib_test.php index 2e5d431cb633b..d7d767d83435c 100644 --- a/lib/tests/questionlib_test.php +++ b/lib/tests/questionlib_test.php @@ -55,4 +55,19 @@ public function test_question_reorder_qtypes() { array(0 => 't1', 1 => 't2', 2 => 't3')); } + public function test_match_grade_options() { + $gradeoptions = question_bank::fraction_options_full(); + + $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.3333333, 'error')); + $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.333333, 'error')); + $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.33333, 'error')); + $this->assertFalse(match_grade_options($gradeoptions, 0.3333, 'error')); + + $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.3333333, 'nearest')); + $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.333333, 'nearest')); + $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.33333, 'nearest')); + $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.33, 'nearest')); + + $this->assertEquals(-0.1428571, match_grade_options($gradeoptions, -0.15, 'nearest')); + } } From fd0680ff4a3506354aabb89272cafe687590dcf7 Mon Sep 17 00:00:00 2001 From: Andrew Robert Nicols <andrew.nicols@luns.net.uk> Date: Thu, 16 Aug 2012 20:58:12 +0100 Subject: [PATCH 36/90] MDL-34936 Warn if the sectioncache property is missing in a get_fast_modinfo call --- lib/modinfolib.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/modinfolib.php b/lib/modinfolib.php index fc24b9b5837ca..6c8997c1499db 100644 --- a/lib/modinfolib.php +++ b/lib/modinfolib.php @@ -1204,6 +1204,10 @@ function get_fast_modinfo(&$course, $userid=0) { debugging('Coding problem - missing course modinfo property in get_fast_modinfo() call'); } + if (!property_exists($course, 'sectioncache')) { + debugging('Coding problem - missing course sectioncache property in get_fast_modinfo() call'); + } + unset($cache[$course->id]); // prevent potential reference problems when switching users $cache[$course->id] = new course_modinfo($course, $userid); From aa9d6e4300cff2458102908f253202f1d665dbc6 Mon Sep 17 00:00:00 2001 From: Tim Hunt <T.J.Hunt@open.ac.uk> Date: Fri, 24 Aug 2012 14:19:27 +0100 Subject: [PATCH 37/90] MDL-32464 qformat multianswer: fix missing include. Also, add a sample file that can be used for testing, and add a unit test to verify this is working. --- question/format/multianswer/format.php | 31 ++++---- .../tests/fixtures/questions.multianswer.txt | 8 ++ .../tests/multianswerformat_test.php | 76 +++++++++++++++++++ question/type/multianswer/questiontype.php | 6 +- 4 files changed, 104 insertions(+), 17 deletions(-) create mode 100644 question/format/multianswer/tests/fixtures/questions.multianswer.txt create mode 100644 question/format/multianswer/tests/multianswerformat_test.php diff --git a/question/format/multianswer/format.php b/question/format/multianswer/format.php index 45179d8d9f639..d7a7ac788a434 100644 --- a/question/format/multianswer/format.php +++ b/question/format/multianswer/format.php @@ -17,10 +17,9 @@ /** * Embedded answer (Cloze) question importer. * - * @package qformat - * @subpackage multianswer - * @copyright 2003 Henrik Kaipe - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package qformat_multianswer + * @copyright 2003 Henrik Kaipe + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -31,32 +30,36 @@ * Importer that imports a text file containing a single Multianswer question * from a text file. * - * @copyright 2003 Henrik Kaipe - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2003 Henrik Kaipe + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class qformat_multianswer extends qformat_default { public function provide_import() { - return true; + return true; } - protected function readquestions($lines) { + public function readquestions($lines) { + question_bank::get_qtype('multianswer'); // Ensure the multianswer code is loaded. + // For this class the method has been simplified as // there can never be more than one question for a - // multianswer import + // multianswer import. $questions = array(); $questiontext = array(); $questiontext['text'] = implode('', $lines); - $questiontext['format'] = 0 ; + $questiontext['format'] = FORMAT_MOODLE; $questiontext['itemid'] = ''; $question = qtype_multianswer_extract_question($questiontext); - $question->questiontext = $question->questiontext['text'] ; - $question->questiontextformat = 0 ; + $question->questiontext = $question->questiontext['text']; + $question->questiontextformat = 0; - $question->qtype = MULTIANSWER; + $question->qtype = 'multianswer'; $question->generalfeedback = ''; - $question->course = $this->course; + $question->generalfeedbackformat = FORMAT_MOODLE; + $question->length = 1; + $question->penalty = 0.3333333; if (!empty($question)) { $name = html_to_text(implode(' ', $lines)); diff --git a/question/format/multianswer/tests/fixtures/questions.multianswer.txt b/question/format/multianswer/tests/fixtures/questions.multianswer.txt new file mode 100644 index 0000000000000..57a56a975f23f --- /dev/null +++ b/question/format/multianswer/tests/fixtures/questions.multianswer.txt @@ -0,0 +1,8 @@ +Match the following cities with the correct state: +* San Francisco: {1:MULTICHOICE:=California#OK~Arizona#Wrong} +* Tucson: {1:MULTICHOICE:California#Wrong~%100%Arizona#OK} +* Los Angeles: {1:MULTICHOICE:=California#OK~Arizona#Wrong} +* Phoenix: {1:MULTICHOICE:%0%California#Wrong~=Arizona#OK} +The capital of France is {1:SHORTANSWER:%100%Paris#Congratulations! +~%50%Marseille#No, that is the second largest city in France (after +Paris).~*#Wrong answer. The capital of France is Paris, of course.}. diff --git a/question/format/multianswer/tests/multianswerformat_test.php b/question/format/multianswer/tests/multianswerformat_test.php new file mode 100644 index 0000000000000..29c32241cb17f --- /dev/null +++ b/question/format/multianswer/tests/multianswerformat_test.php @@ -0,0 +1,76 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Unit tests for the Embedded answer (Cloze) question importer. + * + * @package qformat_multianswer + * @copyright 2012 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->dirroot . '/question/format.php'); +require_once($CFG->dirroot . '/question/format/multianswer/format.php'); +require_once($CFG->dirroot . '/question/engine/tests/helpers.php'); + + +/** + * Unit tests for the Embedded answer (Cloze) question importer. + * + * @copyright 2012 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qformat_multianswer_test extends question_testcase { + + public function test_import() { + $lines = file(__DIR__ . '/fixtures/questions.multianswer.txt'); + + $importer = new qformat_multianswer(); + $qs = $importer->readquestions($lines); + + $expectedq = (object) array( + 'name' => 'Match the following cities with the ...', + 'questiontext' => 'Match the following cities with the correct state: +* San Francisco: {#1} +* Tucson: {#2} +* Los Angeles: {#3} +* Phoenix: {#4} +The capital of France is {#5}. +', + 'questiontextformat' => FORMAT_MOODLE, + 'generalfeedback' => '', + 'generalfeedbackformat' => FORMAT_MOODLE, + 'qtype' => 'multianswer', + 'defaultmark' => 5, + 'penalty' => 0.3333333, + 'length' => 1, + ); + + $this->assertEquals(1, count($qs)); + $this->assert(new question_check_specified_fields_expectation($expectedq), $qs[0]); + + $this->assertEquals('multichoice', $qs[0]->options->questions[1]->qtype); + $this->assertEquals('multichoice', $qs[0]->options->questions[2]->qtype); + $this->assertEquals('multichoice', $qs[0]->options->questions[3]->qtype); + $this->assertEquals('multichoice', $qs[0]->options->questions[4]->qtype); + $this->assertEquals('shortanswer', $qs[0]->options->questions[5]->qtype); + } +} diff --git a/question/type/multianswer/questiontype.php b/question/type/multianswer/questiontype.php index c87e8e7012eb3..b39848d8b7a85 100644 --- a/question/type/multianswer/questiontype.php +++ b/question/type/multianswer/questiontype.php @@ -304,7 +304,7 @@ function qtype_multianswer_extract_question($text) { $question->defaultmark = 0; // Will be increased for each answer norm for ($positionkey = 1; - preg_match('/'.ANSWER_REGEX.'/', $question->questiontext['text'], $answerregs); + preg_match('/'.ANSWER_REGEX.'/s', $question->questiontext['text'], $answerregs); ++$positionkey) { $wrapped = new stdClass(); $wrapped->generalfeedback['text'] = ''; @@ -390,7 +390,7 @@ function qtype_multianswer_extract_question($text) { $answerindex = 0; $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES]; - while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/', $remainingalts, $altregs)) { + while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/s', $remainingalts, $altregs)) { if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) { $wrapped->fraction["$answerindex"] = '1'; } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]) { @@ -412,7 +412,7 @@ function qtype_multianswer_extract_question($text) { } if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL]) - && preg_match('~'.NUMERICAL_ALTERNATIVE_REGEX.'~', + && preg_match('~'.NUMERICAL_ALTERNATIVE_REGEX.'~s', $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) { $wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER]; if (array_key_exists(NUMERICAL_ABS_ERROR_MARGIN, $numregs)) { From 9b90584f435e864e741eb4b9314b119fa73f9de4 Mon Sep 17 00:00:00 2001 From: Andreas Grabs <moodle@grabs-edv.de> Date: Fri, 24 Aug 2012 21:53:45 +0200 Subject: [PATCH 38/90] MDL-34952 - change get_info() to public access --- mod/feedback/item/multichoice/lib.php | 2 +- mod/feedback/item/multichoicerated/lib.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mod/feedback/item/multichoice/lib.php b/mod/feedback/item/multichoice/lib.php index 4b7a841bf8860..c129b45fa05f6 100644 --- a/mod/feedback/item/multichoice/lib.php +++ b/mod/feedback/item/multichoice/lib.php @@ -609,7 +609,7 @@ public function get_hasvalue() { return 1; } - private function get_info($item) { + public function get_info($item) { $presentation = empty($item->presentation) ? '' : $item->presentation; $info = new stdClass(); diff --git a/mod/feedback/item/multichoicerated/lib.php b/mod/feedback/item/multichoicerated/lib.php index 00ee832a7d151..696afda58d57d 100644 --- a/mod/feedback/item/multichoicerated/lib.php +++ b/mod/feedback/item/multichoicerated/lib.php @@ -465,7 +465,7 @@ public function get_hasvalue() { return 1; } - private function get_info($item) { + public function get_info($item) { $presentation = empty($item->presentation) ? '' : $item->presentation; $info = new stdClass(); From 2c65cd8462b6a436b9cdb3c37ffccdf20c561753 Mon Sep 17 00:00:00 2001 From: Andreas Grabs <moodle@grabs-edv.de> Date: Fri, 24 Aug 2012 23:08:55 +0200 Subject: [PATCH 39/90] MDL-34996 - Improve help text for Feedback dependancies --- mod/feedback/lang/en/feedback.php | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/mod/feedback/lang/en/feedback.php b/mod/feedback/lang/en/feedback.php index c35b24cadd82d..2e27645f27700 100644 --- a/mod/feedback/lang/en/feedback.php +++ b/mod/feedback/lang/en/feedback.php @@ -62,28 +62,26 @@ $string['delete_old_items'] = 'Delete old items'; $string['delete_template'] = 'Delete template'; $string['delete_templates'] = 'Delete template...'; -$string['depending'] = 'depending items'; -$string['depending_help'] = 'Depending items allow you to show items depend on values from other items.<br /> -<strong>Here an build example to use it:</strong><br /> +$string['depending'] = 'Dependencies'; +$string['depending_help'] = 'It is possible to show an item depending on the value of another item.<br /> +<strong>Here is an example.</strong><br /> <ul> -<li>First create an item on which value other items depends.</li> -<li>Next add a pagebreak.</li> -<li>Next add the items depend on the item-value before<br /> -Choose in the item creation-form the item in the list "depend item" and put the needed value into the textbox "depend value".</li> +<li>First, create an item on which another item will depend on.</li> +<li>Next, add a pagebreak.</li> +<li>Then add the items dependant on the value of the item created before. Choose the item from the list labelled "Dependence item" and write the required value in the textbox labelled "Dependence value".</li> </ul> -<strong>The structure should looks like this:</strong> +<strong>The item structure should look like this.</strong> <ol> -<li>Item Q: do you have a car? A: yes/no</li> +<li>Item Q: Do you have a car? A: yes/no</li> <li>Pagebreak</li> -<li>Item Q: what color has your car?<br /> +<li>Item Q: What colour is your car?<br /> (this item depends on item 1 with value = yes)</li> -<li>Item Q: why you have not a car?<br /> +<li>Item Q: Why don\'t you have a car?<br /> (this item depends on item 1 with value = no)</li> <li> ... other items</li> -</ol> -That is all. Have fun!'; -$string['dependitem'] = 'depend item'; -$string['dependvalue'] = 'depend value'; +</ol>'; +$string['dependitem'] = 'Dependence item'; +$string['dependvalue'] = 'Dependence value'; $string['description'] = 'Description'; $string['do_not_analyse_empty_submits'] = 'Do not analyse empty submits'; $string['dropdown'] = 'Multiple choice - single answer allowed (dropdownlist)'; From 33c7a2d121b950ba042e02e7501c2ece46901795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= <commits@skodak.org> Date: Sat, 25 Aug 2012 10:14:38 +0200 Subject: [PATCH 40/90] MDL-35060 remove unused session test file --- lib/session-test.php | 39 --------------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 lib/session-test.php diff --git a/lib/session-test.php b/lib/session-test.php deleted file mode 100644 index f38479f3ade1c..0000000000000 --- a/lib/session-test.php +++ /dev/null @@ -1,39 +0,0 @@ -<?php -// This file is part of Moodle - http://moodle.org/ -// -// Moodle is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Moodle is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Moodle. If not, see <http://www.gnu.org/licenses/>. - -/** - * This is a tiny standalone diagnostic script to test that sessions - * are working correctly on a given server. - * - * Just run it from a browser. The first time you run it will - * set a new variable, and after that it will try to find it again. - * The random number is just to prevent browser caching. - * - * @todo add code that actually tests moodle sessions, the old one only tested - * PHP sessions used from installer, not the real moodle sessions - * @package moodlecore - * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -/** Include config {@see config.php} */ -require '../config.php'; - -$PAGE->set_url('/lib/session-test.php'); - -error('session test not reimplemented yet'); //DO NOT localize or use print_error()! -// -//TODO: add code that actually tests moodle sessions, the old one only tested PHP sessions used from installer, not the real moodle sessions From 19de315e839fb2f99166300881314d79ab8beff2 Mon Sep 17 00:00:00 2001 From: Tim Hunt <T.J.Hunt@open.ac.uk> Date: Thu, 12 Jul 2012 19:11:06 +0100 Subject: [PATCH 41/90] MDL-34306 gift question format: allow import of general feedback This change introduces #### as a separator for general feedback. You need to add ####General feedback goes here as the last thing inside the {...}. For example // question: 123 name: Shortanswer ::Shortanswer::Which is the best animal?{ =Frog#Good! =%50%Cat#What is it with Moodlers and cats? =%0%*#Completely wrong ####Here is some general feedback! } Note that this change is not entirely backwards compatible. It will break any existing GIFT file where the character sequence #### us used between the {} as part of the question. This seems highly unlikely. --- question/format/gift/format.php | 68 ++++++++-- .../gift/tests/fixtures/questions.gift.txt | 5 +- .../format/gift/tests/giftformat_test.php | 117 ++++++++++++++++++ 3 files changed, 175 insertions(+), 15 deletions(-) diff --git a/question/format/gift/format.php b/question/format/gift/format.php index 06bcebc564a17..9373b066bfb26 100644 --- a/question/format/gift/format.php +++ b/question/format/gift/format.php @@ -213,42 +213,56 @@ public function readquestion($lines) { $question->name = false; } - - // FIND ANSWER section - // no answer means its a description + // Find the answer section. $answerstart = strpos($text, '{'); $answerfinish = strpos($text, '}'); $description = false; - if (($answerstart === false) and ($answerfinish === false)) { + if ($answerstart === false && $answerfinish === false) { + // No answer means it's a description. $description = true; $answertext = ''; $answerlength = 0; - } else if (!(($answerstart !== false) and ($answerfinish !== false))) { + + } else if ($answerstart === false || $answerfinish === false) { $this->error(get_string('braceerror', 'qformat_gift'), $text); return false; + } else { $answerlength = $answerfinish - $answerstart; $answertext = trim(substr($text, $answerstart + 1, $answerlength - 1)); } - // Format QUESTION TEXT without answer, inserting "_____" as necessary + // Format the question text, without answer, inserting "_____" as necessary. if ($description) { $questiontext = $text; } else if (substr($text, -1) == "}") { - // no blank line if answers follow question, outside of closing punctuation - $questiontext = substr_replace($text, "", $answerstart, $answerlength+1); + // No blank line if answers follow question, outside of closing punctuation. + $questiontext = substr_replace($text, "", $answerstart, $answerlength + 1); } else { - // inserts blank line for missing word format - $questiontext = substr_replace($text, "_____", $answerstart, $answerlength+1); + // Inserts blank line for missing word format. + $questiontext = substr_replace($text, "_____", $answerstart, $answerlength + 1); } - // Get questiontext format from questiontext + // Look to see if there is any general feedback. + $gfseparator = strrpos($answertext, '####'); + if ($gfseparator === false) { + $generalfeedback = ''; + } else { + $generalfeedback = substr($answertext, $gfseparator + 4); + $answertext = trim(substr($answertext, 0, $gfseparator)); + } + + // Get questiontext format from questiontext. $text = $this->parse_text_with_format($questiontext); $question->questiontextformat = $text['format']; - $question->generalfeedbackformat = $text['format']; $question->questiontext = $text['text']; + // Get generalfeedback format from questiontext. + $text = $this->parse_text_with_format($generalfeedback, $question->questiontextformat); + $question->generalfeedback = $text['text']; + $question->generalfeedbackformat = $text['format']; + // set question name if not already set if ($question->name === false) { $question->name = $question->questiontext; @@ -609,6 +623,27 @@ public function write_questiontext($text, $format, $defaultformat = FORMAT_MOODL return $output; } + /** + * Outputs the general feedback for the question, if any. This needs to be the + * last thing before the }. + * @param object $question the question data. + * @param string $indent to put before the general feedback. Defaults to a tab. + * If this is not blank, a newline is added after the line. + */ + public function write_general_feedback($question, $indent = "\t") { + $generalfeedback = $this->write_questiontext($question->generalfeedback, + $question->generalfeedbackformat, $question->questiontextformat); + + if ($generalfeedback) { + $generalfeedback = '####' . $generalfeedback; + if ($indent) { + $generalfeedback = $indent . $generalfeedback . "\n"; + } + } + + return $generalfeedback; + } + public function writequestion($question) { global $OUTPUT; @@ -631,7 +666,9 @@ public function writequestion($question) { case ESSAY: $expout .= $this->write_name($question->name); $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat); - $expout .= "{}\n"; + $expout .= "{"; + $expout .= $this->write_general_feedback($question, ''); + $expout .= "}\n"; break; case TRUEFALSE: @@ -662,6 +699,7 @@ public function writequestion($question) { if ($rightfeedback) { $expout .= '#' . $rightfeedback; } + $expout .= $this->write_general_feedback($question, ''); $expout .= "}\n"; break; @@ -686,6 +724,7 @@ public function writequestion($question) { } $expout .= "\n"; } + $expout .= $this->write_general_feedback($question); $expout .= "}\n"; break; @@ -699,6 +738,7 @@ public function writequestion($question) { '#' . $this->write_questiontext($answer->feedback, $answer->feedbackformat, $question->questiontextformat) . "\n"; } + $expout .= $this->write_general_feedback($question); $expout .= "}\n"; break; @@ -717,6 +757,7 @@ public function writequestion($question) { $answer->feedbackformat, $question->questiontextformat) . "\n"; } } + $expout .= $this->write_general_feedback($question); $expout .= "}\n"; break; @@ -729,6 +770,7 @@ public function writequestion($question) { $subquestion->questiontextformat, $question->questiontextformat) . ' -> ' . $this->repchar($subquestion->answertext) . "\n"; } + $expout .= $this->write_general_feedback($question); $expout .= "}\n"; break; diff --git a/question/format/gift/tests/fixtures/questions.gift.txt b/question/format/gift/tests/fixtures/questions.gift.txt index e7ad9a611a691..c434dcdc09669 100644 --- a/question/format/gift/tests/fixtures/questions.gift.txt +++ b/question/format/gift/tests/fixtures/questions.gift.txt @@ -37,9 +37,10 @@ =%0%*#Completely wrong } -// true/false +// true/false, with general feedback ::Q1:: 42 is the Absolute Answer to everything.{ -FALSE#42 is the Ultimate Answer.#You gave the right answer.}"; +FALSE#42 is the Ultimate Answer.#You gave the right answer. +####This is, of course, a Hitchiker's Guide to the Galaxy reference.}"; // name 0-11 ::2-08 TSL::TSL is blablabla.{T} diff --git a/question/format/gift/tests/giftformat_test.php b/question/format/gift/tests/giftformat_test.php index e06a12344afa5..54fcdb308c924 100644 --- a/question/format/gift/tests/giftformat_test.php +++ b/question/format/gift/tests/giftformat_test.php @@ -673,6 +673,62 @@ public function test_import_shortanswer() { $this->assert(new question_check_specified_fields_expectation($expectedq), $q); } + public function test_import_shortanswer_with_general_feedback() { + $gift = " +// question: 666 name: Shortanswer +::Shortanswer::Which is the best animal?{ + =Frog#Good! + =%50%Cat#What is it with Moodlers and cats? + =%0%*#Completely wrong + ####[html]Here is some general feedback! +}"; + $lines = preg_split('/[\\n\\r]/', str_replace("\r\n", "\n", $gift)); + + $importer = new qformat_gift(); + $q = $importer->readquestion($lines); + + $expectedq = (object) array( + 'name' => 'Shortanswer', + 'questiontext' => "Which is the best animal?", + 'questiontextformat' => FORMAT_MOODLE, + 'generalfeedback' => 'Here is some general feedback!', + 'generalfeedbackformat' => FORMAT_HTML, + 'qtype' => 'shortanswer', + 'defaultmark' => 1, + 'penalty' => 0.3333333, + 'length' => 1, + 'answer' => array( + 'Frog', + 'Cat', + '*', + ), + 'fraction' => array(1, 0.5, 0), + 'feedback' => array( + 0 => array( + 'text' => 'Good!', + 'format' => FORMAT_MOODLE, + 'files' => array(), + ), + 1 => array( + 'text' => "What is it with Moodlers and cats?", + 'format' => FORMAT_MOODLE, + 'files' => array(), + ), + 2 => array( + 'text' => "Completely wrong", + 'format' => FORMAT_MOODLE, + 'files' => array(), + ), + ), + ); + + // Repeated test for better failure messages. + $this->assertEquals($expectedq->answer, $q->answer); + $this->assertEquals($expectedq->fraction, $q->fraction); + $this->assertEquals($expectedq->feedback, $q->feedback); + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + public function test_export_shortanswer() { $qdata = (object) array( 'id' => 666 , @@ -728,6 +784,67 @@ public function test_export_shortanswer() { \t=%0%*#Completely wrong } +"; + + $this->assert_same_gift($expectedgift, $gift); + } + + public function test_export_shortanswer_with_general_feedback() { + $qdata = (object) array( + 'id' => 666 , + 'name' => 'Shortanswer', + 'questiontext' => "Which is the best animal?", + 'questiontextformat' => FORMAT_MOODLE, + 'generalfeedback' => 'Here is some general feedback!', + 'generalfeedbackformat' => FORMAT_HTML, + 'defaultmark' => 1, + 'penalty' => 1, + 'length' => 1, + 'qtype' => 'shortanswer', + 'options' => (object) array( + 'id' => 123, + 'question' => 666, + 'usecase' => 1, + 'answers' => array( + 1 => (object) array( + 'id' => 1, + 'answer' => 'Frog', + 'answerformat' => 0, + 'fraction' => 1, + 'feedback' => 'Good!', + 'feedbackformat' => FORMAT_MOODLE, + ), + 2 => (object) array( + 'id' => 2, + 'answer' => 'Cat', + 'answerformat' => 0, + 'fraction' => 0.5, + 'feedback' => "What is it with Moodlers and cats?", + 'feedbackformat' => FORMAT_MOODLE, + ), + 3 => (object) array( + 'id' => 3, + 'answer' => '*', + 'answerformat' => 0, + 'fraction' => 0, + 'feedback' => "Completely wrong", + 'feedbackformat' => FORMAT_MOODLE, + ), + ), + ), + ); + + $exporter = new qformat_gift(); + $gift = $exporter->writequestion($qdata); + + $expectedgift = "// question: 666 name: Shortanswer +::Shortanswer::Which is the best animal?{ +\t=%100%Frog#Good! +\t=%50%Cat#What is it with Moodlers and cats? +\t=%0%*#Completely wrong +\t####[html]Here is some general feedback! +} + "; $this->assert_same_gift($expectedgift, $gift); From 84bc3773cceb12e4b4f3d568872ce7e30544ab03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= <commits@skodak.org> Date: Sun, 26 Aug 2012 12:17:11 +0200 Subject: [PATCH 42/90] MDL-35064 add option to keep enrolments when uninstalling plugin --- admin/enrol.php | 51 ++++++-- enrol/manual/locallib.php | 101 +++++++++++++++- enrol/manual/tests/lib_test.php | 204 ++++++++++++++++++++++++++++++++ lang/en/enrol.php | 7 +- 4 files changed, 354 insertions(+), 9 deletions(-) create mode 100644 enrol/manual/tests/lib_test.php diff --git a/admin/enrol.php b/admin/enrol.php index 9fc196f54c037..b0818ddf5b3f5 100644 --- a/admin/enrol.php +++ b/admin/enrol.php @@ -23,12 +23,15 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +define('NO_OUTPUT_BUFFERING', true); + require_once('../config.php'); require_once($CFG->libdir.'/adminlib.php'); $action = required_param('action', PARAM_ALPHANUMEXT); $enrol = required_param('enrol', PARAM_PLUGIN); $confirm = optional_param('confirm', 0, PARAM_BOOL); +$migrate = optional_param('migrate', 0, PARAM_BOOL); $PAGE->set_url('/admin/enrol.php'); $PAGE->set_context(context_system::instance()); @@ -94,24 +97,58 @@ break; case 'uninstall': - echo $OUTPUT->header(); - echo $OUTPUT->heading(get_string('enrolments', 'enrol')); - if (get_string_manager()->string_exists('pluginname', 'enrol_'.$enrol)) { $strplugin = get_string('pluginname', 'enrol_'.$enrol); } else { $strplugin = $enrol; } + echo $PAGE->set_title($strplugin); + echo $OUTPUT->header(); + if (!$confirm) { - $uurl = new moodle_url('/admin/enrol.php', array('action'=>'uninstall', 'enrol'=>$enrol, 'sesskey'=>sesskey(), 'confirm'=>1)); - echo $OUTPUT->confirm(get_string('uninstallconfirm', 'enrol', $strplugin), $uurl, $return); + echo $OUTPUT->heading(get_string('enrolments', 'enrol')); + + $deleteurl = new moodle_url('/admin/enrol.php', array('action'=>'uninstall', 'enrol'=>$enrol, 'sesskey'=>sesskey(), 'confirm'=>1, 'migrate'=>0)); + $migrateurl = new moodle_url('/admin/enrol.php', array('action'=>'uninstall', 'enrol'=>$enrol, 'sesskey'=>sesskey(), 'confirm'=>1, 'migrate'=>1)); + + $migrate = new single_button($migrateurl, get_string('uninstallmigrate', 'enrol')); + $delete = new single_button($deleteurl, get_string('uninstalldelete', 'enrol')); + $cancel = new single_button($return, get_string('cancel'), 'get'); + + $buttons = $OUTPUT->render($delete) . $OUTPUT->render($cancel); + if ($enrol !== 'manual') { + $buttons = $OUTPUT->render($migrate) . $buttons; + } + + echo $OUTPUT->box_start('generalbox', 'notice'); + echo html_writer::tag('p', markdown_to_html(get_string('uninstallconfirm', 'enrol', $strplugin))); + echo html_writer::tag('div', $buttons, array('class' => 'buttons')); + echo $OUTPUT->box_end(); + echo $OUTPUT->footer(); exit; - } else { // Delete everything!! + } else { + // This may take a long time. + set_time_limit(0); + + // Disable plugin to prevent concurrent cron execution. + unset($enabled[$enrol]); + set_config('enrol_plugins_enabled', implode(',', array_keys($enabled))); + + if ($migrate) { + echo $OUTPUT->heading(get_string('uninstallmigrating', 'enrol', 'enrol_'.$enrol)); + + require_once("$CFG->dirroot/enrol/manual/locallib.php"); + enrol_manual_migrate_plugin_enrolments($enrol); + + echo $OUTPUT->notification(get_string('success'), 'notifysuccess'); + } + + // Delete everything!! uninstall_plugin('enrol', $enrol); - $syscontext->mark_dirty(); // resets all enrol caches + $syscontext->mark_dirty(); // Resets all enrol caches. $a = new stdClass(); $a->plugin = $strplugin; diff --git a/enrol/manual/locallib.php b/enrol/manual/locallib.php index 1d09681ad1221..7b1c23590f330 100644 --- a/enrol/manual/locallib.php +++ b/enrol/manual/locallib.php @@ -353,4 +353,103 @@ public function process(course_enrolment_manager $manager, array $users, stdClas } return true; } -} \ No newline at end of file +} + + +/** + * Migrates all enrolments of the given plugin to enrol_manual plugin, + * this is used for example during plugin uninstallation. + * + * NOTE: this function does not trigger role and enrolment related events. + * + * @param string $enrol + */ +function enrol_manual_migrate_plugin_enrolments($enrol) { + global $DB; + + if ($enrol === 'manual') { + // We can not migrate to self. + return; + } + + $manualplugin = enrol_get_plugin('manual'); + + $params = array('enrol'=>$enrol); + $sql = "SELECT e.id, e.courseid, e.status, MIN(me.id) AS mid, COUNT(ue.id) AS cu + FROM {enrol} e + JOIN {user_enrolments} ue ON (ue.enrolid = e.id) + JOIN {course} c ON (c.id = e.courseid) + LEFT JOIN {enrol} me ON (me.courseid = e.courseid AND me.enrol='manual') + WHERE e.enrol = :enrol + GROUP BY e.id, e.courseid, e.status + ORDER BY e.id"; + $rs = $DB->get_recordset_sql($sql, $params); + + foreach($rs as $e) { + $minstance = false; + if (!$e->mid) { + // Manual instance does not exist yet, add a new one. + $course = $DB->get_record('course', array('id'=>$e->courseid), '*', MUST_EXIST); + if ($minstance = $DB->get_record('enrol', array('courseid'=>$course->id, 'enrol'=>'manual'))) { + // Already created by previous iteration. + $e->mid = $minstance->id; + } else if ($e->mid = $manualplugin->add_default_instance($course)) { + $minstance = $DB->get_record('enrol', array('id'=>$e->mid)); + if ($e->status != ENROL_INSTANCE_ENABLED) { + $DB->set_field('enrol', 'status', ENROL_INSTANCE_DISABLED, array('id'=>$e->mid)); + $minstance->status = ENROL_INSTANCE_DISABLED; + } + } + } else { + $minstance = $DB->get_record('enrol', array('id'=>$e->mid)); + } + + if (!$minstance) { + // This should never happen unless adding of default instance fails unexpectedly. + continue; + } + + // First delete potential role duplicates. + $params = array('id'=>$e->id, 'component'=>'enrol_'.$enrol, 'empty'=>$DB->sql_empty()); + $sql = "SELECT ra.id + FROM {role_assignments} ra + JOIN {role_assignments} mra ON (mra.contextid = ra.contextid AND mra.userid = ra.userid AND mra.roleid = ra.roleid AND mra.component = :empty AND mra.itemid = 0) + WHERE ra.component = :component AND ra.itemid = :id"; + $ras = $DB->get_records_sql($sql, $params); + $ras = array_keys($ras); + $DB->delete_records_list('role_assignments', 'id', $ras); + unset($ras); + + // Migrate roles. + $sql = "UPDATE {role_assignments} + SET itemid = 0, component = :empty + WHERE itemid = :id AND component = :component"; + $params = array('empty'=>$DB->sql_empty(), 'id'=>$e->id, 'component'=>'enrol_'.$enrol); + $DB->execute($sql, $params); + + // Delete potential enrol duplicates. + $params = array('id'=>$e->id, 'mid'=>$e->mid); + $sql = "SELECT ue.id + FROM {user_enrolments} ue + JOIN {user_enrolments} mue ON (mue.userid = ue.userid AND mue.enrolid = :mid) + WHERE ue.enrolid = :id"; + $ues = $DB->get_records_sql($sql, $params); + $ues = array_keys($ues); + $DB->delete_records_list('user_enrolments', 'id', $ues); + unset($ues); + + // Migrate to manual enrol instance. + $params = array('id'=>$e->id, 'mid'=>$e->mid); + if ($e->status != ENROL_INSTANCE_ENABLED and $minstance->status == ENROL_INSTANCE_ENABLED) { + $status = ", status = :disabled"; + $params['disabled'] = ENROL_USER_SUSPENDED; + } else { + $status = ""; + } + $sql = "UPDATE {user_enrolments} + SET enrolid = :mid $status + WHERE enrolid = :id"; + $DB->execute($sql, $params); + } + $rs->close(); +} diff --git a/enrol/manual/tests/lib_test.php b/enrol/manual/tests/lib_test.php new file mode 100644 index 0000000000000..ed18c4f81386a --- /dev/null +++ b/enrol/manual/tests/lib_test.php @@ -0,0 +1,204 @@ +<?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/>. + +/** + * Manual enrolment tests. + * + * @package enrol_manual + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Manual enrolment tests. + * + * @package enrol_manual + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class enrol_manual_lib_testcase extends advanced_testcase { + /** + * Test enrol migration function used when uninstalling enrol plugins. + */ + public function test_migrate_plugin_enrolments() { + global $DB, $CFG; + require_once($CFG->dirroot.'/enrol/manual/locallib.php'); + + $this->resetAfterTest(); + + $manplugin = enrol_get_plugin('manual'); + + // Setup a few courses and users. + + $studentrole = $DB->get_record('role', array('shortname'=>'student')); + $this->assertNotEmpty($studentrole); + $teacherrole = $DB->get_record('role', array('shortname'=>'teacher')); + $this->assertNotEmpty($teacherrole); + + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course3 = $this->getDataGenerator()->create_course(); + $course4 = $this->getDataGenerator()->create_course(); + $course5 = $this->getDataGenerator()->create_course(); + + $context1 = context_course::instance($course1->id); + $context2 = context_course::instance($course2->id); + $context3 = context_course::instance($course3->id); + $context4 = context_course::instance($course4->id); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + $user4 = $this->getDataGenerator()->create_user(); + + // We expect manual, self and guest instances to be created by default. + + $this->assertEquals(5, $DB->count_records('enrol', array('enrol'=>'manual'))); + $this->assertEquals(5, $DB->count_records('enrol', array('enrol'=>'self'))); + $this->assertEquals(5, $DB->count_records('enrol', array('enrol'=>'guest'))); + $this->assertEquals(15, $DB->count_records('enrol', array())); + + $this->assertEquals(0, $DB->count_records('user_enrolments', array())); + + // Enrol some users to manual instances. + + $maninstance1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'manual'), '*', MUST_EXIST); + $DB->set_field('enrol', 'status', ENROL_INSTANCE_DISABLED, array('id'=>$maninstance1->id)); + $maninstance1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'manual'), '*', MUST_EXIST); + $maninstance2 = $DB->get_record('enrol', array('courseid'=>$course2->id, 'enrol'=>'manual'), '*', MUST_EXIST); + $DB->delete_records('enrol', array('courseid'=>$course3->id, 'enrol'=>'manual')); + $DB->delete_records('enrol', array('courseid'=>$course4->id, 'enrol'=>'manual')); + $DB->delete_records('enrol', array('courseid'=>$course5->id, 'enrol'=>'manual')); + + $manplugin->enrol_user($maninstance1, $user1->id, $studentrole->id); + $manplugin->enrol_user($maninstance1, $user2->id, $studentrole->id); + $manplugin->enrol_user($maninstance1, $user3->id, $teacherrole->id); + $manplugin->enrol_user($maninstance2, $user3->id, $teacherrole->id); + + $this->assertEquals(4, $DB->count_records('user_enrolments', array())); + + // Set up some bogus enrol plugin instances and enrolments. + + $xxxinstance1 = $DB->insert_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'xxx', 'status'=>ENROL_INSTANCE_ENABLED)); + $xxxinstance1 = $DB->get_record('enrol', array('id'=>$xxxinstance1)); + $xxxinstance3 = $DB->insert_record('enrol', array('courseid'=>$course3->id, 'enrol'=>'xxx', 'status'=>ENROL_INSTANCE_DISABLED)); + $xxxinstance3 = $DB->get_record('enrol', array('id'=>$xxxinstance3)); + $xxxinstance4 = $DB->insert_record('enrol', array('courseid'=>$course4->id, 'enrol'=>'xxx', 'status'=>ENROL_INSTANCE_ENABLED)); + $xxxinstance4 = $DB->get_record('enrol', array('id'=>$xxxinstance4)); + $xxxinstance4b = $DB->insert_record('enrol', array('courseid'=>$course4->id, 'enrol'=>'xxx', 'status'=>ENROL_INSTANCE_DISABLED)); + $xxxinstance4b = $DB->get_record('enrol', array('id'=>$xxxinstance4b)); + + + $DB->insert_record('user_enrolments', array('enrolid'=>$xxxinstance1->id, 'userid'=>$user1->id, 'status'=>ENROL_USER_SUSPENDED)); + role_assign($studentrole->id, $user1->id, $context1->id, 'enrol_xxx', $xxxinstance1->id); + role_assign($teacherrole->id, $user1->id, $context1->id, 'enrol_xxx', $xxxinstance1->id); + $DB->insert_record('user_enrolments', array('enrolid'=>$xxxinstance1->id, 'userid'=>$user4->id, 'status'=>ENROL_USER_ACTIVE)); + role_assign($studentrole->id, $user4->id, $context1->id, 'enrol_xxx', $xxxinstance1->id); + $this->assertEquals(2, $DB->count_records('user_enrolments', array('enrolid'=>$xxxinstance1->id))); + $this->assertEquals(6, $DB->count_records('role_assignments', array('contextid'=>$context1->id))); + + + $DB->insert_record('user_enrolments', array('enrolid'=>$xxxinstance3->id, 'userid'=>$user1->id, 'status'=>ENROL_USER_ACTIVE)); + role_assign($studentrole->id, $user1->id, $context3->id, 'enrol_xxx', $xxxinstance3->id); + $DB->insert_record('user_enrolments', array('enrolid'=>$xxxinstance3->id, 'userid'=>$user2->id, 'status'=>ENROL_USER_SUSPENDED)); + $this->assertEquals(2, $DB->count_records('user_enrolments', array('enrolid'=>$xxxinstance3->id))); + $this->assertEquals(1, $DB->count_records('role_assignments', array('contextid'=>$context3->id))); + + $DB->insert_record('user_enrolments', array('enrolid'=>$xxxinstance4->id, 'userid'=>$user1->id, 'status'=>ENROL_USER_ACTIVE)); + role_assign($studentrole->id, $user1->id, $context4->id, 'enrol_xxx', $xxxinstance4->id); + $DB->insert_record('user_enrolments', array('enrolid'=>$xxxinstance4->id, 'userid'=>$user2->id, 'status'=>ENROL_USER_ACTIVE)); + role_assign($studentrole->id, $user2->id, $context4->id, 'enrol_xxx', $xxxinstance4->id); + $DB->insert_record('user_enrolments', array('enrolid'=>$xxxinstance4b->id, 'userid'=>$user1->id, 'status'=>ENROL_USER_SUSPENDED)); + role_assign($teacherrole->id, $user1->id, $context4->id, 'enrol_xxx', $xxxinstance4b->id); + $DB->insert_record('user_enrolments', array('enrolid'=>$xxxinstance4b->id, 'userid'=>$user4->id, 'status'=>ENROL_USER_ACTIVE)); + role_assign($teacherrole->id, $user4->id, $context4->id, 'enrol_xxx', $xxxinstance4b->id); + $this->assertEquals(2, $DB->count_records('user_enrolments', array('enrolid'=>$xxxinstance4->id))); + $this->assertEquals(2, $DB->count_records('user_enrolments', array('enrolid'=>$xxxinstance4b->id))); + $this->assertEquals(4, $DB->count_records('role_assignments', array('contextid'=>$context4->id))); + + // Finally do the migration. + + enrol_manual_migrate_plugin_enrolments('xxx'); + + // Verify results. + + $this->assertEquals(1, $DB->count_records('enrol', array('courseid'=>$course1->id, 'enrol'=>'manual'))); + $this->assertEquals(1, $DB->count_records('enrol', array('courseid'=>$course1->id, 'enrol'=>'xxx'))); + $maninstance1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'manual'), '*', MUST_EXIST); + $this->assertEquals(ENROL_INSTANCE_DISABLED, $maninstance1->status); + $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$maninstance1->id, 'userid'=>$user1->id, 'status'=>ENROL_USER_ACTIVE))); + $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$maninstance1->id, 'userid'=>$user2->id, 'status'=>ENROL_USER_ACTIVE))); + $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$maninstance1->id, 'userid'=>$user3->id, 'status'=>ENROL_USER_ACTIVE))); + $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$maninstance1->id, 'userid'=>$user4->id, 'status'=>ENROL_USER_ACTIVE))); + $this->assertEquals(4, $DB->count_records('user_enrolments', array('enrolid'=>$maninstance1->id))); + $this->assertEquals(0, $DB->count_records('user_enrolments', array('enrolid'=>$xxxinstance1->id))); + $this->assertTrue($DB->record_exists('role_assignments', array('itemid'=>0, 'component'=>$DB->sql_empty(), 'userid'=>$user1->id, 'roleid'=>$studentrole->id, 'contextid'=>$context1->id))); + $this->assertTrue($DB->record_exists('role_assignments', array('itemid'=>0, 'component'=>$DB->sql_empty(), 'userid'=>$user1->id, 'roleid'=>$teacherrole->id, 'contextid'=>$context1->id))); + $this->assertTrue($DB->record_exists('role_assignments', array('itemid'=>0, 'component'=>$DB->sql_empty(), 'userid'=>$user2->id, 'roleid'=>$studentrole->id, 'contextid'=>$context1->id))); + $this->assertTrue($DB->record_exists('role_assignments', array('itemid'=>0, 'component'=>$DB->sql_empty(), 'userid'=>$user3->id, 'roleid'=>$teacherrole->id, 'contextid'=>$context1->id))); + $this->assertTrue($DB->record_exists('role_assignments', array('itemid'=>0, 'component'=>$DB->sql_empty(), 'userid'=>$user4->id, 'roleid'=>$studentrole->id, 'contextid'=>$context1->id))); + $this->assertEquals(5, $DB->count_records('role_assignments', array('contextid'=>$context1->id))); + + + $this->assertEquals(1, $DB->count_records('enrol', array('courseid'=>$course2->id, 'enrol'=>'manual'))); + $this->assertEquals(0, $DB->count_records('enrol', array('courseid'=>$course2->id, 'enrol'=>'xxx'))); + $maninstance2 = $DB->get_record('enrol', array('courseid'=>$course2->id, 'enrol'=>'manual'), '*', MUST_EXIST); + $this->assertEquals(ENROL_INSTANCE_ENABLED, $maninstance2->status); + + + $this->assertEquals(1, $DB->count_records('enrol', array('courseid'=>$course3->id, 'enrol'=>'manual'))); + $this->assertEquals(1, $DB->count_records('enrol', array('courseid'=>$course3->id, 'enrol'=>'xxx'))); + $maninstance3 = $DB->get_record('enrol', array('courseid'=>$course3->id, 'enrol'=>'manual'), '*', MUST_EXIST); + $this->assertEquals(ENROL_INSTANCE_DISABLED, $maninstance3->status); + $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$maninstance3->id, 'userid'=>$user1->id, 'status'=>ENROL_USER_ACTIVE))); + $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$maninstance3->id, 'userid'=>$user2->id, 'status'=>ENROL_USER_SUSPENDED))); + $this->assertEquals(2, $DB->count_records('user_enrolments', array('enrolid'=>$maninstance3->id))); + $this->assertEquals(0, $DB->count_records('user_enrolments', array('enrolid'=>$xxxinstance3->id))); + $this->assertTrue($DB->record_exists('role_assignments', array('itemid'=>0, 'component'=>$DB->sql_empty(), 'userid'=>$user1->id, 'roleid'=>$studentrole->id, 'contextid'=>$context3->id))); + $this->assertEquals(1, $DB->count_records('role_assignments', array('contextid'=>$context3->id))); + + + $this->assertEquals(1, $DB->count_records('enrol', array('courseid'=>$course4->id, 'enrol'=>'manual'))); + $this->assertEquals(2, $DB->count_records('enrol', array('courseid'=>$course4->id, 'enrol'=>'xxx'))); + $maninstance4 = $DB->get_record('enrol', array('courseid'=>$course4->id, 'enrol'=>'manual'), '*', MUST_EXIST); + $this->assertEquals(ENROL_INSTANCE_ENABLED, $maninstance4->status); + $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$maninstance4->id, 'userid'=>$user1->id, 'status'=>ENROL_USER_ACTIVE))); + $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$maninstance4->id, 'userid'=>$user2->id, 'status'=>ENROL_USER_ACTIVE))); + $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$maninstance4->id, 'userid'=>$user4->id, 'status'=>ENROL_USER_SUSPENDED))); + $this->assertEquals(3, $DB->count_records('user_enrolments', array('enrolid'=>$maninstance4->id))); + $this->assertEquals(0, $DB->count_records('user_enrolments', array('enrolid'=>$xxxinstance4->id))); + $this->assertEquals(0, $DB->count_records('user_enrolments', array('enrolid'=>$xxxinstance4b->id))); + $this->assertTrue($DB->record_exists('role_assignments', array('itemid'=>0, 'component'=>$DB->sql_empty(), 'userid'=>$user1->id, 'roleid'=>$studentrole->id, 'contextid'=>$context4->id))); + $this->assertTrue($DB->record_exists('role_assignments', array('itemid'=>0, 'component'=>$DB->sql_empty(), 'userid'=>$user1->id, 'roleid'=>$teacherrole->id, 'contextid'=>$context4->id))); + $this->assertTrue($DB->record_exists('role_assignments', array('itemid'=>0, 'component'=>$DB->sql_empty(), 'userid'=>$user2->id, 'roleid'=>$studentrole->id, 'contextid'=>$context4->id))); + $this->assertTrue($DB->record_exists('role_assignments', array('itemid'=>0, 'component'=>$DB->sql_empty(), 'userid'=>$user4->id, 'roleid'=>$teacherrole->id, 'contextid'=>$context4->id))); + $this->assertEquals(4, $DB->count_records('role_assignments', array('contextid'=>$context4->id))); + + + $this->assertEquals(0, $DB->count_records('enrol', array('courseid'=>$course5->id, 'enrol'=>'manual'))); + $this->assertEquals(0, $DB->count_records('enrol', array('courseid'=>$course5->id, 'enrol'=>'xxx'))); + + // Make sure wrong params are ignored. + + enrol_manual_migrate_plugin_enrolments('manual'); + enrol_manual_migrate_plugin_enrolments('yyyy'); + } +} diff --git a/lang/en/enrol.php b/lang/en/enrol.php index e0ac69087709b..ac5edf69c89dc 100644 --- a/lang/en/enrol.php +++ b/lang/en/enrol.php @@ -98,8 +98,13 @@ $string['unenrolme'] = 'Unenrol me from {$a}'; $string['unenrolnotpermitted'] = 'You do not have permission or can not unenrol this user from this course.'; $string['unenrolroleusers'] = 'Unenrol users'; -$string['uninstallconfirm'] = 'You are about to completely delete the enrol plugin \'{$a}\'. This will completely delete everything in the database associated with this enrolment type. Are you SURE you want to continue?'; +$string['uninstallconfirm'] = 'You are about to completely uninstall the enrol plugin \'{$a}\'. This will completely delete everything in the database associated with this enrolment type. Deleting of enrolments removes also users\' grades, group membership, subscriptions and other course related data or preferences. + +Are you SURE you want to continue?'; +$string['uninstalldelete'] = 'Delete all enrolments and uninstall'; $string['uninstalldeletefiles'] = 'All data associated with the enrol plugin \'{$a->plugin}\' has been deleted from the database. To complete the deletion (and prevent the plugin re-installing itself), you should now delete this directory from your server: {$a->directory}'; +$string['uninstallmigrate'] = 'Uninstall but keep all enrolments'; +$string['uninstallmigrating'] = 'Migrating "{$a}" enrolments'; $string['unknowajaxaction'] = 'Unknown action requested'; $string['unlimitedduration'] = 'Unlimited'; $string['usersearch'] = 'Search '; From 882fb835197c1018643a87420c8b3f73bb498e0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= <commits@skodak.org> Date: Sat, 25 Aug 2012 11:45:20 +0200 Subject: [PATCH 43/90] MDL-35061 add more custom fields for enrol instances This delays the splitting of plugin specific enrol info from shared enrol table. --- backup/moodle2/backup_stepslib.php | 9 ++++--- lib/db/install.xml | 21 ++++++++++----- lib/db/upgrade.php | 43 ++++++++++++++++++++++++++++++ version.php | 2 +- 4 files changed, 64 insertions(+), 11 deletions(-) diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index b4399f2434464..6b17bc295f11c 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -522,9 +522,12 @@ protected function define_structure() { $enrol = new backup_nested_element('enrol', array('id'), array( 'enrol', 'status', 'sortorder', 'name', 'enrolperiod', 'enrolstartdate', 'enrolenddate', 'expirynotify', 'expirytreshold', 'notifyall', - 'password', 'cost', 'currency', 'roleid', 'customint1', 'customint2', 'customint3', - 'customint4', 'customchar1', 'customchar2', 'customdec1', 'customdec2', - 'customtext1', 'customtext2', 'timecreated', 'timemodified')); + 'password', 'cost', 'currency', 'roleid', + 'customint1', 'customint2', 'customint3', 'customint4', 'customint5', 'customint6', 'customint7', 'customint8', + 'customchar1', 'customchar2', 'customchar3', + 'customdec1', 'customdec2', + 'customtext1', 'customtext2', 'customtext3', 'customtext4', + 'timecreated', 'timemodified')); $userenrolments = new backup_nested_element('user_enrolments'); diff --git a/lib/db/install.xml b/lib/db/install.xml index e54cf890e9730..8c01fd2bff46b 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8" ?> -<XMLDB PATH="lib/db" VERSION="20120717" COMMENT="XMLDB file for core Moodle tables" +<XMLDB PATH="lib/db" VERSION="20120825" COMMENT="XMLDB file for core Moodle tables" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd" > @@ -235,14 +235,21 @@ <FIELD NAME="customint1" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Custom - general int" PREVIOUS="roleid" NEXT="customint2"/> <FIELD NAME="customint2" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Custom - general int" PREVIOUS="customint1" NEXT="customint3"/> <FIELD NAME="customint3" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Custom - general int" PREVIOUS="customint2" NEXT="customint4"/> - <FIELD NAME="customint4" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Custom - general int" PREVIOUS="customint3" NEXT="customchar1"/> - <FIELD NAME="customchar1" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Custom - general short name" PREVIOUS="customint4" NEXT="customchar2"/> - <FIELD NAME="customchar2" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Custom - general short name" PREVIOUS="customchar1" NEXT="customdec1"/> - <FIELD NAME="customdec1" TYPE="number" LENGTH="12" NOTNULL="false" SEQUENCE="false" DECIMALS="7" COMMENT="Custom - general decimal" PREVIOUS="customchar2" NEXT="customdec2"/> + <FIELD NAME="customint4" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Custom - general int" PREVIOUS="customint3" NEXT="customint5"/> + <FIELD NAME="customint5" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Custom - general int" PREVIOUS="customint4" NEXT="customint6"/> + <FIELD NAME="customint6" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Custom - general int" PREVIOUS="customint5" NEXT="customint7"/> + <FIELD NAME="customint7" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Custom - general int" PREVIOUS="customint6" NEXT="customint8"/> + <FIELD NAME="customint8" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Custom - general int" PREVIOUS="customint7" NEXT="customchar1"/> + <FIELD NAME="customchar1" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Custom - general short name" PREVIOUS="customint8" NEXT="customchar2"/> + <FIELD NAME="customchar2" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Custom - general short name" PREVIOUS="customchar1" NEXT="customchar3"/> + <FIELD NAME="customchar3" TYPE="char" LENGTH="1333" NOTNULL="false" SEQUENCE="false" COMMENT="Custom - general short name" PREVIOUS="customchar2" NEXT="customdec1"/> + <FIELD NAME="customdec1" TYPE="number" LENGTH="12" NOTNULL="false" SEQUENCE="false" DECIMALS="7" COMMENT="Custom - general decimal" PREVIOUS="customchar3" NEXT="customdec2"/> <FIELD NAME="customdec2" TYPE="number" LENGTH="12" NOTNULL="false" SEQUENCE="false" DECIMALS="7" COMMENT="Custom - general decimal" PREVIOUS="customdec1" NEXT="customtext1"/> <FIELD NAME="customtext1" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Custom - general text" PREVIOUS="customdec2" NEXT="customtext2"/> - <FIELD NAME="customtext2" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Custom - general text" PREVIOUS="customtext1" NEXT="timecreated"/> - <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="customtext2" NEXT="timemodified"/> + <FIELD NAME="customtext2" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Custom - general text" PREVIOUS="customtext1" NEXT="customtext3"/> + <FIELD NAME="customtext3" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Custom - general text" PREVIOUS="customtext2" NEXT="customtext4"/> + <FIELD NAME="customtext4" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Custom - general text" PREVIOUS="customtext3" NEXT="timecreated"/> + <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="customtext4" NEXT="timemodified"/> <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="timecreated"/> </FIELDS> <KEYS> diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index d5b98563d2c9d..58bad7dbaa9e4 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -1119,6 +1119,49 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2012081600.01); } + if ($oldversion < 2012082300.01) { + // Add more custom enrol fields. + $table = new xmldb_table('enrol'); + + $field = new xmldb_field('customint5', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'customint4'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + $field = new xmldb_field('customint6', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'customint5'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + $field = new xmldb_field('customint7', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'customint6'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + $field = new xmldb_field('customint8', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'customint7'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + $field = new xmldb_field('customchar3', XMLDB_TYPE_CHAR, '1333', null, null, null, null, 'customchar2'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + $field = new xmldb_field('customtext3', XMLDB_TYPE_TEXT, null, null, null, null, null, 'customtext2'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + $field = new xmldb_field('customtext4', XMLDB_TYPE_TEXT, null, null, null, null, null, 'customtext3'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2012082300.01); + } + return true; } diff --git a/version.php b/version.php index 528c92696df61..9c9909f1d3162 100644 --- a/version.php +++ b/version.php @@ -30,7 +30,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2012082300.00; // YYYYMMDD = weekly release date of this DEV branch +$version = 2012082300.01; // YYYYMMDD = weekly release date of this DEV branch // RR = release increments - 00 in DEV branches // .XX = incremental changes From dd6b1f15cfc8e68e609fdc41299e7c3df18c7af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= <commits@skodak.org> Date: Sun, 26 Aug 2012 15:22:40 +0200 Subject: [PATCH 44/90] MDL-23875 add option to limit self-enrol to cohort members only --- cohort/lib.php | 12 +++++++++++ enrol/self/edit.php | 8 +++++--- enrol/self/edit_form.php | 34 +++++++++++++++++++++++++++++++ enrol/self/lang/en/enrol_self.php | 3 +++ enrol/self/lib.php | 24 +++++++++++++++++++++- enrol/self/version.php | 4 ++-- 6 files changed, 79 insertions(+), 6 deletions(-) diff --git a/cohort/lib.php b/cohort/lib.php index 223644b52e6e3..67c2bf4c2f694 100644 --- a/cohort/lib.php +++ b/cohort/lib.php @@ -155,6 +155,18 @@ function cohort_remove_member($cohortid, $userid) { events_trigger('cohort_member_removed', (object)array('cohortid'=>$cohortid, 'userid'=>$userid)); } +/** + * Is this user a cohort member? + * @param int $cohortid + * @param int $userid + * @return bool + */ +function cohort_is_member($cohortid, $userid) { + global $DB; + + return $DB->record_exists('cohort_members', array('cohortid'=>$cohortid, 'userid'=>$userid)); +} + /** * Returns list of visible cohorts in course. * diff --git a/enrol/self/edit.php b/enrol/self/edit.php index 4b38df4daba9f..5422e2dcc6a7c 100644 --- a/enrol/self/edit.php +++ b/enrol/self/edit.php @@ -54,8 +54,9 @@ // no instance yet, we have to add new instance navigation_node::override_active_url(new moodle_url('/enrol/instances.php', array('id'=>$course->id))); $instance = new stdClass(); - $instance->id = null; - $instance->courseid = $course->id; + $instance->id = null; + $instance->courseid = $course->id; + $instance->customint5 = 0; } $mform = new enrol_self_edit_form(NULL, array($instance, $plugin, $context)); @@ -74,6 +75,7 @@ $instance->customint2 = $data->customint2; $instance->customint3 = $data->customint3; $instance->customint4 = $data->customint4; + $instance->customint5 = $data->customint5; $instance->customtext1 = $data->customtext1; $instance->roleid = $data->roleid; $instance->enrolperiod = $data->enrolperiod; @@ -88,7 +90,7 @@ } else { $fields = array('status'=>$data->status, 'name'=>$data->name, 'password'=>$data->password, 'customint1'=>$data->customint1, 'customint2'=>$data->customint2, - 'customint3'=>$data->customint3, 'customint4'=>$data->customint4, 'customtext1'=>$data->customtext1, + 'customint3'=>$data->customint3, 'customint4'=>$data->customint4, 'customint5'=>$data->customint5, 'customtext1'=>$data->customtext1, 'roleid'=>$data->roleid, 'enrolperiod'=>$data->enrolperiod, 'enrolstartdate'=>$data->enrolstartdate, 'enrolenddate'=>$data->enrolenddate); $plugin->add_instance($course, $fields); } diff --git a/enrol/self/edit_form.php b/enrol/self/edit_form.php index 7f77cfce25b24..1e80714e4d9c0 100644 --- a/enrol/self/edit_form.php +++ b/enrol/self/edit_form.php @@ -32,6 +32,8 @@ class enrol_self_edit_form extends moodleform { function definition() { + global $DB; + $mform = $this->_form; list($instance, $plugin, $context) = $this->_customdata; @@ -100,6 +102,38 @@ function definition() { $mform->addHelpButton('customint3', 'maxenrolled', 'enrol_self'); $mform->setType('customint3', PARAM_INT); + $cohorts = array(0 => get_string('no')); + list($sqlparents, $params) = $DB->get_in_or_equal($context->get_parent_context_ids(), SQL_PARAMS_NAMED); + $params['current'] = $instance->customint5; + $sql = "SELECT id, name, idnumber, contextid + FROM {cohort} + WHERE contextid $sqlparents OR id = :current + ORDER BY name ASC, idnumber ASC"; + $rs = $DB->get_recordset_sql($sql, $params); + foreach ($rs as $c) { + $ccontext = context::instance_by_id($c->contextid); + if ($c->id != $instance->customint5 and !has_capability('moodle/cohort:view', $ccontext)) { + continue; + } + $cohorts[$c->id] = format_string($c->name, true, array('context'=>$context)); + if ($c->idnumber) { + $cohorts[$c->id] .= ' ['.s($c->idnumber).']'; + } + } + if (!isset($cohorts[$instance->customint5])) { + // Somebody deleted a cohort, better keep the wrong value so that random ppl can not enrol. + $cohorts[$instance->customint5] = get_string('unknowncohort', 'cohort', $instance->customint5); + } + $rs->close(); + if (count($cohorts) > 1) { + $mform->addElement('select', 'customint5', get_string('cohortonly', 'enrol_self'), $cohorts); + $mform->addHelpButton('customint5', 'cohortonly', 'enrol_self'); + } else { + $mform->addElement('hidden', 'customint5'); + $mform->setType('customint5', PARAM_INT); + $mform->setConstant('customint5', 0); + } + $mform->addElement('advcheckbox', 'customint4', get_string('sendcoursewelcomemessage', 'enrol_self')); $mform->setDefault('customint4', $plugin->get_config('sendcoursewelcomemessage')); $mform->addHelpButton('customint4', 'sendcoursewelcomemessage', 'enrol_self'); diff --git a/enrol/self/lang/en/enrol_self.php b/enrol/self/lang/en/enrol_self.php index 95595fc7f9551..03677277ce384 100644 --- a/enrol/self/lang/en/enrol_self.php +++ b/enrol/self/lang/en/enrol_self.php @@ -24,6 +24,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['cohortnonmemberinfo'] = 'Only members of cohort \'{$a}\' can self-enrol.'; +$string['cohortonly'] = 'Only cohort members'; +$string['cohortonly_help'] = 'Select a cohort if you want to restrict self enrolment only to members of this cohort. Change of this setting does not affect existing enrolments.'; $string['customwelcomemessage'] = 'Custom welcome message'; $string['customwelcomemessage_help'] = 'A custom welcome message may be added as plain text or Moodle-auto format, including HTML tags and multi-lang tags. diff --git a/enrol/self/lib.php b/enrol/self/lib.php index 771ec763e0b18..8724aef8c39d2 100644 --- a/enrol/self/lib.php +++ b/enrol/self/lib.php @@ -101,7 +101,16 @@ public function allow_manage(stdClass $instance) { } public function show_enrolme_link(stdClass $instance) { - return ($instance->status == ENROL_INSTANCE_ENABLED); + global $CFG, $USER; + + if ($instance->status != ENROL_INSTANCE_ENABLED) { + return false; + } + if ($instance->customint5) { + require_once("$CFG->dirroot/cohort/lib.php"); + return cohort_is_member($instance->customint5, $USER->id); + } + return true; } /** @@ -189,6 +198,18 @@ public function enrol_page_hook(stdClass $instance) { return null; } + if ($instance->customint5) { + require_once("$CFG->dirroot/cohort/lib.php"); + if (!cohort_is_member($instance->customint5, $USER->id)) { + $cohort = $DB->get_record('cohort', array('id'=>$instance->customint5)); + if (!$cohort) { + return null; + } + $a = format_string($cohort->name, true, array('context'=>context::instance_by_id($cohort->contextid))); + return $OUTPUT->box(markdown_to_html(get_string('cohortnonmemberinfo', 'enrol_self', $a))); + } + } + require_once("$CFG->dirroot/enrol/self/locallib.php"); require_once("$CFG->dirroot/group/lib.php"); @@ -245,6 +266,7 @@ public function add_default_instance($course) { 'customint2' => $this->get_config('longtimenosee'), 'customint3' => $this->get_config('maxenrolled'), 'customint4' => $this->get_config('sendcoursewelcomemessage'), + 'customint5' => 0, 'enrolperiod' => $this->get_config('enrolperiod', 0), 'status' => $this->get_config('status'), 'roleid' => $this->get_config('roleid', 0)); diff --git a/enrol/self/version.php b/enrol/self/version.php index 73f1cdeefc5b8..e5a04c8391806 100644 --- a/enrol/self/version.php +++ b/enrol/self/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2012061700; // The current plugin version (Date: YYYYMMDDXX) -$plugin->requires = 2012061700; // Requires this Moodle version +$plugin->version = 2012082300; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2012082300; // Requires this Moodle version $plugin->component = 'enrol_self'; // Full name of the plugin (used for diagnostics) $plugin->cron = 180; \ No newline at end of file From d9669db9b326c056b51aad7544ef57f7d4bfcbdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= <commits@skodak.org> Date: Sun, 26 Aug 2012 15:50:40 +0200 Subject: [PATCH 45/90] MDL-35070 coding style cleanup in enrol_self --- enrol/self/db/access.php | 6 ++-- enrol/self/db/install.php | 4 +-- enrol/self/edit.php | 8 ++--- enrol/self/edit_form.php | 6 ++-- enrol/self/editenrolment.php | 58 ++++++++++++++----------------- enrol/self/editenrolment_form.php | 6 ++-- enrol/self/lang/en/enrol_self.php | 6 ++-- enrol/self/lib.php | 47 ++++++++++++------------- enrol/self/locallib.php | 14 ++++---- enrol/self/settings.php | 4 +-- enrol/self/unenrolself.php | 8 ++--- enrol/self/version.php | 3 +- 12 files changed, 74 insertions(+), 96 deletions(-) diff --git a/enrol/self/db/access.php b/enrol/self/db/access.php index 314720c29690e..9cdd3c3cc9150 100644 --- a/enrol/self/db/access.php +++ b/enrol/self/db/access.php @@ -26,6 +26,7 @@ $capabilities = array( + /* Add or edit enrol-self instance in course. */ 'enrol/self:config' => array( 'captype' => 'write', @@ -36,6 +37,7 @@ ) ), + /* Manage user self-enrolments. */ 'enrol/self:manage' => array( 'captype' => 'write', @@ -46,6 +48,7 @@ ) ), + /* Voluntarily unenrol self from course - watch out for data loss. */ 'enrol/self:unenrolself' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, @@ -54,6 +57,7 @@ ) ), + /* Unenrol anybody from course (including self) - watch out for data loss. */ 'enrol/self:unenrol' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, @@ -64,5 +68,3 @@ ), ); - - diff --git a/enrol/self/db/install.php b/enrol/self/db/install.php index 82d8c2f148183..320fc08323d64 100644 --- a/enrol/self/db/install.php +++ b/enrol/self/db/install.php @@ -1,5 +1,4 @@ <?php - // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify @@ -18,8 +17,7 @@ /** * Self enrol plugin installation script * - * @package enrol - * @subpackage self + * @package enrol_self * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ diff --git a/enrol/self/edit.php b/enrol/self/edit.php index 5422e2dcc6a7c..97448cabd12a4 100644 --- a/enrol/self/edit.php +++ b/enrol/self/edit.php @@ -1,5 +1,4 @@ <?php - // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify @@ -19,8 +18,7 @@ * Adds new instance of enrol_self to specified course * or edits current instance. * - * @package enrol - * @subpackage self + * @package enrol_self * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -29,7 +27,7 @@ require_once('edit_form.php'); $courseid = required_param('courseid', PARAM_INT); -$instanceid = optional_param('id', 0, PARAM_INT); // instanceid +$instanceid = optional_param('id', 0, PARAM_INT); $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST); $context = context_course::instance($course->id, MUST_EXIST); @@ -51,7 +49,7 @@ $instance = $DB->get_record('enrol', array('courseid'=>$course->id, 'enrol'=>'self', 'id'=>$instanceid), '*', MUST_EXIST); } else { require_capability('moodle/course:enrolconfig', $context); - // no instance yet, we have to add new instance + // No instance yet, we have to add new instance. navigation_node::override_active_url(new moodle_url('/enrol/instances.php', array('id'=>$course->id))); $instance = new stdClass(); $instance->id = null; diff --git a/enrol/self/edit_form.php b/enrol/self/edit_form.php index 1e80714e4d9c0..f094b9b740445 100644 --- a/enrol/self/edit_form.php +++ b/enrol/self/edit_form.php @@ -1,5 +1,4 @@ <?php - // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify @@ -19,8 +18,7 @@ * Adds new instance of enrol_self to specified course * or edits current instance. * - * @package enrol - * @subpackage self + * @package enrol_self * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -193,7 +191,7 @@ function validation($data, $files) { } /** - * Gets a list of roles that this user can assign for the course as the default for self-enrolment + * Gets a list of roles that this user can assign for the course as the default for self-enrolment. * * @param context $context the context. * @param integer $defaultrole the id of the role that is set as the default for self-enrolment diff --git a/enrol/self/editenrolment.php b/enrol/self/editenrolment.php index 418359ea9df16..c63d7583c0a4b 100644 --- a/enrol/self/editenrolment.php +++ b/enrol/self/editenrolment.php @@ -20,48 +20,44 @@ * This page allows the current user to edit a self user enrolment. * It is not compatible with the frontpage. * - * @package enrol - * @subpackage self + * @package enrol_self * @copyright 2011 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require('../../config.php'); -require_once("$CFG->dirroot/enrol/locallib.php"); // Required for the course enrolment manager -require_once("$CFG->dirroot/enrol/renderer.php"); // Required for the course enrolment users table -require_once("$CFG->dirroot/enrol/self/editenrolment_form.php"); // Forms for this page - -$ueid = required_param('ue', PARAM_INT); // user enrolment id -$filter = optional_param('ifilter', 0, PARAM_INT); // table filter for return url - -// Get the user enrolment object -$ue = $DB->get_record('user_enrolments', array('id' => $ueid), '*', MUST_EXIST); -// Get the user for whom the enrolment is -$user = $DB->get_record('user', array('id'=>$ue->userid), '*', MUST_EXIST); -// Get the course the enrolment is to -list($ctxsql, $ctxjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx'); -$sql = "SELECT c.* $ctxsql +require_once("$CFG->dirroot/enrol/locallib.php"); // Required for the course enrolment manager. +require_once("$CFG->dirroot/enrol/renderer.php"); // Required for the course enrolment users table. +require_once("$CFG->dirroot/enrol/self/editenrolment_form.php"); // Forms for this page. + +$ueid = required_param('ue', PARAM_INT); +$filter = optional_param('ifilter', 0, PARAM_INT); // Table filter for return url. + +// Get the user enrolment object. +$ue = $DB->get_record('user_enrolments', array('id' => $ueid), '*', MUST_EXIST); +// Get the user for whom the enrolment is. +$user = $DB->get_record('user', array('id'=>$ue->userid), '*', MUST_EXIST); +// Get the course the enrolment is to. +$sql = "SELECT c.* FROM {course} c LEFT JOIN {enrol} e ON e.courseid = c.id - $ctxjoin WHERE e.id = :enrolid"; $params = array('enrolid' => $ue->enrolid); $course = $DB->get_record_sql($sql, $params, MUST_EXIST); -context_instance_preload($course); -// Make sure the course isn't the front page +// Make sure the course isn't the front page. if ($course->id == SITEID) { redirect(new moodle_url('/')); } -// Obvioulsy +// Obviously. require_login($course); -// The user must be able to manage self enrolments within the course +// The user must be able to manage self enrolments within the course. require_capability("enrol/self:manage", context_course::instance($course->id, MUST_EXIST)); -// Get the enrolment manager for this course +// Get the enrolment manager for this course. $manager = new course_enrolment_manager($PAGE, $course, $filter); -// Get an enrolment users table object. Doign this will automatically retrieve the the URL params +// Get an enrolment users table object. Doing this will automatically retrieve the the URL params // relating to table the user was viewing before coming here, and allows us to return the user to the // exact page of the users screen they can from. $table = new course_enrolment_users_table($manager, $PAGE); @@ -70,30 +66,28 @@ $usersurl = new moodle_url('/enrol/users.php', array('id' => $course->id)); // The URl to return the user too after this screen. $returnurl = new moodle_url($usersurl, $manager->get_url_params()+$table->get_url_params()); -// The URL of this page +// The URL of this page. $url = new moodle_url('/enrol/self/editenrolment.php', $returnurl->params()); $PAGE->set_url($url); $PAGE->set_pagelayout('admin'); navigation_node::override_active_url($usersurl); -// Gets the compontents of the user enrolment +// Gets the components of the user enrolment. list($instance, $plugin) = $manager->get_user_enrolment_components($ue); -// Check that the user can manage this instance, and that the instance is of the correct type +// Check that the user can manage this instance, and that the instance is of the correct type. if (!$plugin->allow_manage($instance) || $instance->enrol != 'self' || !($plugin instanceof enrol_self_plugin)) { print_error('erroreditenrolment', 'enrol'); } -// Get the self enrolment edit form +// Get the self enrolment edit form. $mform = new enrol_self_user_enrolment_form($url, array('user'=>$user, 'course'=>$course, 'ue'=>$ue)); $mform->set_data($PAGE->url->params()); -// Check the form hasn't been cancelled if ($mform->is_cancelled()) { redirect($returnurl); -} else if ($mform->is_submitted() && $mform->is_validated() && confirm_sesskey()) { - // The forms been submit, validated and the sesskey has been checked ... edit the enrolment. - $data = $mform->get_data(); + +} else if ($data = $mform->get_data()) { if ($manager->edit_enrolment($ue, $data)) { redirect($returnurl); } @@ -110,4 +104,4 @@ echo $OUTPUT->header(); echo $OUTPUT->heading($fullname); $mform->display(); -echo $OUTPUT->footer(); \ No newline at end of file +echo $OUTPUT->footer(); diff --git a/enrol/self/editenrolment_form.php b/enrol/self/editenrolment_form.php index 014312263e856..24d6525217c2d 100644 --- a/enrol/self/editenrolment_form.php +++ b/enrol/self/editenrolment_form.php @@ -1,5 +1,4 @@ <?php - // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify @@ -18,8 +17,7 @@ /** * Contains the form used to edit self enrolments for a user. * - * @package enrol - * @subpackage self + * @package enrol_self * @copyright 2011 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -80,4 +78,4 @@ function validation($data, $files) { return $errors; } -} \ No newline at end of file +} diff --git a/enrol/self/lang/en/enrol_self.php b/enrol/self/lang/en/enrol_self.php index 03677277ce384..53a2681db91dd 100644 --- a/enrol/self/lang/en/enrol_self.php +++ b/enrol/self/lang/en/enrol_self.php @@ -1,5 +1,4 @@ <?php - // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify @@ -16,10 +15,9 @@ // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** - * Strings for component 'enrol_self', language 'en', branch 'MOODLE_20_STABLE' + * Strings for component 'enrol_self', language 'en'. * - * @package enrol - * @subpackage self + * @package enrol_self * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ diff --git a/enrol/self/lib.php b/enrol/self/lib.php index 8724aef8c39d2..fda2414301b79 100644 --- a/enrol/self/lib.php +++ b/enrol/self/lib.php @@ -1,5 +1,4 @@ <?php - // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify @@ -18,8 +17,7 @@ /** * Self enrolment plugin. * - * @package enrol - * @subpackage self + * @package enrol_self * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -66,7 +64,7 @@ public function get_info_icons(array $instances) { /** * Returns localised name of enrol instance * - * @param object $instance (null is accepted too) + * @param stdClass $instance (null is accepted too) * @return string */ public function get_instance_name($instance) { @@ -86,17 +84,17 @@ public function get_instance_name($instance) { } public function roles_protected() { - // users may tweak the roles later + // Users may tweak the roles later. return false; } public function allow_unenrol(stdClass $instance) { - // users with unenrol cap may unenrol other users manually manually + // Users with unenrol cap may unenrol other users manually manually. return true; } public function allow_manage(stdClass $instance) { - // users with manage cap may tweak period and status + // Users with manage cap may tweak period and status. return true; } @@ -116,7 +114,8 @@ public function show_enrolme_link(stdClass $instance) { /** * Sets up navigation entries. * - * @param object $instance + * @param stdClass $instancesnode + * @param stdClass $instance * @return void */ public function add_course_navigation($instancesnode, stdClass $instance) { @@ -165,7 +164,7 @@ public function get_newinstance_link($courseid) { if (!has_capability('moodle/course:enrolconfig', $context) or !has_capability('enrol/self:config', $context)) { return NULL; } - // multiple instances supported - different roles with different password + // Multiple instances supported - different roles with different password. return new moodle_url('/enrol/self/edit.php', array('courseid'=>$courseid)); } @@ -180,7 +179,7 @@ public function enrol_page_hook(stdClass $instance) { global $CFG, $OUTPUT, $SESSION, $USER, $DB; if (isguestuser()) { - // can not enrol guest!! + // Can not enrol guest!! return null; } if ($DB->record_exists('user_enrolments', array('userid'=>$USER->id, 'enrolid'=>$instance->id))) { @@ -227,7 +226,7 @@ public function enrol_page_hook(stdClass $instance) { } $this->enrol_user($instance, $USER->id, $instance->roleid, $timestart, $timeend); - add_to_log($instance->courseid, 'course', 'enrol', '../enrol/users.php?id='.$instance->courseid, $instance->courseid); //there should be userid somewhere! + add_to_log($instance->courseid, 'course', 'enrol', '../enrol/users.php?id='.$instance->courseid, $instance->courseid); //TODO: There should be userid somewhere! if ($instance->password and $instance->customint1 and $data->enrolpassword !== $instance->password) { // it must be a group enrolment, let's assign group too @@ -242,7 +241,7 @@ public function enrol_page_hook(stdClass $instance) { } } } - // send welcome + // Send welcome message. if ($instance->customint4) { $this->email_welcome_message($instance, $USER); } @@ -258,7 +257,7 @@ public function enrol_page_hook(stdClass $instance) { /** * Add new instance of enrol plugin with default settings. - * @param object $course + * @param stdClass $course * @return int id of new instance */ public function add_default_instance($course) { @@ -279,10 +278,10 @@ public function add_default_instance($course) { } /** - * Send welcome email to specified user + * Send welcome email to specified user. * - * @param object $instance - * @param object $user user record + * @param stdClass $instance + * @param stdClass $user user record * @return void */ protected function email_welcome_message($instance, $user) { @@ -326,12 +325,12 @@ protected function email_welcome_message($instance, $user) { $contact = generate_email_supportuser(); } - //directly emailing welcome message rather than using messaging + // Directly emailing welcome message rather than using messaging. email_to_user($user, $contact, $subject, $messagetext, $messagehtml); } /** - * Enrol self cron support + * Enrol self cron support. * @return void */ public function cron() { @@ -345,10 +344,10 @@ public function cron() { $now = time(); - //note: the logic of self enrolment guarantees that user logged in at least once (=== u.lastaccess set) - // and that user accessed course at least once too (=== user_lastaccess record exists) + // Note: the logic of self enrolment guarantees that user logged in at least once (=== u.lastaccess set) + // and that user accessed course at least once too (=== user_lastaccess record exists). - // first deal with users that did not log in for a really long time + // First deal with users that did not log in for a really long time. $sql = "SELECT e.*, ue.userid FROM {user_enrolments} ue JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'self' AND e.customint2 > 0) @@ -363,7 +362,7 @@ public function cron() { } $rs->close(); - // now unenrol from course user did not visit for a long time + // Now unenrol from course user did not visit for a long time. $sql = "SELECT e.*, ue.userid FROM {user_enrolments} ue JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'self' AND e.customint2 > 0) @@ -382,7 +381,7 @@ public function cron() { } /** - * Gets an array of the user enrolment actions + * Gets an array of the user enrolment actions. * * @param course_enrolment_manager $manager * @param stdClass $ue A user enrolment object @@ -410,7 +409,7 @@ public function get_user_enrolment_actions(course_enrolment_manager $manager, $u * Indicates API features that the enrol plugin supports. * * @param string $feature - * @return mixed True if yes (some features may use other values) + * @return mixed true if yes (some features may use other values) */ function enrol_self_supports($feature) { switch($feature) { diff --git a/enrol/self/locallib.php b/enrol/self/locallib.php index a189e7d406666..df78a2c1ce092 100644 --- a/enrol/self/locallib.php +++ b/enrol/self/locallib.php @@ -1,5 +1,4 @@ <?php - // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify @@ -18,8 +17,7 @@ /** * Self enrol plugin implementation. * - * @package enrol - * @subpackage self + * @package enrol_self * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -33,7 +31,7 @@ class enrol_self_enrol_form extends moodleform { protected $toomany = false; /** - * Overriding this function to get unique form id for multiple self enrolments + * Overriding this function to get unique form id for multiple self enrolments. * * @return string form identifier */ @@ -54,10 +52,10 @@ public function definition() { $mform->addElement('header', 'selfheader', $heading); if ($instance->customint3 > 0) { - // max enrol limit specified + // Max enrol limit specified. $count = $DB->count_records('user_enrolments', array('enrolid'=>$instance->id)); if ($count >= $instance->customint3) { - // bad luck, no more self enrolments here + // Bad luck, no more self enrolments here. $this->toomany = true; $mform->addElement('static', 'notice', '', get_string('maxenrolledreached', 'enrol_self')); return; @@ -65,7 +63,7 @@ public function definition() { } if ($instance->password) { - //change the id of self enrolment key input as there can be multiple self enrolment methods + // Change the id of self enrolment key input as there can be multiple self enrolment methods. $mform->addElement('passwordunmask', 'enrolpassword', get_string('password', 'enrol_self'), array('id' => 'enrolpassword_'.$instance->id)); } else { @@ -109,7 +107,7 @@ public function validation($data, $files) { } } if (!$found) { - // we can not hint because there are probably multiple passwords + // We can not hint because there are probably multiple passwords. $errors['enrolpassword'] = get_string('passwordinvalid', 'enrol_self'); } diff --git a/enrol/self/settings.php b/enrol/self/settings.php index bab0909616ac1..c8c2f02fa0d0c 100644 --- a/enrol/self/settings.php +++ b/enrol/self/settings.php @@ -1,5 +1,4 @@ <?php - // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify @@ -18,8 +17,7 @@ /** * Self enrolment plugin settings and presets. * - * @package enrol - * @subpackage self + * @package enrol_self * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ diff --git a/enrol/self/unenrolself.php b/enrol/self/unenrolself.php index 02670f6abb2a0..7e7d269042b05 100644 --- a/enrol/self/unenrolself.php +++ b/enrol/self/unenrolself.php @@ -1,5 +1,4 @@ <?php - // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify @@ -18,8 +17,7 @@ /** * Self enrolment plugin - support for user self unenrolment. * - * @package enrol - * @subpackage self + * @package enrol_self * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -41,7 +39,7 @@ $plugin = enrol_get_plugin('self'); -// security defined inside following function +// Security defined inside following function. if (!$plugin->get_unenrolself_link($instance)) { redirect(new moodle_url('/course/view.php', array('id'=>$course->id))); } @@ -51,7 +49,7 @@ if ($confirm and confirm_sesskey()) { $plugin->unenrol_user($instance, $USER->id); - add_to_log($course->id, 'course', 'unenrol', '../enrol/users.php?id='.$course->id, $course->id); //there should be userid somewhere! + add_to_log($course->id, 'course', 'unenrol', '../enrol/users.php?id='.$course->id, $course->id); //TODO: there should be userid somewhere! redirect(new moodle_url('/index.php')); } diff --git a/enrol/self/version.php b/enrol/self/version.php index e5a04c8391806..1153b05a80871 100644 --- a/enrol/self/version.php +++ b/enrol/self/version.php @@ -17,8 +17,7 @@ /** * Self enrolment plugin version specification. * - * @package enrol - * @subpackage self + * @package enrol_self * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ From 005e57a2259cf6f34451c7e3cc10010523c622e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= <commits@skodak.org> Date: Sat, 25 Aug 2012 13:29:28 +0200 Subject: [PATCH 46/90] MDL-35052 show all enrolled users in enrol UI included those enrolled via disabled plugins --- enrol/ajax.php | 2 +- enrol/locallib.php | 53 ++++++++++++++++++++++++++++-------- enrol/manual/ajax.php | 5 +++- enrol/renderer.php | 2 +- enrol/self/editenrolment.php | 5 ++++ enrol/upgrade.txt | 3 ++ 6 files changed, 56 insertions(+), 14 deletions(-) diff --git a/enrol/ajax.php b/enrol/ajax.php index 67ae497c1b01a..ede3c5a377cf0 100644 --- a/enrol/ajax.php +++ b/enrol/ajax.php @@ -63,7 +63,7 @@ case 'unenrol': $ue = $DB->get_record('user_enrolments', array('id'=>required_param('ue', PARAM_INT)), '*', MUST_EXIST); list ($instance, $plugin) = $manager->get_user_enrolment_components($ue); - if (!$instance || !$plugin || !$plugin->allow_unenrol_user($instance, $ue) || !has_capability("enrol/$instance->enrol:unenrol", $manager->get_context()) || !$manager->unenrol_user($ue)) { + if (!$instance || !$plugin || !enrol_is_enabled($instance->enrol) || !$plugin->allow_unenrol_user($instance, $ue) || !has_capability("enrol/$instance->enrol:unenrol", $manager->get_context()) || !$manager->unenrol_user($ue)) { throw new enrol_ajax_exception('unenrolnotpermitted'); } break; diff --git a/enrol/locallib.php b/enrol/locallib.php index b19a716b2bad8..596320a7ee6da 100644 --- a/enrol/locallib.php +++ b/enrol/locallib.php @@ -97,6 +97,7 @@ class course_enrolment_manager { private $_instances = null; private $_inames = null; private $_plugins = null; + private $_allplugins = null; private $_roles = null; private $_assignableroles = null; private $_assignablerolesothers = null; @@ -397,11 +398,13 @@ protected function get_instance_sql() { /** * Returns all of the enrolment instances for this course. * + * NOTE: since 2.4 it includes instances of disabled plugins too. + * * @return array */ public function get_enrolment_instances() { if ($this->_instances === null) { - $this->_instances = enrol_get_instances($this->course->id, true); + $this->_instances = enrol_get_instances($this->course->id, false); } return $this->_instances; } @@ -409,12 +412,14 @@ public function get_enrolment_instances() { /** * Returns the names for all of the enrolment instances for this course. * + * NOTE: since 2.4 it includes instances of disabled plugins too. + * * @return array */ public function get_enrolment_instance_names() { if ($this->_inames === null) { $instances = $this->get_enrolment_instances(); - $plugins = $this->get_enrolment_plugins(); + $plugins = $this->get_enrolment_plugins(false); foreach ($instances as $key=>$instance) { if (!isset($plugins[$instance->enrol])) { // weird, some broken stuff in plugin @@ -430,13 +435,29 @@ public function get_enrolment_instance_names() { /** * Gets all of the enrolment plugins that are active for this course. * + * @param bool $onlyenabled return only enabled enrol plugins * @return array */ - public function get_enrolment_plugins() { + public function get_enrolment_plugins($onlyenabled = true) { if ($this->_plugins === null) { $this->_plugins = enrol_get_plugins(true); } - return $this->_plugins; + + if ($onlyenabled) { + return $this->_plugins; + } + + if ($this->_allplugins === null) { + // Make sure we have the same objects in _allplugins and _plugins. + $this->_allplugins = $this->_plugins; + foreach (enrol_get_plugins(false) as $name=>$plugin) { + if (!isset($this->_allplugins[$name])) { + $this->_allplugins[$name] = $plugin; + } + } + } + + return $this->_allplugins; } /** @@ -522,7 +543,7 @@ public function get_user_enrolment_components($userenrolment) { $userenrolment = $DB->get_record('user_enrolments', array('id'=>(int)$userenrolment)); } $instances = $this->get_enrolment_instances(); - $plugins = $this->get_enrolment_plugins(); + $plugins = $this->get_enrolment_plugins(false); if (!$userenrolment || !isset($instances[$userenrolment->enrolid])) { return array(false, false); } @@ -675,7 +696,7 @@ public function get_user_roles($userid) { } /** - * Gets the enrolments this user has in the course + * Gets the enrolments this user has in the course - including all suspended plugins and instances. * * @global moodle_database $DB * @param int $userid @@ -687,7 +708,7 @@ public function get_user_enrolments($userid) { $params['userid'] = $userid; $userenrolments = $DB->get_records_select('user_enrolments', "enrolid $instancessql AND userid = :userid", $params); $instances = $this->get_enrolment_instances(); - $plugins = $this->get_enrolment_plugins(); + $plugins = $this->get_enrolment_plugins(false); $inames = $this->get_enrolment_instance_names(); foreach ($userenrolments as &$ue) { $ue->enrolmentinstance = $instances[$ue->enrolid]; @@ -829,6 +850,8 @@ public function get_users_for_display(course_enrolment_manager $manager, $sort, $url = new moodle_url($pageurl, $this->get_url_params()); $extrafields = get_extra_user_fields($context); + $enabledplugins = $this->get_enrolment_plugins(true); + $userdetails = array(); foreach ($users as $user) { $details = $this->prepare_user_for_display($user, $extrafields, $now); @@ -849,7 +872,15 @@ public function get_users_for_display(course_enrolment_manager $manager, $sort, // Enrolments $details['enrolments'] = array(); foreach ($this->get_user_enrolments($user->id) as $ue) { - if ($ue->timestart and $ue->timeend) { + if (!isset($enabledplugins[$ue->enrolmentinstance->enrol])) { + $details['enrolments'][$ue->id] = array( + 'text' => $ue->enrolmentinstancename, + 'period' => null, + 'dimmed' => true, + 'actions' => array() + ); + continue; + } else if ($ue->timestart and $ue->timeend) { $period = get_string('periodstartend', 'enrol', array('start'=>userdate($ue->timestart), 'end'=>userdate($ue->timeend))); $periodoutside = ($ue->timestart && $ue->timeend && $now < $ue->timestart && $now > $ue->timeend); } else if ($ue->timestart) { @@ -909,7 +940,7 @@ private function prepare_user_for_display($user, $extrafields, $now) { } public function get_manual_enrol_buttons() { - $plugins = $this->get_enrolment_plugins(); + $plugins = $this->get_enrolment_plugins(true); // Skip disabled plugins. $buttons = array(); foreach ($plugins as $plugin) { $newbutton = $plugin->get_manual_enrol_button($this); @@ -941,7 +972,7 @@ public function has_instance($enrolpluginname) { */ public function get_filtered_enrolment_plugin() { $instances = $this->get_enrolment_instances(); - $plugins = $this->get_enrolment_plugins(); + $plugins = $this->get_enrolment_plugins(false); if (empty($this->instancefilter) || !array_key_exists($this->instancefilter, $instances)) { return false; @@ -965,7 +996,7 @@ public function get_users_enrolments(array $userids) { global $DB; $instances = $this->get_enrolment_instances(); - $plugins = $this->get_enrolment_plugins(); + $plugins = $this->get_enrolment_plugins(false); if (!empty($this->instancefilter)) { $instancesql = ' = :instanceid'; diff --git a/enrol/manual/ajax.php b/enrol/manual/ajax.php index 140b4cd8dfe79..fabb08e15bf57 100644 --- a/enrol/manual/ajax.php +++ b/enrol/manual/ajax.php @@ -113,11 +113,14 @@ $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST); $instances = $manager->get_enrolment_instances(); - $plugins = $manager->get_enrolment_plugins(); + $plugins = $manager->get_enrolment_plugins(true); // Do not allow actions on disabled plugins. if (!array_key_exists($enrolid, $instances)) { throw new enrol_ajax_exception('invalidenrolinstance'); } $instance = $instances[$enrolid]; + if (!isset($plugins[$instance->enrol])) { + throw new enrol_ajax_exception('enrolnotpermitted'); + } $plugin = $plugins[$instance->enrol]; if ($plugin->allow_enrol($instance) && has_capability('enrol/'.$plugin->get_name().':enrol', $context)) { $plugin->enrol_user($instance, $user->id, $roleid, $timestart, $timeend); diff --git a/enrol/renderer.php b/enrol/renderer.php index fafbfd790e334..c7fa8eaa2440a 100644 --- a/enrol/renderer.php +++ b/enrol/renderer.php @@ -449,7 +449,7 @@ public function __construct(course_enrolment_manager $manager) { // Collect the bulk operations for the currently filtered plugin if there is one. $plugin = $manager->get_filtered_enrolment_plugin(); - if ($plugin) { + if ($plugin and enrol_is_enabled($plugin->get_name())) { $this->bulkoperations = $plugin->get_bulk_operations($manager); } } diff --git a/enrol/self/editenrolment.php b/enrol/self/editenrolment.php index 418359ea9df16..3cb51bb2dc71a 100644 --- a/enrol/self/editenrolment.php +++ b/enrol/self/editenrolment.php @@ -54,6 +54,11 @@ redirect(new moodle_url('/')); } +// Do not allow any changes if plugin disabled. +if (!enrol_is_enabled('self')) { + redirect(new moodle_url('/course/view.php', array('id'=>$course->id))); +} + // Obvioulsy require_login($course); // The user must be able to manage self enrolments within the course diff --git a/enrol/upgrade.txt b/enrol/upgrade.txt index 868912d19fd54..8fcd760ef9738 100644 --- a/enrol/upgrade.txt +++ b/enrol/upgrade.txt @@ -8,6 +8,9 @@ required changes in code: * use role_get_name() or role_fix_names() if you need any role names, using role.name directly from database is not correct any more +other changes: +* course enrolment manager now works with disabled plugins too + === 2.2 === From 6f6c9e5c6a8269d3906b9070231a736a6e974f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= <commits@skodak.org> Date: Sun, 26 Aug 2012 16:37:49 +0200 Subject: [PATCH 47/90] MDL-35072 coding style cleanup in enrol_manual --- enrol/manual/ajax.php | 10 +++----- enrol/manual/bulkchangeforms.php | 6 ++--- enrol/manual/db/access.php | 6 ++++- enrol/manual/db/install.php | 4 +-- enrol/manual/edit.php | 8 +++--- enrol/manual/edit_form.php | 6 ++--- enrol/manual/editenrolment.php | 38 ++++++++++++----------------- enrol/manual/editenrolment_form.php | 6 ++--- enrol/manual/externallib.php | 33 ++++++++++++------------- enrol/manual/lib.php | 24 +++++++++--------- enrol/manual/locallib.php | 26 +++++++++----------- enrol/manual/manage.php | 11 ++++----- enrol/manual/settings.php | 5 +--- enrol/manual/unenrolself.php | 8 +++--- enrol/manual/version.php | 3 +-- 15 files changed, 84 insertions(+), 110 deletions(-) diff --git a/enrol/manual/ajax.php b/enrol/manual/ajax.php index 140b4cd8dfe79..556170e94b5c9 100644 --- a/enrol/manual/ajax.php +++ b/enrol/manual/ajax.php @@ -20,8 +20,7 @@ * The general idea behind this file is that any errors should throw exceptions * which will be returned and acted upon by the calling AJAX script. * - * @package enrol - * @subpackage manual + * @package enrol_manual * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -32,8 +31,7 @@ require_once($CFG->dirroot.'/enrol/locallib.php'); require_once($CFG->dirroot.'/group/lib.php'); -// Must have the sesskey -$id = required_param('id', PARAM_INT); // course id +$id = required_param('id', PARAM_INT); // Course id. $action = required_param('action', PARAM_ALPHANUMEXT); $PAGE->set_url(new moodle_url('/enrol/ajax.php', array('id'=>$id, 'action'=>$action))); @@ -49,7 +47,7 @@ require_capability('moodle/course:enrolreview', $context); require_sesskey(); -echo $OUTPUT->header(); // send headers +echo $OUTPUT->header(); // Send headers. $manager = new course_enrolment_manager($PAGE, $course); @@ -135,4 +133,4 @@ throw new enrol_ajax_exception('unknowajaxaction'); } -echo json_encode($outcome); \ No newline at end of file +echo json_encode($outcome); diff --git a/enrol/manual/bulkchangeforms.php b/enrol/manual/bulkchangeforms.php index 8ee73008cdc48..05980e6f60c2e 100644 --- a/enrol/manual/bulkchangeforms.php +++ b/enrol/manual/bulkchangeforms.php @@ -1,5 +1,4 @@ <?php - // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify @@ -18,8 +17,7 @@ /** * This file contains form for bulk changing user enrolments. * - * @package core - * @subpackage enrol + * @package enrol_manual * @copyright 2011 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -29,7 +27,7 @@ require_once("$CFG->dirroot/enrol/bulkchange_forms.php"); /** - * The form to collect required information when bulk editing users enrolments + * The form to collect required information when bulk editing users enrolments. * * @copyright 2011 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later diff --git a/enrol/manual/db/access.php b/enrol/manual/db/access.php index 561ef29a39d91..5f14032f9e2fb 100644 --- a/enrol/manual/db/access.php +++ b/enrol/manual/db/access.php @@ -26,6 +26,7 @@ $capabilities = array( + /* Add, edit or remove manual enrol instance. */ 'enrol/manual:config' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, @@ -34,6 +35,7 @@ ) ), + /* Enrol anybody. */ 'enrol/manual:enrol' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, @@ -43,6 +45,7 @@ ) ), + /* Manage enrolments of users. */ 'enrol/manual:manage' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, @@ -52,6 +55,7 @@ ) ), + /* Unenrol anybody (including self) - watch out for data loss. */ 'enrol/manual:unenrol' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, @@ -61,6 +65,7 @@ ) ), + /* Unenrol self - watch out for data loss. */ 'enrol/manual:unenrolself' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, @@ -69,4 +74,3 @@ ), ); - diff --git a/enrol/manual/db/install.php b/enrol/manual/db/install.php index 3d99542881261..99584351821e2 100644 --- a/enrol/manual/db/install.php +++ b/enrol/manual/db/install.php @@ -1,5 +1,4 @@ <?php - // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify @@ -18,8 +17,7 @@ /** * Manual enrol plugin installation script * - * @package enrol - * @subpackage manual + * @package enrol_manual * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ diff --git a/enrol/manual/edit.php b/enrol/manual/edit.php index 978a6eb498341..c4ff53d504976 100644 --- a/enrol/manual/edit.php +++ b/enrol/manual/edit.php @@ -1,5 +1,4 @@ <?php - // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify @@ -19,8 +18,7 @@ * Adds new instance of enrol_manual to specified course * or edits current instance. * - * @package enrol - * @subpackage manual + * @package enrol_manual * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -49,14 +47,14 @@ if ($instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'enrol'=>'manual'), 'id ASC')) { $instance = array_shift($instances); if ($instances) { - // oh - we allow only one instance per course!! + // Oh - we allow only one instance per course!! foreach ($instances as $del) { $plugin->delete_instance($del); } } } else { require_capability('moodle/course:enrolconfig', $context); - // no instance yet, we have to add new instance + // No instance yet, we have to add new instance. navigation_node::override_active_url(new moodle_url('/enrol/instances.php', array('id'=>$course->id))); $instance = new stdClass(); $instance->id = null; diff --git a/enrol/manual/edit_form.php b/enrol/manual/edit_form.php index 7ff2792d34b31..eb210991167fe 100644 --- a/enrol/manual/edit_form.php +++ b/enrol/manual/edit_form.php @@ -1,5 +1,4 @@ <?php - // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify @@ -19,8 +18,7 @@ * Adds new instance of enrol_manual to specified course * or edits current instance. * - * @package enrol - * @subpackage manual + * @package enrol_manual * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -62,4 +60,4 @@ function definition() { $this->set_data($instance); } -} \ No newline at end of file +} diff --git a/enrol/manual/editenrolment.php b/enrol/manual/editenrolment.php index 2d8a7003751b4..b0f0b2fb1db25 100644 --- a/enrol/manual/editenrolment.php +++ b/enrol/manual/editenrolment.php @@ -20,46 +20,42 @@ * This page allows the current user to edit a manual user enrolment. * It is not compatible with the frontpage. * - * @package enrol - * @subpackage manual + * @package enrol_manual * @copyright 2011 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require('../../config.php'); require_once("$CFG->dirroot/enrol/locallib.php"); -require_once("$CFG->dirroot/enrol/renderer.php"); // Required for the course enrolment manager table +require_once("$CFG->dirroot/enrol/renderer.php"); // Required for the course enrolment manager table. require_once("$CFG->dirroot/enrol/manual/editenrolment_form.php"); -$ueid = required_param('ue', PARAM_INT); // user enrolment id +$ueid = required_param('ue', PARAM_INT); // User enrolment id. $filter = optional_param('ifilter', 0, PARAM_INT); -// Get the user enrolment object -$ue = $DB->get_record('user_enrolments', array('id' => $ueid), '*', MUST_EXIST); -// Get the user for whom the enrolment is -$user = $DB->get_record('user', array('id'=>$ue->userid), '*', MUST_EXIST); -// Get the course the enrolment is to -list($ctxsql, $ctxjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx'); -$sql = "SELECT c.* $ctxsql +// Get the user enrolment object. +$ue = $DB->get_record('user_enrolments', array('id' => $ueid), '*', MUST_EXIST); +// Get the user for whom the enrolment is. +$user = $DB->get_record('user', array('id'=>$ue->userid), '*', MUST_EXIST); +// Get the course the enrolment is to. +$sql = "SELECT c.* FROM {course} c LEFT JOIN {enrol} e ON e.courseid = c.id - $ctxjoin WHERE e.id = :enrolid"; $params = array('enrolid' => $ue->enrolid); $course = $DB->get_record_sql($sql, $params, MUST_EXIST); -context_instance_preload($course); -// Make sure its not the front page course +// Make sure its not the front page course. if ($course->id == SITEID) { redirect(new moodle_url('/')); } -// Obviously +// Obviously. require_login($course); -// Make sure the user can manage manual enrolments for this course +// Make sure the user can manage manual enrolments for this course. require_capability("enrol/manual:manage", context_course::instance($course->id, MUST_EXIST)); -// Get the enrolment manager for this course +// Get the enrolment manager for this course. $manager = new course_enrolment_manager($PAGE, $course, $filter); // Get an enrolment users table object. Doign this will automatically retrieve the the URL params // relating to table the user was viewing before coming here, and allows us to return the user to the @@ -70,7 +66,7 @@ $usersurl = new moodle_url('/enrol/users.php', array('id' => $course->id)); // The URl to return the user too after this screen. $returnurl = new moodle_url($usersurl, $manager->get_url_params()+$table->get_url_params()); -// The URL of this page +// The URL of this page. $url = new moodle_url('/enrol/manual/editenrolment.php', $returnurl->params()); $PAGE->set_url($url); @@ -88,9 +84,7 @@ // Check the form hasn't been cancelled if ($mform->is_cancelled()) { redirect($returnurl); -} else if ($mform->is_submitted() && $mform->is_validated() && confirm_sesskey()) { - // The forms been submit, validated and the sesskey has been checked ... edit the enrolment. - $data = $mform->get_data(); +} else if ($data = $mform->get_data()) { if ($manager->edit_enrolment($ue, $data)) { redirect($returnurl); } @@ -107,4 +101,4 @@ echo $OUTPUT->header(); echo $OUTPUT->heading($fullname); $mform->display(); -echo $OUTPUT->footer(); \ No newline at end of file +echo $OUTPUT->footer(); diff --git a/enrol/manual/editenrolment_form.php b/enrol/manual/editenrolment_form.php index ea9205d2e179d..b73fc7c08041f 100644 --- a/enrol/manual/editenrolment_form.php +++ b/enrol/manual/editenrolment_form.php @@ -1,5 +1,4 @@ <?php - // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify @@ -18,8 +17,7 @@ /** * Contains the form used to edit manual enrolments for a user. * - * @package enrol - * @subpackage manual + * @package enrol_manual * @copyright 2011 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -80,4 +78,4 @@ function validation($data, $files) { return $errors; } -} \ No newline at end of file +} diff --git a/enrol/manual/externallib.php b/enrol/manual/externallib.php index 94fde8100722c..01686da8e662a 100644 --- a/enrol/manual/externallib.php +++ b/enrol/manual/externallib.php @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. - /** * External course participation api. * @@ -32,7 +31,7 @@ require_once("$CFG->libdir/externallib.php"); /** - * Manual enrolment external functions + * Manual enrolment external functions. * * @package enrol_manual * @category external @@ -43,7 +42,7 @@ class enrol_manual_external extends external_api { /** - * Returns description of method parameters + * Returns description of method parameters. * * @return external_function_parameters * @since Moodle 2.2 @@ -68,7 +67,7 @@ public static function enrol_users_parameters() { } /** - * Enrolment of users + * Enrolment of users. * * Function throw an exception at the first error encountered. * @param array $enrolments An array of user enrolment @@ -82,24 +81,24 @@ public static function enrol_users($enrolments) { $params = self::validate_parameters(self::enrol_users_parameters(), array('enrolments' => $enrolments)); - $transaction = $DB->start_delegated_transaction(); //rollback all enrolment if an error occurs - //(except if the DB doesn't support it) + $transaction = $DB->start_delegated_transaction(); // Rollback all enrolment if an error occurs + // (except if the DB doesn't support it). - //retrieve the manual enrolment plugin + // Retrieve the manual enrolment plugin. $enrol = enrol_get_plugin('manual'); if (empty($enrol)) { throw new moodle_exception('manualpluginnotinstalled', 'enrol_manual'); } foreach ($params['enrolments'] as $enrolment) { - // Ensure the current user is allowed to run this function in the enrolment context + // Ensure the current user is allowed to run this function in the enrolment context. $context = context_course::instance($enrolment['courseid'], IGNORE_MISSING); self::validate_context($context); - //check that the user has the permission to manual enrol + // Check that the user has the permission to manual enrol. require_capability('enrol/manual:enrol', $context); - //throw an exception if user is not able to assign the role + // Throw an exception if user is not able to assign the role. $roles = get_assignable_roles($context); if (!key_exists($enrolment['roleid'], $roles)) { $errorparams = new stdClass(); @@ -109,7 +108,7 @@ public static function enrol_users($enrolments) { throw new moodle_exception('wsusercannotassign', 'enrol_manual', '', $errorparams); } - //check manual enrolment plugin instance is enabled/exist + // Check manual enrolment plugin instance is enabled/exist. $enrolinstances = enrol_get_instances($enrolment['courseid'], true); foreach ($enrolinstances as $courseenrolinstance) { if ($courseenrolinstance->enrol == "manual") { @@ -123,7 +122,7 @@ public static function enrol_users($enrolments) { throw new moodle_exception('wsnoinstance', 'enrol_manual', $errorparams); } - //check that the plugin accept enrolment (it should always the case, it's hard coded in the plugin) + // Check that the plugin accept enrolment (it should always the case, it's hard coded in the plugin). if (!$enrol->allow_enrol($instance)) { $errorparams = new stdClass(); $errorparams->roleid = $enrolment['roleid']; @@ -132,7 +131,7 @@ public static function enrol_users($enrolments) { throw new moodle_exception('wscannotenrol', 'enrol_manual', '', $errorparams); } - //finally proceed the enrolment + // Finally proceed the enrolment. $enrolment['timestart'] = isset($enrolment['timestart']) ? $enrolment['timestart'] : 0; $enrolment['timeend'] = isset($enrolment['timeend']) ? $enrolment['timeend'] : 0; $enrolment['status'] = (isset($enrolment['suspend']) && !empty($enrolment['suspend'])) ? @@ -147,7 +146,7 @@ public static function enrol_users($enrolments) { } /** - * Returns description of method result value + * Returns description of method result value. * * @return null * @since Moodle 2.2 @@ -159,7 +158,7 @@ public static function enrol_users_returns() { } /** - * Deprecated manual enrolment external functions + * Deprecated manual enrolment external functions. * * @package enrol_manual * @copyright 2011 Jerome Mouneyrac @@ -172,7 +171,7 @@ public static function enrol_users_returns() { class moodle_enrol_manual_external extends external_api { /** - * Returns description of method parameters + * Returns description of method parameters. * * @return external_function_parameters * @since Moodle 2.0 @@ -199,7 +198,7 @@ public static function manual_enrol_users($enrolments) { } /** - * Returns description of method result value + * Returns description of method result value. * * @return nul * @since Moodle 2.0 diff --git a/enrol/manual/lib.php b/enrol/manual/lib.php index 5320f6aae5470..2cf94bb779398 100644 --- a/enrol/manual/lib.php +++ b/enrol/manual/lib.php @@ -1,5 +1,4 @@ <?php - // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify @@ -18,8 +17,7 @@ /** * Manual enrolment plugin main library file. * - * @package enrol - * @subpackage manual + * @package enrol_manual * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -29,22 +27,22 @@ class enrol_manual_plugin extends enrol_plugin { public function roles_protected() { - // users may tweak the roles later + // Users may tweak the roles later. return false; } public function allow_enrol(stdClass $instance) { - // users with enrol cap may unenrol other users manually manually + // Users with enrol cap may unenrol other users manually manually. return true; } public function allow_unenrol(stdClass $instance) { - // users with unenrol cap may unenrol other users manually manually + // Users with unenrol cap may unenrol other users manually manually. return true; } public function allow_manage(stdClass $instance) { - // users with manage cap may tweak period and status + // Users with manage cap may tweak period and status. return true; } @@ -52,7 +50,7 @@ public function allow_manage(stdClass $instance) { * Returns link to manual enrol UI if exists. * Does the access control tests automatically. * - * @param object $instance + * @param stdClass $instance * @return moodle_url */ public function get_manual_enrol_link($instance) { @@ -96,7 +94,7 @@ public function add_course_navigation($instancesnode, stdClass $instance) { } /** - * Returns edit icons for the page with list of instances + * Returns edit icons for the page with list of instances. * @param stdClass $instance * @return array */ @@ -145,7 +143,7 @@ public function get_newinstance_link($courseid) { /** * Add new instance of enrol plugin with default settings. - * @param object $course + * @param stdClass $course * @return int id of new instance, null if can not be created */ public function add_default_instance($course) { @@ -155,7 +153,7 @@ public function add_default_instance($course) { /** * Add new instance of enrol plugin. - * @param object $course + * @param stdClass $course * @param array instance fields * @return int id of new instance, null if can not be created */ @@ -260,7 +258,7 @@ public function get_manual_enrol_button(course_enrolment_manager $manager) { } /** - * Gets an array of the user enrolment actions + * Gets an array of the user enrolment actions. * * @param course_enrolment_manager $manager * @param stdClass $ue A user enrolment object @@ -284,7 +282,7 @@ public function get_user_enrolment_actions(course_enrolment_manager $manager, $u } /** - * The manual plugin has several bulk operations that can be performed + * The manual plugin has several bulk operations that can be performed. * @param course_enrolment_manager $manager * @return array */ diff --git a/enrol/manual/locallib.php b/enrol/manual/locallib.php index 1d09681ad1221..1a3d58039fc6d 100644 --- a/enrol/manual/locallib.php +++ b/enrol/manual/locallib.php @@ -17,8 +17,7 @@ /** * Auxiliary manual user enrolment lib, the main purpose is to lower memory requirements... * - * @package enrol - * @subpackage manual + * @package enrol_manual * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -30,7 +29,7 @@ /** - * Enrol candidates + * Enrol candidates. */ class enrol_manual_potential_participant extends user_selector_base { protected $enrolid; @@ -42,12 +41,12 @@ public function __construct($name, $options) { /** * Candidate users - * @param <type> $search + * @param string $search * @return array */ public function find_users($search) { global $DB; - //by default wherecondition retrieves all users except the deleted, not confirmed and guest + // By default wherecondition retrieves all users except the deleted, not confirmed and guest. list($wherecondition, $params) = $this->search_sql($search, 'u'); $params['enrolid'] = $this->enrolid; @@ -92,7 +91,7 @@ protected function get_options() { } /** - * Enroled users + * Enrolled users. */ class enrol_manual_current_participant extends user_selector_base { protected $courseid; @@ -105,12 +104,12 @@ public function __construct($name, $options) { /** * Candidate users - * @param <type> $search + * @param string $search * @return array */ public function find_users($search) { global $DB; - //by default wherecondition retrieves all users except the deleted, not confirmed and guest + // By default wherecondition retrieves all users except the deleted, not confirmed and guest. list($wherecondition, $params) = $this->search_sql($search, 'u'); $params['enrolid'] = $this->enrolid; @@ -182,7 +181,6 @@ public function get_identifier() { /** * Processes the bulk operation request for the given userids with the provided properties. * - * @global moodle_database $DB * @param course_enrolment_manager $manager * @param array $userids * @param stdClass $properties The data returned by the form. @@ -194,7 +192,7 @@ public function process(course_enrolment_manager $manager, array $users, stdClas return false; } - // Get all of the user enrolment id's + // Get all of the user enrolment id's. $ueids = array(); $instances = array(); foreach ($users as $user) { @@ -237,15 +235,15 @@ public function process(course_enrolment_manager $manager, array $users, stdClas return true; } - // Update the modifierid + // Update the modifierid. $updatesql[] = 'modifierid = :modifierid'; $params['modifierid'] = (int)$USER->id; - // Update the time modified + // Update the time modified. $updatesql[] = 'timemodified = :timemodified'; $params['timemodified'] = time(); - // Build the SQL statement + // Build the SQL statement. $updatesql = join(', ', $updatesql); $sql = "UPDATE {user_enrolments} SET $updatesql @@ -353,4 +351,4 @@ public function process(course_enrolment_manager $manager, array $users, stdClas } return true; } -} \ No newline at end of file +} diff --git a/enrol/manual/manage.php b/enrol/manual/manage.php index 0a4f69865a9c7..46d1b77e87414 100644 --- a/enrol/manual/manage.php +++ b/enrol/manual/manage.php @@ -17,8 +17,7 @@ /** * Manual user enrolment UI. * - * @package enrol - * @subpackage manual + * @package enrol_manual * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -47,7 +46,7 @@ $roles = array('0'=>get_string('none')) + $roles; if (!isset($roles[$roleid])) { - // weird - security always first! + // Weird - security always first! $roleid = 0; } @@ -88,14 +87,14 @@ $today = time(); $today = make_timestamp(date('Y', $today), date('m', $today), date('d', $today), 0, 0, 0); -// enrolment start +// Enrolment start. $basemenu = array(); if ($course->startdate > 0) { $basemenu[2] = get_string('coursestart') . ' (' . userdate($course->startdate, $timeformat) . ')'; } $basemenu[3] = get_string('today') . ' (' . userdate($today, $timeformat) . ')' ; -// process add and removes +// Process add and removes. if (optional_param('add', false, PARAM_BOOL) && confirm_sesskey()) { $userstoassign = $potentialuserselector->get_selected_users(); if (!empty($userstoassign)) { @@ -126,7 +125,7 @@ } } -// Process incoming role unassignments +// Process incoming role unassignments. if (optional_param('remove', false, PARAM_BOOL) && confirm_sesskey()) { $userstounassign = $currentuserselector->get_selected_users(); if (!empty($userstounassign)) { diff --git a/enrol/manual/settings.php b/enrol/manual/settings.php index 13d72ed92590d..79686aab7922c 100644 --- a/enrol/manual/settings.php +++ b/enrol/manual/settings.php @@ -1,5 +1,4 @@ <?php - // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify @@ -18,8 +17,7 @@ /** * Manual enrolment plugin settings and presets. * - * @package enrol - * @subpackage manual + * @package enrol_manual * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -55,4 +53,3 @@ get_string('defaultrole', 'role'), '', $student->id, $options)); } } - diff --git a/enrol/manual/unenrolself.php b/enrol/manual/unenrolself.php index fb1744de71623..c7304fb5f5805 100644 --- a/enrol/manual/unenrolself.php +++ b/enrol/manual/unenrolself.php @@ -1,5 +1,4 @@ <?php - // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify @@ -18,8 +17,7 @@ /** * Manual enrolment plugin - support for user self unenrolment. * - * @package enrol - * @subpackage manual + * @package enrol_manual * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -41,7 +39,7 @@ $plugin = enrol_get_plugin('manual'); -// security defined inside following function +// Security defined inside following function. if (!$plugin->get_unenrolself_link($instance)) { redirect(new moodle_url('/course/view.php', array('id'=>$course->id))); } @@ -51,7 +49,7 @@ if ($confirm and confirm_sesskey()) { $plugin->unenrol_user($instance, $USER->id); - add_to_log($course->id, 'course', 'unenrol', '../enrol/users.php?id='.$course->id, $course->id); //there should be userid somewhere! + add_to_log($course->id, 'course', 'unenrol', '../enrol/users.php?id='.$course->id, $course->id); //TODO: there should be userid somewhere! redirect(new moodle_url('/index.php')); } diff --git a/enrol/manual/version.php b/enrol/manual/version.php index 8d2ff58fe7b8b..2107a671994a6 100644 --- a/enrol/manual/version.php +++ b/enrol/manual/version.php @@ -17,8 +17,7 @@ /** * Manual enrolment plugin version specification. * - * @package enrol - * @subpackage manual + * @package enrol_manual * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ From 96a25065dacf48878b2dde8d9884bf68d76e8d1f Mon Sep 17 00:00:00 2001 From: Mary Evans <lazydaisy@visible-expression.co.uk> Date: Sun, 26 Aug 2012 20:02:15 +0100 Subject: [PATCH 48/90] MDL-35048 theme_formal_white: removed p {margin:0} from style/formal_white.css which was causing a regression in some areas of theme --- theme/formal_white/style/formal_white.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/theme/formal_white/style/formal_white.css b/theme/formal_white/style/formal_white.css index 2a262c9ccfcbb..248ea332c09e2 100644 --- a/theme/formal_white/style/formal_white.css +++ b/theme/formal_white/style/formal_white.css @@ -24,8 +24,6 @@ h4 {font-weight:bold;} h1.headerheading {margin:14px 11px 8px 11px;float:left;font-size:200%;} h2.main, h3.main, h4.main {margin:1em;padding:0;text-align:center;} -p {margin:0} - /* page-header */ #page-header{line-height:0;overflow:hidden;} From 1caeb4b4503c4315880afc27177e6162f5761aa0 Mon Sep 17 00:00:00 2001 From: Aaron Barnes <aaronb@catalyst.net.nz> Date: Fri, 20 Jul 2012 13:40:42 +1200 Subject: [PATCH 49/90] MDL-32386 completion: Fix incorrect method parameters --- lib/completionlib.php | 6 +++++- lib/tests/completionlib_test.php | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/completionlib.php b/lib/completionlib.php index c96203790d29c..4d6ea823aa292 100644 --- a/lib/completionlib.php +++ b/lib/completionlib.php @@ -621,16 +621,20 @@ public function set_module_viewed($cm, $userid=0) { debugging('set_module_viewed must be called before header is printed', DEBUG_DEVELOPER); } + // Don't do anything if view condition is not turned on if ($cm->completionview == COMPLETION_VIEW_NOT_REQUIRED || !$this->is_enabled($cm)) { return; } + // Get current completion state - $data = $this->get_data($cm, $userid); + $data = $this->get_data($cm, false, $userid); + // If we already viewed it, don't do anything if ($data->viewed == COMPLETION_VIEWED) { return; } + // OK, change state, save it, and update completion $data->viewed = COMPLETION_VIEWED; $this->internal_set_data($cm, $data); diff --git a/lib/tests/completionlib_test.php b/lib/tests/completionlib_test.php index 6e81cd710ecc8..50928315d01cc 100644 --- a/lib/tests/completionlib_test.php +++ b/lib/tests/completionlib_test.php @@ -257,7 +257,7 @@ function test_set_module_viewed() { ->will($this->returnValue(true)); $c->expects($this->at(1)) ->method('get_data') - ->with($cm, 1337) + ->with($cm, false, 1337) ->will($this->returnValue((object)array('viewed'=>COMPLETION_NOT_VIEWED))); $c->expects($this->at(2)) ->method('internal_set_data') From 3b062da97b3faf400a6516e76ad8b1d6f2538e50 Mon Sep 17 00:00:00 2001 From: AMOS bot <amos@moodle.org> Date: Mon, 27 Aug 2012 00:32:45 +0000 Subject: [PATCH 50/90] Automatically generated installer lang files --- install/lang/lt/admin.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/install/lang/lt/admin.php b/install/lang/lt/admin.php index 3c005d87957d3..9977e2b25433c 100644 --- a/install/lang/lt/admin.php +++ b/install/lang/lt/admin.php @@ -34,9 +34,11 @@ $string['cliansweryes'] = 't'; $string['cliincorrectvalueerror'] = 'Klaida, klaidinga "{$a->option}" reikÅ¡mÄ— "{$a->value}"'; $string['cliincorrectvalueretry'] = 'Klaidinga reikÅ¡mÄ—, bandykite dar kartÄ…'; -$string['clitypevalue'] = 'tipo reikÅ¡mÄ—'; -$string['clitypevaluedefault'] = 'tipo reikÅ¡mÄ—, paspauskite „Enter“, jei norite naudoti numatytÄ…jÄ… reikÅ¡mÄ™ ({$a})'; -$string['cliunknowoption'] = 'Neatpažintos parinktys: {$a} naudokite --žinyno parinktį.'; +$string['clitypevalue'] = 'įveskite reikÅ¡mÄ™'; +$string['clitypevaluedefault'] = 'įveskite reikÅ¡mÄ™, paspauskite „Enter“, jei norite naudoti numatytÄ…jÄ… reikÅ¡mÄ™ ({$a})'; +$string['cliunknowoption'] = 'Neatpažintos parinktys: +{$a} +Naudokite --help parinktį.'; $string['cliyesnoprompt'] = 'įveskite t (taip) arba n (ne)'; $string['environmentrequireinstall'] = 'turi bÅ«ti įdiegta ir įgalinta'; $string['environmentrequireversion'] = 'reikalinga {$a->needed} versija, o JÅ«s turite {$a->current}'; From 13a20081d304139b2dd3afbca01b5cb49432dbec Mon Sep 17 00:00:00 2001 From: Aparup Banerjee <aparup@moodle.com> Date: Mon, 27 Aug 2012 12:07:15 +0800 Subject: [PATCH 51/90] MDL-34429 fixed whitespace. --- course/lib.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/course/lib.php b/course/lib.php index 2eaf092cc8694..cf8f3723c714f 100644 --- a/course/lib.php +++ b/course/lib.php @@ -572,7 +572,7 @@ function print_log_csv($course, $user, $date, $order='l.time DESC', $modname, $csvexporter->set_filename('logs', '.txt'); $title = array(get_string('savedat').userdate(time(), $strftimedatetime)); - $csvexporter->add_data($title); + $csvexporter->add_data($title); $csvexporter->add_data($header); if (empty($logs['logs'])) { From 764094585bbffa8399241558237e63754523a9fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= <commits@skodak.org> Date: Mon, 27 Aug 2012 17:19:07 +0200 Subject: [PATCH 52/90] MDL-35072 fix bogus left enrol join Credit goes to Eloy, thanks. --- enrol/manual/editenrolment.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enrol/manual/editenrolment.php b/enrol/manual/editenrolment.php index b0f0b2fb1db25..538f45818f9cc 100644 --- a/enrol/manual/editenrolment.php +++ b/enrol/manual/editenrolment.php @@ -40,7 +40,7 @@ // Get the course the enrolment is to. $sql = "SELECT c.* FROM {course} c - LEFT JOIN {enrol} e ON e.courseid = c.id + JOIN {enrol} e ON e.courseid = c.id WHERE e.id = :enrolid"; $params = array('enrolid' => $ue->enrolid); $course = $DB->get_record_sql($sql, $params, MUST_EXIST); From eef59b125435f674b3ddc6577d223f35dc579211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= <commits@skodak.org> Date: Mon, 27 Aug 2012 17:20:47 +0200 Subject: [PATCH 53/90] MDL-35070 fix incorrect enrol join --- enrol/self/editenrolment.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enrol/self/editenrolment.php b/enrol/self/editenrolment.php index c63d7583c0a4b..e97bc2a911399 100644 --- a/enrol/self/editenrolment.php +++ b/enrol/self/editenrolment.php @@ -40,7 +40,7 @@ // Get the course the enrolment is to. $sql = "SELECT c.* FROM {course} c - LEFT JOIN {enrol} e ON e.courseid = c.id + JOIN {enrol} e ON e.courseid = c.id WHERE e.id = :enrolid"; $params = array('enrolid' => $ue->enrolid); $course = $DB->get_record_sql($sql, $params, MUST_EXIST); From 96729f6d2df80e7173255a2a752a675feffb51f7 Mon Sep 17 00:00:00 2001 From: Adrian Greeve <adrian@moodle.com> Date: Mon, 13 Aug 2012 14:54:27 +0800 Subject: [PATCH 54/90] MDL-34075 - lib - Alteration to the csv import lib to include rfc-4180 compliance --- lib/csvlib.class.php | 84 +++++++++++++++++++------------------ lib/tests/csvclass_test.php | 42 +++++++++++++++++++ 2 files changed, 86 insertions(+), 40 deletions(-) diff --git a/lib/csvlib.class.php b/lib/csvlib.class.php index 421058a4128f1..bb09dd0d80260 100644 --- a/lib/csvlib.class.php +++ b/lib/csvlib.class.php @@ -76,9 +76,10 @@ function csv_import_reader($iid, $type) { * @param string $encoding content encoding * @param string $delimiter_name separator (comma, semicolon, colon, cfg) * @param string $column_validation name of function for columns validation, must have one param $columns + * @param string $enclosure field wrapper. One character only. * @return bool false if error, count of data lines if ok; use get_error() to get error string */ - function load_csv_content(&$content, $encoding, $delimiter_name, $column_validation=null) { + function load_csv_content(&$content, $encoding, $delimiter_name, $column_validation=null, $enclosure='"') { global $USER, $CFG; $this->close(); @@ -89,62 +90,65 @@ function load_csv_content(&$content, $encoding, $delimiter_name, $column_validat $content = textlib::trim_utf8_bom($content); // Fix mac/dos newlines $content = preg_replace('!\r\n?!', "\n", $content); - // is there anyting in file? - $columns = strtok($content, "\n"); - if ($columns === false) { - $this->_error = get_string('csvemptyfile', 'error'); - return false; - } + $csv_delimiter = csv_import_reader::get_delimiter($delimiter_name); - $csv_encode = csv_import_reader::get_encoded_delimiter($delimiter_name); + // $csv_encode = csv_import_reader::get_encoded_delimiter($delimiter_name); + + // create a temporary file and store the csv file there. + $fp = tmpfile(); + fwrite($fp, $content); + fseek($fp, 0); + // Create an array to store the imported data for error checking. + $columns = array(); + // str_getcsv doesn't iterate through the csv data properly. It has + // problems with line returns. + while ($fgetdata = fgetcsv($fp, 0, $csv_delimiter, $enclosure)) { + $columns[] = $fgetdata; + } + $col_count = 0; // process header - list of columns - $columns = explode($csv_delimiter, $columns); - $col_count = count($columns); - if ($col_count === 0) { + if (!isset($columns[0])) { $this->_error = get_string('csvemptyfile', 'error'); + fclose($fp); return false; + } else { + $col_count = count($columns[0]); } - foreach ($columns as $key=>$value) { - $columns[$key] = str_replace($csv_encode, $csv_delimiter, trim($value)); - } + // Column validation. if ($column_validation) { - $result = $column_validation($columns); + $result = $column_validation($columns[0]); if ($result !== true) { $this->_error = $result; + fclose($fp); return false; } } - $this->_columns = $columns; // cached columns - // open file for writing - $filename = $CFG->tempdir.'/csvimport/'.$this->_type.'/'.$USER->id.'/'.$this->_iid; - $fp = fopen($filename, "w"); - fwrite($fp, serialize($columns)."\n"); - - // again - do we have any data for processing? - $line = strtok("\n"); - $data_count = 0; - while ($line !== false) { - $line = explode($csv_delimiter, $line); - foreach ($line as $key=>$value) { - $line[$key] = str_replace($csv_encode, $csv_delimiter, trim($value)); - } - if (count($line) !== $col_count) { - // this is critical!! + $this->_columns = $columns[0]; // cached columns + // check to make sure that the data columns match up with the headers. + foreach ($columns as $rowdata) { + if (count($rowdata) !== $col_count) { $this->_error = get_string('csvweirdcolumns', 'error'); fclose($fp); $this->cleanup(); return false; } - fwrite($fp, serialize($line)."\n"); - $data_count++; - $line = strtok("\n"); } + $filename = $CFG->tempdir.'/csvimport/'.$this->_type.'/'.$USER->id.'/'.$this->_iid; + $filepointer = fopen($filename, "w"); + // The information has been stored in csv format, as serialized data has issues + // with special characters and line returns. + $storedata = csv_export_writer::print_array($columns, ',', '"', true); + fwrite($filepointer, $storedata); + fclose($fp); - return $data_count; + fclose($filepointer); + + $datacount = count($columns); + return $datacount; } /** @@ -164,12 +168,12 @@ function get_columns() { return false; } $fp = fopen($filename, "r"); - $line = fgets($fp); + $line = fgetcsv($fp); fclose($fp); if ($line === false) { return false; } - $this->_columns = unserialize($line); + $this->_columns = $line; return $this->_columns; } @@ -194,7 +198,7 @@ function init() { return false; } //skip header - return (fgets($this->_fp) !== false); + return (fgetcsv($this->_fp) !== false); } /** @@ -206,8 +210,8 @@ function next() { if (empty($this->_fp) or feof($this->_fp)) { return false; } - if ($ser = fgets($this->_fp)) { - return unserialize($ser); + if ($ser = fgetcsv($this->_fp)) { + return $ser; } else { return false; } diff --git a/lib/tests/csvclass_test.php b/lib/tests/csvclass_test.php index ff932279aa3dd..0fdce11d2f8a6 100644 --- a/lib/tests/csvclass_test.php +++ b/lib/tests/csvclass_test.php @@ -32,6 +32,8 @@ class csvclass_testcase extends advanced_testcase { var $testdata = array(); var $teststring = ''; + var $teststring2 = ''; + var $teststring3 = ''; protected function setUp(){ @@ -57,6 +59,11 @@ protected function setUp(){ "Phillip Jenkins","<p>This field has </p> <p>Multiple lines</p> <p>and also contains ""double quotes""</p>",Yebisu +'; + + $this->teststring2 = 'fullname,"description of things",beer +"Fred Flint","<p>Find the stone inside the box</p>",Asahi,"A fourth column" +"Sarah Smith","<p>How are the people next door?</p>,Yebisu,"Forget the next" '; } @@ -71,5 +78,40 @@ public function test_csv_functions() { $test_data = csv_export_writer::print_array($this->testdata, 'comma', '"', true); $this->assertEquals($test_data, $this->teststring); + + // Testing that the content is imported correctly. + $iid = csv_import_reader::get_new_iid('lib'); + $csvimport = new csv_import_reader($iid, 'lib'); + $contentcount = $csvimport->load_csv_content($this->teststring, 'utf-8', 'comma'); + $csvimport->init(); + $dataset = array(); + $dataset[] = $csvimport->get_columns(); + while ($record = $csvimport->next()) { + $dataset[] = $record; + } + $csvimport->cleanup(); + $csvimport->close(); + $this->assertEquals($dataset, $this->testdata); + + // Testing for the wrong count of columns. + $errortext = get_string('csvweirdcolumns', 'error'); + $iid = csv_import_reader::get_new_iid('lib'); + $csvimport = new csv_import_reader($iid, 'lib'); + $contentcount = $csvimport->load_csv_content($this->teststring2, 'utf-8', 'comma'); + $importerror = $csvimport->get_error(); + $csvimport->cleanup(); + $csvimport->close(); + $this->assertEquals($importerror, $errortext); + + // Testing for empty content + $errortext = get_string('csvemptyfile', 'error'); + + $iid = csv_import_reader::get_new_iid('lib'); + $csvimport = new csv_import_reader($iid, 'lib'); + $contentcount = $csvimport->load_csv_content($this->teststring3, 'utf-8', 'comma'); + $importerror = $csvimport->get_error(); + $csvimport->cleanup(); + $csvimport->close(); + $this->assertEquals($importerror, $errortext); } } From a3c94686aab3146bdce2dc1fc48ddae4920a65b3 Mon Sep 17 00:00:00 2001 From: Marina Glancy <marina@moodle.com> Date: Tue, 31 Jul 2012 08:51:27 +0800 Subject: [PATCH 55/90] MDL-34290 curl class: add functions to return error code and to download one file --- lib/filelib.php | 69 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/lib/filelib.php b/lib/filelib.php index af71f16188300..133a0983b9a06 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -2752,6 +2752,8 @@ class curl { public $info; /** @var string error */ public $error; + /** @var int error code */ + public $errno; /** @var array cURL options */ private $options; @@ -2895,6 +2897,7 @@ public function cleanopt(){ unset($this->options['CURLOPT_INFILE']); unset($this->options['CURLOPT_INFILESIZE']); unset($this->options['CURLOPT_CUSTOMREQUEST']); + unset($this->options['CURLOPT_FILE']); } /** @@ -3119,6 +3122,7 @@ protected function request($url, $options = array()){ $this->info = curl_getinfo($curl); $this->error = curl_error($curl); + $this->errno = curl_errno($curl); if ($this->debug){ echo '<h1>Return Data</h1>'; @@ -3202,6 +3206,62 @@ public function get($url, $params = array(), $options = array()){ return $this->request($url, $options); } + /** + * Downloads one file and writes it to the specified file handler + * + * <code> + * $c = new curl(); + * $file = fopen('savepath', 'w'); + * $result = $c->download_one('http://localhost/', null, + * array('file' => $file, 'timeout' => 5, 'followlocation' => true, 'maxredirs' => 3)); + * fclose($file); + * $download_info = $c->get_info(); + * if ($result === true) { + * // file downloaded successfully + * } else { + * $error_text = $result; + * $error_code = $c->get_errno(); + * } + * </code> + * + * <code> + * $c = new curl(); + * $result = $c->download_one('http://localhost/', null, + * array('filepath' => 'savepath', 'timeout' => 5, 'followlocation' => true, 'maxredirs' => 3)); + * // ... see above, no need to close handle and remove file if unsuccessful + * </code> + * + * @param string $url + * @param array|null $params key-value pairs to be added to $url as query string + * @param array $options request options. Must include either 'file' or 'filepath' + * @return bool|string true on success or error string on failure + */ + public function download_one($url, $params, $options = array()) { + $options['CURLOPT_HTTPGET'] = 1; + $options['CURLOPT_BINARYTRANSFER'] = true; + if (!empty($params)){ + $url .= (stripos($url, '?') !== false) ? '&' : '?'; + $url .= http_build_query($params, '', '&'); + } + if (!empty($options['filepath']) && empty($options['file'])) { + // open file + if (!($options['file'] = fopen($options['filepath'], 'w'))) { + $this->errno = 100; + return get_string('cannotwritefile', 'error', $options['filepath']); + } + $filepath = $options['filepath']; + } + unset($options['filepath']); + $result = $this->request($url, $options); + if (isset($filepath)) { + fclose($options['file']); + if ($result !== true) { + unlink($filepath); + } + } + return $result; + } + /** * HTTP PUT method * @@ -3279,6 +3339,15 @@ public function options($url, $options = array()){ public function get_info() { return $this->info; } + + /** + * Get curl error code + * + * @return int + */ + public function get_errno() { + return $this->errno; + } } /** From 2cdd5d8571456c0c71b3685db93f7ada234e76e5 Mon Sep 17 00:00:00 2001 From: Aparup Banerjee <aparup@moodle.com> Date: Tue, 28 Aug 2012 12:29:17 +0800 Subject: [PATCH 56/90] MDL-34549 added IGNORE_MISSING to context call for bc --- question/engine/questionusage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/question/engine/questionusage.php b/question/engine/questionusage.php index d6fbae8d639d7..22bc3b8721318 100644 --- a/question/engine/questionusage.php +++ b/question/engine/questionusage.php @@ -708,7 +708,7 @@ public static function load_from_records($records, $qubaid) { } $quba = new question_usage_by_activity($record->component, - context::instance_by_id($record->contextid)); + context::instance_by_id($record->contextid, IGNORE_MISSING)); $quba->set_id_from_database($record->qubaid); $quba->set_preferred_behaviour($record->preferredbehaviour); From af29ef049e4f4e6b606a41b06ef306fcbc101ea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= <commits@skodak.org> Date: Tue, 28 Aug 2012 08:32:23 +0200 Subject: [PATCH 57/90] MDL-23875 improve cohort only help Credit goes to Helen Foster. --- enrol/self/lang/en/enrol_self.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enrol/self/lang/en/enrol_self.php b/enrol/self/lang/en/enrol_self.php index 03677277ce384..cae46daa41c2b 100644 --- a/enrol/self/lang/en/enrol_self.php +++ b/enrol/self/lang/en/enrol_self.php @@ -26,7 +26,7 @@ $string['cohortnonmemberinfo'] = 'Only members of cohort \'{$a}\' can self-enrol.'; $string['cohortonly'] = 'Only cohort members'; -$string['cohortonly_help'] = 'Select a cohort if you want to restrict self enrolment only to members of this cohort. Change of this setting does not affect existing enrolments.'; +$string['cohortonly_help'] = 'Self enrolment may be restricted to members of a specified cohort only. Note that changing this setting has no effect on existing enrolments.'; $string['customwelcomemessage'] = 'Custom welcome message'; $string['customwelcomemessage_help'] = 'A custom welcome message may be added as plain text or Moodle-auto format, including HTML tags and multi-lang tags. From ddcea181af6914af5c6dba06e1572ed40e5df62d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= <commits@skodak.org> Date: Tue, 28 Aug 2012 10:14:47 +0200 Subject: [PATCH 58/90] MDL-34955 fix sloppy class typo and add MUC TODO info Credit goes to Aparup Banerjee, thanks. --- lib/editor/tinymce/adminlib.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/editor/tinymce/adminlib.php b/lib/editor/tinymce/adminlib.php index d8497f96fdcac..b145f9967439c 100644 --- a/lib/editor/tinymce/adminlib.php +++ b/lib/editor/tinymce/adminlib.php @@ -49,7 +49,7 @@ public function get_settings_url() { } public function is_enabled() { - static $disabledsubplugins = null; // TODO: remove this once get_config() is cached via MUC! + static $disabledsubplugins = null; // TODO: MDL-34344 remove this once get_config() is cached via MUC! if (is_null($disabledsubplugins)) { $disabledsubplugins = array(); @@ -200,7 +200,7 @@ public function output_html($data, $query='') { // Add available buttons. $buttons = implode(', ', $plugin->get_buttons()); - $buttons = html_writer::tag('span', $buttons, array('class'=>'tinamcebuttons')); + $buttons = html_writer::tag('span', $buttons, array('class'=>'tinymcebuttons')); // Add settings link. if (!$version) { From dbd0529ae5336814923fab8922d2b36208e30af3 Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" <stronk7@moodle.org> Date: Tue, 28 Aug 2012 14:16:15 +0200 Subject: [PATCH 59/90] MDL-34192 mod_assign: prevent ambiguous column use for Oracle. Credit goes to Raymond Antonio from NetSpot. --- mod/assign/gradingtable.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod/assign/gradingtable.php b/mod/assign/gradingtable.php index fb5e4ffb9de9a..40bed6632284e 100644 --- a/mod/assign/gradingtable.php +++ b/mod/assign/gradingtable.php @@ -88,7 +88,7 @@ function __construct(assign $assignment, $perpage, $filter, $rowoffset, $quickgr $params['assignmentid1'] = (int)$this->assignment->get_instance()->id; $params['assignmentid2'] = (int)$this->assignment->get_instance()->id; - $fields = user_picture::fields('u') . ', u.id as userid, u.firstname as firstname, u.lastname as lastname, '; + $fields = user_picture::fields('u') . ', u.id as userid, '; $fields .= 's.status as status, s.id as submissionid, s.timecreated as firstsubmission, s.timemodified as timesubmitted, '; $fields .= 'g.id as gradeid, g.grade as grade, g.timemodified as timemarked, g.timecreated as firstmarked, g.mailed as mailed, g.locked as locked'; $from = '{user} u LEFT JOIN {assign_submission} s ON u.id = s.userid AND s.assignment = :assignmentid1' . From 45fd6edfa448738e298015f80ea44601f1f77632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= <commits@skodak.org> Date: Tue, 28 Aug 2012 15:26:23 +0200 Subject: [PATCH 60/90] MDL-35064 improve enrol plugin uninstall confirmation Credit goes to Helen Foster, thanks! --- lang/en/enrol.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/en/enrol.php b/lang/en/enrol.php index ac5edf69c89dc..48baef7301495 100644 --- a/lang/en/enrol.php +++ b/lang/en/enrol.php @@ -98,7 +98,7 @@ $string['unenrolme'] = 'Unenrol me from {$a}'; $string['unenrolnotpermitted'] = 'You do not have permission or can not unenrol this user from this course.'; $string['unenrolroleusers'] = 'Unenrol users'; -$string['uninstallconfirm'] = 'You are about to completely uninstall the enrol plugin \'{$a}\'. This will completely delete everything in the database associated with this enrolment type. Deleting of enrolments removes also users\' grades, group membership, subscriptions and other course related data or preferences. +$string['uninstallconfirm'] = 'You are about to uninstall the enrolment plugin \'{$a}\'. This will result in the deletion of all data associated with this enrolment type, including users\' grades, group membership, forum subscriptions and any other course-related data. Are you SURE you want to continue?'; $string['uninstalldelete'] = 'Delete all enrolments and uninstall'; From 5d605db1dcbe21712fb5d52a83248b39ab4d095d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= <commits@skodak.org> Date: Tue, 28 Aug 2012 15:28:17 +0200 Subject: [PATCH 61/90] MDL-35064 improve enrol test comments Thanks Aparup Banerjee for the feedback. --- enrol/manual/tests/lib_test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enrol/manual/tests/lib_test.php b/enrol/manual/tests/lib_test.php index ed18c4f81386a..745bac07b65cc 100644 --- a/enrol/manual/tests/lib_test.php +++ b/enrol/manual/tests/lib_test.php @@ -196,7 +196,7 @@ public function test_migrate_plugin_enrolments() { $this->assertEquals(0, $DB->count_records('enrol', array('courseid'=>$course5->id, 'enrol'=>'manual'))); $this->assertEquals(0, $DB->count_records('enrol', array('courseid'=>$course5->id, 'enrol'=>'xxx'))); - // Make sure wrong params are ignored. + // Make sure wrong params do not produce errors or notices. enrol_manual_migrate_plugin_enrolments('manual'); enrol_manual_migrate_plugin_enrolments('yyyy'); From 71c3b0479af1b5f17975a887f9bd0f5c5a626feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= <commits@skodak.org> Date: Tue, 28 Aug 2012 18:40:58 +0200 Subject: [PATCH 62/90] MDL-34955 fix use of uninitialised disabledsubplugins setting Thanks Eloy Lafuente for spotting it! --- lib/editor/tinymce/lib.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/editor/tinymce/lib.php b/lib/editor/tinymce/lib.php index 73f01f774b1cc..d079dae7498a6 100644 --- a/lib/editor/tinymce/lib.php +++ b/lib/editor/tinymce/lib.php @@ -122,6 +122,9 @@ protected function get_init_params($elementid, array $options=null) { $context = empty($options['context']) ? context_system::instance() : $options['context']; $config = get_config('editor_tinymce'); + if (!isset($config->disabledsubplugins)) { + $config->disabledsubplugins = ''; + } $fontselectlist = empty($config->fontselectlist) ? '' : $config->fontselectlist; $fontbutton = ($fontselectlist === '') ? '' : 'fontselect,'; From 2ab9b983be5ee83729c55eee294e7938cfe9f3a1 Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" <stronk7@moodle.org> Date: Tue, 28 Aug 2012 20:16:54 +0200 Subject: [PATCH 63/90] MDL-25492 bb6 import: bump version after big changes. --- question/format/blackboard_six/version.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/question/format/blackboard_six/version.php b/question/format/blackboard_six/version.php index c413d77ef4218..a4048f4eb03f1 100644 --- a/question/format/blackboard_six/version.php +++ b/question/format/blackboard_six/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'qformat_blackboard_six'; -$plugin->version = 2012061700; +$plugin->version = 2012061701; $plugin->requires = 2012061700; From 111938abd98bc5a04436437d9c1d8d5f6f1ecb11 Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" <stronk7@moodle.org> Date: Tue, 28 Aug 2012 20:37:46 +0200 Subject: [PATCH 64/90] MDL-34250 navigation: Always look for correct parent context. --- lib/navigationlib.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/navigationlib.php b/lib/navigationlib.php index 43dfe9eefb403..d3c0664532cc6 100644 --- a/lib/navigationlib.php +++ b/lib/navigationlib.php @@ -1131,7 +1131,13 @@ public function initialise() { $addedcategories[$category->id] = $categoryparent->add($category->name, $url, self::TYPE_CATEGORY, $category->name, $category->id); if (!$category->visible) { - if (!has_capability('moodle/category:viewhiddencategories', context_coursecat::instance($category->parent))) { + // Let's decide the context where viewhidden cap checks will happen. + if ($category->parent == '0') { + $contexttocheck = context_system::instance(); + } else { + $contexttocheck = context_coursecat::instance($category->parent); + } + if (!has_capability('moodle/category:viewhiddencategories', $contexttocheck)) { $addedcategories[$category->id]->display = false; } else { $addedcategories[$category->id]->hidden = true; From 77a7da60db48cce912872c9cf2a26d0de0292b24 Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" <stronk7@moodle.org> Date: Wed, 29 Aug 2012 00:48:10 +0200 Subject: [PATCH 65/90] MDL-31973 Groups: Bump version after changing versions in prev merge with conflicts. --- version.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.php b/version.php index 9c9909f1d3162..c4ad2bf4f0edb 100644 --- a/version.php +++ b/version.php @@ -30,7 +30,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2012082300.01; // YYYYMMDD = weekly release date of this DEV branch +$version = 2012082300.02; // YYYYMMDD = weekly release date of this DEV branch // RR = release increments - 00 in DEV branches // .XX = incremental changes From 14b7e50001cf3cf03a5d80db1c2350692c8ac73c Mon Sep 17 00:00:00 2001 From: Marina Glancy <marina@moodle.com> Date: Tue, 31 Jul 2012 09:02:13 +0800 Subject: [PATCH 66/90] MDL-34290 update all references after the source is changed - When several records in {files} have the same record in {files_reference} and the synchronisation is performed, we need to update all records in {files} so all files know if source is changed and that sync was performed; - also when local moodle file content is changed we immediately update all files referencing to it (therefore sync of references to the local files is unnecessary); --- lib/filelib.php | 16 ++++---- lib/filestorage/file_storage.php | 69 ++++++++++++++++++++++++++++++++ lib/filestorage/stored_file.php | 46 ++++++++++++--------- 3 files changed, 103 insertions(+), 28 deletions(-) diff --git a/lib/filelib.php b/lib/filelib.php index 133a0983b9a06..c802acdb76f7f 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -804,10 +804,6 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea continue; } - // Replaced file content - if ($oldfile->get_contenthash() != $newfile->get_contenthash()) { - $oldfile->replace_content_with($newfile); - } // Updated author if ($oldfile->get_author() != $newfile->get_author()) { $oldfile->set_author($newfile->get_author()); @@ -827,16 +823,18 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea $oldfile->set_sortorder($newfile->get_sortorder()); } - // Update file size - if ($oldfile->get_filesize() != $newfile->get_filesize()) { - $oldfile->set_filesize($newfile->get_filesize()); - } - // Update file timemodified if ($oldfile->get_timemodified() != $newfile->get_timemodified()) { $oldfile->set_timemodified($newfile->get_timemodified()); } + // Replaced file content + if ($oldfile->get_contenthash() != $newfile->get_contenthash() || $oldfile->get_filesize() != $newfile->get_filesize()) { + $oldfile->replace_content_with($newfile); + // push changes to all local files that are referencing this file + $fs->update_references_to_storedfile($this); + } + // unchanged file or directory - we keep it as is unset($newhashes[$oldhash]); if (!$oldfile->is_directory()) { diff --git a/lib/filestorage/file_storage.php b/lib/filestorage/file_storage.php index 2b248d130360f..94cdf335bbc4c 100644 --- a/lib/filestorage/file_storage.php +++ b/lib/filestorage/file_storage.php @@ -1805,6 +1805,39 @@ public function get_references_count_by_storedfile(stored_file $storedfile) { return $this->search_references_count(self::pack_reference($params)); } + /** + * Updates all files that are referencing this file with the new contenthash + * and filesize + * + * @param stored_file $storedfile + */ + public function update_references_to_storedfile(stored_file $storedfile) { + global $CFG; + $params = array(); + $params['contextid'] = $storedfile->get_contextid(); + $params['component'] = $storedfile->get_component(); + $params['filearea'] = $storedfile->get_filearea(); + $params['itemid'] = $storedfile->get_itemid(); + $params['filename'] = $storedfile->get_filename(); + $params['filepath'] = $storedfile->get_filepath(); + $reference = self::pack_reference($params); + $referencehash = sha1($reference); + + $sql = "SELECT repositoryid, id FROM {files_reference} + WHERE referencehash = ? and reference = ?"; + $rs = $DB->get_recordset_sql($sql, array($referencehash, $reference)); + + $now = time(); + foreach ($rs as $record) { + require_once($CFG->dirroot.'/repository/lib.php'); + $repo = repository::get_instance($record->repositoryid); + $lifetime = $repo->get_reference_file_lifetime($reference); + $this->update_references($record->id, $now, $lifetime, + $storedfile->get_contenthash(), $storedfile->get_filesize(), 0); + } + $rs->close(); + } + /** * Convert file alias to local file * @@ -1997,4 +2030,40 @@ private function get_referencefileid($repositoryid, $reference, $strictness) { return $DB->get_field('files_reference', 'id', array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness); } + + /** + * Updates a reference to the external resource and all files that use it + * + * This function is called after synchronisation of an external file and updates the + * contenthash, filesize and status of all files that reference this external file + * as well as time last synchronised and sync lifetime (how long we don't need to call + * synchronisation for this reference). + * + * @param int $referencefileid + * @param int $lastsync + * @param int $lifetime + * @param string $contenthash + * @param int $filesize + * @param int $status 0 if ok or 666 if source is missing + */ + public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status) { + global $DB; + $referencefileid = clean_param($referencefileid, PARAM_INT); + $lastsync = clean_param($lastsync, PARAM_INT); + $lifetime = clean_param($lifetime, PARAM_INT); + validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED); + $filesize = clean_param($filesize, PARAM_INT); + $status = clean_param($status, PARAM_INT); + $params = array('contenthash' => $contenthash, + 'filesize' => $filesize, + 'status' => $status, + 'referencefileid' => $referencefileid, + 'lastsync' => $lastsync, + 'lifetime' => $lifetime); + $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize, + status = :status, referencelastsync = :lastsync, referencelifetime = :lifetime + WHERE referencefileid = :referencefileid', $params); + $data = array('id' => $referencefileid, 'lastsync' => $lastsync, 'lifetime' => $lifetime); + $DB->update_record('files_reference', (object)$data); + } } diff --git a/lib/filestorage/stored_file.php b/lib/filestorage/stored_file.php index e9410bd40a464..4c5cd0c643ef8 100644 --- a/lib/filestorage/stored_file.php +++ b/lib/filestorage/stored_file.php @@ -195,6 +195,7 @@ public function rename($filepath, $filename) { public function replace_content_with(stored_file $storedfile) { $contenthash = $storedfile->get_contenthash(); $this->set_contenthash($contenthash); + $this->set_filesize($storedfile->get_filesize()); } /** @@ -877,36 +878,43 @@ public function get_reference_details() { * We update contenthash, filesize and status in files table if changed * and we always update lastsync in files_reference table * - * @param type $contenthash - * @param type $filesize + * @param string $contenthash + * @param int $filesize + * @param int $status + * @param int $lifetime the life time of this synchronisation results */ - public function set_synchronized($contenthash, $filesize, $status = 0) { + public function set_synchronized($contenthash, $filesize, $status = 0, $lifetime = null) { global $DB; if (!$this->is_external_file()) { return; } $now = time(); - $filerecord = new stdClass(); - if ($this->get_contenthash() !== $contenthash) { - $filerecord->contenthash = $contenthash; + if ($contenthash != $this->file_record->contenthash) { + $oldcontenthash = $this->file_record->contenthash; } - if ($this->get_filesize() != $filesize) { - $filerecord->filesize = $filesize; + if ($lifetime === null) { + $lifetime = $this->file_record->referencelifetime; } - if ($this->get_status() != $status) { - $filerecord->status = $status; - } - $filerecord->referencelastsync = $now; // TODO MDL-33416 remove this - if (!empty($filerecord)) { - $this->update($filerecord); + // this will update all entries in {files} that have the same filereference id + $this->fs->update_references($this->file_record->referencefileid, $now, $lifetime, $contenthash, $filesize, $status); + // we don't need to call update() for this object, just set the values of changed fields + $this->file_record->contenthash = $contenthash; + $this->file_record->filesize = $filesize; + $this->file_record->status = $status; + $this->file_record->referencelastsync = $now; + $this->file_record->referencelifetime = $lifetime; + if (isset($oldcontenthash)) { + $this->fs->deleted_file_cleanup($oldcontenthash); } - - $DB->set_field('files_reference', 'lastsync', $now, array('id'=>$this->get_referencefileid())); - // $this->file_record->lastsync = $now; // TODO MDL-33416 uncomment or remove } - public function set_missingsource() { - $this->set_synchronized($this->get_contenthash(), 0, 666); + /** + * Sets the error status for a file that could not be synchronised + * + * @param int $lifetime the life time of this synchronisation results + */ + public function set_missingsource($lifetime = null) { + $this->set_synchronized($this->get_contenthash(), $this->get_filesize(), 666, $lifetime); } /** From 8d8a6009e8d7bd91c1887719d4e42cc47fb2cf19 Mon Sep 17 00:00:00 2001 From: Marina Glancy <marina@moodle.com> Date: Tue, 31 Jul 2012 09:11:11 +0800 Subject: [PATCH 67/90] MDL-34290 class oauth_helper, added API to pass options to curl (such as timeout) --- lib/oauthlib.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/oauthlib.php b/lib/oauthlib.php index 587ba195b60a2..ed29777b33d50 100644 --- a/lib/oauthlib.php +++ b/lib/oauthlib.php @@ -59,6 +59,8 @@ class oauth_helper { protected $access_token_api; /** @var curl */ protected $http; + /** @var array options to pass to the next curl request */ + protected $http_options; /** * Contructor for oauth_helper. @@ -106,6 +108,7 @@ function __construct($args) { $this->access_token_secret = $args['access_token_secret']; } $this->http = new curl(array('debug'=>false)); + $this->http_options = array(); } /** @@ -202,6 +205,15 @@ public function setup_oauth_http_header($params) { $this->http->setHeader($str); } + /** + * Sets the options for the next curl request + * + * @param array $options + */ + public function setup_oauth_http_options($options) { + $this->http_options = $options; + } + /** * Request token for authentication * This is the first step to use OAuth, it will return oauth_token and oauth_token_secret @@ -210,7 +222,7 @@ public function setup_oauth_http_header($params) { public function request_token() { $this->sign_secret = $this->consumer_secret.'&'; $params = $this->prepare_oauth_parameters($this->request_token_api, array(), 'GET'); - $content = $this->http->get($this->request_token_api, $params); + $content = $this->http->get($this->request_token_api, $params, $this->http_options); // Including: // oauth_token // oauth_token_secret @@ -252,7 +264,7 @@ public function get_access_token($token, $secret, $verifier='') { $this->sign_secret = $this->consumer_secret.'&'.$secret; $params = $this->prepare_oauth_parameters($this->access_token_api, array('oauth_token'=>$token, 'oauth_verifier'=>$verifier), 'POST'); $this->setup_oauth_http_header($params); - $content = $this->http->post($this->access_token_api, $params); + $content = $this->http->post($this->access_token_api, $params, $this->http_options); $keys = $this->parse_result($content); $this->set_access_token($keys['oauth_token'], $keys['oauth_token_secret']); return $keys; @@ -276,7 +288,7 @@ public function request($method, $url, $params=array(), $token='', $secret='') { $this->sign_secret = $this->consumer_secret.'&'.$secret; $oauth_params = $this->prepare_oauth_parameters($url, array('oauth_token'=>$token), $method); $this->setup_oauth_http_header($oauth_params); - $content = call_user_func_array(array($this->http, strtolower($method)), array($url, $params)); + $content = call_user_func_array(array($this->http, strtolower($method)), array($url, $params, $this->http_options)); return $content; } From 2d222a3243e7bdaa912d28f49ce793ef0016b66f Mon Sep 17 00:00:00 2001 From: Marina Glancy <marina@moodle.com> Date: Tue, 31 Jul 2012 09:55:33 +0800 Subject: [PATCH 68/90] MDL-34290 repository API: allow get_file_by_reference return only filesize we want to allow repositories to perform quick synchronisation, without downloading the file. In this case they return only filesize, without the contenthash --- repository/lib.php | 48 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/repository/lib.php b/repository/lib.php index e8a60f4b72e06..de8448bb82019 100644 --- a/repository/lib.php +++ b/repository/lib.php @@ -1213,14 +1213,27 @@ public function cache_file_by_reference($reference, $storedfile) { /** * Returns information about file in this repository by reference - * {@link repository::get_file_reference()} - * {@link repository::get_file()} * - * Returns null if file not found or is not readable + * This function must be implemented for repositories supporting FILE_REFERENCE, it is called + * for existing aliases when the lifetime of the previous syncronisation has expired. + * + * Returns null if file not found or is not readable or timeout occured during request. + * Note that this function may be run for EACH file that needs to be synchronised at the + * moment. If anything is being downloaded or requested from external sources there + * should be a small timeout. The synchronisation is performed to update the size of + * the file and/or to update image and re-generated image preview. There is nothing + * fatal if syncronisation fails but it is fatal if syncronisation takes too long + * and hangs the script generating a page. * - * @param stdClass $reference file reference db record + * If get_file_by_reference() returns filesize just the record in {files} table is being updated. + * If filepath, handle or content are returned - the file is also stored in moodle filepool + * (recommended for images to generate the thumbnails). For non-image files it is not + * recommended to download them to moodle during syncronisation since it may take + * unnecessary long time. + * + * @param stdClass $reference record from DB table {files_reference} * @return stdClass|null contains one of the following: - * - 'contenthash' and 'filesize' + * - 'filesize' and optionally 'contenthash' * - 'filepath' * - 'handle' * - 'content' @@ -2257,7 +2270,7 @@ public static function reset_caches() { } /** - * Call to request proxy file sync with repository source. + * Performs synchronisation of reference to an external file if the previous one has expired. * * @param stored_file $file * @param bool $resetsynchistory whether to reset all history of sync (used by phpunit) @@ -2300,20 +2313,31 @@ public static function sync_external_file($file, $resetsynchistory = false) { return false; } + $lifetime = $repository->get_reference_file_lifetime($reference); $fileinfo = $repository->get_file_by_reference($reference); if ($fileinfo === null) { // does not exist any more - set status to missing - $file->set_missingsource(); - //TODO: purge content from pool if we set some other content hash and it is no used any more + $file->set_missingsource($lifetime); $synchronized[$file->get_id()] = true; return true; } $contenthash = null; $filesize = null; - if (!empty($fileinfo->contenthash)) { - // contenthash returned, file already in moodle - $contenthash = $fileinfo->contenthash; + if (!empty($fileinfo->filesize)) { + // filesize returned + if (!empty($fileinfo->contenthash) && $fs->content_exists($fileinfo->contenthash)) { + // contenthash is specified and valid + $contenthash = $fileinfo->contenthash; + } else if ($fileinfo->filesize == $file->get_filesize()) { + // we don't know the new contenthash but the filesize did not change, + // assume the contenthash did not change either + $contenthash = $file->get_contenthash(); + } else { + // we can't save empty contenthash so generate contenthash from empty string + $fs->add_string_to_pool(''); + $contenthash = sha1(''); + } $filesize = $fileinfo->filesize; } else if (!empty($fileinfo->filepath)) { // File path returned @@ -2336,7 +2360,7 @@ public static function sync_external_file($file, $resetsynchistory = false) { } // update files table - $file->set_synchronized($contenthash, $filesize); + $file->set_synchronized($contenthash, $filesize, 0, $lifetime); $synchronized[$file->get_id()] = true; return true; } From 59cb75985017b2852a35876d4e01cfc4f3ec53b3 Mon Sep 17 00:00:00 2001 From: Marina Glancy <marina@moodle.com> Date: Tue, 31 Jul 2012 10:17:07 +0800 Subject: [PATCH 69/90] MDL-34290 repository API: do not confuse source and reference make sure that repository function get_file_source_info receives as argument the source of the file, and get_file receives a reference; reference is a value of DB field files_reference.reference and result of get_file_reference(source). Fix dropbox as the only repository that have different values in those fields; also added information about user in dropbox reference and original --- repository/dropbox/lib.php | 49 +++++++++++++++++++++------------- repository/filepicker.php | 17 +++++++----- repository/lib.php | 20 +++++++++----- repository/repository_ajax.php | 19 +++++++------ 4 files changed, 66 insertions(+), 39 deletions(-) diff --git a/repository/dropbox/lib.php b/repository/dropbox/lib.php index 186e63b59c127..9d73e3558b103 100644 --- a/repository/dropbox/lib.php +++ b/repository/dropbox/lib.php @@ -281,15 +281,22 @@ public function get_option($config = '') { } /** + * Downloads a file from external repository and saves it in temp dir * - * @param string $photo_id - * @param string $file - * @return string + * @throws moodle_exception when file could not be downloaded + * + * @param string $reference the content of files.reference field + * @param string $filename filename (without path) to save the downloaded file in the + * temporary directory, if omitted or file already exists the new filename will be generated + * @return array with elements: + * path: internal location of the file + * url: URL to the source (from parameters) */ - public function get_file($filepath, $saveas = '') { - $this->dropbox->set_access_token($this->access_key, $this->access_secret); + public function get_file($reference, $saveas = '') { + $reference = unserialize($reference); + $this->dropbox->set_access_token($reference->access_key, $reference->access_secret); $saveas = $this->prepare_file($saveas); - return $this->dropbox->get_file($filepath, $saveas); + return $this->dropbox->get_file($reference->path, $saveas); } /** * Add Plugin settings input to Moodle form @@ -354,10 +361,13 @@ public function supported_returntypes() { * @return string file referece */ public function get_file_reference($source) { + global $USER; $reference = new stdClass; $reference->path = $source; $reference->access_key = get_user_preferences($this->setting.'_access_key', ''); $reference->access_secret = get_user_preferences($this->setting.'_access_secret', ''); + $reference->userid = $USER->id; + $reference->username = fullname($USER); return serialize($reference); } @@ -380,13 +390,13 @@ public function get_file_by_reference($reference) { $this->set_access_secret($reference->access_secret); $path = $this->get_file($reference->path); $cachedfilepath = cache_file::create_from_file($reference, $path['path']); - } + } if ($cachedfilepath && is_readable($cachedfilepath)) { return (object)array('filepath' => $cachedfilepath); } else { return null; } - } + } /** * Get file from external repository by reference @@ -399,8 +409,8 @@ public function get_file_by_reference($reference) { */ public function cache_file_by_reference($reference, $storedfile) { $reference = unserialize($reference); - $path = $this->get_file($reference->path); - cache_file::create_from_file($reference, $path['path']); + $path = $this->get_file($reference); + cache_file::create_from_file($reference->path, $path['path']); } /** @@ -412,8 +422,12 @@ public function cache_file_by_reference($reference, $storedfile) { * @return string */ public function get_reference_details($reference, $filestatus = 0) { + global $USER; $ref = unserialize($reference); $details = $this->get_name(); + if (isset($ref->userid) && $ref->userid != $USER->id && isset($ref->username)) { + $details .= ' ('.$ref->username.')'; + } if (isset($ref->path)) { $details .= ': '. $ref->path; } @@ -428,11 +442,12 @@ public function get_reference_details($reference, $filestatus = 0) { /** * Return the source information * - * @param stdClass $filepath - * @return string|null + * @param string $source + * @return string */ - public function get_file_source_info($filepath) { - return 'Dropbox: ' . $filepath; + public function get_file_source_info($source) { + global $USER; + return 'Dropbox ('.fullname($USER).'): ' . $source; } /** @@ -471,10 +486,8 @@ public function cron() { $cachedfile = cache_file::get($reference); if ($cachedfile === false) { // Re-fetch resource. - $this->set_access_key($reference->access_key); - $this->set_access_secret($reference->access_secret); - $path = $this->get_file($reference->path); - cache_file::create_from_file($reference, $path['path']); + $path = $this->get_file($reference); + cache_file::create_from_file($reference->path, $path['path']); } } } diff --git a/repository/filepicker.php b/repository/filepicker.php index fd16e6676ca32..1b673318b1af7 100644 --- a/repository/filepicker.php +++ b/repository/filepicker.php @@ -282,6 +282,11 @@ if (!$repo->file_is_accessible($fileurl)) { print_error('storedfilecannotread'); } + $record = new stdClass(); + $reference = $repo->get_file_reference($fileurl); + + $sourcefield = $repo->get_file_source_info($fileurl); + $record->source = repository::build_source_field($sourcefield); // If file is already a reference, set $fileurl = file source, $repo = file repository // note that in this case user may not have permission to access the source file directly @@ -289,13 +294,14 @@ if ($repo->has_moodle_files()) { $file = repository::get_moodle_file($fileurl); if ($file && $file->is_external_file()) { - $fileurl = $file->get_reference(); + $sourcefield = $file->get_source(); // remember the original source + $record->source = $repo::build_source_field($sourcefield); + $reference = $file->get_reference(); $repo_id = $file->get_repository_id(); $repo = repository::get_repository_by_id($repo_id, $contextid, $repooptions); } } - $record = new stdClass(); $record->filepath = $savepath; $record->filename = $filename; $record->component = 'user'; @@ -311,14 +317,11 @@ $record->contextid = $user_context->id; $record->sortorder = 0; - $sourcefield = $repo->get_file_source_info($fileurl); - $record->source = repository::build_source_field($sourcefield); - if ($repo->has_moodle_files()) { - $fileinfo = $repo->copy_to_area($fileurl, $record, $maxbytes); + $fileinfo = $repo->copy_to_area($reference, $record, $maxbytes); redirect($home_url, get_string('downloadsucc', 'repository')); } else { - $thefile = $repo->get_file($fileurl, $filename); + $thefile = $repo->get_file($reference, $filename); if (!empty($thefile['path'])) { $filesize = filesize($thefile['path']); if ($maxbytes != -1 && $filesize>$maxbytes) { diff --git a/repository/lib.php b/repository/lib.php index de8448bb82019..8510f951b9df7 100644 --- a/repository/lib.php +++ b/repository/lib.php @@ -1164,9 +1164,8 @@ public function sync_individual_file(stored_file $storedfile) { /** * Return human readable reference information - * {@link stored_file::get_reference()} * - * @param string $reference + * @param string $reference value of DB field files_reference.reference * @param int $filestatus status of the file, 0 - ok, 666 - source missing * @return string */ @@ -1258,14 +1257,23 @@ public function get_file_by_reference($reference) { /** * Return the source information * - * @param stdClass $url + * The result of the function is stored in files.source field. It may be analysed + * when the source file is lost or repository may use it to display human-readable + * location of reference original. + * + * This method is called when file is picked for the first time only. When file + * (either copy or a reference) is already in moodle and it is being picked + * again to another file area (also as a copy or as a reference), the value of + * files.source is copied. + * + * @param string $source the value that repository returned in listing as 'source' * @return string|null */ - public function get_file_source_info($url) { + public function get_file_source_info($source) { if ($this->has_moodle_files()) { - return $this->get_reference_details($url, 0); + return $this->get_reference_details($source, 0); } - return $url; + return $source; } /** diff --git a/repository/repository_ajax.php b/repository/repository_ajax.php index d9f3cea305fd7..baf59ef30303e 100644 --- a/repository/repository_ajax.php +++ b/repository/repository_ajax.php @@ -220,24 +220,27 @@ throw new file_exception('storedfilecannotread'); } + // {@link repository::build_source_field()} + $sourcefield = $repo->get_file_source_info($source); + $record->source = $repo::build_source_field($sourcefield); + + $reference = $repo->get_file_reference($source); + // If file is already a reference, set $source = file source, $repo = file repository // note that in this case user may not have permission to access the source file directly // so no file_browser/file_info can be used below if ($repo->has_moodle_files()) { $file = repository::get_moodle_file($source); if ($file && $file->is_external_file()) { - $source = $file->get_reference(); + $sourcefield = $file->get_source(); // remember the original source + $record->source = $repo::build_source_field($sourcefield); + $reference = $file->get_reference(); $repo_id = $file->get_repository_id(); $repo = repository::get_repository_by_id($repo_id, $contextid, $repooptions); } } - // {@link repository::build_source_field()} - $sourcefield = $repo->get_file_source_info($source); - $record->source = $repo::build_source_field($sourcefield); - if ($usefilereference) { - $reference = $repo->get_file_reference($source); // get reference life time from repo $record->referencelifetime = $repo->get_reference_file_lifetime($reference); // Check if file exists. @@ -281,13 +284,13 @@ // If the moodle file is an alias we copy this alias, otherwise we copy the file // {@link repository::copy_to_area()}. - $fileinfo = $repo->copy_to_area($source, $record, $maxbytes); + $fileinfo = $repo->copy_to_area($reference, $record, $maxbytes); echo json_encode($fileinfo); die; } else { // Download file to moodle. - $downloadedfile = $repo->get_file($source, $saveas_filename); + $downloadedfile = $repo->get_file($reference, $saveas_filename); if (empty($downloadedfile['path'])) { $err->error = get_string('cannotdownload', 'repository'); die(json_encode($err)); From 72a5655566a06e42fe078a2e9454ca1fab64eb9a Mon Sep 17 00:00:00 2001 From: Marina Glancy <marina@moodle.com> Date: Tue, 31 Jul 2012 10:31:36 +0800 Subject: [PATCH 70/90] MDL-34290 repository API: add timeout to get_file and throw exception - get_file should have a request timeout and throw an exception with the details (i.e. timeout reached) if download can not be completed - corrected phpdocs --- lang/en/repository.php | 1 + repository/alfresco/lib.php | 7 +-- repository/equella/lib.php | 34 +++++++++------ repository/flickr/lib.php | 9 +--- repository/flickr_public/lib.php | 8 +--- repository/lib.php | 75 +++++++++++++++++--------------- 6 files changed, 65 insertions(+), 69 deletions(-) diff --git a/lang/en/repository.php b/lang/en/repository.php index 12454261e1fba..8682378aa37ae 100644 --- a/lang/en/repository.php +++ b/lang/en/repository.php @@ -100,6 +100,7 @@ $string['errornotyourfile'] = 'You cannot pick file which is not added by your'; $string['erroruniquename'] = 'Repository instance name should be unique'; $string['errorpostmaxsize'] = 'The uploaded file may exceed max_post_size directive in php.ini.'; +$string['errorwhiledownload'] = 'An error occured while downloading the file: {$a}'; $string['existingrepository'] = 'This repository already exists'; $string['federatedsearch'] = 'Federated search'; $string['fileexists'] = 'File name already being used, please use another name'; diff --git a/repository/alfresco/lib.php b/repository/alfresco/lib.php index 9a34b9d5b5306..b991d2e49d6a3 100644 --- a/repository/alfresco/lib.php +++ b/repository/alfresco/lib.php @@ -202,12 +202,7 @@ public function get_listing($uuid = '', $path = '') { public function get_file($uuid, $file = '') { $node = $this->user_session->getNode($this->store, $uuid); $url = $this->get_url($node); - $path = $this->prepare_file($file); - $fp = fopen($path, 'w'); - $c = new curl; - $c->download(array(array('url'=>$url, 'file'=>$fp))); - fclose($fp); - return array('path'=>$path, 'url'=>$url); + return parent::get_file($url, $file); } /** diff --git a/repository/equella/lib.php b/repository/equella/lib.php index d520a2c84ffaa..afab067d02fef 100644 --- a/repository/equella/lib.php +++ b/repository/equella/lib.php @@ -125,23 +125,31 @@ public function get_file_reference($source) { /** * Download a file, this function can be overridden by subclass. {@link curl} * - * @param string $url the url of file - * @param string $filename save location - * @return string the location of the file + * @param string $reference the source of the file + * @param string $filename filename (without path) to save the downloaded file in the + * temporary directory + * @return null|array null if download failed or array with elements: + * path: internal location of the file + * url: URL to the source (from parameters) */ - public function get_file($url, $filename = '') { + public function get_file($reference, $filename = '') { global $USER; - $cookiename = uniqid('', true) . '.cookie'; - $dir = make_temp_directory('repository/equella/' . $USER->id); - $cookiepathname = $dir . '/' . $cookiename; + $ref = @unserialize(base64_decode($reference)); + if (!isset($ref->url) || !($url = $this->appendtoken($ref->url))) { + // Occurs when the user isn't known.. + return null; + } $path = $this->prepare_file($filename); - $fp = fopen($path, 'w'); + $cookiepathname = $this->prepare_file($USER->id. '_'. uniqid('', true). '.cookie'); $c = new curl(array('cookie'=>$cookiepathname)); - $c->download(array(array('url'=>$url, 'file'=>$fp)), array('CURLOPT_FOLLOWLOCATION'=>true)); - // Close file handler. - fclose($fp); + $result = $c->download_one($url, null, array('filepath' => $path, 'followlocation' => true, 'timeout' => self::GETFILE_TIMEOUT)); // Delete cookie jar. - unlink($cookiepathname); + if (file_exists($cookiepathname)) { + unlink($cookiepathname); + } + if ($result !== true) { + throw new moodle_exception('errorwhiledownload', 'repository', '', $result); + } return array('path'=>$path, 'url'=>$url); } @@ -170,7 +178,7 @@ public function get_file_by_reference($reference) { // Cache the file. $path = $this->get_file($url); $cachedfilepath = cache_file::create_from_file($url, $path['path']); - } + } if ($cachedfilepath && is_readable($cachedfilepath)) { return (object)array('filepath' => $cachedfilepath); diff --git a/repository/flickr/lib.php b/repository/flickr/lib.php index da01736ab4649..478e53b0b0d81 100644 --- a/repository/flickr/lib.php +++ b/repository/flickr/lib.php @@ -252,14 +252,7 @@ public function get_link($photoid) { */ public function get_file($photoid, $file = '') { $url = $this->build_photo_url($photoid); - $path = $this->prepare_file($file); - $fp = fopen($path, 'w'); - $c = new curl; - $c->download(array( - array('url'=>$url, 'file'=>$fp) - )); - fclose($fp); - return array('path'=>$path, 'url'=>$url); + return parent::get_file($url, $file); } /** diff --git a/repository/flickr_public/lib.php b/repository/flickr_public/lib.php index 083e4c72e37a5..b31adaf82d0f0 100644 --- a/repository/flickr_public/lib.php +++ b/repository/flickr_public/lib.php @@ -449,12 +449,8 @@ public function get_file($photoid, $file = '') { $source = $result[2]['source']; $url = $result[2]['url']; } - $path = $this->prepare_file($file); - $fp = fopen($path, 'w'); - $c = new curl; - $c->download(array(array('url'=>$source, 'file'=>$fp))); - // must close file handler, otherwise gd lib will fail to process it - fclose($fp); + $result = parent::get_file($source, $file); + $path = $result['path']; if (!empty($this->usewatermarks)) { $img = new moodle_image($path); $img->watermark($copyright, array(10,10), array('ttf'=>true, 'fontsize'=>12))->saveas($path); diff --git a/repository/lib.php b/repository/lib.php index 8510f951b9df7..9eaa0786e02da 100644 --- a/repository/lib.php +++ b/repository/lib.php @@ -16,10 +16,9 @@ /** * This file contains classes used to manage the repository plugins in Moodle - * and was introduced as part of the changes occuring in Moodle 2.0 * * @since 2.0 - * @package repository + * @package core_repository * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -49,7 +48,7 @@ * - When you create a type for a plugin that can't have multiple instances, a * instance is automatically created. * - * @package repository + * @package core_repository * @copyright 2009 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -460,12 +459,18 @@ public function delete($downloadcontents = false) { * To create repository plugin, see: {@link http://docs.moodle.org/dev/Repository_plugins} * See an example: {@link repository_boxnet} * - * @package repository - * @category repository + * @package core_repository * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class repository { + /** Timeout in seconds for downloading the external file into moodle */ + const GETFILE_TIMEOUT = 30; + /** Timeout in seconds for syncronising the external file size */ + const SYNCFILE_TIMEOUT = 1; + /** Timeout in seconds for downloading an image file from external repository during syncronisation */ + const SYNCIMAGE_TIMEOUT = 3; + // $disabled can be set to true to disable a plugin by force // example: self::$disabled = true /** @var bool force disable repository instance */ @@ -1342,8 +1347,7 @@ public static function move_to_filepool($thefile, $record) { * @param string $search searched string * @param bool $dynamicmode no recursive call is done when in dynamic mode * @param array $list the array containing the files under the passed $fileinfo - * @returns int the number of files found - * + * @return int the number of files found */ public static function build_tree($fileinfo, $search, $dynamicmode, &$list) { global $CFG, $OUTPUT; @@ -1559,6 +1563,7 @@ public function get_file_reference($source) { } return $source; } + /** * Decide where to save the file, can be overwriten by subclass * @@ -1567,17 +1572,9 @@ public function get_file_reference($source) { */ public function prepare_file($filename) { global $CFG; - if (!file_exists($CFG->tempdir.'/download')) { - mkdir($CFG->tempdir.'/download/', $CFG->directorypermissions, true); - } - if (is_dir($CFG->tempdir.'/download')) { - $dir = $CFG->tempdir.'/download/'; - } - if (empty($filename)) { - $filename = uniqid('repo', true).'_'.time().'.tmp'; - } - if (file_exists($dir.$filename)) { - $filename = uniqid('m').$filename; + $dir = make_temp_directory('download/'.get_class($this).'/'); + while (empty($filename) || file_exists($dir.$filename)) { + $filename = uniqid('', true).'_'.time().'.tmp'; } return $dir.$filename; } @@ -1604,25 +1601,33 @@ public function get_link($url) { } /** - * Download a file, this function can be overridden by subclass. {@link curl} + * Downloads a file from external repository and saves it in temp dir * - * @param string $url the url of file - * @param string $filename save location + * Function get_file() must be implemented by repositories that support returntypes + * FILE_INTERNAL or FILE_REFERENCE. It is invoked to pick up the file and copy it + * to moodle. This function is not called for moodle repositories, the function + * {@link repository::copy_to_area()} is used instead. + * + * This function can be overridden by subclass if the files.reference field contains + * not just URL or if request should be done differently. + * + * @see curl + * @throws file_exception when error occured + * + * @param string $url the content of files.reference field, in this implementaion + * it is asssumed that it contains the string with URL of the file + * @param string $filename filename (without path) to save the downloaded file in the + * temporary directory, if omitted or file already exists the new filename will be generated * @return array with elements: * path: internal location of the file * url: URL to the source (from parameters) */ public function get_file($url, $filename = '') { - global $CFG; $path = $this->prepare_file($filename); - $fp = fopen($path, 'w'); $c = new curl; - $result = $c->download(array(array('url'=>$url, 'file'=>$fp))); - // Close file handler. - fclose($fp); - if (empty($result)) { - unlink($path); - return null; + $result = $c->download_one($url, null, array('filepath' => $path, 'timeout' => self::GETFILE_TIMEOUT)); + if ($result !== true) { + throw new moodle_exception('errorwhiledownload', 'repository', '', $result); } return array('path'=>$path, 'url'=>$url); } @@ -2042,7 +2047,7 @@ public static function prepare_listing($listing) { * * @param string $search_text search key word * @param int $page page - * @return mixed {@see repository::get_listing} + * @return mixed see {@link repository::get_listing()} */ public function search($search_text, $page = 0) { $list = array(); @@ -2397,8 +2402,7 @@ public static function build_source_field($source) { * Exception class for repository api * * @since 2.0 - * @package repository - * @category repository + * @package core_repository * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -2409,8 +2413,7 @@ class repository_exception extends moodle_exception { * This is a class used to define a repository instance form * * @since 2.0 - * @package repository - * @category repository + * @package core_repository * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -2524,8 +2527,7 @@ public function validation($data, $files) { * This is a class used to define a repository type setting form * * @since 2.0 - * @package repository - * @category repository + * @package core_repository * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -2739,6 +2741,7 @@ function initialise_filepicker($args) { } return $return; } + /** * Small function to walk an array to attach repository ID * From 96221c605af35f688b3612f5c382d5a61cabdcb1 Mon Sep 17 00:00:00 2001 From: Marina Glancy <marina@moodle.com> Date: Tue, 31 Jul 2012 10:40:31 +0800 Subject: [PATCH 71/90] MDL-34290 repository_filesystem add original info function and reduce ref lifetime --- repository/filesystem/lib.php | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/repository/filesystem/lib.php b/repository/filesystem/lib.php index 6eb3b3b376d4c..fc7cd9ef49e6e 100644 --- a/repository/filesystem/lib.php +++ b/repository/filesystem/lib.php @@ -244,6 +244,33 @@ public function supported_returntypes() { return FILE_INTERNAL | FILE_REFERENCE; } + /** + * Return reference file life time + * + * @param string $ref + * @return int + */ + public function get_reference_file_lifetime($ref) { + // Does not cost us much to synchronise within our own filesystem, set to 1 minute + return 60; + } + + /** + * Return human readable reference information + * + * @param string $reference value of DB field files_reference.reference + * @param int $filestatus status of the file, 0 - ok, 666 - source missing + * @return string + */ + public function get_reference_details($reference, $filestatus = 0) { + $details = $this->get_name().': '.$reference; + if ($filestatus) { + return get_string('lostsource', 'repository', $details); + } else { + return $details; + } + } + /** * Returns information about file in this repository by reference * {@link repository::get_file_reference()} From bc6f241ca2c6b5e192b7daac992b7a0a54824526 Mon Sep 17 00:00:00 2001 From: Marina Glancy <marina@moodle.com> Date: Tue, 31 Jul 2012 10:50:05 +0800 Subject: [PATCH 72/90] MDL-34290 repository API: add repository function to import referenced file it must be independed from sync_external_file because sync often does not actually download contents, it is used just to retrieve the size of the file. Besides the timeouts for get_file and sync requests are very different. Also add option to send_stored_file() to ignore reference and send cached contents --- lib/filelib.php | 2 +- lib/filestorage/file_storage.php | 10 ++--- lib/filestorage/stored_file.php | 12 ++++++ repository/lib.php | 71 ++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 6 deletions(-) diff --git a/lib/filelib.php b/lib/filelib.php index c802acdb76f7f..4d03189bd65a5 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -2326,7 +2326,7 @@ function send_stored_file($stored_file, $lifetime=86400 , $filter=0, $forcedownl } // handle external resource - if ($stored_file && $stored_file->is_external_file()) { + if ($stored_file && $stored_file->is_external_file() && !isset($options['sendcachedexternalfile'])) { $stored_file->send_file($lifetime, $filter, $forcedownload, $options); die; } diff --git a/lib/filestorage/file_storage.php b/lib/filestorage/file_storage.php index 94cdf335bbc4c..71c8fc8389a13 100644 --- a/lib/filestorage/file_storage.php +++ b/lib/filestorage/file_storage.php @@ -1841,15 +1841,15 @@ public function update_references_to_storedfile(stored_file $storedfile) { /** * Convert file alias to local file * + * @throws moodle_exception if file could not be downloaded + * * @param stored_file $storedfile a stored_file instances + * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit) * @return stored_file stored_file */ - public function import_external_file(stored_file $storedfile) { + public function import_external_file(stored_file $storedfile, $maxbytes = 0) { global $CFG; - require_once($CFG->dirroot.'/repository/lib.php'); - // sync external file - repository::sync_external_file($storedfile); - // Remove file references + $storedfile->import_external_file_contents($maxbytes); $storedfile->delete_reference(); return $storedfile; } diff --git a/lib/filestorage/stored_file.php b/lib/filestorage/stored_file.php index 4c5cd0c643ef8..ea8a8cd1d0eb4 100644 --- a/lib/filestorage/stored_file.php +++ b/lib/filestorage/stored_file.php @@ -928,4 +928,16 @@ public function set_missingsource($lifetime = null) { public function send_file($lifetime, $filter, $forcedownload, $options) { $this->repository->send_file($this, $lifetime, $filter, $forcedownload, $options); } + + /** + * Imports the contents of an external file into moodle filepool. + * + * @throws moodle_exception if file could not be downloaded or is too big + * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit) + */ + public function import_external_file_contents($maxbytes = 0) { + if ($this->repository) { + $this->repository->import_external_file_contents($this, $maxbytes); + } + } } diff --git a/repository/lib.php b/repository/lib.php index 9eaa0786e02da..88af58f6e3076 100644 --- a/repository/lib.php +++ b/repository/lib.php @@ -1632,6 +1632,77 @@ public function get_file($url, $filename = '') { return array('path'=>$path, 'url'=>$url); } + /** + * Downloads the file from external repository and saves it in moodle filepool. + * This function is different from {@link repository::sync_external_file()} because it has + * bigger request timeout and always downloads the content. + * + * This function is invoked when we try to unlink the file from the source and convert + * a reference into a true copy. + * + * @throws exception when file could not be imported + * + * @param stored_file $file + * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit) + */ + public function import_external_file_contents(stored_file $file, $maxbytes = 0) { + if (!$file->is_external_file()) { + // nothing to import if the file is not a reference + return; + } else if ($file->get_repository_id() != $this->id) { + // error + debugging('Repository instance id does not match'); + return; + } else if ($this->has_moodle_files()) { + // files that are references to local files are already in moodle filepool + // just validate the size + if ($maxbytes > 0 && $file->get_filesize() > $maxbytes) { + throw new file_exception('maxbytes'); + } + return; + } else { + if ($maxbytes > 0 && $file->get_filesize() > $maxbytes) { + // note that stored_file::get_filesize() also calls synchronisation + throw new file_exception('maxbytes'); + } + $fs = get_file_storage(); + $contentexists = $fs->content_exists($file->get_contenthash()); + if ($contentexists && $file->get_filesize() && $file->get_contenthash() === sha1('')) { + // even when 'file_storage::content_exists()' returns true this may be an empty + // content for the file that was not actually downloaded + $contentexists = false; + } + $now = time(); + if ($file->get_referencelastsync() + $file->get_referencelifetime() >= $now && + !$file->get_status() && + $contentexists) { + // we already have the content in moodle filepool and it was synchronised recently. + // Repositories may overwrite it if they want to force synchronisation anyway! + return; + } else { + // attempt to get a file + try { + $fileinfo = $this->get_file($file->get_reference()); + if (isset($fileinfo['path'])) { + list($contenthash, $filesize, $newfile) = $fs->add_file_to_pool($fileinfo['path']); + // set this file and other similar aliases synchronised + $lifetime = $this->get_reference_file_lifetime($file->get_reference()); + $file->set_synchronized($contenthash, $filesize, 0, $lifetime); + } else { + throw new moodle_exception('errorwhiledownload', 'repository', '', ''); + } + } catch (Exception $e) { + if ($contentexists) { + // better something than nothing. We have a copy of file. It's sync time + // has expired but it is still very likely that it is the last version + } else { + throw($e); + } + } + } + } + } + /** * Return size of a file in bytes. * From f24b0f69eee92cb49b9ab2036f5c2edbe5addcba Mon Sep 17 00:00:00 2001 From: Marina Glancy <marina@moodle.com> Date: Tue, 31 Jul 2012 10:54:48 +0800 Subject: [PATCH 73/90] MDL-34290 repository_boxnet, boxlib use request timeouts boxlib receives additional argument as request timeout repository_boxnet::get_file_by_reference respects request timeouts and downloads file into moodle only if it is image also some improvements to repository_boxnet source display functions; also do not cache result of request in retrieving of listing, user is unable to see the new files he added to box. --- lib/boxlib.php | 22 ++++++++++++---------- repository/boxnet/lib.php | 39 ++++++++++++++++++++++++++------------- 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/lib/boxlib.php b/lib/boxlib.php index 0cee0da8ac79a..974124f89ffc0 100644 --- a/lib/boxlib.php +++ b/lib/boxlib.php @@ -176,7 +176,7 @@ function getfiletree($path, $params = array()) { $params['action'] = 'get_account_tree'; $params['onelevel'] = 1; $params['params[]'] = 'nozip'; - $c = new curl(array('debug'=>$this->debug, 'cache'=>true, 'module_cache'=>'repository')); + $c = new curl(array('debug'=>$this->debug)); $c->setopt(array('CURLOPT_FOLLOWLOCATION'=>1)); try { $args = array(); @@ -196,23 +196,25 @@ function getfiletree($path, $params = array()) { * Get box.net file info * * @param string $fileid - * @return string|null + * @param int $timeout request timeout in seconds + * @return stdClass|null */ - function get_file_info($fileid) { + function get_file_info($fileid, $timeout = 0) { $this->_clearErrors(); $params = array(); $params['action'] = 'get_file_info'; $params['file_id'] = $fileid; $params['auth_token'] = $this->auth_token; $params['api_key'] = $this->api_key; - $http = new curl(array('debug'=>$this->debug, 'cache'=>true, 'module_cache'=>'repository')); - $xml = $http->get($this->_box_api_url, $params); - $o = simplexml_load_string(trim($xml)); - if ($o->status == 's_get_file_info') { - return $o->info; - } else { - return null; + $http = new curl(array('debug'=>$this->debug)); + $xml = $http->get($this->_box_api_url, $params, array('timeout' => $timeout)); + if (!$http->get_errno()) { + $o = simplexml_load_string(trim($xml)); + if ($o->status == 's_get_file_info') { + return $o->info; + } } + return null; } /** diff --git a/repository/boxnet/lib.php b/repository/boxnet/lib.php index a60b1f4b5f9f9..7a19ec947ddd8 100644 --- a/repository/boxnet/lib.php +++ b/repository/boxnet/lib.php @@ -277,12 +277,21 @@ public function get_file_reference($source) { * @return null|stdClass with attribute 'filepath' */ public function get_file_by_reference($reference) { - $boxnetfile = $this->get_file($reference->reference); - // Please note that here we will ALWAYS receive a file - // If source file has been removed from external server, box.com still returns - // a plain/text file with content 'no such file' (filesize will be 12 bytes) - if (!empty($boxnetfile['path'])) { - return (object)array('filepath' => $boxnetfile['path']); + $array = explode('/', $reference->reference); + $fileid = array_pop($array); + $fileinfo = $this->boxclient->get_file_info($fileid, self::SYNCFILE_TIMEOUT); + if ($fileinfo) { + $size = (int)$fileinfo->size; + if (file_extension_in_typegroup($fileinfo->file_name, 'web_image')) { + // this is an image - download it to moodle + $path = $this->prepare_file(''); + $c = new curl; + $result = $c->download_one($reference->reference, null, array('filepath' => $path, 'timeout' => self::SYNCIMAGE_TIMEOUT)); + if ($result === true) { + return (object)array('filepath' => $path); + } + } + return (object)array('filesize' => $size); } return null; } @@ -297,13 +306,16 @@ public function get_file_by_reference($reference) { */ public function get_reference_details($reference, $filestatus = 0) { // Indicate it's from box.net repository + secure URL + $array = explode('/', $reference); + $fileid = array_pop($array); + $fileinfo = $this->boxclient->get_file_info($fileid, self::SYNCFILE_TIMEOUT); + if (!empty($fileinfo)) { + $reference = (string)$fileinfo->file_name; + } $details = $this->get_name() . ': ' . $reference; - if (!$filestatus) { + if (!empty($fileinfo)) { return $details; } else { - // at the moment for box.net files we never can be sure that source is missing - // because box.com never returns 404 error. - // So we never change the status and actually this part is unreachable return get_string('lostsource', 'repository', $details); } } @@ -315,13 +327,14 @@ public function get_reference_details($reference, $filestatus = 0) { * @return string|null */ public function get_file_source_info($url) { + global $USER; $array = explode('/', $url); $fileid = array_pop($array); - $fileinfo = $this->boxclient->get_file_info($fileid); + $fileinfo = $this->boxclient->get_file_info($fileid, self::SYNCFILE_TIMEOUT); if (!empty($fileinfo)) { - return 'Box: ' . (string)$fileinfo->file_name; + return 'Box ('. fullname($USER). '): '. (string)$fileinfo->file_name. ': '. $url; } else { - return $url; + return 'Box: '. $url; } } From fa746096cfcd5ab1ea7089ec5715ee42b3328c5d Mon Sep 17 00:00:00 2001 From: Marina Glancy <marina@moodle.com> Date: Tue, 31 Jul 2012 10:57:54 +0800 Subject: [PATCH 74/90] MDL-34290 repository_equella: do not download files when not needed repository_equella::get_file_by_reference respects request timeouts, downloads only images (for thumbnail generation), does not use cache_file class (content is cached in moodle filepool if needed); also repository_equella has counter of unsuccessfull connect attempts and do not perform any more if 3 failed (within one request) --- repository/equella/lib.php | 90 +++++++++++++++++++++++++++++++------- 1 file changed, 74 insertions(+), 16 deletions(-) diff --git a/repository/equella/lib.php b/repository/equella/lib.php index afab067d02fef..14afc6ec20a11 100644 --- a/repository/equella/lib.php +++ b/repository/equella/lib.php @@ -122,6 +122,49 @@ public function get_file_reference($source) { return $source; } + /** + * Counts the number of failed connections. + * + * If we received the connection timeout more than 3 times in a row, we don't attemt to + * connect to the server any more during this request. + * + * This function is used by {@link repository_equella::get_file_by_reference()} that + * synchronises the file size of referenced files. + * + * @param int $errno omit if we just want to know the return value, the last curl_errno otherwise + * @return bool true if we had less than 3 failed connections, false if no more connections + * attempts recommended + */ + private function connection_result($errno = null) { + static $countfailures = array(); + $sess = sesskey(); + if (!array_key_exists($sess, $countfailures)) { + $countfailures[$sess] = 0; + } + if ($errno !== null) { + if ($errno == 0) { + // reset count of failed connections + $countfailures[$sess] = 0; + } else if ($errno == 7 /*CURLE_COULDNT_CONNECT*/ || $errno == 9 /*CURLE_REMOTE_ACCESS_DENIED*/) { + // problems with server + $countfailures[$sess]++; + } + } + return ($countfailures[$sess] < 3); + } + + /** + * Decide whether or not the file should be synced + * + * @param stored_file $storedfile + * @return bool + */ + public function sync_individual_file(stored_file $storedfile) { + // if we had several unsuccessfull attempts to connect to server - do not try any more + return $this->connection_result(); + } + + /** * Download a file, this function can be overridden by subclass. {@link curl} * @@ -155,35 +198,50 @@ public function get_file($reference, $filename = '') { /** * Returns information about file in this repository by reference - * {@link repository::get_file_reference()} - * {@link repository::get_file()} * + * If the file is an image we download the contents and save it in our filesystem + * so we can generate thumbnails. Otherwise we just request the file size. * Returns null if file not found or can not be accessed * * @param stdClass $reference file reference db record - * @return null|stdClass containing attribute 'filepath' + * @return stdClass|null contains one of the following: + * - 'filesize' (for non-image files or files we failed to retrieve fully because of timeout) + * - 'filepath' (for image files that we retrieived and saved) */ public function get_file_by_reference($reference) { - $ref = unserialize(base64_decode($reference->reference)); - $url = $this->appendtoken($ref->url); - - if (!$url) { + global $USER; + $ref = @unserialize(base64_decode($reference->reference)); + if (!isset($ref->url) || !($url = $this->appendtoken($ref->url))) { // Occurs when the user isn't known.. return null; } - // We use this cache to get the correct file size. - $cachedfilepath = cache_file::get($url, array('ttl' => 0)); - if ($cachedfilepath === false) { - // Cache the file. - $path = $this->get_file($url); - $cachedfilepath = cache_file::create_from_file($url, $path['path']); + $return = null; + $cookiepathname = $this->prepare_file($USER->id. '_'. uniqid('', true). '.cookie'); + $c = new curl(array('cookie' => $cookiepathname)); + if (file_extension_in_typegroup($ref->filename, 'web_image')) { + $path = $this->prepare_file(''); + $result = $c->download_one($url, null, array('filepath' => $path, 'followlocation' => true, 'timeout' => self::SYNCIMAGE_TIMEOUT)); + if ($result === true) { + $return = (object)array('filepath' => $path); } + } else { + $result = $c->head($url, array('followlocation' => true, 'timeout' => self::SYNCFILE_TIMEOUT)); + } + // Delete cookie jar. + if (file_exists($cookiepathname)) { + unlink($cookiepathname); + } - if ($cachedfilepath && is_readable($cachedfilepath)) { - return (object)array('filepath' => $cachedfilepath); + $this->connection_result($c->get_errno()); + $curlinfo = $c->get_info(); + if ($return === null && isset($curlinfo['http_code']) && $curlinfo['http_code'] == 200 + && array_key_exists('download_content_length', $curlinfo) + && $curlinfo['download_content_length'] >= 0) { + // we received a correct header and at least can tell the file size + $return = (object)array('filesize' => $curlinfo['download_content_length']); } - return null; + return $return; } /** From 7e1e775fa4de0056ddfdb450a1f0b901e7c2469e Mon Sep 17 00:00:00 2001 From: Marina Glancy <marina@moodle.com> Date: Tue, 7 Aug 2012 11:35:51 +0800 Subject: [PATCH 75/90] MDL-34290 Allow to have several requests to the same instance of oauth_helper --- lib/filelib.php | 7 +++++++ lib/oauthlib.php | 3 +++ 2 files changed, 10 insertions(+) diff --git a/lib/filelib.php b/lib/filelib.php index 4d03189bd65a5..14217a72c9af8 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -2898,6 +2898,13 @@ public function cleanopt(){ unset($this->options['CURLOPT_FILE']); } + /** + * Resets the HTTP Request headers (to prepare for the new request) + */ + public function resetHeader() { + $this->header = array(); + } + /** * Set HTTP Request Header * diff --git a/lib/oauthlib.php b/lib/oauthlib.php index ed29777b33d50..bc198b936bf33 100644 --- a/lib/oauthlib.php +++ b/lib/oauthlib.php @@ -289,6 +289,9 @@ public function request($method, $url, $params=array(), $token='', $secret='') { $oauth_params = $this->prepare_oauth_parameters($url, array('oauth_token'=>$token), $method); $this->setup_oauth_http_header($oauth_params); $content = call_user_func_array(array($this->http, strtolower($method)), array($url, $params, $this->http_options)); + // reset http header and options to prepare for the next request + $this->http->resetHeader(); + // return request return value return $content; } From 6ec6842933e440b322d366223070c8cedb6ff557 Mon Sep 17 00:00:00 2001 From: Marina Glancy <marina@moodle.com> Date: Tue, 7 Aug 2012 11:10:24 +0800 Subject: [PATCH 76/90] MDL-34290 oauthlib_helper support for POST request --- lib/oauthlib.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/oauthlib.php b/lib/oauthlib.php index bc198b936bf33..7f5ea64fe084d 100644 --- a/lib/oauthlib.php +++ b/lib/oauthlib.php @@ -286,7 +286,11 @@ public function request($method, $url, $params=array(), $token='', $secret='') { } // to access protected resource, sign_secret will alwasy be consumer_secret+token_secret $this->sign_secret = $this->consumer_secret.'&'.$secret; - $oauth_params = $this->prepare_oauth_parameters($url, array('oauth_token'=>$token), $method); + if (strtolower($method) === 'post' && !empty($params)) { + $oauth_params = $this->prepare_oauth_parameters($url, array('oauth_token'=>$token) + $params, $method); + } else { + $oauth_params = $this->prepare_oauth_parameters($url, array('oauth_token'=>$token), $method); + } $this->setup_oauth_http_header($oauth_params); $content = call_user_func_array(array($this->http, strtolower($method)), array($url, $params, $this->http_options)); // reset http header and options to prepare for the next request From 75dd40b265087300e32722401fb35aeda7406c84 Mon Sep 17 00:00:00 2001 From: Marina Glancy <marina@moodle.com> Date: Tue, 7 Aug 2012 11:26:19 +0800 Subject: [PATCH 77/90] MDL-34290 repository_dropbox reference handling - When Dropbox file is inserted by reference, the shared link is created and stored - Added a function to fix old references (containing access_key/secret) with the proper ones - Added support for external links in Dropbox (FILE_EXTERNAL), using the shared link API - Make sure that repository::get_link() receives reference and not source (other repositories than Dropbox have those fields identical) - Function get_file_by_reference respects request timeouts, downloads only images (for thumbnail generation), - Function get_file respects request timeout - do not use cache_file class (content is cached in moodle filepool if needed) - added parameter for maximum size of files to cache - added 'Manage' link for Filepicker - added user name to - added user name (if different from current) to 'Original' field - added/corrected phpdocs --- repository/dropbox/db/upgrade.php | 39 +++ .../dropbox/lang/en/repository_dropbox.php | 4 +- repository/dropbox/lib.php | 290 +++++++++++++++--- repository/dropbox/locallib.php | 101 ++++-- repository/dropbox/version.php | 5 +- repository/repository_ajax.php | 6 +- 6 files changed, 360 insertions(+), 85 deletions(-) create mode 100644 repository/dropbox/db/upgrade.php diff --git a/repository/dropbox/db/upgrade.php b/repository/dropbox/db/upgrade.php new file mode 100644 index 0000000000000..f562cf45628f1 --- /dev/null +++ b/repository/dropbox/db/upgrade.php @@ -0,0 +1,39 @@ +<?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/>. + +/** + * @param int $oldversion the version we are upgrading from + * @return bool result + */ +function xmldb_repository_dropbox_upgrade($oldversion) { + global $CFG, $DB; + + $dbman = $DB->get_manager(); + + // Moodle v2.3.0 release upgrade line + // Put any upgrade step following this + + if ($oldversion < 2012080702) { + // Set the default value for dropbox_cachelimit + $value = get_config('dropbox', 'dropbox_cachelimit'); + if (empty($value)) { + set_config('dropbox_cachelimit', 1024*1024, 'dropbox'); + } + upgrade_plugin_savepoint(true, 2012080702, 'repository', 'dropbox'); + } + + return true; +} diff --git a/repository/dropbox/lang/en/repository_dropbox.php b/repository/dropbox/lang/en/repository_dropbox.php index 4cda0af391049..4937c1303ff08 100644 --- a/repository/dropbox/lang/en/repository_dropbox.php +++ b/repository/dropbox/lang/en/repository_dropbox.php @@ -1,5 +1,4 @@ <?php - // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify @@ -31,4 +30,7 @@ $string['dropbox'] = 'Dropbox'; $string['secret'] = 'Dropbox secret'; $string['instruction'] = 'You can get your API Key and secret from <a href="http://www.dropbox.com/developers/apps">Dropbox developers</a>. When setting up your key please select "Full Dropbox" as the "Access level".'; +$string['cachelimit'] = 'Cache limit'; +$string['cachelimit_info'] = 'Enter the maximum size of files (in bytes) to be cached on server for Dropbox aliases/shortcuts. Cached files will be served when the source is no longer available. Empty value or zero mean caching of all files regardless of size.'; +$string['error_cachelimit'] = 'Must be a positive integer or empty value'; $string['dropbox:view'] = 'View a Dropbox folder'; diff --git a/repository/dropbox/lib.php b/repository/dropbox/lib.php index 9d73e3558b103..008b566514229 100644 --- a/repository/dropbox/lib.php +++ b/repository/dropbox/lib.php @@ -19,12 +19,20 @@ * * @since 2.0 * @package repository_dropbox + * @copyright 2012 Marina Glancy * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot . '/repository/lib.php'); require_once(dirname(__FILE__).'/locallib.php'); +/** + * Repository to access Dropbox files + * + * @package repository_dropbox + * @copyright 2010 Dongsheng Cai + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ class repository_dropbox extends repository { /** @var dropbox the instance of dropbox client */ private $dropbox; @@ -32,6 +40,8 @@ class repository_dropbox extends repository { public $files; /** @var bool flag of login status */ public $logged=false; + /** @var int maximum size of file to cache in moodle filepool */ + public $cachelimit=null; /** @var int cached file ttl */ private $cachedfilettl = null; @@ -166,7 +176,7 @@ public function get_listing($path = '', $page = '1') { $list = array(); $list['list'] = array(); - $list['manage'] = false; + $list['manage'] = 'https://www.dropbox.com/home'; $list['dynload'] = true; $list['nosearch'] = true; // process breadcrumb trail @@ -256,8 +266,13 @@ public function set_option($options = array()) { if (!empty($options['dropbox_secret'])) { set_config('dropbox_secret', trim($options['dropbox_secret']), 'dropbox'); } + if (!empty($options['dropbox_cachelimit'])) { + $this->cachelimit = (int)trim($options['dropbox_cachelimit']); + set_config('dropbox_cachelimit', $this->cachelimit, 'dropbox'); + } unset($options['dropbox_key']); unset($options['dropbox_secret']); + unset($options['dropbox_cachelimit']); $ret = parent::set_option($options); return $ret; } @@ -272,36 +287,118 @@ public function get_option($config = '') { return trim(get_config('dropbox', 'dropbox_key')); } elseif ($config==='dropbox_secret') { return trim(get_config('dropbox', 'dropbox_secret')); + } elseif ($config==='dropbox_cachelimit') { + return $this->max_cache_bytes(); } else { + $options = parent::get_option(); $options['dropbox_key'] = trim(get_config('dropbox', 'dropbox_key')); $options['dropbox_secret'] = trim(get_config('dropbox', 'dropbox_secret')); + $options['dropbox_cachelimit'] = $this->max_cache_bytes(); } - $options = parent::get_option($config); return $options; } + /** + * Fixes references in DB that contains user credentials + * + * @param string $reference contents of DB field files_reference.reference + */ + public function fix_old_style_reference($reference) { + $ref = unserialize($reference); + if (!isset($ref->url)) { + $this->dropbox->set_access_token($ref->access_key, $ref->access_secret); + $ref->url = $this->dropbox->get_file_share_link($ref->path, self::GETFILE_TIMEOUT); + if (!$ref->url) { + // some error occurred, do not fix reference for now + return $reference; + } + } + unset($ref->access_key); + unset($ref->access_secret); + $newreference = serialize($ref); + if ($newreference !== $reference) { + // we need to update references in the database + global $DB; + $params = array( + 'newreference' => $newreference, + 'newhash' => sha1($newreference), + 'reference' => $reference, + 'hash' => sha1($reference), + 'repoid' => $this->id + ); + $refid = $DB->get_field_sql('SELECT id FROM {files_reference} + WHERE reference = :reference AND referencehash = :hash + AND repositoryid = :repoid', $params); + if (!$refid) { + return $newreference; + } + $existingrefid = $DB->get_field_sql('SELECT id FROM {files_reference} + WHERE reference = :newreference AND referencehash = :newhash + AND repositoryid = :repoid', $params); + if ($existingrefid) { + // the same reference already exists, we unlink all files from it, + // link them to the current reference and remove the old one + $DB->execute('UPDATE {files} SET referencefileid = :refid + WHERE referencefileid = :existingrefid', + array('refid' => $refid, 'existingrefid' => $existingrefid)); + $DB->delete_records('files_reference', array('id' => $existingrefid)); + } + // update the reference + $params['refid'] = $refid; + $DB->execute('UPDATE {files_reference} + SET reference = :newreference, referencehash = :newhash + WHERE id = :refid', $params); + } + return $newreference; + } + + /** + * Converts a URL received from dropbox API function 'shares' into URL that + * can be used to download/access file directly + * + * @param string $sharedurl + * @return string + */ + private function get_file_download_link($sharedurl) { + return preg_replace('|^(\w*://)www(.dropbox.com)|','\1dl\2',$sharedurl); + } + /** * Downloads a file from external repository and saves it in temp dir * * @throws moodle_exception when file could not be downloaded * - * @param string $reference the content of files.reference field - * @param string $filename filename (without path) to save the downloaded file in the + * @param string $reference the content of files.reference field or result of + * function {@link repository_dropbox::get_file_reference()} + * @param string $saveas filename (without path) to save the downloaded file in the * temporary directory, if omitted or file already exists the new filename will be generated * @return array with elements: * path: internal location of the file * url: URL to the source (from parameters) */ public function get_file($reference, $saveas = '') { - $reference = unserialize($reference); - $this->dropbox->set_access_token($reference->access_key, $reference->access_secret); + $ref = unserialize($reference); $saveas = $this->prepare_file($saveas); - return $this->dropbox->get_file($reference->path, $saveas); + if (isset($ref->access_key) && isset($ref->access_secret) && isset($ref->path)) { + $this->dropbox->set_access_token($ref->access_key, $ref->access_secret); + return $this->dropbox->get_file($ref->path, $saveas, self::GETFILE_TIMEOUT); + } else if (isset($ref->url)) { + $c = new curl; + $url = $this->get_file_download_link($ref->url); + $result = $c->download_one($url, null, array('filepath' => $saveas, 'timeout' => self::GETFILE_TIMEOUT, 'followlocation' => true)); + $info = $c->get_info(); + if ($result !== true || !isset($info['http_code']) || $info['http_code'] != 200) { + throw new moodle_exception('errorwhiledownload', 'repository', '', $result); + } + return array('path'=>$saveas, 'url'=>$url); + } + throw new moodle_exception('cannotdownload', 'repository'); } /** * Add Plugin settings input to Moodle form * - * @param object $mform + * @param moodleform $mform Moodle form (passed by reference) + * @param string $classname repository class name */ public static function type_config_form($mform, $classname = 'repository') { global $CFG; @@ -325,6 +422,25 @@ public static function type_config_form($mform, $classname = 'repository') { $mform->addRule('dropbox_secret', $strrequired, 'required', null, 'client'); $str_getkey = get_string('instruction', 'repository_dropbox'); $mform->addElement('static', null, '', $str_getkey); + + $mform->addElement('text', 'dropbox_cachelimit', get_string('cachelimit', 'repository_dropbox'), array('size' => '40')); + $mform->addElement('static', 'dropbox_cachelimit_info', '', get_string('cachelimit_info', 'repository_dropbox')); + } + + /** + * Validate Admin Settings Moodle form + * + * @param moodleform $mform Moodle form (passed by reference) + * @param array $data array of ("fieldname"=>value) of submitted data + * @param array $errors array of ("fieldname"=>errormessage) of errors + * @return array array of errors + */ + public static function type_form_validation($mform, $data, $errors) { + if (!empty($data['dropbox_cachelimit']) && (!is_number($data['dropbox_cachelimit']) || + (int)$data['dropbox_cachelimit']<0)) { + $errors['dropbox_cachelimit'] = get_string('error_cachelimit', 'repository_dropbox'); + } + return $errors; } /** @@ -333,7 +449,7 @@ public static function type_config_form($mform, $classname = 'repository') { * @return array */ public static function get_type_option_names() { - return array('dropbox_key', 'dropbox_secret', 'pluginname'); + return array('dropbox_key', 'dropbox_secret', 'pluginname', 'dropbox_cachelimit'); } /** @@ -351,7 +467,18 @@ public function supported_filetypes() { * @return int */ public function supported_returntypes() { - return FILE_INTERNAL | FILE_REFERENCE; + return FILE_INTERNAL | FILE_REFERENCE | FILE_EXTERNAL; + } + + /** + * Return file URL for external link + * + * @param string $reference the result of get_file_reference() + * @return string + */ + public function get_link($reference) { + $ref = unserialize($reference); + return $this->get_file_download_link($ref->url); } /** @@ -364,10 +491,24 @@ public function get_file_reference($source) { global $USER; $reference = new stdClass; $reference->path = $source; - $reference->access_key = get_user_preferences($this->setting.'_access_key', ''); - $reference->access_secret = get_user_preferences($this->setting.'_access_secret', ''); $reference->userid = $USER->id; $reference->username = fullname($USER); + $reference->access_key = get_user_preferences($this->setting.'_access_key', ''); + $reference->access_secret = get_user_preferences($this->setting.'_access_secret', ''); + + // by API we don't know if we need this reference to just download a file from dropbox + // into moodle filepool or create a reference. Since we need to create a shared link + // only in case of reference we analyze the script parameter + $usefilereference = optional_param('usefilereference', false, PARAM_BOOL); + if ($usefilereference) { + $this->dropbox->set_access_token($reference->access_key, $reference->access_secret); + $url = $this->dropbox->get_file_share_link($source, self::GETFILE_TIMEOUT); + if ($url) { + unset($reference->access_key); + unset($reference->access_secret); + $reference->url = $url; + } + } return serialize($reference); } @@ -382,35 +523,51 @@ public function get_file_reference($source) { * @return null|stdClass that has 'filepath' property */ public function get_file_by_reference($reference) { - $reference = unserialize($reference->reference); - $cachedfilepath = cache_file::get($reference, array('ttl' => $this->cachedfilettl)); - if ($cachedfilepath === false) { - // Cache the file. - $this->set_access_key($reference->access_key); - $this->set_access_secret($reference->access_secret); - $path = $this->get_file($reference->path); - $cachedfilepath = cache_file::create_from_file($reference, $path['path']); - } - if ($cachedfilepath && is_readable($cachedfilepath)) { - return (object)array('filepath' => $cachedfilepath); - } else { + global $USER; + $ref = unserialize($reference->reference); + if (!isset($ref->url)) { + // this is an old-style reference in DB. We need to fix it + $ref = unserialize($this->fix_old_style_reference($reference->reference)); + } + if (!isset($ref->url)) { return null; } + $c = new curl; + $url = $this->get_file_download_link($ref->url); + if (file_extension_in_typegroup($ref->path, 'web_image')) { + $saveas = $this->prepare_file(''); + try { + $result = $c->download_one($url, array(), array('filepath' => $saveas, 'timeout' => self::SYNCIMAGE_TIMEOUT, 'followlocation' => true)); + $info = $c->get_info(); + if ($result === true && isset($info['http_code']) && $info['http_code'] == 200) { + return (object)array('filepath' => $saveas); + } + } catch (Exception $e) {} } + $c->get($url, null, array('timeout' => self::SYNCIMAGE_TIMEOUT, 'followlocation' => true, 'nobody' => true)); + $info = $c->get_info(); + if (isset($info['http_code']) && $info['http_code'] == 200 && + array_key_exists('download_content_length', $info) && + $info['download_content_length'] >= 0) { + return (object)array('filesize' => (int)$info['download_content_length']); + } + return null; + } /** - * Get file from external repository by reference - * {@link repository::get_file_reference()} - * {@link repository::get_file()} + * Cache file from external repository by reference + * + * Dropbox repository regularly caches all external files that are smaller than + * {@link repository_dropbox::max_cache_bytes()} * * @param string $reference this reference is generated by * repository::get_file_reference() * @param stored_file $storedfile created file reference */ public function cache_file_by_reference($reference, $storedfile) { - $reference = unserialize($reference); - $path = $this->get_file($reference); - cache_file::create_from_file($reference->path, $path['path']); + try { + $this->import_external_file_contents($storedfile, $this->max_cache_bytes()); + } catch (Exception $e) {} } /** @@ -424,17 +581,21 @@ public function cache_file_by_reference($reference, $storedfile) { public function get_reference_details($reference, $filestatus = 0) { global $USER; $ref = unserialize($reference); - $details = $this->get_name(); + $detailsprefix = $this->get_name(); if (isset($ref->userid) && $ref->userid != $USER->id && isset($ref->username)) { - $details .= ' ('.$ref->username.')'; + $detailsprefix .= ' ('.$ref->username.')'; } + $details = $detailsprefix; if (isset($ref->path)) { - $details .= ': '. $ref->path; + $details .= ': '. $ref->path; } if (isset($ref->path) && !$filestatus) { // Indicate this is from dropbox with path return $details; } else { + if (isset($ref->url)) { + $details = $detailsprefix. ': '. $ref->url; + } return get_string('lostsource', 'repository', $details); } } @@ -450,6 +611,24 @@ public function get_file_source_info($source) { return 'Dropbox ('.fullname($USER).'): ' . $source; } + /** + * Returns the maximum size of the Dropbox files to cache in moodle + * + * Note that {@link repository_dropbox::get_file_by_reference()} called by + * {@link repository::sync_external_file()} will try to cache images even + * when they are bigger in order to generate thumbnails. However there is + * a small timeout for downloading images for synchronisation and it will + * probably fail if the image is too big. + * + * @return int + */ + public function max_cache_bytes() { + if ($this->cachelimit === null) { + $this->cachelimit = (int)get_config('dropbox', 'dropbox_cachelimit'); + } + return $this->cachelimit; + } + /** * Repository method to serve the referenced file * @@ -464,31 +643,42 @@ public function get_file_source_info($source) { * @param array $options additional options affecting the file serving */ public function send_file($storedfile, $lifetime=86400 , $filter=0, $forcedownload=false, array $options = null) { - $fileinfo = $this->get_file_by_reference((object)array('reference' => $storedfile->get_reference())); - if ($fileinfo && !empty($fileinfo->filepath) && is_readable($fileinfo->filepath)) { - $filename = $storedfile->get_filename(); - if ($options && isset($options['filename'])) { - $filename = $options['filename']; + $ref = unserialize($storedfile->get_reference()); + if ($storedfile->get_filesize() > $this->max_cache_bytes()) { + header('Location: '.$this->get_file_download_link($ref->url)); + die; + } + try { + $this->import_external_file_contents($storedfile, $this->max_cache_bytes()); + if (!is_array($options)) { + $options = array(); } - $dontdie = ($options && isset($options['dontdie'])); - send_file($fileinfo->filepath, $filename, $lifetime , $filter, false, $forcedownload, '', $dontdie); - } else { - send_file_not_found(); + $options['sendcachedexternalfile'] = true; + send_stored_file($storedfile, $lifetime, $filter, $forcedownload, $options); + } catch (moodle_exception $e) { + // redirect to Dropbox, it will show the error. + // We redirect to Dropbox shared link, not to download link here! + header('Location: '.$ref->url); + die; } } + /** + * Caches all references to Dropbox files in moodle filepool + * + * Invoked by {@link repository_dropbox_cron()}. Only files smaller than + * {@link repository_dropbox::max_cache_bytes()} and only files which + * synchronisation timeout have not expired are cached. + */ public function cron() { $fs = get_file_storage(); $files = $fs->get_external_files($this->id); foreach ($files as $file) { - $reference = unserialize($file->get_reference()); - - $cachedfile = cache_file::get($reference); - if ($cachedfile === false) { - // Re-fetch resource. - $path = $this->get_file($reference); - cache_file::create_from_file($reference->path, $path['path']); - } + try { + // This call will cache all files that are smaller than max_cache_bytes() + // and synchronise file size of all others + $this->import_external_file_contents($file, $this->max_cache_bytes()); + } catch (moodle_exception $e) {} } } } diff --git a/repository/dropbox/locallib.php b/repository/dropbox/locallib.php index 2fb322ae247c4..89c7e735dbdbf 100644 --- a/repository/dropbox/locallib.php +++ b/repository/dropbox/locallib.php @@ -1,5 +1,4 @@ <?php - // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify @@ -16,34 +15,50 @@ // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** - * dropbox class * A helper class to access dropbox resources * * @since 2.0 - * @package repository - * @subpackage dropbox + * @package repository_dropbox + * @copyright 2012 Marina Glancy * @copyright 2010 Dongsheng Cai * @author Dongsheng Cai <dongsheng@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - require_once(dirname(dirname(dirname(__FILE__))).'/config.php'); require_once($CFG->libdir.'/oauthlib.php'); +/** + * Authentication class to access Dropbox API + * + * @package repository_dropbox + * @copyright 2010 Dongsheng Cai + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ class dropbox extends oauth_helper { - /** dropbox access type, can be dropbox or sandbox */ + /** @var string dropbox access type, can be dropbox or sandbox */ private $mode = 'dropbox'; - /** dropbox api url*/ + /** @var string dropbox api url*/ private $dropbox_api = 'https://api.dropbox.com/1'; - /** dropbox content api url*/ + /** @var string dropbox content api url*/ private $dropbox_content_api = 'https://api-content.dropbox.com/1'; + /** + * Constructor for dropbox class + * + * @param array $args + */ function __construct($args) { parent::__construct($args); } + /** * Get file listing from dropbox + * + * @param string $path + * @param string $token + * @param string $secret + * @return array */ public function get_listing($path='/', $token='', $secret='') { $url = $this->dropbox_api.'/metadata/'.$this->mode.$path; @@ -53,9 +68,12 @@ public function get_listing($path='/', $token='', $secret='') { } /** - * Download a file + * Prepares the filename to pass to Dropbox API as part of URL + * + * @param string $filepath + * @return string */ - public function get_file($filepath, $saveas = '') { + protected function prepare_filepath($filepath) { $info = pathinfo($filepath); $dirname = $info['dirname']; $basename = $info['basename']; @@ -64,33 +82,60 @@ public function get_file($filepath, $saveas = '') { $filepath = $dirname . '/' . $basename; $filepath = str_replace("%2F", "/", rawurlencode($filepath)); } - - $url = $this->dropbox_content_api.'/files/'.$this->mode.$filepath; - $content = $this->get($url, array()); - file_put_contents($saveas, $content); - return array('path'=>$saveas, 'url'=>$url); + return $filepath; } /** - * Get file url + * Downloads a file from Dropbox and saves it locally * - * @param string $filepath file path - * @return string file url + * @throws moodle_exception when file could not be downloaded + * + * @param string $filepath local path in Dropbox + * @param string $saveas path to file to save the result + * @param int $timeout request timeout in seconds, 0 means no timeout + * @return array with attributes 'path' and 'url' */ - public function get_file_url($filepath) { - $info = pathinfo($filepath); - $dirname = $info['dirname']; - $basename = $info['basename']; - $filepath = $dirname . rawurlencode($basename); - if ($dirname != '/') { - $filepath = $dirname . '/' . $basename; - $filepath = str_replace("%2F", "/", rawurlencode($filepath)); + public function get_file($filepath, $saveas, $timeout = 0) { + $url = $this->dropbox_content_api.'/files/'.$this->mode.$this->prepare_filepath($filepath); + if (!($fp = fopen($saveas, 'w'))) { + throw new moodle_exception('cannotwritefile', 'error', '', $saveas); } + $this->setup_oauth_http_options(array('timeout' => $timeout, 'file' => $fp, 'BINARYTRANSFER' => true)); + $result = $this->get($url); + fclose($fp); + if ($result === true) { + return array('path'=>$saveas, 'url'=>$url); + } else { + unlink($saveas); + throw new moodle_exception('errorwhiledownload', 'repository', '', $result); + } + } - $url = $this->dropbox_content_api.'/files/'.$this->mode.$filepath; - return $url; + /** + * Returns direct link to Dropbox file + * + * @param string $filepath local path in Dropbox + * @param int $timeout request timeout in seconds, 0 means no timeout + * @return string|null information object or null if request failed with an error + */ + public function get_file_share_link($filepath, $timeout = 0) { + $url = $this->dropbox_api.'/shares/'.$this->mode.$this->prepare_filepath($filepath); + $this->setup_oauth_http_options(array('timeout' => $timeout)); + $result = $this->post($url, array('short_url'=>0)); + if (!$this->http->get_errno()) { + $data = json_decode($result); + if (isset($data->url)) { + return $data->url; + } + } + return null; } + /** + * Sets Dropbox API mode (dropbox or sandbox, default dropbox) + * + * @param string $mode + */ public function set_mode($mode) { $this->mode = $mode; } diff --git a/repository/dropbox/version.php b/repository/dropbox/version.php index ac70aab07601f..2df0544339cbf 100644 --- a/repository/dropbox/version.php +++ b/repository/dropbox/version.php @@ -17,8 +17,7 @@ /** * Version details * - * @package repository - * @subpackage dropbox + * @package repository_dropbox * @copyright 2010 Dongsheng Cai * @author Dongsheng Cai <dongsheng@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later @@ -26,6 +25,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2012061700; // The current plugin version (Date: YYYYMMDDXX) +$plugin->version = 2012080702; // The current plugin version (Date: YYYYMMDDXX) $plugin->requires = 2012061700; // Requires this Moodle version $plugin->component = 'repository_dropbox'; // Full name of the plugin (used for diagnostics) diff --git a/repository/repository_ajax.php b/repository/repository_ajax.php index baf59ef30303e..8c9fc7faf6d6e 100644 --- a/repository/repository_ajax.php +++ b/repository/repository_ajax.php @@ -180,10 +180,12 @@ // allow external links in url element all the time $allowexternallink = ($allowexternallink || ($env == 'url')); + $reference = $repo->get_file_reference($source); + // Use link of the files if ($allowexternallink and $linkexternal === 'yes' and ($repo->supported_returntypes() & FILE_EXTERNAL)) { // use external link - $link = $repo->get_link($source); + $link = $repo->get_link($reference); $info = array(); $info['file'] = $saveas_filename; $info['type'] = 'link'; @@ -224,8 +226,6 @@ $sourcefield = $repo->get_file_source_info($source); $record->source = $repo::build_source_field($sourcefield); - $reference = $repo->get_file_reference($source); - // If file is already a reference, set $source = file source, $repo = file repository // note that in this case user may not have permission to access the source file directly // so no file_browser/file_info can be used below From db02d84a40fd282c9d74b5f02315515c15033e9c Mon Sep 17 00:00:00 2001 From: Marina Glancy <marina@moodle.com> Date: Tue, 31 Jul 2012 11:08:25 +0800 Subject: [PATCH 78/90] MDL-34290 remove class cache_file as not used in fact we have moodle filepool that can work perfectly for caching files, no need to create new class and storage --- lib/cronlib.php | 4 -- lib/filelib.php | 167 ------------------------------------------------ 2 files changed, 171 deletions(-) diff --git a/lib/cronlib.php b/lib/cronlib.php index 457336af0c90c..c0b86afb8c1e5 100644 --- a/lib/cronlib.php +++ b/lib/cronlib.php @@ -466,10 +466,6 @@ function cron_run() { $fs = get_file_storage(); $fs->cron(); - mtrace("Clean up cached external files"); - // 1 week - cache_file::cleanup(array(), 60 * 60 * 24 * 7); - mtrace("Cron script completed correctly"); $difftime = microtime_diff($starttime, microtime()); diff --git a/lib/filelib.php b/lib/filelib.php index 14217a72c9af8..cef949a58de54 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -4271,170 +4271,3 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) { } } - -/** - * Universe file cacheing class - * - * @package core_files - * @category files - * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class cache_file { - /** @var string */ - public $cachedir = ''; - - /** - * static method to create cache_file class instance - * - * @param array $options caching ooptions - */ - public static function get_instance($options = array()) { - return new cache_file($options); - } - - /** - * Constructor - * - * @param array $options - */ - private function __construct($options = array()) { - global $CFG; - - // Path to file caches. - if (isset($options['cachedir'])) { - $this->cachedir = $options['cachedir']; - } else { - $this->cachedir = $CFG->cachedir . '/filedir'; - } - - // Create cache directory. - if (!file_exists($this->cachedir)) { - mkdir($this->cachedir, $CFG->directorypermissions, true); - } - - // When use cache_file::get, it will check ttl. - if (isset($options['ttl']) && is_numeric($options['ttl'])) { - $this->ttl = $options['ttl']; - } else { - // One day. - $this->ttl = 60 * 60 * 24; - } - } - - /** - * Get cached file, false if file expires - * - * @param mixed $param - * @param array $options caching options - * @return bool|string - */ - public static function get($param, $options = array()) { - $instance = self::get_instance($options); - $filepath = $instance->generate_filepath($param); - if (file_exists($filepath)) { - $lasttime = filemtime($filepath); - if (time() - $lasttime > $instance->ttl) { - // Remove cache file. - unlink($filepath); - return false; - } else { - return $filepath; - } - } else { - return false; - } - } - - /** - * Static method to create cache from a file - * - * @param mixed $ref - * @param string $srcfile - * @param array $options - * @return string cached file path - */ - public static function create_from_file($ref, $srcfile, $options = array()) { - $instance = self::get_instance($options); - $cachedfilepath = $instance->generate_filepath($ref); - copy($srcfile, $cachedfilepath); - return $cachedfilepath; - } - - /** - * Static method to create cache from url - * - * @param mixed $ref file reference - * @param string $url file url - * @param array $options options - * @return string cached file path - */ - public static function create_from_url($ref, $url, $options = array()) { - $instance = self::get_instance($options); - $cachedfilepath = $instance->generate_filepath($ref); - $fp = fopen($cachedfilepath, 'w'); - $curl = new curl; - $curl->download(array(array('url'=>$url, 'file'=>$fp))); - // Must close file handler. - fclose($fp); - return $cachedfilepath; - } - - /** - * Static method to create cache from string - * - * @param mixed $ref file reference - * @param string $url file url - * @param array $options options - * @return string cached file path - */ - public static function create_from_string($ref, $string, $options = array()) { - $instance = self::get_instance($options); - $cachedfilepath = $instance->generate_filepath($ref); - $fp = fopen($cachedfilepath, 'w'); - fwrite($fp, $string); - // Must close file handler. - fclose($fp); - return $cachedfilepath; - } - - /** - * Build path to cache file - * - * @param mixed $ref - * @return string - */ - private function generate_filepath($ref) { - global $CFG; - $hash = sha1(serialize($ref)); - $l1 = $hash[0].$hash[1]; - $l2 = $hash[2].$hash[3]; - $dir = $this->cachedir . "/$l1/$l2"; - if (!file_exists($dir)) { - mkdir($dir, $CFG->directorypermissions, true); - } - return "$dir/$hash"; - } - - /** - * Remove cache files - * - * @param array $options options - * @param int $expire The number of seconds before expiry - */ - public static function cleanup($options = array(), $expire) { - global $CFG; - $instance = self::get_instance($options); - if ($dir = opendir($instance->cachedir)) { - while (($file = readdir($dir)) !== false) { - if (!is_dir($file) && $file != '.' && $file != '..') { - $lasttime = @filemtime($instance->cachedir . $file); - if(time() - $lasttime > $expire){ - @unlink($instance->cachedir . $file); - } - } - } - closedir($dir); - } - } -} From f4fe646b71debe541ef1432b6c9a51c9a8c427bc Mon Sep 17 00:00:00 2001 From: Marina Glancy <marina@moodle.com> Date: Wed, 1 Aug 2012 15:01:38 +0800 Subject: [PATCH 79/90] MDL-34665 Dropbox displays thumbnails and return info about file size and date last modified --- repository/dropbox/lib.php | 56 +++++++++++++++++++++++++++----- repository/dropbox/locallib.php | 26 +++++++++++++++ repository/dropbox/thumbnail.php | 44 +++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 repository/dropbox/thumbnail.php diff --git a/repository/dropbox/lib.php b/repository/dropbox/lib.php index 008b566514229..c0edef34cb62d 100644 --- a/repository/dropbox/lib.php +++ b/repository/dropbox/lib.php @@ -220,28 +220,68 @@ public function get_listing($path = '', $page = '1') { return $list; } $files = $result->contents; + $dirslist = array(); + $fileslist = array(); foreach ($files as $file) { if ($file->is_dir) { - $list['list'][] = array( + $dirslist[] = array( 'title' => substr($file->path, strpos($file->path, $current_path)+strlen($current_path)), 'path' => file_correct_filepath($file->path), - 'size' => $file->size, - 'date' => $file->modified, - 'thumbnail' => $OUTPUT->pix_url(file_folder_icon(90))->out(false), + 'date' => strtotime($file->modified), + 'thumbnail' => $OUTPUT->pix_url(file_folder_icon(64))->out(false), + 'thumbnail_height' => 64, + 'thumbnail_width' => 64, 'children' => array(), ); } else { - $list['list'][] = array( + $thumbnail = null; + if ($file->thumb_exists) { + $thumburl = new moodle_url('/repository/dropbox/thumbnail.php', + array('repo_id' => $this->id, + 'ctx_id' => $this->context->id, + 'source' => $file->path, + 'rev' => $file->rev // include revision to avoid cache problems + )); + $thumbnail = $thumburl->out(false); + } + $fileslist[] = array( 'title' => substr($file->path, strpos($file->path, $current_path)+strlen($current_path)), 'source' => $file->path, - 'size' => $file->size, - 'date' => $file->modified, - 'thumbnail' => $OUTPUT->pix_url(file_extension_icon($file->path, 90))->out(false) + 'size' => $file->bytes, + 'date' => strtotime($file->modified), + 'thumbnail' => $OUTPUT->pix_url(file_extension_icon($file->path, 64))->out(false), + 'realthumbnail' => $thumbnail, + 'thumbnail_height' => 64, + 'thumbnail_width' => 64, ); } } + $fileslist = array_filter($fileslist, array($this, 'filter')); + $list['list'] = array_merge($dirslist, array_values($fileslist)); return $list; } + + /** + * Displays a thumbnail for current user's dropbox file + * + * @param string $string + */ + public function send_thumbnail($source) { + $saveas = $this->prepare_file(''); + try { + $access_key = get_user_preferences($this->setting.'_access_key', ''); + $access_secret = get_user_preferences($this->setting.'_access_secret', ''); + $this->dropbox->set_access_token($access_key, $access_secret); + $this->dropbox->get_thumbnail($source, $saveas, self::SYNCIMAGE_TIMEOUT); + $content = file_get_contents($saveas); + unlink($saveas); + // set 30 days lifetime for the image. If the image is changed in dropbox it will have + // different revision number and URL will be different. It is completely safe + // to cache thumbnail in the browser for a long time + send_file($content, basename($source), 30*24*60*60, 0, true); + } catch (Exception $e) {} + } + /** * Logout from dropbox * @return array diff --git a/repository/dropbox/locallib.php b/repository/dropbox/locallib.php index 89c7e735dbdbf..ea83d36b97672 100644 --- a/repository/dropbox/locallib.php +++ b/repository/dropbox/locallib.php @@ -85,6 +85,32 @@ protected function prepare_filepath($filepath) { return $filepath; } + /** + * Retrieves the default (64x64) thumbnail for dropbox file + * + * @throws moodle_exception when file could not be downloaded + * + * @param string $filepath local path in Dropbox + * @param string $saveas path to file to save the result + * @param int $timeout request timeout in seconds, 0 means no timeout + * @return array with attributes 'path' and 'url' + */ + public function get_thumbnail($filepath, $saveas, $timeout = 0) { + $url = $this->dropbox_content_api.'/thumbnails/'.$this->mode.$this->prepare_filepath($filepath); + if (!($fp = fopen($saveas, 'w'))) { + throw new moodle_exception('cannotwritefile', 'error', '', $saveas); + } + $this->setup_oauth_http_options(array('timeout' => $timeout, 'file' => $fp, 'BINARYTRANSFER' => true)); + $result = $this->get($url); + fclose($fp); + if ($result === true) { + return array('path'=>$saveas, 'url'=>$url); + } else { + unlink($saveas); + throw new moodle_exception('errorwhiledownload', 'repository', '', $result); + } + } + /** * Downloads a file from Dropbox and saves it locally * diff --git a/repository/dropbox/thumbnail.php b/repository/dropbox/thumbnail.php new file mode 100644 index 0000000000000..6a4887e7b80e9 --- /dev/null +++ b/repository/dropbox/thumbnail.php @@ -0,0 +1,44 @@ +<?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/>. + +/** + * This script displays one thumbnail of the image in current user's dropbox. + * If {@link repository_dropbox::send_thumbnail()} can not display image + * the default 64x64 filetype icon is returned + * + * @package repository_dropbox + * @copyright 2012 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(dirname(dirname(__FILE__))).'/config.php'); +require_once(dirname(__FILE__).'/lib.php'); + +$repo_id = optional_param('repo_id', 0, PARAM_INT); // Repository ID +$contextid = optional_param('ctx_id', SYSCONTEXTID, PARAM_INT); // Context ID +$source = optional_param('source', '', PARAM_TEXT); // File path in current user's dropbox + +if (isloggedin() && $repo_id && $source + && ($repo = repository::get_repository_by_id($repo_id, $contextid)) + && method_exists($repo, 'send_thumbnail')) { + // try requesting thumbnail and outputting it. This function exits if thumbnail was retrieved + $repo->send_thumbnail($source); +} + +// send default icon for the file type +$fileicon = file_extension_icon($source, 64); +send_file($CFG->dirroot.'/pix/'.$fileicon.'.png', basename($fileicon).'.png'); From 437f5dc4cf0e3c4a4614aeaf298aac4ab0f8284a Mon Sep 17 00:00:00 2001 From: Marina Glancy <marina@moodle.com> Date: Thu, 9 Aug 2012 13:50:42 +0800 Subject: [PATCH 80/90] MDL-34290 Auto synchronise newly created references when possible --- lib/filestorage/file_storage.php | 29 +++++++++++++++++++++++------ repository/repository_ajax.php | 9 +++++++-- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/lib/filestorage/file_storage.php b/lib/filestorage/file_storage.php index 71c8fc8389a13..b18105b18debb 100644 --- a/lib/filestorage/file_storage.php +++ b/lib/filestorage/file_storage.php @@ -1266,22 +1266,39 @@ public function create_file_from_reference($filerecord, $repositoryid, $referenc $transaction = $DB->start_delegated_transaction(); try { - $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference, - $filerecord->referencelastsync, $filerecord->referencelifetime); + $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference); } catch (Exception $e) { throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage()); } - // External file doesn't have content in moodle. - // So we create an empty file for it. - list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null); + if (isset($filerecord->contenthash) && $this->content_exists($filerecord->contenthash)) { + // there was specified the contenthash for a file already stored in moodle filepool + if (empty($filerecord->filesize)) { + $filepathname = $this->path_from_hash($filerecord->contenthash) . '/' . $filerecord->contenthash; + $filerecord->filesize = filesize($filepathname); + } else { + $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT); + } + } else { + // atempt to get the result of last synchronisation for this reference + $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid), + 'id, contenthash, filesize', IGNORE_MULTIPLE); + if ($lastcontent) { + $filerecord->contenthash = $lastcontent->contenthash; + $filerecord->filesize = $lastcontent->filesize; + } else { + // External file doesn't have content in moodle. + // So we create an empty file for it. + list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null); + } + } $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename); try { $filerecord->id = $DB->insert_record('files', $filerecord); } catch (dml_exception $e) { - if ($newfile) { + if (!empty($newfile)) { $this->deleted_file_cleanup($filerecord->contenthash); } throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, diff --git a/repository/repository_ajax.php b/repository/repository_ajax.php index 8c9fc7faf6d6e..17d679c00348d 100644 --- a/repository/repository_ajax.php +++ b/repository/repository_ajax.php @@ -234,6 +234,8 @@ if ($file && $file->is_external_file()) { $sourcefield = $file->get_source(); // remember the original source $record->source = $repo::build_source_field($sourcefield); + $record->contenthash = $file->get_contenthash(); + $record->filesize = $file->get_filesize(); $reference = $file->get_reference(); $repo_id = $file->get_repository_id(); $repo = repository::get_repository_by_id($repo_id, $contextid, $repooptions); @@ -241,8 +243,11 @@ } if ($usefilereference) { - // get reference life time from repo - $record->referencelifetime = $repo->get_reference_file_lifetime($reference); + if ($repo->has_moodle_files()) { + $sourcefile = repository::get_moodle_file($reference); + $record->contenthash = $sourcefile->get_contenthash(); + $record->filesize = $sourcefile->get_filesize(); + } // Check if file exists. if (repository::draftfile_exists($itemid, $saveas_path, $saveas_filename)) { // File name being used, rename it. From 42aa6e15bb077169076f23db7ed28baf514fec63 Mon Sep 17 00:00:00 2001 From: Marina Glancy <marina@moodle.com> Date: Fri, 10 Aug 2012 12:26:19 +0800 Subject: [PATCH 81/90] MDL-34290, MDL-33416 prepare to deprecate fields files.referencelastsync and referencelifetime --- lib/filestorage/file_storage.php | 32 +++++++++++++------------------- lib/filestorage/stored_file.php | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/lib/filestorage/file_storage.php b/lib/filestorage/file_storage.php index b18105b18debb..ae8df9c911e88 100644 --- a/lib/filestorage/file_storage.php +++ b/lib/filestorage/file_storage.php @@ -976,10 +976,6 @@ public function create_file_from_pathname($filerecord, $pathname) { $filerecord->sortorder = 0; } - $filerecord->referencefileid = !isset($filerecord->referencefileid) ? 0 : $filerecord->referencefileid; - $filerecord->referencelastsync = !isset($filerecord->referencelastsync) ? 0 : $filerecord->referencelastsync; - $filerecord->referencelifetime = !isset($filerecord->referencelifetime) ? 0 : $filerecord->referencelifetime; - $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH); if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) { // path must start and end with '/' @@ -1094,9 +1090,6 @@ public function create_file_from_string($filerecord, $content) { } else { $filerecord->sortorder = 0; } - $filerecord->referencefileid = !isset($filerecord->referencefileid) ? 0 : $filerecord->referencefileid; - $filerecord->referencelastsync = !isset($filerecord->referencelastsync) ? 0 : $filerecord->referencelastsync; - $filerecord->referencelifetime = !isset($filerecord->referencelifetime) ? 0 : $filerecord->referencelifetime; $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH); if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) { @@ -1217,9 +1210,10 @@ public function create_file_from_reference($filerecord, $repositoryid, $referenc $filerecord->sortorder = 0; } - $filerecord->referencefileid = empty($filerecord->referencefileid) ? 0 : $filerecord->referencefileid; - $filerecord->referencelastsync = empty($filerecord->referencelastsync) ? 0 : $filerecord->referencelastsync; - $filerecord->referencelifetime = empty($filerecord->referencelifetime) ? 0 : $filerecord->referencelifetime; + // TODO MDL-33416 [2.4] fields referencelastsync and referencelifetime to be removed from {files} table completely + unset($filerecord->referencelastsync); + unset($filerecord->referencelifetime); + $filerecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype; $filerecord->userid = empty($filerecord->userid) ? null : $filerecord->userid; $filerecord->source = empty($filerecord->source) ? null : $filerecord->source; @@ -1309,10 +1303,8 @@ public function create_file_from_reference($filerecord, $repositoryid, $referenc $transaction->allow_commit(); - // Adding repositoryid and reference to file record to create stored_file instance - $filerecord->repositoryid = $repositoryid; - $filerecord->reference = $reference; - return $this->get_file_instance($filerecord); + // this will retrieve all reference information from DB as well + return $this->get_file_by_id($filerecord->id); } /** @@ -1973,10 +1965,12 @@ private static function instance_sql_fields($filesprefix, $filesreferenceprefix) // else problems like MDL-33172 occur. $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea', 'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source', - 'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid', - 'referencelastsync', 'referencelifetime'); + 'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid'); - $referencefields = array('repositoryid', 'reference'); + $referencefields = array('repositoryid' => 'repositoryid', + 'reference' => 'reference', + 'lastsync' => 'referencelastsync', + 'lifetime' => 'referencelifetime'); // id is specifically named to prevent overlaping between the two tables. $fields = array(); @@ -1985,8 +1979,8 @@ private static function instance_sql_fields($filesprefix, $filesreferenceprefix) $fields[] = "{$filesprefix}.{$field}"; } - foreach ($referencefields as $field) { - $fields[] = "{$filesreferenceprefix}.{$field}"; + foreach ($referencefields as $field => $alias) { + $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}"; } return implode(', ', $fields); diff --git a/lib/filestorage/stored_file.php b/lib/filestorage/stored_file.php index ea8a8cd1d0eb4..fee7bfa8e4b7d 100644 --- a/lib/filestorage/stored_file.php +++ b/lib/filestorage/stored_file.php @@ -70,6 +70,12 @@ public function __construct(file_storage $fs, stdClass $file_record, $filedir) { } else { $this->repository = null; } + // make sure all reference fields exist in file_record even when it is not a reference + foreach (array('referencelastsync', 'referencelifetime', 'referencefileid', 'reference', 'repositoryid') as $key) { + if (empty($this->file_record->$key)) { + $this->file_record->$key = null; + } + } } /** @@ -142,12 +148,18 @@ protected function update($dataobject) { } } - if ($field === 'referencefileid' or $field === 'referencelastsync' or $field === 'referencelifetime') { + if ($field === 'referencefileid') { if (!is_null($value) and !is_number($value)) { throw new file_exception('storedfileproblem', 'Invalid reference info'); } } + if ($field === 'referencelastsync' or $field === 'referencelifetime') { + // do not update those fields + // TODO MDL-33416 [2.4] fields referencelastsync and referencelifetime to be removed from {files} table completely + continue; + } + // adding the field $this->file_record->$field = $value; } else { @@ -226,8 +238,6 @@ public function delete_reference() { // Update the underlying record in the database. $update = new stdClass(); $update->referencefileid = null; - $update->referencelastsync = null; - $update->referencelifetime = null; $this->update($update); $transaction->allow_commit(); From 7bb7bd2e79ea43728889329965c66dd9e1910f46 Mon Sep 17 00:00:00 2001 From: Marina Glancy <marina@moodle.com> Date: Tue, 31 Jul 2012 11:09:35 +0800 Subject: [PATCH 82/90] MDL-34290 Added timeout to googledocs request to download a file --- lib/googleapi.php | 17 +++++++++++++---- repository/googledocs/lib.php | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/googleapi.php b/lib/googleapi.php index fe2b9335b5989..c2d529fd6b7ab 100644 --- a/lib/googleapi.php +++ b/lib/googleapi.php @@ -185,12 +185,21 @@ public function send_file($file) { * * @param string $url url of file * @param string $path path to save file to + * @param int $timeout request timeout, default 0 which means no timeout * @return array stucture for repository download_file */ - public function download_file($url, $path) { - $content = $this->googleoauth->get($url); - file_put_contents($path, $content); - return array('path'=>$path, 'url'=>$url); + public function download_file($url, $path, $timeout = 0) { + $result = $this->googleoauth->download_one($url, null, array('filepath' => $path, 'timeout' => $timeout)); + if ($result === true) { + $info = $this->googleoauth->get_info(); + if (isset($info['http_code']) && $info['http_code'] == 200) { + return array('path'=>$path, 'url'=>$url); + } else { + throw new moodle_exception('cannotdownload', 'repository'); + } + } else { + throw new moodle_exception('errorwhiledownload', 'repository', '', $result); + } } } diff --git a/repository/googledocs/lib.php b/repository/googledocs/lib.php index c3a00e1a628a9..38299decb73de 100644 --- a/repository/googledocs/lib.php +++ b/repository/googledocs/lib.php @@ -95,7 +95,7 @@ public function get_file($url, $file = '') { $gdocs = new google_docs($this->googleoauth); $path = $this->prepare_file($file); - return $gdocs->download_file($url, $path); + return $gdocs->download_file($url, $path, self::GETFILE_TIMEOUT); } public function supported_filetypes() { From 7e3f70bed70b609e20962f942506b933b0ff464a Mon Sep 17 00:00:00 2001 From: Dan Poltawski <dan@moodle.com> Date: Tue, 14 Aug 2012 10:49:20 +0800 Subject: [PATCH 83/90] MDL-34290 using moodleform cast to int in dropbox repository settings --- .../dropbox/lang/en/repository_dropbox.php | 1 - repository/dropbox/lib.php | 18 ++---------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/repository/dropbox/lang/en/repository_dropbox.php b/repository/dropbox/lang/en/repository_dropbox.php index 4937c1303ff08..abf16c196857b 100644 --- a/repository/dropbox/lang/en/repository_dropbox.php +++ b/repository/dropbox/lang/en/repository_dropbox.php @@ -32,5 +32,4 @@ $string['instruction'] = 'You can get your API Key and secret from <a href="http://www.dropbox.com/developers/apps">Dropbox developers</a>. When setting up your key please select "Full Dropbox" as the "Access level".'; $string['cachelimit'] = 'Cache limit'; $string['cachelimit_info'] = 'Enter the maximum size of files (in bytes) to be cached on server for Dropbox aliases/shortcuts. Cached files will be served when the source is no longer available. Empty value or zero mean caching of all files regardless of size.'; -$string['error_cachelimit'] = 'Must be a positive integer or empty value'; $string['dropbox:view'] = 'View a Dropbox folder'; diff --git a/repository/dropbox/lib.php b/repository/dropbox/lib.php index c0edef34cb62d..a0af82fae2de2 100644 --- a/repository/dropbox/lib.php +++ b/repository/dropbox/lib.php @@ -464,25 +464,11 @@ public static function type_config_form($mform, $classname = 'repository') { $mform->addElement('static', null, '', $str_getkey); $mform->addElement('text', 'dropbox_cachelimit', get_string('cachelimit', 'repository_dropbox'), array('size' => '40')); + $mform->addRule('dropbox_cachelimit', null, 'numeric', null, 'client'); + $mform->setType('dropbox_cachelimit', PARAM_INT); $mform->addElement('static', 'dropbox_cachelimit_info', '', get_string('cachelimit_info', 'repository_dropbox')); } - /** - * Validate Admin Settings Moodle form - * - * @param moodleform $mform Moodle form (passed by reference) - * @param array $data array of ("fieldname"=>value) of submitted data - * @param array $errors array of ("fieldname"=>errormessage) of errors - * @return array array of errors - */ - public static function type_form_validation($mform, $data, $errors) { - if (!empty($data['dropbox_cachelimit']) && (!is_number($data['dropbox_cachelimit']) || - (int)$data['dropbox_cachelimit']<0)) { - $errors['dropbox_cachelimit'] = get_string('error_cachelimit', 'repository_dropbox'); - } - return $errors; - } - /** * Option names of dropbox plugin * From 63d8ccef8136926985779a87f17b79feae9646a6 Mon Sep 17 00:00:00 2001 From: Marina Glancy <marina@moodle.com> Date: Tue, 28 Aug 2012 12:03:43 +0800 Subject: [PATCH 84/90] MDL-34290 repository_filesystem: do not store files in moodle filepool unless images --- repository/filesystem/lib.php | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/repository/filesystem/lib.php b/repository/filesystem/lib.php index fc7cd9ef49e6e..432e79d4374d4 100644 --- a/repository/filesystem/lib.php +++ b/repository/filesystem/lib.php @@ -273,17 +273,13 @@ public function get_reference_details($reference, $filestatus = 0) { /** * Returns information about file in this repository by reference - * {@link repository::get_file_reference()} - * {@link repository::get_file()} * * Returns null if file not found or is not readable * * @param stdClass $reference file reference db record * @return stdClass|null contains one of the following: - * - 'contenthash' and 'filesize' - * - 'filepath' - * - 'handle' - * - 'content' + * - 'filesize' if file should not be copied to moodle filepool + * - 'filepath' if file should be copied to moodle filepool */ public function get_file_by_reference($reference) { $ref = $reference->reference; @@ -293,7 +289,16 @@ public function get_file_by_reference($reference) { $filepath = $this->root_path.$ref; } if (file_exists($filepath) && is_readable($filepath)) { - return (object)array('filepath' => $filepath); + if (file_extension_in_typegroup($filepath, 'web_image')) { + // return path to image files so it will be copied into moodle filepool + // we need the file in filepool to generate an image thumbnail + return (object)array('filepath' => $filepath); + } else { + // return just the file size so file will NOT be copied into moodle filepool + return (object)array( + 'filesize' => filesize($filepath) + ); + } } else { return null; } From 0878934f582f4caf5803647460a5d71d8164628b Mon Sep 17 00:00:00 2001 From: Marina Glancy <marina@moodle.com> Date: Wed, 29 Aug 2012 13:50:16 +0800 Subject: [PATCH 85/90] MDL-34310 Removed unused options from gradingform_guide --- .../form/guide/lang/en/gradingform_guide.php | 1 - grade/grading/form/guide/lib.php | 15 +++------------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/grade/grading/form/guide/lang/en/gradingform_guide.php b/grade/grading/form/guide/lang/en/gradingform_guide.php index 6ada6f6488d46..11c236ccfaced 100644 --- a/grade/grading/form/guide/lang/en/gradingform_guide.php +++ b/grade/grading/form/guide/lang/en/gradingform_guide.php @@ -76,7 +76,6 @@ $string['saveguide'] = 'Save marking guide and make it ready'; $string['saveguidedraft'] = 'Save as draft'; $string['score'] = 'score'; -$string['showdescriptionstudent'] = 'Display description to those being graded'; $string['showmarkerdesc'] = 'Show marker criterion descriptions'; $string['showmarkspercriterionstudents'] = 'Show marks per criterion to students'; $string['showstudentdesc'] = 'Show student criterion descriptions'; diff --git a/grade/grading/form/guide/lib.php b/grade/grading/form/guide/lib.php index 976b0c5fe4ada..15ef0d7b1de32 100644 --- a/grade/grading/form/guide/lib.php +++ b/grade/grading/form/guide/lib.php @@ -506,14 +506,7 @@ public function render_preview(moodle_page $page) { $comments = $this->definition->guide_comment; $options = $this->get_options(); $guide = ''; - if (has_capability('moodle/grade:managegradingforms', $page->context)) { - $showdescription = true; - } else { - $showdescription = $options['showdescriptionstudent']; - } - if ($showdescription) { - $guide .= $output->box($this->get_formatted_description(), 'gradingform_guide-description'); - } + $guide .= $output->box($this->get_formatted_description(), 'gradingform_guide-description'); if (has_capability('moodle/grade:managegradingforms', $page->context)) { $guide .= $output->display_guide_mapping_explained($this->get_min_max_score()); $guide .= $output->display_guide($criteria, $comments, $options, self::DISPLAY_PREVIEW, 'guide'); @@ -880,10 +873,8 @@ public function render_grading_element($page, $gradingformelement) { $html .= html_writer::tag('div', get_string('restoredfromdraft', 'gradingform_guide'), array('class' => 'gradingform_guide-restored')); } - if (!empty($options['showdescriptionteacher'])) { - $html .= html_writer::tag('div', $this->get_controller()->get_formatted_description(), - array('class' => 'gradingform_guide-description')); - } + $html .= html_writer::tag('div', $this->get_controller()->get_formatted_description(), + array('class' => 'gradingform_guide-description')); $html .= $this->get_controller()->get_renderer($page)->display_guide($criteria, $comments, $options, $mode, $gradingformelement->getName(), $value, $this->validationerrors); return $html; From 5854be258739cdca941994b209957df4553279b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= <commits@skodak.org> Date: Wed, 29 Aug 2012 09:57:32 +0200 Subject: [PATCH 86/90] MDL-34990 fix JS error when moodlenolink button not present in toolbar Thanks Rossiani Wijaya for discovering this issue! --- .../tinymce/plugins/moodlenolink/tinymce/editor_plugin.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/editor/tinymce/plugins/moodlenolink/tinymce/editor_plugin.js b/lib/editor/tinymce/plugins/moodlenolink/tinymce/editor_plugin.js index ba247d94bb8f5..e10fc808d7c6c 100644 --- a/lib/editor/tinymce/plugins/moodlenolink/tinymce/editor_plugin.js +++ b/lib/editor/tinymce/plugins/moodlenolink/tinymce/editor_plugin.js @@ -43,6 +43,10 @@ ed.onNodeChange.add(function(ed, cm, n) { var p, c; c = cm.get('moodlenolink'); + if (!c) { + // Button not used. + return; + } p = ed.dom.getParent(n, 'SPAN'); c.setActive(p && ed.dom.hasClass(p, 'nolink')); From 386a24d7945b900da314354a538b1b3155854bec Mon Sep 17 00:00:00 2001 From: David Monllao <davidm@moodle.com> Date: Wed, 29 Aug 2012 15:01:40 +0800 Subject: [PATCH 87/90] MDL-35119 tool_assignmentupgrade Changing onsubmit form for onclick button --- admin/tool/assignmentupgrade/module.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/tool/assignmentupgrade/module.js b/admin/tool/assignmentupgrade/module.js index 6370e706807ed..7c170f18a7f17 100644 --- a/admin/tool/assignmentupgrade/module.js +++ b/admin/tool/assignmentupgrade/module.js @@ -42,8 +42,8 @@ M.tool_assignmentupgrade = { } }); - var batchform = Y.one('.tool_assignmentupgrade_batchform form'); - batchform.on('submit', function(e) { + var upgradeselectedbutton = Y.one('#id_upgradeselected'); + upgradeselectedbutton.on('click', function(e) { checkboxes = Y.all('td.c0 input'); var selectedassignments = []; checkboxes.each(function(node) { From bba5e716283df27f3c956d654defd1eaa274c58f Mon Sep 17 00:00:00 2001 From: Tim Hunt <T.J.Hunt@open.ac.uk> Date: Thu, 30 Aug 2012 13:09:10 +0100 Subject: [PATCH 88/90] MDL-35147 lesson: fix regression from MDL-25492. I really wish someone would fix lesson to use the question bank. --- mod/lesson/format.php | 87 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/mod/lesson/format.php b/mod/lesson/format.php index 574bee16bbc19..4d8faf1543254 100644 --- a/mod/lesson/format.php +++ b/mod/lesson/format.php @@ -580,6 +580,93 @@ protected function format_question_text($question) { return html_to_text(format_text($question->questiontext, $question->questiontextformat, $formatoptions), 0, false); } + + /** + * Since the lesson module tries to re-use the question bank import classes in + * a crazy way, this is necessary to stop things breaking. + */ + protected function add_blank_combined_feedback($question) { + return $question; + } } +/** + * Since the lesson module tries to re-use the question bank import classes in + * a crazy way, this is necessary to stop things breaking. This should be exactly + * the same as the class defined in question/format.php. + */ +class qformat_based_on_xml extends qformat_default { + /** + * A lot of imported files contain unwanted entities. + * This method tries to clean up all known problems. + * @param string str string to correct + * @return string the corrected string + */ + public function cleaninput($str) { + + $html_code_list = array( + "'" => "'", + "’" => "'", + "“" => "\"", + "”" => "\"", + "–" => "-", + "—" => "-", + ); + $str = strtr($str, $html_code_list); + // Use textlib entities_to_utf8 function to convert only numerical entities. + $str = textlib::entities_to_utf8($str, false); + return $str; + } + + /** + * Return the array moodle is expecting + * for an HTML text. No processing is done on $text. + * qformat classes that want to process $text + * for instance to import external images files + * and recode urls in $text must overwrite this method. + * @param array $text some HTML text string + * @return array with keys text, format and files. + */ + public function text_field($text) { + return array( + 'text' => trim($text), + 'format' => FORMAT_HTML, + 'files' => array(), + ); + } + + /** + * Return the value of a node, given a path to the node + * if it doesn't exist return the default value. + * @param array xml data to read + * @param array path path to node expressed as array + * @param mixed default + * @param bool istext process as text + * @param string error if set value must exist, return false and issue message if not + * @return mixed value + */ + public function getpath($xml, $path, $default, $istext=false, $error='') { + foreach ($path as $index) { + if (!isset($xml[$index])) { + if (!empty($error)) { + $this->error($error); + return false; + } else { + return $default; + } + } + + $xml = $xml[$index]; + } + + if ($istext) { + if (!is_string($xml)) { + $this->error(get_string('invalidxml', 'qformat_xml')); + } + $xml = trim($xml); + } + + return $xml; + } +} From 1c3b1f7aee31ef897c08d16e1e4c094a69d727d5 Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" <stronk7@moodle.org> Date: Thu, 30 Aug 2012 22:58:52 +0200 Subject: [PATCH 89/90] MDL-35147 lesson qformat import: Dirty hack to support array questiontext structures. --- mod/lesson/format.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mod/lesson/format.php b/mod/lesson/format.php index 4d8faf1543254..95bb645efd935 100644 --- a/mod/lesson/format.php +++ b/mod/lesson/format.php @@ -384,6 +384,15 @@ function importprocess($filename, $lesson, $pageid) { $newpage->title = "Page $count"; } $newpage->contents = $question->questiontext; + $newpage->contentsformat = isset($question->questionformat) ? $question->questionformat : FORMAT_HTML; + + // Sometimes, questiontext is not a simple text, but one array + // containing both text and format, so we need to support here + // that case with the following dirty patch. MDL-35147 + if (is_array($question->questiontext)) { + $newpage->contents = isset($question->questiontext['text']) ? $question->questiontext['text'] : ''; + $newpage->contentsformat = isset($question->questiontext['format']) ? $question->questiontext['format'] : FORMAT_HTML; + } // set up page links if ($pageid) { From 569f1ad63b7a2bc31728582a102ae75f92799f9a Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" <stronk7@moodle.org> Date: Fri, 31 Aug 2012 11:36:43 +0200 Subject: [PATCH 90/90] weekly release 2.4dev --- version.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.php b/version.php index c4ad2bf4f0edb..45ef1b66fb142 100644 --- a/version.php +++ b/version.php @@ -30,11 +30,11 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2012082300.02; // YYYYMMDD = weekly release date of this DEV branch +$version = 2012083100.00; // YYYYMMDD = weekly release date of this DEV branch // RR = release increments - 00 in DEV branches // .XX = incremental changes -$release = '2.4dev (Build: 20120823)'; // Human-friendly version name +$release = '2.4dev (Build: 20120831)'; // Human-friendly version name $branch = '24'; // this version's branch $maturity = MATURITY_ALPHA; // this version's maturity level