Merge branch 'master_MDL-31360' of git://github.com/danmarsden/moodle
authorAparup Banerjee <aparup@moodle.com>
Thu, 2 Feb 2012 03:22:58 +0000 (11:22 +0800)
committerAparup Banerjee <aparup@moodle.com>
Thu, 2 Feb 2012 03:22:58 +0000 (11:22 +0800)
50 files changed:
admin/settings/subsystems.php
backup/converter/moodle1/handlerlib.php
backup/moodle2/restore_stepslib.php
blocks/navigation/block_navigation.php
course/modedit.php
lang/en/admin.php
lib/db/upgrade.php
lib/moodlelib.php
message/output/email/lang/en/message_email.php
message/output/email/message_output_email.php
mod/choice/report.php
mod/feedback/lib.php
mod/feedback/show_entries.php
mod/forum/discuss.php
mod/forum/index.php
mod/forum/subscribe.php
mod/quiz/backup/moodle2/backup_quiz_stepslib.php
mod/quiz/mod_form.php
mod/quiz/module.js
mod/resource/lib.php
mod/resource/locallib.php
mod/resource/view.php
mod/scorm/lib.php
mod/scorm/mod_form.php
mod/url/lib.php
mod/url/locallib.php
mod/url/view.php
question/engine/datalib.php
question/engine/questionattempt.php
question/engine/questionattemptstep.php
question/engine/questionusage.php
question/engine/simpletest/testquestionattempt.php
question/engine/simpletest/testquestionusagebyactivity.php
question/engine/simpletest/testunitofwork.php [new file with mode: 0644]
question/engine/upgrade/upgradelib.php
question/type/calculated/datasetdefinitions_form.php
question/type/calculated/datasetitems_form.php
question/type/calculated/questiontype.php
question/type/calculatedsimple/edit_calculatedsimple_form.php
question/type/calculatedsimple/questiontype.php
question/type/edit_question_form.php
question/type/missingtype/questiontype.php
question/type/random/edit_random_form.php
question/type/random/questiontype.php
repository/filepicker.js
repository/lib.php
theme/afterburner/layout/default.php
theme/afterburner/style/afterburner_styles.css
theme/mymobile/style/core.css
version.php

index 00ffb21..7c70af3 100644 (file)
@@ -20,6 +20,8 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $options = array(DAYSECS=>new lang_string('secondstotime86400'), WEEKSECS=>new lang_string('secondstotime604800'), 2620800=>new lang_string('nummonths', 'moodle', 1), 15724800=>new lang_string('nummonths', 'moodle', 6),0=>new lang_string('never'));
     $optionalsubsystems->add(new admin_setting_configselect('messagingdeletereadnotificationsdelay', new lang_string('messagingdeletereadnotificationsdelay', 'admin'), new lang_string('configmessagingdeletereadnotificationsdelay', 'admin'), 604800, $options));
 
+    $optionalsubsystems->add(new admin_setting_configcheckbox('messagingallowemailoverride', new lang_string('messagingallowemailoverride', 'admin'), new lang_string('configmessagingallowemailoverride','admin'), 0));
+
     $optionalsubsystems->add(new admin_setting_configcheckbox('enablestats', new lang_string('enablestats', 'admin'), new lang_string('configenablestats', 'admin'), 0));
 
     $optionalsubsystems->add(new admin_setting_configcheckbox('enablerssfeeds', new lang_string('enablerssfeeds', 'admin'), new lang_string('configenablerssfeeds', 'admin'), 0));
@@ -45,4 +47,4 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $optionalsubsystems->add(new admin_setting_configcheckbox('enableplagiarism', new lang_string('enableplagiarism','plagiarism'), new lang_string('configenableplagiarism','plagiarism'), 0));
 
     $optionalsubsystems->add(new admin_setting_configcheckbox('enablecssoptimiser', new lang_string('enablecssoptimiser','admin'), new lang_string('enablecssoptimiser_desc','admin'), 0));
-}
\ No newline at end of file
+}
index a6b7788..f0c9754 100644 (file)
@@ -345,6 +345,8 @@ class moodle1_root_handler extends moodle1_xml_handler {
         // {@see backup_general_helper::backup_is_samesite()}
         if (isset($backupinfo['original_site_identifier_hash'])) {
             $this->xmlwriter->full_tag('original_site_identifier_hash', $backupinfo['original_site_identifier_hash']);
+        } else {
+            $this->xmlwriter->full_tag('original_site_identifier_hash', null);
         }
         $this->xmlwriter->full_tag('original_course_id', $originalcourseinfo['original_course_id']);
         $this->xmlwriter->full_tag('original_course_fullname', $originalcourseinfo['original_course_fullname']);
index 14d130a..2aebdcf 100644 (file)
@@ -371,40 +371,35 @@ class restore_gradebook_structure_step extends restore_structure_step {
         }
         $rs->close();
 
-        //need to correct the grade category path and parent
+        // Need to correct the grade category path and parent
         $conditions = array(
             'courseid' => $this->get_courseid()
         );
-        $grade_category = new stdclass();
 
         $rs = $DB->get_recordset('grade_categories', $conditions);
-        if (!empty($rs)) {
-            //get all the parents correct first as grade_category::build_path() loads category parents from the DB
-            foreach($rs as $gc) {
-                if (!empty($gc->parent)) {
-                    $grade_category->id = $gc->id;
-                    $grade_category->parent = $this->get_mappingid('grade_category', $gc->parent);
-                    $DB->update_record('grade_categories', $grade_category);
-                }
+        // Get all the parents correct first as grade_category::build_path() loads category parents from the DB
+        foreach ($rs as $gc) {
+            if (!empty($gc->parent)) {
+                $grade_category = new stdClass();
+                $grade_category->id = $gc->id;
+                $grade_category->parent = $this->get_mappingid('grade_category', $gc->parent);
+                $DB->update_record('grade_categories', $grade_category);
             }
         }
-        if (isset($grade_category->parent)) {
-            unset($grade_category->parent);
-        }
         $rs->close();
 
+        // Now we can rebuild all the paths
         $rs = $DB->get_recordset('grade_categories', $conditions);
-        if (!empty($rs)) {
-            //now we can rebuild all the paths
-            foreach($rs as $gc) {
-                $grade_category->id = $gc->id;
-                $grade_category->path = grade_category::build_path($gc);
-                $DB->update_record('grade_categories', $grade_category);
-            }
+        foreach ($rs as $gc) {
+            $grade_category = new stdClass();
+            $grade_category->id = $gc->id;
+            $grade_category->path = grade_category::build_path($gc);
+            $grade_category->depth = substr_count($grade_category->path, '/') - 1;
+            $DB->update_record('grade_categories', $grade_category);
         }
         $rs->close();
 
-        //Restore marks items as needing update. Update everything now.
+        // Restore marks items as needing update. Update everything now.
         grade_regrade_final_grades($this->get_courseid());
     }
 }
index 00f06cf..8bfa9b7 100644 (file)
@@ -283,7 +283,7 @@ class block_navigation extends block_base {
      * @return string The truncated string
      */
     protected function trim_left($textlib, $string, $length) {
-        return '...'.$textlib->substr($string, $textlib->strlen($string)-$length);
+        return '...'.$textlib->substr($string, $textlib->strlen($string)-$length, $length);
     }
     /**
      * Truncate a string from the right
index 5865e4a..9ed3f5d 100644 (file)
@@ -316,6 +316,9 @@ if ($mform->is_cancelled()) {
         $fromform->completiongradeitemnumber = null;
     }
 
+    // the type of event to trigger (mod_created/mod_updated)
+    $eventname = '';
+
     if (!empty($fromform->update)) {
 
         if (!empty($course->groupmodeforce) or !isset($fromform->groupmode)) {
@@ -380,14 +383,7 @@ if ($mform->is_cancelled()) {
             $completion->reset_all_state($cm);
         }
 
-        // Trigger mod_updated event with information about this module.
-        $eventdata = new stdClass();
-        $eventdata->modulename = $fromform->modulename;
-        $eventdata->name       = $fromform->name;
-        $eventdata->cmid       = $fromform->coursemodule;
-        $eventdata->courseid   = $course->id;
-        $eventdata->userid     = $USER->id;
-        events_trigger('mod_updated', $eventdata);
+        $eventname = 'mod_updated';
 
         add_to_log($course->id, "course", "update mod",
                    "../mod/$fromform->modulename/view.php?id=$fromform->coursemodule",
@@ -492,14 +488,7 @@ if ($mform->is_cancelled()) {
             condition_info::update_cm_from_form((object)array('id'=>$fromform->coursemodule), $fromform, false);
         }
 
-        // Trigger mod_created event with information about this module.
-        $eventdata = new stdClass();
-        $eventdata->modulename = $fromform->modulename;
-        $eventdata->name       = $fromform->name;
-        $eventdata->cmid       = $fromform->coursemodule;
-        $eventdata->courseid   = $course->id;
-        $eventdata->userid     = $USER->id;
-        events_trigger('mod_created', $eventdata);
+        $eventname = 'mod_created';
 
         add_to_log($course->id, "course", "add mod",
                    "../mod/$fromform->modulename/view.php?id=$fromform->coursemodule",
@@ -511,6 +500,15 @@ if ($mform->is_cancelled()) {
         print_error('invaliddata');
     }
 
+    // Trigger mod_created/mod_updated event with information about this module.
+    $eventdata = new stdClass();
+    $eventdata->modulename = $fromform->modulename;
+    $eventdata->name       = $fromform->name;
+    $eventdata->cmid       = $fromform->coursemodule;
+    $eventdata->courseid   = $course->id;
+    $eventdata->userid     = $USER->id;
+    events_trigger($eventname, $eventdata);
+
     // sync idnumber with grade_item
     if ($grade_item = grade_item::fetch(array('itemtype'=>'mod', 'itemmodule'=>$fromform->modulename,
                  'iteminstance'=>$fromform->instance, 'itemnumber'=>0, 'courseid'=>$course->id))) {
index 153dcb7..dbf6a26 100644 (file)
@@ -242,6 +242,7 @@ $string['configmaxevents'] = 'Events to Lookahead';
 $string['configmemcachedhosts'] = 'For memcached. Comma-separated list of hosts that are running the memcached daemon. Use IP addresses to avoid DNS latency. memcached does not behave well if you add/remove hosts on a running setup.';
 $string['configmemcachedpconn'] = 'For memcached. Use persistent connections. Use carefully -- it can make Apache/PHP crash after a restart of the memcached daemon.';
 $string['configmessaging'] = 'Should the messaging system between site users be enabled?';
+$string['configmessagingallowemailoverride'] = 'Allow users to have email message notifications sent to an email address other than the email address in their profile';
 $string['configmessaginghidereadnotifications'] = 'Hide read notifications of events like forum posts when viewing messaging history';
 $string['configmessagingdeletereadnotificationsdelay'] = 'Read notifications can be deleted to save space. How long after a notification is read can it be deleted?';
 $string['configminpassworddigits'] = 'Passwords must have at least these many digits.';
@@ -662,6 +663,7 @@ $string['mediapluginyoutube'] = 'Enable YouTube links filter';
 $string['memcachedhosts'] = 'memcached hosts';
 $string['memcachedpconn'] = 'memcached use persistent connections';
 $string['messaging'] = 'Enable messaging system';
+$string['messagingallowemailoverride'] = 'Notification email override';
 $string['messaginghidereadnotifications'] = 'Hide read notifications';
 $string['messagingdeletereadnotificationsdelay'] = 'Delete read notifications';
 $string['minpassworddigits'] = 'Digits';
index ca60e07..2c28ba3 100644 (file)
@@ -135,6 +135,16 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2012020200.03);
     }
 
+    if ($oldversion < 2012020200.06) {
+        // Previously we always allowed users to override their email address via the messaging system
+        // We have now added a setting to allow admins to turn this this ability on and off
+        // While this setting defaults to 0 (off) we're setting it to 1 (on) to maintain the behaviour for upgrading sites
+        set_config('messagingallowemailoverride', 1);
+
+        // Main savepoint reached
+        upgrade_main_savepoint(true, 2012020200.06);
+    }
+
     return true;
 }
 
index 52d2e5d..e4062fe 100644 (file)
@@ -2775,6 +2775,10 @@ function require_login($courseorid = NULL, $autologinguest = true, $cm = NULL, $
                 if ($preventredirect) {
                     throw new require_login_exception('Course is hidden');
                 }
+                // We need to override the navigation URL as the course won't have
+                // been added to the navigation and thus the navigation will mess up
+                // when trying to find it.
+                navigation_node::override_active_url(new moodle_url('/'));
                 notice(get_string('coursehidden'), $CFG->wwwroot .'/');
             }
         }
index ae06d11..2cc5b40 100644 (file)
@@ -32,6 +32,7 @@ $string['configsmtphosts'] = 'Give the full name of one or more local SMTP serve
 $string['configsmtpmaxbulk'] = 'Maximum number of messages sent per SMTP session. Grouping messages may speed up the sending of emails. Values lower than 2 force creation of new SMTP session for each email.';
 $string['configsmtpuser'] = 'If you have specified an SMTP server above, and the server requires authentication, then enter the username and password here.';
 $string['email'] = 'Send email notifications to';
+$string['ifemailleftempty'] = 'Leave empty to send notifications to {$a}';
 $string['mailnewline'] = 'Newline characters in mail';
 $string['noreplyaddress'] = 'No-reply address';
 $string['pluginname'] = 'Email';
index 80262db..0e55902 100644 (file)
@@ -57,7 +57,10 @@ class message_output_email extends message_output {
         //check if the recipient has a different email address specified in their messaging preferences Vs their user profile
         $emailmessagingpreference = get_user_preferences('message_processor_email_email', null, $eventdata->userto);
         $emailmessagingpreference = clean_param($emailmessagingpreference, PARAM_EMAIL);
-        if (!empty($emailmessagingpreference)) {
+
+        // If the recipient has set an email address in their preferences use that instead of the one in their profile
+        // but only if overriding the notification email address is allowed
+        if (!empty($emailmessagingpreference) && !empty($CFG->messagingallowemailoverride)) {
             //clone to avoid altering the actual user object
             $recipient = clone($eventdata->userto);
             $recipient->email = $emailmessagingpreference;
@@ -74,13 +77,17 @@ class message_output_email extends message_output {
      * @param object $mform preferences form class
      */
     function config_form($preferences){
-        global $USER, $OUTPUT;
+        global $USER, $OUTPUT, $CFG;
+
+        if (empty($CFG->messagingallowemailoverride)) {
+            return null;
+        }
 
         $inputattributes = array('size'=>'30', 'name'=>'email_email', 'value'=>$preferences->email_email);
         $string = get_string('email','message_email') . ': ' . html_writer::empty_tag('input', $inputattributes);
 
         if (empty($preferences->email_email) && !empty($preferences->userdefaultemail)) {
-            $string .= ' ('.get_string('default').': '.s($preferences->userdefaultemail).')';
+            $string .= get_string('ifemailleftempty', 'message_email', $preferences->userdefaultemail);
         }
 
         if (!empty($preferences->email_email) && !validate_email($preferences->email_email)) {
index b9ef145..1497376 100644 (file)
         }
         exit;
     }
+    // Show those who haven't answered the question.
+    if (!empty($choice->showunanswered)) {
+        $choice->option[0] = get_string('notanswered', 'choice');
+        $choice->maxanswers[0] = 0;
+    }
 
     $results = prepare_choice_show_results($choice, $course, $cm, $users);
     $renderer = $PAGE->get_renderer('mod_choice');
index 10c033e..2fc64ce 100644 (file)
@@ -958,7 +958,7 @@ function feedback_get_complete_users($cm,
     }
 
     $ufields = user_picture::fields('u');
-    $sql = 'SELECT DISTINCT '.$ufields.'
+    $sql = 'SELECT DISTINCT '.$ufields.', c.timemodified as completed_timemodified
             FROM {user} u, {feedback_completed} c '.$fromgroup.'
             WHERE '.$where.' anonymous_response = :anon
                 AND u.id = c.userid
index 945a23a..00c3d0b 100644 (file)
@@ -122,7 +122,7 @@ if ($do_show == 'showentries') {
         $baseurl = new moodle_url('/mod/feedback/show_entries.php');
         $baseurl->params(array('id'=>$id, 'do_show'=>$do_show, 'showall'=>$showall));
 
-        $tablecolumns = array('userpic', 'fullname', 'c.timemodified');
+        $tablecolumns = array('userpic', 'fullname', 'completed_timemodified');
         $tableheaders = array(get_string('userpic'), get_string('fullnameuser'), get_string('date'));
 
         if (has_capability('mod/feedback:deletesubmissions', $context)) {
index c4c5316..382b7be 100644 (file)
         redirect($return.'&moved=-1&sesskey='.sesskey());
     }
 
-    add_to_log($course->id, 'forum', 'view discussion', $PAGE->url->out(false), $discussion->id, $cm->id);
+    add_to_log($course->id, 'forum', 'view discussion', "discuss.php?d=$discussion->id", $discussion->id, $cm->id);
 
     unset($SESSION->fromdiscussion);
 
index 0ec460b..c15b40b 100644 (file)
@@ -149,8 +149,13 @@ foreach ($modinfo->instances['forum'] as $forumid=>$cm) {
     }
 }
 
-/// Do course wide subscribe/unsubscribe
-if (!is_null($subscribe) and !isguestuser()) {
+// Do course wide subscribe/unsubscribe if requested
+if (!is_null($subscribe)) {
+    if (isguestuser() or !$can_subscribe) {
+        // there should not be any links leading to this place, just redirect
+        redirect(new moodle_url('/mod/forum/index.php', array('id' => $id)), get_string('subscribeenrolledonly', 'forum'));
+    }
+    // Can proceed now, the user is not guest and is enrolled
     foreach ($modinfo->instances['forum'] as $forumid=>$cm) {
         $forum = $forums[$forumid];
         $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
@@ -417,7 +422,8 @@ $PAGE->set_heading($course->fullname);
 $PAGE->set_button($searchform);
 echo $OUTPUT->header();
 
-if (!isguestuser() && isloggedin()) {
+// Show the subscribe all options only to non-guest, enrolled users
+if (!isguestuser() && isloggedin() && $can_subscribe) {
     echo $OUTPUT->box_start('subscription');
     echo html_writer::tag('div',
         html_writer::link(new moodle_url('/mod/forum/index.php', array('id'=>$course->id, 'subscribe'=>1, 'sesskey'=>sesskey())),
index 6cdc2f1..f722456 100644 (file)
@@ -127,6 +127,7 @@ if (forum_is_forcesubscribed($forum)) {
     redirect($returnto, get_string("everyoneisnowsubscribed", "forum"), 1);
 }
 
+$info = new stdClass();
 $info->name  = fullname($user);
 $info->forum = format_string($forum->name);
 
index a8c9e54..bcf7d20 100644 (file)
@@ -81,7 +81,7 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru
         $attempt = new backup_nested_element('attempt', array('id'), array(
             'uniqueid', 'userid', 'attemptnum', 'sumgrades',
             'timestart', 'timefinish', 'timemodified', 'layout',
-            'preview'));
+            'preview', 'currentpage'));
 
         // This module is using questions, so produce the related question states and sessions
         // attaching them to the $attempt element based in 'uniqueid' matching
index 92178f6..e207ad0 100644 (file)
@@ -251,7 +251,7 @@ class mod_quiz_mod_form extends moodleform_mod {
         //-------------------------------------------------------------------------------
         $mform->addElement('header', 'security', get_string('extraattemptrestrictions', 'quiz'));
 
-        // Enforced time delay between quiz attempts.
+        // Require password to begin quiz attempt.
         $mform->addElement('passwordunmask', 'quizpassword', get_string('requirepassword', 'quiz'));
         $mform->setType('quizpassword', PARAM_TEXT);
         $mform->addHelpButton('quizpassword', 'requirepassword', 'quiz');
index d9d217f..63c44d6 100644 (file)
@@ -186,11 +186,11 @@ M.mod_quiz.secure_window = {
         if (window.location.href.substring(0, 4) == 'file') {
             window.location = 'about:blank';
         }
-        Y.delegate('contextmenu', M.mod_quiz.secure_window.prevent, document.body, '*');
-        Y.delegate('mousedown', M.mod_quiz.secure_window.prevent_mouse, document.body, '*');
-        Y.delegate('mouseup', M.mod_quiz.secure_window.prevent_mouse, document.body, '*');
-        Y.delegate('dragstart', M.mod_quiz.secure_window.prevent, document.body, '*');
-        Y.delegate('selectstart', M.mod_quiz.secure_window.prevent, document.body, '*');
+        Y.delegate('contextmenu', M.mod_quiz.secure_window.prevent, document, '*');
+        Y.delegate('mousedown', M.mod_quiz.secure_window.prevent_mouse, document, '*');
+        Y.delegate('mouseup', M.mod_quiz.secure_window.prevent_mouse, document, '*');
+        Y.delegate('dragstart', M.mod_quiz.secure_window.prevent, document, '*');
+        Y.delegate('selectstart', M.mod_quiz.secure_window.prevent, document, '*');
         M.mod_quiz.secure_window.clear_status;
         Y.on('beforeprint', function() {
             Y.one(document.body).setStyle('display', 'none');
index d73697b..f5c80ca 100644 (file)
@@ -285,30 +285,6 @@ function resource_get_coursemodule_info($coursemodule) {
         $fullurl = "$CFG->wwwroot/mod/resource/view.php?id=$coursemodule->id&amp;redirect=1";
         $info->onclick = "window.open('$fullurl'); return false;";
 
-    } else if ($display == RESOURCELIB_DISPLAY_OPEN) {
-        $fullurl = "$CFG->wwwroot/mod/resource/view.php?id=$coursemodule->id&amp;redirect=1";
-        $info->onclick = "window.location.href ='$fullurl';return false;";
-
-    } else if ($display == RESOURCELIB_DISPLAY_DOWNLOAD) {
-        if (empty($mainfile)) {
-            return NULL;
-        }
-        // do not open any window because it would be left there after download
-        $path = '/'.$context->id.'/mod_resource/content/'.$resource->revision.$mainfile->get_filepath().$mainfile->get_filename();
-        $fullurl = addslashes_js(file_encode_url($CFG->wwwroot.'/pluginfile.php', $path, true));
-
-        // When completion information is enabled for download files, make
-        // the JavaScript version go to the view page with redirect set,
-        // instead of directly to the file, otherwise we can't make it tick
-        // the box for them
-        if (!$course = $DB->get_record('course', array('id'=>$coursemodule->course), 'id, enablecompletion')) {
-            return NULL;
-        }
-        $completion = new completion_info($course);
-        if ($completion->is_enabled($coursemodule) == COMPLETION_TRACKING_AUTOMATIC) {
-            $fullurl = "$CFG->wwwroot/mod/resource/view.php?id=$coursemodule->id&amp;redirect=1";
-        }
-        $info->onclick = "window.open('$fullurl'); return false;";
     }
 
     // If any optional extra details are turned on, store in custom data
index dcd8dfe..df2b0b6 100644 (file)
@@ -155,6 +155,7 @@ function resource_display_frame($resource, $cm, $course, $file) {
         $navurl = "$CFG->wwwroot/mod/resource/view.php?id=$cm->id&amp;frameset=top";
         $title = strip_tags(format_string($course->shortname.': '.$resource->name));
         $framesize = $config->framesize;
+        $contentframetitle = format_string($resource->name);
         $modulename = s(get_string('modulename','resource'));
         $dir = get_string('thisdirection', 'langconfig');
 
@@ -167,7 +168,7 @@ function resource_display_frame($resource, $cm, $course, $file) {
   </head>
   <frameset rows="$framesize,*">
     <frame src="$navurl" title="$modulename" />
-    <frame src="$fileurl" title="$modulename" />
+    <frame src="$fileurl" title="$contentframetitle" />
   </frameset>
 </html>
 EOF;
index ee3c694..7901843 100644 (file)
@@ -76,16 +76,26 @@ if (count($files) < 1) {
     unset($files);
 }
 
+$resource->mainfile = $file->get_filename();
+$displaytype = resource_get_final_display_type($resource);
+if ($displaytype == RESOURCELIB_DISPLAY_OPEN || $displaytype == RESOURCELIB_DISPLAY_DOWNLOAD) {
+    // For 'open' and 'download' links, we always redirect to the content - except
+    // if the user just chose 'save and display' from the form then that would be
+    // confusing
+    if (!isset($_SERVER['HTTP_REFERER']) || strpos($_SERVER['HTTP_REFERER'], 'modedit.php') === false) {
+        $redirect = true;
+    }
+}
+
 if ($redirect) {
     // coming from course page or url index page
     // this redirect trick solves caching problems when tracking views ;-)
     $path = '/'.$context->id.'/mod_resource/content/'.$resource->revision.$file->get_filepath().$file->get_filename();
-    $fullurl = file_encode_url($CFG->wwwroot.'/pluginfile.php', $path, false);
+    $fullurl = moodle_url::make_file_url('/pluginfile.php', $path, $displaytype == RESOURCELIB_DISPLAY_DOWNLOAD);
     redirect($fullurl);
 }
 
-$resource->mainfile = $file->get_filename();
-switch (resource_get_final_display_type($resource)) {
+switch ($displaytype) {
     case RESOURCELIB_DISPLAY_EMBED:
         resource_display_embed($resource, $cm, $course, $file);
         break;
index 50906fb..6074888 100644 (file)
@@ -185,7 +185,8 @@ function scorm_update_instance($scorm, $mform=null) {
 
     } else if ($scorm->scormtype === SCORM_TYPE_IMSREPOSITORY) {
         $scorm->reference = $scorm->packageurl;
-
+    } else if ($scorm->scormtype === SCORM_TYPE_AICCURL) {
+        $scorm->reference = $scorm->packageurl;
     } else {
         return false;
     }
index 1f17c00..088634c 100644 (file)
@@ -397,6 +397,7 @@ class mod_scorm_mod_form extends moodleform_mod {
                 case SCORM_TYPE_LOCALSYNC :
                 case SCORM_TYPE_EXTERNAL:
                 case SCORM_TYPE_IMSREPOSITORY:
+                case SCORM_TYPE_AICCURL:
                     $default_values['packageurl'] = $default_values['reference'];
             }
         }
index f8400e2..0e40ab1 100644 (file)
@@ -286,9 +286,6 @@ function url_get_coursemodule_info($coursemodule) {
         $fullurl = "$CFG->wwwroot/mod/url/view.php?id=$coursemodule->id&amp;redirect=1";
         $info->onclick = "window.open('$fullurl'); return false;";
 
-    } else if ($display == RESOURCELIB_DISPLAY_OPEN) {
-        $fullurl = "$CFG->wwwroot/mod/url/view.php?id=$coursemodule->id&amp;redirect=1";
-        $info->onclick = "window.location.href ='$fullurl';return false;";
     }
 
     if ($coursemodule->showdescription) {
index 6f02035..70e7efc 100644 (file)
@@ -228,6 +228,7 @@ function url_display_frame($url, $cm, $course) {
         $title = strip_tags($courseshortname.': '.format_string($url->name));
         $framesize = $config->framesize;
         $modulename = s(get_string('modulename','url'));
+        $contentframetitle = format_string($url->name);
         $dir = get_string('thisdirection', 'langconfig');
 
         $extframe = <<<EOF
@@ -239,7 +240,7 @@ function url_display_frame($url, $cm, $course) {
   </head>
   <frameset rows="$framesize,*">
     <frame src="$navurl" title="$modulename"/>
-    <frame src="$exteurl" title="$modulename"/>
+    <frame src="$exteurl" title="$contentframetitle"/>
   </frameset>
 </html>
 EOF;
index 6840abd..6cb73ab 100644 (file)
@@ -67,6 +67,15 @@ if (empty($exturl) or $exturl === 'http://') {
 }
 unset($exturl);
 
+$displaytype = url_get_final_display_type($url);
+if ($displaytype == RESOURCELIB_DISPLAY_OPEN) {
+    // For 'open' links, we always redirect to the content - except if the user
+    // just chose 'save and display' from the form then that would be confusing
+    if (!isset($_SERVER['HTTP_REFERER']) || strpos($_SERVER['HTTP_REFERER'], 'modedit.php') === false) {
+        $redirect = true;
+    }
+}
+
 if ($redirect) {
     // coming from course page or url index page,
     // the redirection is needed for completion tracking and logging
@@ -74,7 +83,7 @@ if ($redirect) {
     redirect(str_replace('&amp;', '&', $fullurl));
 }
 
-switch (url_get_final_display_type($url)) {
+switch ($displaytype) {
     case RESOURCELIB_DISPLAY_EMBED:
         url_display_embed($url, $cm, $course);
         break;
index 751a90b..0c0f2eb 100644 (file)
 /**
  * Code for loading and saving question attempts to and from the database.
  *
+ * A note for future reference. This code is pretty efficient but there are two
+ * potential optimisations that could be contemplated, at the cost of making the
+ * code more complex:
+ *
+ * 1. (This is the easier one, but probably not worth doing.) In the unit-of-work
+ *    save method, we could get all the ids for steps due to be deleted or modified,
+ *    and delete all the question_attempt_step_data for all of those steps in one
+ *    query. That would save one DB query for each ->stepsupdated. However that number
+ *    is 0 except when re-grading, and when regrading, there are many more inserts
+ *    into question_attempt_step_data than deletes, so it is really hardly worth it.
+ *
+ * 2. A more significant optimisation would be to write an efficient
+ *    $DB->insert_records($arrayofrecords) method (for example using functions
+ *    like pg_copy_from) and then whenever we save stuff (unit_of_work->save and
+ *    insert_questions_usage_by_activity) collect together all the records that
+ *    need to be inserted into question_attempt_step_data, and insert them with
+ *    a single call to $DB->insert_records. This is likely to be the biggest win.
+ *    We do a lot of separate inserts into question_attempt_step_data.
+ *
  * @package    moodlecore
  * @subpackage questionengine
  * @copyright  2009 The Open University
@@ -76,7 +95,7 @@ class question_engine_data_mapper {
      * Store an entire {@link question_attempt} in the database,
      * including all the question_attempt_steps that comprise it.
      * @param question_attempt $qa the question attempt to store.
-     * @param object $context the context of the owning question_usage_by_activity.
+     * @param context $context the context of the owning question_usage_by_activity.
      */
     public function insert_question_attempt(question_attempt $qa, $context) {
         $record = new stdClass();
@@ -105,14 +124,13 @@ class question_engine_data_mapper {
     }
 
     /**
-     * Store a {@link question_attempt_step} in the database.
-     * @param question_attempt_step $qa the step to store.
+     * Helper method used by insert_question_attempt_step and update_question_attempt_step
+     * @param question_attempt_step $step the step to store.
      * @param int $questionattemptid the question attept id this step belongs to.
      * @param int $seq the sequence number of this stop.
-     * @param object $context the context of the owning question_usage_by_activity.
+     * @return stdClass data to insert into the database.
      */
-    public function insert_question_attempt_step(question_attempt_step $step,
-            $questionattemptid, $seq, $context) {
+    protected function make_step_record(question_attempt_step $step, $questionattemptid, $seq) {
         $record = new stdClass();
         $record->questionattemptid = $questionattemptid;
         $record->sequencenumber = $seq;
@@ -120,22 +138,64 @@ class question_engine_data_mapper {
         $record->fraction = $step->get_fraction();
         $record->timecreated = $step->get_timecreated();
         $record->userid = $step->get_user_id();
+        return $record;
+    }
 
-        $record->id = $this->db->insert_record('question_attempt_steps', $record);
-
+    /**
+     * Helper method used by insert_question_attempt_step and update_question_attempt_step
+     * @param question_attempt_step $step the step to store.
+     * @param int $stepid the id of the step.
+     * @param context $context the context of the owning question_usage_by_activity.
+     */
+    protected function insert_step_data(question_attempt_step $step, $stepid, $context) {
         foreach ($step->get_all_data() as $name => $value) {
             if ($value instanceof question_file_saver) {
-                $value->save_files($record->id, $context);
+                $value->save_files($stepid, $context);
             }
 
             $data = new stdClass();
-            $data->attemptstepid = $record->id;
+            $data->attemptstepid = $stepid;
             $data->name = $name;
             $data->value = $value;
             $this->db->insert_record('question_attempt_step_data', $data, false);
         }
     }
 
+    /**
+     * Store a {@link question_attempt_step} in the database.
+     * @param question_attempt_step $step the step to store.
+     * @param int $questionattemptid the question attept id this step belongs to.
+     * @param int $seq the sequence number of this stop.
+     * @param context $context the context of the owning question_usage_by_activity.
+     */
+    public function insert_question_attempt_step(question_attempt_step $step,
+            $questionattemptid, $seq, $context) {
+
+        $record = $this->make_step_record($step, $questionattemptid, $seq);
+        $record->id = $this->db->insert_record('question_attempt_steps', $record);
+
+        $this->insert_step_data($step, $record->id, $context);
+    }
+
+    /**
+     * Update a {@link question_attempt_step} in the database.
+     * @param question_attempt_step $qa the step to store.
+     * @param int $questionattemptid the question attept id this step belongs to.
+     * @param int $seq the sequence number of this stop.
+     * @param context $context the context of the owning question_usage_by_activity.
+     */
+    public function update_question_attempt_step(question_attempt_step $step,
+            $questionattemptid, $seq, $context) {
+
+        $record = $this->make_step_record($step, $questionattemptid, $seq);
+        $record->id = $step->get_id();
+        $this->db->update_record('question_attempt_steps', $record);
+
+        $this->db->delete_records('question_attempt_step_data',
+                array('attemptstepid' => $record->id));
+        $this->insert_step_data($step, $record->id, $context);
+    }
+
     /**
      * Load a {@link question_attempt_step} from the database.
      * @param int $stepid the id of the step to load.
@@ -727,29 +787,27 @@ ORDER BY
     /**
      * Delete all the steps for a question attempt.
      * @param int $qaids question_attempt id.
+     * @param context $context the context that the $quba belongs to.
      */
-    public function delete_steps_for_question_attempts($qaids, $context) {
-        if (empty($qaids)) {
+    public function delete_steps($stepids, $context) {
+        if (empty($stepids)) {
             return;
         }
-        list($test, $params) = $this->db->get_in_or_equal($qaids, SQL_PARAMS_NAMED);
+        list($test, $params) = $this->db->get_in_or_equal($stepids, SQL_PARAMS_NAMED);
 
-        $this->delete_response_files($context->id, "IN (
-                SELECT id
-                FROM {question_attempt_steps}
-                WHERE questionattemptid $test)", $params);
+        if ($deletefiles) {
+            $this->delete_response_files($context->id, $test, $params);
+        }
 
         if ($this->db->get_dbfamily() == 'mysql') {
             $this->delete_attempt_steps_for_mysql($test, $params);
             return;
         }
 
-        $this->db->delete_records_select('question_attempt_step_data', "attemptstepid IN (
-                SELECT qas.id
-                FROM {question_attempt_steps} qas
-                WHERE questionattemptid $test)", $params);
+        $this->db->delete_records_select('question_attempt_step_data',
+                "attemptstepid $test", $params);
         $this->db->delete_records_select('question_attempt_steps',
-                'questionattemptid ' . $test, $params);
+                "attemptstepid $test", $params);
     }
 
     /**
@@ -953,28 +1011,34 @@ class question_engine_unit_of_work implements question_usage_observer {
     protected $modified = false;
 
     /**
-     * @var array list of number in usage => {@link question_attempt}s that
+     * @var array list of slot => {@link question_attempt}s that
      * were already in the usage, and which have been modified.
      */
     protected $attemptsmodified = array();
 
     /**
-     * @var array list of number in usage => {@link question_attempt}s that
+     * @var array list of slot => {@link question_attempt}s that
      * have been added to the usage.
      */
     protected $attemptsadded = array();
 
     /**
-     * @var array list of question attempt ids to delete the steps for, before
-     * inserting new steps.
+     * @var array of array(question_attempt_step, question_attempt id, seq number)
+     * of steps that have been added to question attempts in this usage.
      */
-    protected $attemptstodeletestepsfor = array();
+    protected $stepsadded = array();
 
     /**
-     * @var array list of array(question_attempt_step, question_attempt id, seq number)
-     * of steps that have been added to question attempts in this usage.
+     * @var array of array(question_attempt_step, question_attempt id, seq number)
+     * of steps that have been modified in their attempt.
      */
-    protected $stepsadded = array();
+    protected $stepsmodified = array();
+
+    /**
+     * @var array list of question_attempt_step.id => question_attempt_step of steps
+     * that were previously stored in the database, but which are no longer required.
+     */
+    protected $stepsdeleted = array();
 
     /**
      * Constructor.
@@ -989,9 +1053,9 @@ class question_engine_unit_of_work implements question_usage_observer {
     }
 
     public function notify_attempt_modified(question_attempt $qa) {
-        $no = $qa->get_slot();
-        if (!array_key_exists($no, $this->attemptsadded)) {
-            $this->attemptsmodified[$no] = $qa;
+        $slot = $qa->get_slot();
+        if (!array_key_exists($slot, $this->attemptsadded)) {
+            $this->attemptsmodified[$slot] = $qa;
         }
     }
 
@@ -999,27 +1063,121 @@ class question_engine_unit_of_work implements question_usage_observer {
         $this->attemptsadded[$qa->get_slot()] = $qa;
     }
 
-    public function notify_delete_attempt_steps(question_attempt $qa) {
-
+    public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq) {
         if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
             return;
         }
 
-        $qaid = $qa->get_database_id();
-        foreach ($this->stepsadded as $key => $stepinfo) {
-            if ($stepinfo[1] == $qaid) {
-                unset($this->stepsadded[$key]);
+        if (($key = $this->is_step_added($step)) !== false) {
+            return;
+        }
+
+        if (($key = $this->is_step_modified($step)) !== false) {
+            throw new coding_exception('Cannot add a step that has already been modified.');
+        }
+
+        if (($key = $this->is_step_deleted($step)) !== false) {
+            unset($this->stepsdeleted[$step->get_id()]);
+            $this->stepsmodified[] = array($step, $qa->get_database_id(), $seq);
+            return;
+        }
+
+        $stepid = $step->get_id();
+        if ($stepid) {
+            if (array_key_exists($stepid, $this->stepsdeleted)) {
+                unset($this->stepsdeleted[$stepid]);
             }
+            $this->stepsmodified[] = array($step, $qa->get_database_id(), $seq);
+
+        } else {
+            $this->stepsadded[] = array($step, $qa->get_database_id(), $seq);
+        }
+    }
+
+    public function notify_step_modified(question_attempt_step $step, question_attempt $qa, $seq) {
+        if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
+            return;
         }
 
-        $this->attemptstodeletestepsfor[$qaid] = 1;
+        if (($key = $this->is_step_added($step)) !== false) {
+            return;
+        }
+
+        if (($key = $this->is_step_deleted($step)) !== false) {
+            throw new coding_exception('Cannot modify a step after it has been deleted.');
+        }
+
+        $stepid = $step->get_id();
+        if (empty($stepid)) {
+            throw new coding_exception('Cannot modify a step that has never been stored in the database.');
+        }
+
+        $this->stepsmodified[] = array($step, $qa->get_database_id(), $seq);
     }
 
-    public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq) {
+    public function notify_step_deleted(question_attempt_step $step, question_attempt $qa) {
         if (array_key_exists($qa->get_slot(), $this->attemptsadded)) {
             return;
         }
-        $this->stepsadded[] = array($step, $qa->get_database_id(), $seq);
+
+        if (($key = $this->is_step_added($step)) !== false) {
+            unset($this->stepsadded[$key]);
+            return;
+        }
+
+        if (($key = $this->is_step_modified($step)) !== false) {
+            unset($this->stepsmodified[$key]);
+        }
+
+        $stepid = $step->get_id();
+        if (empty($stepid)) {
+            return; // Was never in the database.
+        }
+
+        $this->stepsdeleted[$stepid] = $step;
+    }
+
+    /**
+     * @param question_attempt_step $step a step
+     * @return int|false if the step is in the list of steps to be added, return
+     *      the key, otherwise return false.
+     */
+    protected function is_step_added(question_attempt_step $step) {
+        foreach ($this->stepsadded as $key => $data) {
+            list($addedstep, $qaid, $seq) = $data;
+            if ($addedstep === $step) {
+                return $key;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @param question_attempt_step $step a step
+     * @return int|false if the step is in the list of steps to be modified, return
+     *      the key, otherwise return false.
+     */
+    protected function is_step_modified(question_attempt_step $step) {
+        foreach ($this->stepsmodified as $key => $data) {
+            list($modifiedstep, $qaid, $seq) = $data;
+            if ($modifiedstep === $step) {
+                return $key;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @param question_attempt_step $step a step
+     * @return bool whether the step is in the list of steps to be deleted.
+     */
+    protected function is_step_deleted(question_attempt_step $step) {
+        foreach ($this->stepsdeleted as $deletedstep) {
+            if ($deletedstep === $step) {
+                return true;
+            }
+        }
+        return false;
     }
 
     /**
@@ -1027,8 +1185,13 @@ class question_engine_unit_of_work implements question_usage_observer {
      * @param question_engine_data_mapper $dm the mapper to use to update the database.
      */
     public function save(question_engine_data_mapper $dm) {
-        $dm->delete_steps_for_question_attempts(array_keys($this->attemptstodeletestepsfor),
-                $this->quba->get_owning_context());
+        $dm->delete_steps(array_keys($this->stepsdeleted), $this->quba->get_owning_context());
+
+        foreach ($this->stepsmodified as $stepinfo) {
+            list($step, $questionattemptid, $seq) = $stepinfo;
+            $dm->update_question_attempt_step($step, $questionattemptid, $seq,
+                    $this->quba->get_owning_context());
+        }
 
         foreach ($this->stepsadded as $stepinfo) {
             list($step, $questionattemptid, $seq) = $stepinfo;
index f31ff3a..bde5115 100644 (file)
@@ -187,7 +187,7 @@ class question_attempt {
      * For internal use only.
      * @param int $slot
      */
-    public function set_number_in_usage($slot) {
+    public function set_slot($slot) {
         $this->slot = $slot;
     }
 
@@ -213,6 +213,15 @@ class question_attempt {
         $this->id = $id;
     }
 
+    /**
+     * You should almost certainly not call this method from your code. It is for
+     * internal use only.
+     * @param question_usage_observer that should be used to tracking changes made to this qa.
+     */
+    public function set_observer($observer) {
+        $this->observer = $observer;
+    }
+
     /** @return int|string the id of the {@link question_usage_by_activity} we belong to. */
     public function get_usage_id() {
         return $this->usageid;
@@ -802,9 +811,11 @@ class question_attempt {
      * @param array $submitteddata optional, used when re-starting to keep the same initial state.
      * @param int $timestamp optional, the timstamp to record for this action. Defaults to now.
      * @param int $userid optional, the user to attribute this action to. Defaults to the current user.
+     * @param int $existingstepid optional, if this step is going to replace an existing step
+     *      (for example, during a regrade) this is the id of the previous step we are replacing.
      */
     public function start($preferredbehaviour, $variant, $submitteddata = array(),
-            $timestamp = null, $userid = null) {
+            $timestamp = null, $userid = null, $existingstepid = null) {
 
         // Initialise the behaviour.
         $this->variant = $variant;
@@ -820,7 +831,7 @@ class question_attempt {
         $this->minfraction = $this->behaviour->get_min_fraction();
 
         // Initialise the first step.
-        $firststep = new question_attempt_step($submitteddata, $timestamp, $userid);
+        $firststep = new question_attempt_step($submitteddata, $timestamp, $userid, $existingstepid);
         $firststep->set_state(question_state::$todo);
         if ($submitteddata) {
             $this->question->apply_attempt_state($firststep);
@@ -1041,8 +1052,8 @@ class question_attempt {
      * @param int $timestamp the time to record for the action. (If not given, use now.)
      * @param int $userid the user to attribute the aciton to. (If not given, use the current user.)
      */
-    public function process_action($submitteddata, $timestamp = null, $userid = null) {
-        $pendingstep = new question_attempt_pending_step($submitteddata, $timestamp, $userid);
+    public function process_action($submitteddata, $timestamp = null, $userid = null, $existingstepid = null) {
+        $pendingstep = new question_attempt_pending_step($submitteddata, $timestamp, $userid, $existingstepid);
         if ($this->behaviour->process_action($pendingstep) == self::KEEP) {
             $this->add_step($pendingstep);
             if ($pendingstep->response_summary_changed()) {
@@ -1074,13 +1085,14 @@ class question_attempt {
     public function regrade(question_attempt $oldqa, $finished) {
         $first = true;
         foreach ($oldqa->get_step_iterator() as $step) {
+            $this->observer->notify_step_deleted($step, $this);
             if ($first) {
                 $first = false;
                 $this->start($oldqa->behaviour, $oldqa->get_variant(), $step->get_all_data(),
-                        $step->get_timecreated(), $step->get_user_id());
+                        $step->get_timecreated(), $step->get_user_id(), $step->get_id());
             } else {
                 $this->process_action($step->get_submitted_data(),
-                        $step->get_timecreated(), $step->get_user_id());
+                        $step->get_timecreated(), $step->get_user_id(), $step->get_id());
             }
         }
         if ($finished) {
@@ -1147,7 +1159,7 @@ class question_attempt {
      *
      * @param Iterator $records Raw records loaded from the database.
      * @param int $questionattemptid The id of the question_attempt to extract.
-     * @return question_attempt The newly constructed question_attempt_step.
+     * @return question_attempt The newly constructed question_attempt.
      */
     public static function load_from_records($records, $questionattemptid,
             question_usage_observer $observer, $preferredbehaviour) {
@@ -1172,7 +1184,7 @@ class question_attempt {
         $qa = new question_attempt($question, $record->questionusageid,
                 null, $record->maxmark + 0);
         $qa->set_database_id($record->questionattemptid);
-        $qa->set_number_in_usage($record->slot);
+        $qa->set_slot($record->slot);
         $qa->variant = $record->variant + 0;
         $qa->minfraction = $record->minfraction + 0;
         $qa->set_flagged($record->flagged);
@@ -1278,7 +1290,7 @@ class question_attempt_with_restricted_history extends question_attempt {
     public function set_flagged($flagged) {
         coding_exception('Cannot modify a question_attempt_with_restricted_history.');
     }
-    public function set_number_in_usage($slot) {
+    public function set_slot($slot) {
         coding_exception('Cannot modify a question_attempt_with_restricted_history.');
     }
     public function set_question_summary($questionsummary) {
index 9f0b5c0..4cd813a 100644 (file)
@@ -98,8 +98,11 @@ class question_attempt_step {
      * @param array $data the submitted data that defines this step.
      * @param int $timestamp the time to record for the action. (If not given, use now.)
      * @param int $userid the user to attribute the aciton to. (If not given, use the current user.)
+     * @param int $existingstepid if this step is going to replace an existing step
+     *      (for example, during a regrade) this is the id of the previous step we are replacing.
      */
-    public function __construct($data = array(), $timecreated = null, $userid = null) {
+    public function __construct($data = array(), $timecreated = null, $userid = null,
+            $existingstepid = null) {
         global $USER;
 
         if (!is_array($data)) {
@@ -117,6 +120,18 @@ class question_attempt_step {
         } else {
             $this->userid = $userid;
         }
+
+        if (!is_null($existingstepid)) {
+            $this->id = $existingstepid;
+        }
+    }
+
+    /**
+     * @return int|null The id of this step in the database. null if this step
+     * is not stored in the database.
+     */
+    public function get_id() {
+        return $this->id;
     }
 
     /** @return question_state The state after this step. */
index 33aeb5f..46ea5cf 100644 (file)
@@ -124,11 +124,6 @@ class question_usage_by_activity {
         return $this->id;
     }
 
-    /** @return question_usage_observer that is tracking changes made to this usage. */
-    public function get_observer() {
-        return $this->observer;
-    }
-
     /**
      * For internal use only. Used by {@link question_engine_data_mapper} to set
      * the id when a usage is saved to the database.
@@ -141,6 +136,23 @@ class question_usage_by_activity {
         }
     }
 
+    /** @return question_usage_observer that is tracking changes made to this usage. */
+    public function get_observer() {
+        return $this->observer;
+    }
+
+    /**
+     * You should almost certainly not call this method from your code. It is for
+     * internal use only.
+     * @param question_usage_observer that should be used to tracking changes made to this usage.
+     */
+    public function set_observer($observer) {
+        $this->observer = $observer;
+        foreach ($this->questionattempts as $qa) {
+            $qa->set_observer($observer);
+        }
+    }
+
     /**
      * Add another question to this usage.
      *
@@ -159,7 +171,7 @@ class question_usage_by_activity {
         } else {
             $this->questionattempts[] = $qa;
         }
-        $qa->set_number_in_usage(end(array_keys($this->questionattempts)));
+        $qa->set_slot(end(array_keys($this->questionattempts)));
         $this->observer->notify_attempt_added($qa);
         return $qa->get_slot();
     }
@@ -647,11 +659,10 @@ class question_usage_by_activity {
             $newmaxmark = $oldqa->get_max_mark();
         }
 
-        $this->observer->notify_delete_attempt_steps($oldqa);
-
         $newqa = new question_attempt($oldqa->get_question(), $oldqa->get_usage_id(),
                 $this->observer, $newmaxmark);
         $newqa->set_database_id($oldqa->get_database_id());
+        $newqa->set_slot($oldqa->get_slot());
         $newqa->regrade($oldqa, $finished);
 
         $this->questionattempts[$slot] = $newqa;
@@ -676,7 +687,7 @@ class question_usage_by_activity {
      *
      * @param Iterator $records Raw records loaded from the database.
      * @param int $questionattemptid The id of the question_attempt to extract.
-     * @return question_attempt The newly constructed question_attempt_step.
+     * @return question_usage_by_activity The newly constructed usage.
      */
     public static function load_from_records($records, $qubaid) {
         $record = $records->current();
@@ -808,19 +819,28 @@ interface question_usage_observer {
     public function notify_attempt_added(question_attempt $qa);
 
     /**
-     * Called we want to delete the old step records for an attempt, prior to
-     * inserting newones. This is used by regrading.
-     * @param question_attempt $qa the question attempt to delete the steps for.
+     * Called when a new step is added to a question attempt in this usage.
+     * @param question_attempt_step $step the new step.
+     * @param question_attempt $qa the usage it is being added to.
+     * @param int $seq the sequence number of the new step.
      */
-    public function notify_delete_attempt_steps(question_attempt $qa);
+    public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq);
 
     /**
-     * Called when a new step is added to a question attempt in this usage.
-     * @param $step the new step.
-     * @param $qa the usage it is being added to.
-     * @param $seq the sequence number of the new step.
+     * Called when a new step is updated in a question attempt in this usage.
+     * @param question_attempt_step $step the step that was updated.
+     * @param question_attempt $qa the usage it is being added to.
+     * @param int $seq the sequence number of the new step.
      */
-    public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq);
+    public function notify_step_modified(question_attempt_step $step, question_attempt $qa, $seq);
+
+    /**
+     * Called when a new step is updated in a question attempt in this usage.
+     * @param question_attempt_step $step the step to delete.
+     * @param question_attempt $qa the usage it is being added to.
+     */
+    public function notify_step_deleted(question_attempt_step $step, question_attempt $qa);
+
 }
 
 
@@ -838,8 +858,10 @@ class question_usage_null_observer implements question_usage_observer {
     }
     public function notify_attempt_added(question_attempt $qa) {
     }
-    public function notify_delete_attempt_steps(question_attempt $qa) {
-    }
     public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq) {
     }
+    public function notify_step_modified(question_attempt_step $step, question_attempt $qa, $seq) {
+    }
+    public function notify_step_deleted(question_attempt_step $step, question_attempt $qa) {
+    }
 }
index 1387336..84a4d46 100644 (file)
@@ -70,8 +70,8 @@ class question_attempt_test extends UnitTestCase {
         $this->assertEqual(2, $qa->get_max_mark());
     }
 
-    public function test_get_set_number_in_usage() {
-        $this->qa->set_number_in_usage(7);
+    public function test_get_set_slot() {
+        $this->qa->set_slot(7);
         $this->assertEqual(7, $this->qa->get_slot());
     }
 
@@ -97,7 +97,7 @@ class question_attempt_test extends UnitTestCase {
     }
 
     public function test_get_field_prefix() {
-        $this->qa->set_number_in_usage(7);
+        $this->qa->set_slot(7);
         $name = $this->qa->get_field_prefix();
         $this->assertPattern('/' . preg_quote($this->usageid) . '/', $name);
         $this->assertPattern('/' . preg_quote($this->qa->get_slot()) . '/', $name);
index bad1854..c317b7c 100644 (file)
@@ -158,4 +158,66 @@ class question_usage_by_activity_test extends UnitTestCase {
         $this->expectException('question_out_of_sequence_exception');
         $quba->process_all_actions($slot, $postdata);
     }
-}
\ No newline at end of file
+}
+
+/**
+ * Unit tests for loading data into the {@link question_usage_by_activity} class.
+ *
+ * @copyright  2012 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_usage_db_test extends data_loading_method_test_base {
+    public function test_load() {
+        $records = new test_recordset(array(
+        array('qubaid', 'contextid', 'component', 'preferredbehaviour',
+                                               'questionattemptid', 'contextid', 'questionusageid', 'slot',
+                                                              'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'flagged',
+                                                                                                             'questionsummary', 'rightanswer', 'responsesummary', 'timemodified',
+                                                                                                                                     'attemptstepid', 'sequencenumber', 'state', 'fraction',
+                                                                                                                                                                     'timecreated', 'userid', 'name', 'value'),
+        array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 1, 0, 'todo',             null, 1256233700, 1,       null, null),
+        array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 2, 1, 'todo',             null, 1256233705, 1,   'answer',  '1'),
+        array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 2.0000000, 0.0000000, 0, '', '', '', 1256233790, 5, 2, 'gradedright', 1.0000000, 1256233720, 1,  '-finish',  '1'),
+        ));
+
+        $question = test_question_maker::make_question('truefalse', 'true');
+        $question->id = -1;
+
+        question_bank::start_unit_test();
+        question_bank::load_test_question_data($question);
+        $quba = question_usage_by_activity::load_from_records($records, 1);
+        question_bank::end_unit_test();
+
+        $this->assertEqual('unit_test', $quba->get_owning_component());
+        $this->assertEqual(1, $quba->get_id());
+        $this->assertIsA($quba->get_observer(), 'question_engine_unit_of_work');
+        $this->assertEqual('interactive', $quba->get_preferred_behaviour());
+
+        $qa = $quba->get_question_attempt(1);
+
+        $this->assertEqual($question->questiontext, $qa->get_question()->questiontext);
+
+        $this->assertEqual(3, $qa->get_num_steps());
+
+        $step = $qa->get_step(0);
+        $this->assertEqual(question_state::$todo, $step->get_state());
+        $this->assertNull($step->get_fraction());
+        $this->assertEqual(1256233700, $step->get_timecreated());
+        $this->assertEqual(1, $step->get_user_id());
+        $this->assertEqual(array(), $step->get_all_data());
+
+        $step = $qa->get_step(1);
+        $this->assertEqual(question_state::$todo, $step->get_state());
+        $this->assertNull($step->get_fraction());
+        $this->assertEqual(1256233705, $step->get_timecreated());
+        $this->assertEqual(1, $step->get_user_id());
+        $this->assertEqual(array('answer' => '1'), $step->get_all_data());
+
+        $step = $qa->get_step(2);
+        $this->assertEqual(question_state::$gradedright, $step->get_state());
+        $this->assertEqual(1, $step->get_fraction());
+        $this->assertEqual(1256233720, $step->get_timecreated());
+        $this->assertEqual(1, $step->get_user_id());
+        $this->assertEqual(array('-finish' => '1'), $step->get_all_data());
+    }
+}
diff --git a/question/engine/simpletest/testunitofwork.php b/question/engine/simpletest/testunitofwork.php
new file mode 100644 (file)
index 0000000..e76085c
--- /dev/null
@@ -0,0 +1,296 @@
+<?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 file contains tests for the question_engine_unit_of_work class.
+ *
+ * @package    moodlecore
+ * @subpackage questionengine
+ * @copyright  2012 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(dirname(__FILE__) . '/../lib.php');
+require_once(dirname(__FILE__) . '/helpers.php');
+
+
+/**
+ * Test subclass to allow access to some protected data so that the correct
+ * behaviour can be verified.
+ *
+ * @copyright  2012 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testable_question_engine_unit_of_work extends question_engine_unit_of_work {
+    public function get_modified() {
+        return $this->modified;
+    }
+
+    public function get_attempts_added() {
+        return $this->attemptsadded;
+    }
+
+    public function get_attempts_modified() {
+        return $this->attemptsmodified;
+    }
+
+    public function get_steps_added() {
+        return $this->stepsadded;
+    }
+
+    public function get_steps_modified() {
+        return $this->stepsmodified;
+    }
+
+    public function get_steps_deleted() {
+        return $this->stepsdeleted;
+    }
+}
+
+
+/**
+ * Unit tests for the {@link question_engine_unit_of_work} class.
+ *
+ * @copyright  2012 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_engine_unit_of_work_test extends data_loading_method_test_base {
+    /** @var question_usage_by_activity the test question usage. */
+    protected $quba;
+
+    /** @var int the slot number of the one qa in the test usage.*/
+    protected $slot;
+
+    /** @var testable_question_engine_unit_of_work the unit of work we are testing. */
+    protected $observer;
+
+    public function setUp() {
+        // Create a usage in an initial state, with one shortanswer question added,
+        // and attempted in interactive mode submitted responses 'toad' then 'frog'.
+        // Then set it to use a new unit of work for any subsequent changes.
+        // Create a short answer question.
+        $question = test_question_maker::make_question('shortanswer');
+        $question->hints = array(
+            new question_hint(0, 'This is the first hint.', FORMAT_HTML),
+            new question_hint(0, 'This is the second hint.', FORMAT_HTML),
+        );
+        $question->id = -1;
+        question_bank::start_unit_test();
+        question_bank::load_test_question_data($question);
+
+        $this->setup_initial_test_state($this->get_test_data());
+     }
+
+    public function testDown() {
+        question_bank::end_unit_test();
+    }
+
+    protected function setup_initial_test_state($testdata) {
+        $records = new test_recordset($testdata);
+
+        $this->quba = question_usage_by_activity::load_from_records($records, 1);
+
+        $this->slot = 1;
+        $this->observer = new testable_question_engine_unit_of_work($this->quba);
+        $this->quba->set_observer($this->observer);
+    }
+
+    protected function get_test_data() {
+        return array(
+        array('qubaid', 'contextid', 'component', 'preferredbehaviour',
+                                                'questionattemptid', 'contextid', 'questionusageid', 'slot',
+                                                               'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'flagged',
+                                                                                                              'questionsummary', 'rightanswer', 'responsesummary', 'timemodified',
+                                                                                                                                     'attemptstepid', 'sequencenumber', 'state', 'fraction',
+                                                                                                                                                                     'timecreated', 'userid', 'name', 'value'),
+        array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 0, '', '', '', 1256233790, 1, 0, 'todo',             null, 1256233700, 1, '-_triesleft', 3),
+        array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 0, '', '', '', 1256233790, 2, 1, 'todo',             null, 1256233720, 1, 'answer',     'toad'),
+        array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 0, '', '', '', 1256233790, 2, 1, 'todo',             null, 1256233720, 1, '-submit',     1),
+        array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 0, '', '', '', 1256233790, 2, 1, 'todo',             null, 1256233720, 1, '-_triesleft', 1),
+        array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 0, '', '', '', 1256233790, 3, 2, 'todo',             null, 1256233740, 1, '-tryagain',   1),
+        array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 0, '', '', '', 1256233790, 5, 3, 'gradedright',      null, 1256233790, 1, 'answer',     'frog'),
+        array(1, 1, 'unit_test', 'interactive', 1, 123, 1, 1, 'interactive', -1, 1, 1.0000000, 0.0000000, 0, '', '', '', 1256233790, 5, 3, 'gradedright', 1.0000000, 1256233790, 1, '-finish',     1),
+        );
+    }
+
+    public function test_initial_state() {
+        $this->assertFalse($this->observer->get_modified());
+        $this->assertEqual(0, count($this->observer->get_attempts_added()));
+        $this->assertEqual(0, count($this->observer->get_attempts_modified()));
+        $this->assertEqual(0, count($this->observer->get_steps_added()));
+        $this->assertEqual(0, count($this->observer->get_steps_modified()));
+        $this->assertEqual(0, count($this->observer->get_steps_deleted()));
+    }
+
+    public function test_update_usage() {
+
+        $this->quba->set_preferred_behaviour('deferredfeedback');
+
+        $this->assertTrue($this->observer->get_modified());
+    }
+
+    public function test_add_question() {
+
+        $slot = $this->quba->add_question(test_question_maker::make_question('truefalse'));
+
+        $newattempts = $this->observer->get_attempts_added();
+        $this->assertEqual(1, count($newattempts));
+        $this->assertIdentical($this->quba->get_question_attempt($slot), reset($newattempts));
+        $this->assertIdentical($slot, key($newattempts));
+    }
+
+    public function test_add_and_start_question() {
+
+        $slot = $this->quba->add_question(test_question_maker::make_question('truefalse'));
+                $this->quba->start_question($slot);
+
+        // The point here is that, although we have added a step, it is not listed
+        // separately becuase it is part of a newly added attempt, and all steps
+        // for a newly added attempt are automatically added to the DB, so it does
+        // not need to be tracked separately.
+        $newattempts = $this->observer->get_attempts_added();
+        $this->assertEqual(1, count($newattempts));
+        $this->assertIdentical($this->quba->get_question_attempt($slot),
+                reset($newattempts));
+        $this->assertIdentical($slot, key($newattempts));
+        $this->assertEqual(0, count($this->observer->get_steps_added()));
+    }
+
+    public function test_process_action() {
+
+        $this->quba->manual_grade($this->slot, 'Acutally, that is not quite right', 0.5);
+
+        // Here, however, were we are adding a step to an existing qa, we do need to track that.
+        $this->assertEqual(0, count($this->observer->get_attempts_added()));
+
+        $updatedattempts = $this->observer->get_attempts_modified();
+        $this->assertEqual(1, count($updatedattempts));
+
+        $updatedattempt = reset($updatedattempts);
+        $this->assertIdentical($this->quba->get_question_attempt($this->slot), $updatedattempt);
+        $this->assertIdentical($this->slot, key($updatedattempts));
+
+        $newsteps = $this->observer->get_steps_added();
+        $this->assertEqual(1, count($newsteps));
+
+        list($newstep, $qaid, $seq) = reset($newsteps);
+        $this->assertIdentical($this->quba->get_question_attempt($this->slot)->get_last_step(), $newstep);
+    }
+
+    public function test_regrade_same_steps() {
+
+        // Change the question in a minor way and regrade.
+        $this->quba->get_question($this->slot)->answer[14]->fraction = 0.5;
+        $this->quba->regrade_all_questions();
+
+        // Here, the qa, and all the steps, should be marked as updated.
+        // Here, however, were we are adding a step to an existing qa, we do need to track that.
+        $this->assertEqual(0, count($this->observer->get_attempts_added()));
+        $this->assertEqual(0, count($this->observer->get_steps_added()));
+        $this->assertEqual(0, count($this->observer->get_steps_deleted()));
+
+        $updatedattempts = $this->observer->get_attempts_modified();
+        $this->assertEqual(1, count($updatedattempts));
+
+        $updatedattempt = reset($updatedattempts);
+        $this->assertIdentical($this->quba->get_question_attempt($this->slot), $updatedattempt);
+
+        $updatedsteps = $this->observer->get_steps_modified();
+        $this->assertEqual($updatedattempt->get_num_steps(), count($updatedsteps));
+
+        foreach ($updatedattempt->get_step_iterator() as $seq => $step) {
+            $this->assertIdentical(array($step, $updatedattempt->get_database_id(), $seq),
+                    $updatedsteps[$seq]);
+        }
+    }
+
+    public function test_regrade_losing_steps() {
+
+        // Change the question so that 'toad' is also right, and regrade. This
+        // will mean that the try again, and second try states are no longer
+        // needed, so they should be dropped.
+        $this->quba->get_question($this->slot)->answers[14]->fraction = 1;
+        $this->quba->regrade_all_questions();
+
+        $this->assertEqual(0, count($this->observer->get_attempts_added()));
+        $this->assertEqual(0, count($this->observer->get_steps_added()));
+
+        $updatedattempts = $this->observer->get_attempts_modified();
+        $this->assertEqual(1, count($updatedattempts));
+
+        $updatedattempt = reset($updatedattempts);
+        $this->assertIdentical($this->quba->get_question_attempt($this->slot), $updatedattempt);
+
+        $updatedsteps = $this->observer->get_steps_modified();
+        $this->assertEqual($updatedattempt->get_num_steps(), count($updatedsteps));
+
+        foreach ($updatedattempt->get_step_iterator() as $seq => $step) {
+            $this->assertIdentical(array($step, $updatedattempt->get_database_id(), $seq),
+                    $updatedsteps[$seq]);
+        }
+
+        $deletedsteps = $this->observer->get_steps_deleted();
+        $this->assertEqual(2, count($deletedsteps));
+
+        $firstdeletedstep = reset($deletedsteps);
+        $this->assertEqual(array('-tryagain' => 1), $firstdeletedstep->get_all_data());
+
+        $seconddeletedstep = end($deletedsteps);
+        $this->assertEqual(array('answer' => 'frog', '-finish' => 1),
+                $seconddeletedstep->get_all_data());
+    }
+
+    public function test_tricky_regrade() {
+
+        // The tricky thing here is that we take a half-complete question-attempt,
+        // and then as one transaction, we submit some more responses, and then
+        // change the question attempt as in test_regrade_losing_steps, and regrade
+        // before the steps are even written to the database the first time.
+        $somedata = $this->get_test_data();
+        $somedata = array_slice($somedata, 0, 5);
+        $this->setup_initial_test_state($somedata);
+
+        $this->quba->process_action($this->slot, array('-tryagain' => 1));
+        $this->quba->process_action($this->slot, array('answer' => 'frog', '-submit' => 1));
+        $this->quba->finish_all_questions();
+
+        $this->quba->get_question($this->slot)->answers[14]->fraction = 1;
+        $this->quba->regrade_all_questions();
+
+        $this->assertEqual(0, count($this->observer->get_attempts_added()));
+
+        $updatedattempts = $this->observer->get_attempts_modified();
+        $this->assertEqual(1, count($updatedattempts));
+
+        $updatedattempt = reset($updatedattempts);
+        $this->assertIdentical($this->quba->get_question_attempt($this->slot), $updatedattempt);
+
+        $this->assertEqual(0, count($this->observer->get_steps_added()));
+
+        $updatedsteps = $this->observer->get_steps_modified();
+        $this->assertEqual($updatedattempt->get_num_steps(), count($updatedsteps));
+
+        foreach ($updatedattempt->get_step_iterator() as $seq => $step) {
+            $this->assertIdentical(array($step, $updatedattempt->get_database_id(), $seq),
+                    $updatedsteps[$seq]);
+        }
+
+        $this->assertEqual(0, count($this->observer->get_steps_deleted()));
+    }
+}
index 7e682f0..8c6e175 100644 (file)
@@ -88,8 +88,9 @@ class question_engine_attempt_upgrader {
         global $CFG, $DB;
 
         // Look to see if the admin has set things up to only upgrade certain attempts.
-        $partialupgradefile = $CFG->dirroot . '/local/qeupgradehelper/partialupgrade.php';
-        $partialupgradefunction = 'local_qeupgradehelper_get_quizzes_to_upgrade';
+        $partialupgradefile = $CFG->dirroot . '/' . $CFG->admin .
+                '/tool/qeupgradehelper/partialupgrade.php';
+        $partialupgradefunction = 'tool_qeupgradehelper_get_quizzes_to_upgrade';
         if (is_readable($partialupgradefile)) {
             include_once($partialupgradefile);
             if (function_exists($partialupgradefunction)) {
index 7821cd6..23a15f3 100644 (file)
@@ -26,6 +26,8 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+require_once($CFG->dirroot . '/question/type/edit_question_form.php');
+
 
 /**
  * Calculated question data set definitions editing form definition.
@@ -33,7 +35,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2007 Jamie Pratt me@jamiep.org
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class question_dataset_dependent_definitions_form extends moodleform {
+class question_dataset_dependent_definitions_form extends question_wizard_form {
     /**
      * Question object with options and answers already loaded by get_question_options
      * Be careful how you use this it is needed sometimes to set up the structure of the
@@ -149,26 +151,11 @@ class question_dataset_dependent_definitions_form extends moodleform {
 
         $this->add_action_buttons(false, get_string('nextpage', 'qtype_calculated'));
 
-        // Hidden elements
-        $mform->addElement('hidden', 'returnurl');
-        $mform->setType('returnurl', PARAM_LOCALURL);
-        $mform->setDefault('returnurl', 0);
-        $mform->addElement('hidden', 'id');
-        $mform->setType('id', PARAM_INT);
+        $this->add_hidden_fields();
 
         $mform->addElement('hidden', 'category');
-        $mform->setType('category', PARAM_RAW);
-        $mform->setDefault('category', array('contexts' => array($this->categorycontext)));
-
-        $mform->addElement('hidden', 'courseid');
-        $mform->setType('courseid', PARAM_INT);
-        $mform->setDefault('courseid', 0);
-
-        $mform->addElement('hidden', 'cmid');
-        $mform->setType('cmid', PARAM_INT);
-        $mform->setDefault('cmid', 0);
+        $mform->setType('category', PARAM_SEQUENCE);
 
-        $mform->setType('id', PARAM_INT);
         $mform->addElement('hidden', 'wizard', 'datasetitems');
         $mform->setType('wizard', PARAM_ALPHA);
     }
index 3d07083..6cf349a 100644 (file)
@@ -26,6 +26,8 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+require_once($CFG->dirroot . '/question/type/edit_question_form.php');
+
 
 /**
  * Calculated question data set items editing form definition.
@@ -33,7 +35,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2007 Jamie Pratt me@jamiep.org
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class question_dataset_dependent_items_form extends moodleform {
+class question_dataset_dependent_items_form extends question_wizard_form {
     /**
      * Question object with options and answers already loaded by get_question_options
      * Be careful how you use this it is needed sometimes to set up the structure of the
@@ -328,28 +330,14 @@ class question_dataset_dependent_items_form extends moodleform {
             $mform->addElement('submit', 'savechanges', get_string('savechanges'));
             $mform->closeHeaderBefore('savechanges');
         }
-        //hidden elements
-        $mform->addElement('hidden', 'id');
-        $mform->setType('id', PARAM_INT);
 
-        $mform->addElement('hidden', 'courseid');
-        $mform->setType('courseid', PARAM_INT);
-        $mform->setDefault('courseid', 0);
+        $this->add_hidden_fields();
 
         $mform->addElement('hidden', 'category');
-        $mform->setType('category', PARAM_RAW);
-        $mform->setDefault('category', array('contexts' => array($this->categorycontext)));
-
-        $mform->addElement('hidden', 'cmid');
-        $mform->setType('cmid', PARAM_INT);
-        $mform->setDefault('cmid', 0);
+        $mform->setType('category', PARAM_SEQUENCE);
 
         $mform->addElement('hidden', 'wizard', 'datasetitems');
         $mform->setType('wizard', PARAM_ALPHA);
-
-        $mform->addElement('hidden', 'returnurl');
-        $mform->setType('returnurl', PARAM_LOCALURL);
-        $mform->setDefault('returnurl', 0);
     }
 
     public function set_data($question) {
index 99f4b7b..a86bb20 100644 (file)
@@ -384,15 +384,15 @@ class qtype_calculated extends question_type {
         }
         return true;
     }
-    public function finished_edit_wizard(&$form) {
+    public function finished_edit_wizard($form) {
         return isset($form->savechanges);
     }
     public function wizardpagesnumber() {
         return 3;
     }
     // This gets called by editquestion.php after the standard question is saved
-    public function print_next_wizard_page(&$question, &$form, $course) {
-        global $CFG, $USER, $SESSION, $COURSE;
+    public function print_next_wizard_page($question, $form, $course) {
+        global $CFG, $SESSION, $COURSE;
 
         // Catch invalid navigation & reloads
         if (empty($question->id) && empty($SESSION->calculated)) {
@@ -456,19 +456,20 @@ class qtype_calculated extends question_type {
      * @param object $question
      * @param string $wizardnow is '' for first page.
      */
-    public function display_question_editing_page(&$mform, $question, $wizardnow) {
+    public function display_question_editing_page($mform, $question, $wizardnow) {
         global $OUTPUT;
         switch ($wizardnow) {
             case '':
-                //on first page default display is fine
+                // On the first page, the default display is fine.
                 parent::display_question_editing_page($mform, $question, $wizardnow);
                 return;
-                break;
+
             case 'datasetdefinitions':
                 echo $OUTPUT->heading_with_help(
                         get_string('choosedatasetproperties', 'qtype_calculated'),
                         'questiondatasets', 'qtype_calculated');
                 break;
+
             case 'datasetitems':
                 echo $OUTPUT->heading_with_help(get_string('editdatasets', 'qtype_calculated'),
                         'questiondatasets', 'qtype_calculated');
index d9dae12..7ca68f0 100644 (file)
@@ -316,13 +316,7 @@ class qtype_calculatedsimple_edit_form extends qtype_calculated_edit_form {
                 get_string('findwildcards', 'qtype_calculatedsimple'));
         $mform->registerNoSubmitButton('analyzequestion');
         $mform->closeHeaderBefore('analyzequestion');
-        if (optional_param('analyzequestion', false, PARAM_BOOL)) {
-
-            $this->wizarddisplay = true;
-
-        } else {
-            $this->wizwarddisplay = false;
-        }
+        $this->wizarddisplay = optional_param('analyzequestion', false, PARAM_BOOL);
         if ($this->maxnumber != -1) {
             $this->noofitems = $this->maxnumber;
         } else {
index 8235bda..92eb65c 100644 (file)
@@ -232,7 +232,7 @@ class qtype_calculatedsimple extends qtype_calculated {
         return true;
     }
 
-    public function finished_edit_wizard(&$form) {
+    public function finished_edit_wizard($form) {
         return true;
     }
 
index e7505f3..f8e21a1 100644 (file)
 defined('MOODLE_INTERNAL') || die();
 
 
+abstract class question_wizard_form extends moodleform {
+    /**
+     * Add all the hidden form fields used by question/question.php.
+     */
+    protected function add_hidden_fields() {
+        $mform = $this->_form;
+
+        $mform->addElement('hidden', 'id');
+        $mform->setType('id', PARAM_INT);
+
+        $mform->addElement('hidden', 'inpopup');
+        $mform->setType('inpopup', PARAM_INT);
+
+        $mform->addElement('hidden', 'cmid');
+        $mform->setType('cmid', PARAM_INT);
+
+        $mform->addElement('hidden', 'courseid');
+        $mform->setType('courseid', PARAM_INT);
+
+        $mform->addElement('hidden', 'returnurl');
+        $mform->setType('returnurl', PARAM_LOCALURL);
+
+        $mform->addElement('hidden', 'scrollpos');
+        $mform->setType('scrollpos', PARAM_INT);
+
+        $mform->addElement('hidden', 'appendqnumstring');
+        $mform->setType('appendqnumstring', PARAM_ALPHA);
+    }
+}
+
 /**
  * Form definition base class. This defines the common fields that
  * all question types need. Question types should define their own
@@ -36,7 +66,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2006 The Open University
  * @license    http://www.gnu.org/copyleft/gpl.html GNU Public License
  */
-abstract class question_edit_form extends moodleform {
+abstract class question_edit_form extends question_wizard_form {
     const DEFAULT_NUM_HINTS = 2;
 
     /**
@@ -193,45 +223,17 @@ abstract class question_edit_form extends moodleform {
             }
         }
 
-        // Standard fields at the end of the form.
-        $mform->addElement('hidden', 'id');
-        $mform->setType('id', PARAM_INT);
-
-        $mform->addElement('hidden', 'qtype');
-        $mform->setType('qtype', PARAM_ALPHA);
-
-        $mform->addElement('hidden', 'inpopup');
-        $mform->setType('inpopup', PARAM_INT);
-
-        $mform->addElement('hidden', 'versioning');
-        $mform->setType('versioning', PARAM_BOOL);
+        $this->add_hidden_fields();
 
         $mform->addElement('hidden', 'movecontext');
         $mform->setType('movecontext', PARAM_BOOL);
 
-        $mform->addElement('hidden', 'cmid');
-        $mform->setType('cmid', PARAM_INT);
-        $mform->setDefault('cmid', 0);
-
-        $mform->addElement('hidden', 'courseid');
-        $mform->setType('courseid', PARAM_INT);
-        $mform->setDefault('courseid', 0);
-
-        $mform->addElement('hidden', 'returnurl');
-        $mform->setType('returnurl', PARAM_LOCALURL);
-        $mform->setDefault('returnurl', 0);
-
-        $mform->addElement('hidden', 'scrollpos');
-        $mform->setType('scrollpos', PARAM_INT);
-        $mform->setDefault('scrollpos', 0);
-
-        $mform->addElement('hidden', 'appendqnumstring');
-        $mform->setType('appendqnumstring', PARAM_ALPHA);
-        $mform->setDefault('appendqnumstring', 0);
+        $mform->addElement('hidden', 'qtype');
+        $mform->setType('qtype', PARAM_ALPHA);
 
         $buttonarray = array();
         if (!empty($this->question->id)) {
-            //editing / moving question
+            // Editing / moving question
             if ($this->question->formoptions->movecontext) {
                 $buttonarray[] = $mform->createElement('submit', 'submitbutton',
                         get_string('moveq', 'question'));
@@ -247,7 +249,7 @@ abstract class question_edit_form extends moodleform {
             }
             $buttonarray[] = $mform->createElement('cancel');
         } else {
-            // adding new question
+            // Adding new question
             $buttonarray[] = $mform->createElement('submit', 'submitbutton',
                     get_string('savechanges'));
             $buttonarray[] = $mform->createElement('cancel');
index 51e33d3..2946d66 100644 (file)
@@ -87,7 +87,7 @@ class qtype_missingtype extends question_type {
         return null;
     }
 
-    public function display_question_editing_page(&$mform, $question, $wizardnow) {
+    public function display_question_editing_page($mform, $question, $wizardnow) {
         global $OUTPUT;
         echo $OUTPUT->heading(get_string('warningmissingtype', 'qtype_missingtype'));
 
index de58ca4..5d0d80b 100644 (file)
@@ -42,11 +42,6 @@ class qtype_random_edit_form extends question_edit_form {
      * override this method and remove the ones you don't want with $mform->removeElement().
      */
     protected function definition() {
-        global $COURSE, $CFG;
-
-        $qtype = $this->qtype();
-        $langfile = "qtype_$qtype";
-
         $mform = $this->_form;
 
         // Standard fields at the start of the form.
@@ -58,41 +53,10 @@ class qtype_random_edit_form extends question_edit_form {
         $mform->addElement('advcheckbox', 'questiontext[text]',
                 get_string('includingsubcategories', 'qtype_random'), null, null, array(0, 1));
 
-        $mform->addElement('hidden', 'name');
-        $mform->setType('name', PARAM_ALPHA);
-        $mform->setDefault('name', '');
-
-        $mform->addElement('hidden', 'tags[]');
-        $mform->setType('tags[]', PARAM_ALPHA);
-        $mform->setDefault('tags[]', '');
-
-        // Standard fields at the end of the form.
-        $mform->addElement('hidden', 'questiontextformat', 0);
-        $mform->setType('questiontextformat', PARAM_INT);
-
-        $mform->addElement('hidden', 'id');
-        $mform->setType('id', PARAM_INT);
-
         $mform->addElement('hidden', 'qtype');
         $mform->setType('qtype', PARAM_ALPHA);
 
-        $mform->addElement('hidden', 'inpopup');
-        $mform->setType('inpopup', PARAM_INT);
-
-        $mform->addElement('hidden', 'versioning');
-        $mform->setType('versioning', PARAM_BOOL);
-
-        $mform->addElement('hidden', 'cmid');
-        $mform->setType('cmid', PARAM_INT);
-        $mform->setDefault('cmid', 0);
-
-        $mform->addElement('hidden', 'courseid');
-        $mform->setType('courseid', PARAM_INT);
-        $mform->setDefault('courseid', 0);
-
-        $mform->addElement('hidden', 'returnurl');
-        $mform->setType('returnurl', PARAM_LOCALURL);
-        $mform->setDefault('returnurl', 0);
+        $this->add_hidden_fields();
 
         $buttonarray = array();
         $buttonarray[] = $mform->createElement('submit', 'submitbutton', get_string('savechanges'));
index 836e88a..7ef7de2 100644 (file)
@@ -115,10 +115,10 @@ class qtype_random extends question_type {
         $this->manualqtypes = implode(',', $manualqtypes);
     }
 
-    public function display_question_editing_page(&$mform, $question, $wizardnow) {
+    public function display_question_editing_page($mform, $question, $wizardnow) {
         global $OUTPUT;
         $heading = $this->get_heading(empty($question->id));
-        echo $OUTPUT->heading_with_help($heading, $this->name(), $this->plugin_name());
+        echo $OUTPUT->heading_with_help($heading, 'pluginname', $this->plugin_name());
         $mform->display();
     }
 
@@ -151,6 +151,9 @@ class qtype_random extends question_type {
 
     public function save_question($question, $form) {
         $form->name = '';
+        $form->questiontextformat = FORMAT_MOODLE;
+        $form->tags = array();
+
         // Name is not a required field for random questions, but
         // parent::save_question Assumes that it is.
         return parent::save_question($question, $form);
index 1bfadc8..804ff3f 100644 (file)
@@ -797,16 +797,22 @@ M.core_filepicker.init = function(Y, options) {
             Y.on('contentready', function(el) {
                 var list = Y.one(el);
                 var count = 0;
+                // Resort the repositories by sortorder
+                var sorted_repositories = new Array();
                 for (var i in r) {
-                    var id = 'repository-'+client_id+'-'+r[i].id;
+                    sorted_repositories[r[i].sortorder - 1] = r[i];
+                }
+                for (var i in sorted_repositories){
+                    repository = sorted_repositories[i];
+                    var id = 'repository-'+client_id+'-'+repository.id;
                     var link_id = id + '-link';
-                    list.append('<li id="'+id+'"><a class="fp-repo-name" id="'+link_id+'" href="###">'+r[i].name+'</a></li>');
-                    Y.one('#'+link_id).prepend('<img src="'+r[i].icon+'" width="16" height="16" />&nbsp;');
+                    list.append('<li id="'+id+'"><a class="fp-repo-name" id="'+link_id+'" href="###">'+repository.name+'</a></li>');
+                    Y.one('#'+link_id).prepend('<img src="'+repository.icon+'" width="16" height="16" />&nbsp;');
                     Y.one('#'+link_id).on('click', function(e, scope, repository_id) {
                         YAHOO.util.Cookie.set('recentrepository', repository_id);
                         scope.repository_id = repository_id;
                         this.list({'repo_id':repository_id});
-                    }, this /*handler running scope*/, this/*second argument*/, r[i].id/*third argument of handler*/);
+                    }, this /*handler running scope*/, this/*second argument*/, repository.id/*third argument of handler*/);
                     count++;
                 }
                 if (count==0) {
index 5db4888..8652aa9 100644 (file)
@@ -826,6 +826,7 @@ abstract class repository {
             $options['visible'] = $record->visible;
             $options['type']    = $record->repositorytype;
             $options['typeid']  = $record->typeid;
+            $options['sortorder'] = $record->sortorder;
             // tell instance what file types will be accepted by file picker
             $classname = 'repository_' . $record->repositorytype;
 
@@ -1451,6 +1452,7 @@ abstract class repository {
         $meta->icon = $OUTPUT->pix_url('icon', 'repository_'.$meta->type)->out(false);
         $meta->supported_types = $ft->get_extensions($this->supported_filetypes());
         $meta->return_types = $this->supported_returntypes();
+        $meta->sortorder = $this->options['sortorder'];
         return $meta;
     }
 
index a6b39d0..fa3713a 100644 (file)
@@ -42,8 +42,7 @@ echo $OUTPUT->doctype() ?>
    <?php if ($hasheading || $hasnavbar) { ?>
     <div id="page-header">
         <?php if ($hasheading) { ?>
-         <div id="logo">
-         </div>
+         <a class="logo" href="<?php echo $CFG->wwwroot; ?>" title="<?php print_string('home'); ?>"></a>
          <div class="headermenu"><?php
             if ($haslogininfo) {
                 echo $OUTPUT->login_info();
@@ -132,4 +131,4 @@ echo $OUTPUT->doctype() ?>
 </div>
 <?php echo $OUTPUT->standard_end_of_body_html() ?>
 </body>
-</html>
\ No newline at end of file
+</html>
index 7cdceaa..c03d7b2 100644 (file)
@@ -45,7 +45,7 @@ Header and Logo
     width: 100%;
     background: #fff;
 }
-#logo {
+a.logo {
     background: url([[setting:logo]]) no-repeat 0 0;
     width: 320px;
     height: 75px;
@@ -439,4 +439,4 @@ tab styles for ie6 & ie7
 }
 .yui-skin-sam .yui-panel-container {
     z-index: 999999!important;
-}
\ No newline at end of file
+}
index ada1a8e..545c28b 100644 (file)
@@ -483,6 +483,22 @@ input[name="mailnow"] {
 .loginbox #loginbtn {
     width: 100% !important;
 }
+.loginbox .loginform {
+    width: auto;
+}
+.loginbox .loginform .form-label {
+    float: none;
+    text-align: center;
+    width: auto;
+}
+.loginbox .loginform .form-input {
+    float: none;
+    width: auto;
+}
+.loginbox h2 {
+    margin: 0 0;
+    padding: 0 0;
+}
 .mform fieldset div {
 margin:0;
 }
@@ -1578,25 +1594,6 @@ body .ui-loader.ui-body-e h1 {
     padding-bottom: 0.6em;
     padding-top: 0.5em;
 }
-/*header bar gradient corrections */
-.mymobileheader.ui-bar-b {
-    background-image: -moz-linear-gradient(top,
-                            #81a8ce 10%,
-                            #5e87b0 75%);
-    background-image: -webkit-gradient(linear,left top,left bottom,
-        color-stop(0.05,        #81a8ce),
-        color-stop(0.55,        #5e87b0));
-    -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#81a8ce', EndColorStr='#5e87b0')";
-}
-.mymobileheader.ui-bar-a {
-    background-image: -moz-linear-gradient(top,
-                            #3c3c3c,
-                            #111111);
-    background-image: -webkit-gradient(linear,left top,left bottom,
-        color-stop(0.05,    #3c3c3c),
-        color-stop(.55,         #111111));
-    -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#3c3c3c', EndColorStr='#111111')";
-}
 /*split page stuff for tablets */
     div.tablets, #sliderdiv, .has-myblocks .jsetsbar {
         display: none;
index 21aad43..57eeace 100644 (file)
@@ -30,7 +30,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 
-$version  = 2012020200.05;              // YYYYMMDD      = weekly release date of this DEV branch
+$version  = 2012020200.06;              // YYYYMMDD      = weekly release date of this DEV branch
                                         //         RR    = release increments - 00 in DEV branches
                                         //           .XX = incremental changes