Merge branch 'MDL-47875_master' of https://github.com/crazyserver/moodle
authorSam Hemelryk <sam@moodle.com>
Thu, 30 Oct 2014 22:15:12 +0000 (11:15 +1300)
committerSam Hemelryk <sam@moodle.com>
Thu, 30 Oct 2014 22:15:12 +0000 (11:15 +1300)
166 files changed:
admin/settings/appearance.php
admin/tool/messageinbound/classes/edit_handler_form.php
admin/tool/monitor/classes/notification_task.php
admin/tool/monitor/classes/output/managerules/renderable.php
admin/tool/monitor/classes/output/managesubs/renderer.php
admin/tool/monitor/classes/output/managesubs/rules.php
admin/tool/monitor/classes/subscription_manager.php
admin/tool/monitor/lang/en/tool_monitor.php
admin/tool/monitor/tests/behat/rule.feature
admin/tool/monitor/tests/behat/subscription.feature
admin/tool/monitor/tests/eventobservers_test.php
admin/tool/monitor/tests/events_test.php
admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown-debug.js
admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown-min.js
admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown.js
admin/tool/monitor/yui/src/dropdown/js/dropdown.js
admin/user/lib.php
auth/manual/auth.php
auth/tests/behat/behat_auth.php
backup/cc/cc2moodle.php
backup/converter/moodle1/handlerlib.php
badges/tests/behat/award_badge.feature
blocks/recent_activity/block_recent_activity.php
blocks/site_main_menu/block_site_main_menu.php
blocks/social_activities/block_social_activities.php
cache/stores/mongodb/lib.php
calendar/lib.php
calendar/renderer.php
course/format/singleactivity/lib.php
course/mod.php
course/modduplicate.php
course/renderer.php
course/tests/externallib_test.php
grade/edit/tree/category_form.php
grade/edit/tree/item_form.php
grade/edit/tree/lib.php
grade/grading/form/guide/lib.php
grade/lib.php
grade/report/grader/lib.php
grade/report/singleview/classes/local/screen/grade.php
grade/report/singleview/classes/local/screen/tablelike.php
grade/report/singleview/classes/local/screen/user.php
grade/report/singleview/classes/local/ui/finalgrade.php
grade/report/singleview/lang/en/gradereport_singleview.php
grade/report/singleview/styles.css
grade/report/singleview/tests/behat/singleview.feature
grade/report/user/lib.php
grade/tests/behat/grade_calculated_weights.feature
grade/tests/behat/grade_natural_normalisation.feature
grade/tests/behat/grade_scales.feature
grade/tests/behat/grade_scales_aggregation.feature [new file with mode: 0644]
grade/tests/behat/grade_single_item_scales.feature
install/lang/hr/moodle.php
lang/en/admin.php
lang/en/moodle.php
lib/accesslib.php
lib/adminlib.php
lib/badgeslib.php
lib/classes/event/calendar_event_updated.php
lib/classes/message/inbound/handler.php
lib/dml/mysqli_native_moodle_database.php
lib/dmllib.php
lib/editor/atto/plugins/accessibilitychecker/tests/behat/accessibilitychecker.feature
lib/filelib.php
lib/formslib.php
lib/grade/grade_category.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputrenderers.php
lib/setuplib.php
lib/tests/admintree_test.php
lib/tests/formslib_test.php
lib/weblib.php
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js
lib/yui/src/dragdrop/js/dragdrop.js
lib/yui/src/notification/js/dialogue.js
message/output/airnotifier/message_output_airnotifier.php
message/tests/behat/behat_message.php
message/tests/behat/block_users.feature
message/tests/behat/display_history.feature
message/tests/behat/manage_contacts.feature
message/tests/behat/message_participants.feature
message/tests/behat/search_history.feature
mod/assign/feedback/editpdf/classes/page_editor.php
mod/assign/feedback/editpdf/tests/behat/annotate_pdf.feature
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/assign/feedback/editpdf/yui/src/editor/meta/editor.json
mod/assign/locallib.php
mod/data/view.php
mod/forum/classes/post_form.php
mod/forum/db/upgrade.php
mod/forum/discuss.php
mod/forum/externallib.php
mod/forum/index.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/post.php
mod/forum/styles.css
mod/forum/subscribe_ajax.php
mod/forum/tests/behat/discussion_subscriptions.feature
mod/forum/tests/behat/forum_subscriptions.feature
mod/forum/unsubscribeall.php
mod/forum/version.php
mod/forum/yui/build/moodle-mod_forum-subscriptiontoggle/moodle-mod_forum-subscriptiontoggle-debug.js
mod/forum/yui/build/moodle-mod_forum-subscriptiontoggle/moodle-mod_forum-subscriptiontoggle-min.js
mod/forum/yui/build/moodle-mod_forum-subscriptiontoggle/moodle-mod_forum-subscriptiontoggle.js
mod/forum/yui/src/subscriptiontoggle/js/toggle.js
mod/glossary/index.php
mod/lesson/essay.php
mod/quiz/classes/output/edit_renderer.php
mod/quiz/classes/structure.php
mod/quiz/edit_rest.php
mod/quiz/lang/en/quiz.php
mod/quiz/module.js
mod/quiz/styles.css
mod/quiz/tests/behat/behat_mod_quiz.php
mod/quiz/tests/behat/editing_click_delete_icon.feature [new file with mode: 0644]
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-debug.js
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-min.js
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop.js
mod/quiz/yui/build/moodle-mod_quiz-quizbase/moodle-mod_quiz-quizbase-debug.js
mod/quiz/yui/build/moodle-mod_quiz-quizbase/moodle-mod_quiz-quizbase.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js
mod/quiz/yui/build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page-debug.js
mod/quiz/yui/build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page-min.js
mod/quiz/yui/build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page.js
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-debug.js
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-min.js
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot.js
mod/quiz/yui/src/dragdrop/js/resource.js
mod/quiz/yui/src/dragdrop/meta/dragdrop.json
mod/quiz/yui/src/quizbase/js/quizbase.js
mod/quiz/yui/src/toolboxes/js/resource.js
mod/quiz/yui/src/toolboxes/js/toolbox.js
mod/quiz/yui/src/util/js/page.js
mod/quiz/yui/src/util/js/slot.js
mod/scorm/index.php
mod/scorm/loadSCO.php
mod/scorm/player.php
mod/url/lib.php
report/outline/index.php
report/outline/locallib.php
report/stats/graph.php
report/stats/locallib.php
repository/recent/tests/behat/add_recent.feature
repository/tests/behat/cancel_add_file.feature
repository/tests/behat/create_shortcut.feature
repository/tests/behat/delete_files.feature
repository/tests/behat/overwrite_file.feature
theme/base/style/grade.css
theme/bootstrapbase/less/moodle/grade.less
theme/bootstrapbase/style/moodle.css
user/index.php
user/lib.php
version.php
webservice/rest/locallib.php

index fd31903..b904da2 100644 (file)
@@ -23,6 +23,17 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) { // sp
     $temp->add(new admin_setting_configcheckbox('allowuserblockhiding', new lang_string('allowuserblockhiding', 'admin'), new lang_string('configallowuserblockhiding', 'admin'), 1));
     $temp->add(new admin_setting_configcheckbox('allowblockstodock', new lang_string('allowblockstodock', 'admin'), new lang_string('configallowblockstodock', 'admin'), 1));
     $temp->add(new admin_setting_configtextarea('custommenuitems', new lang_string('custommenuitems', 'admin'), new lang_string('configcustommenuitems', 'admin'), '', PARAM_TEXT, '50', '10'));
+    $temp->add(new admin_setting_configtextarea(
+        'customusermenuitems',
+        new lang_string('customusermenuitems', 'admin'),
+        new lang_string('configcustomusermenuitems', 'admin'),
+        'messages,message|/message/index.php|message
+myfiles,moodle|/user/files.php|download
+mybadges,badges|/badges/mybadges.php|award',
+        PARAM_TEXT,
+        '50',
+        '10'
+    ));
     $temp->add(new admin_setting_configcheckbox('enabledevicedetection', new lang_string('enabledevicedetection', 'admin'), new lang_string('configenabledevicedetection', 'admin'), 1));
     $temp->add(new admin_setting_devicedetectregex('devicedetectregex', new lang_string('devicedetectregex', 'admin'), new lang_string('devicedetectregex_desc', 'admin'), ''));
     $ADMIN->add('themes', $temp);
index bcb6af5..a3c36ac 100644 (file)
@@ -67,7 +67,7 @@ class tool_messageinbound_edit_handler_form extends moodleform {
             DAYSECS => get_string('oneday', 'tool_messageinbound'),
             WEEKSECS => get_string('oneweek', 'tool_messageinbound'),
             YEARSECS => get_string('oneyear', 'tool_messageinbound'),
-            '' => get_string('noexpiry', 'tool_messageinbound'),
+            0 => get_string('noexpiry', 'tool_messageinbound'),
         );
         $mform->addElement('select', 'defaultexpiration', get_string('defaultexpiration', 'tool_messageinbound'), $options);
         $mform->addHelpButton('defaultexpiration', 'defaultexpiration', 'tool_messageinbound');
index ffb1f4a..150129c 100644 (file)
@@ -45,7 +45,9 @@ class notification_task extends \core\task\adhoc_task {
             $subscriptionids = $data->subscriptionids;
             foreach ($subscriptionids as $id) {
                 if ($message = $this->generate_message($id, $eventobj)) {
+                    mtrace("Sending message to the user with id " . $message->userto->id . " for the subscription with id $id...");
                     message_send($message);
+                    mtrace("Sent.");
                 }
             }
         }
@@ -68,6 +70,10 @@ class notification_task extends \core\task\adhoc_task {
             return false;
         }
         $user = \core_user::get_user($subscription->userid);
+        if (empty($user)) {
+            // User doesn't exist. Should never happen, nothing to do return.
+            return false;
+        }
         $context = \context_user::instance($user->id, IGNORE_MISSING);
         if ($context === false) {
             // User context doesn't exist. Should never happen, nothing to do return.
index 0819aae..c42f837 100644 (file)
@@ -64,11 +64,13 @@ class renderable extends \table_sql implements \renderable {
     public function __construct($uniqueid, \moodle_url $url, $courseid = 0, $perpage = 100) {
         parent::__construct($uniqueid);
 
+        $this->set_attribute('id', 'toolmonitorrules_table');
         $this->set_attribute('class', 'toolmonitor managerules generaltable generalbox');
-        $this->define_columns(array('name', 'description', 'plugin', 'eventname', 'filters', 'manage'));
+        $this->define_columns(array('name', 'description', 'context', 'plugin', 'eventname', 'filters', 'manage'));
         $this->define_headers(array(
                 get_string('name'),
                 get_string('description'),
+                get_string('context', 'tool_monitor'),
                 get_string('plugin'),
                 get_string('eventname'),
                 get_string('frequency', 'tool_monitor'),
@@ -198,4 +200,15 @@ class renderable extends \table_sql implements \renderable {
             $this->initialbars($total > $pagesize);
         }
     }
+
+    /**
+     * Generate content for context column.
+     *
+     * @param \tool_monitor\rule $rule rule object
+     *
+     * @return string html used to display the context column field.
+     */
+    public function col_context(\tool_monitor\rule $rule) {
+        return ($rule->courseid == 0) ? get_string('system', 'tool_monitor') : get_string('course');
+    }
 }
index edf8b49..30d6b66 100644 (file)
@@ -71,8 +71,9 @@ class renderer extends \plugin_renderer_base {
      * @return string to display on the mangesubs page.
      */
     protected function render_course_select(rules $renderable) {
-        $select = $renderable->get_user_courses_select();
-        return $this->render($select);;
+        if ($select = $renderable->get_user_courses_select()) {
+            return $this->render($select);
+        }
     }
 
     /**
index f891e90..62182cc 100644 (file)
@@ -129,6 +129,7 @@ class rules extends \table_sql implements \renderable {
     public function col_select(\tool_monitor\rule $rule) {
         global $OUTPUT;
         $select = $rule->get_module_select($this->courseid);
+        $select->set_label(get_string('subscribeto', 'tool_monitor', $rule->get_name($this->context)), array('class' => 'accesshide'));
         return is_object($select) ? $OUTPUT->render($select) : $select;
     }
 
@@ -154,14 +155,24 @@ class rules extends \table_sql implements \renderable {
     /**
      * Gets a list of courses where the current user can subscribe to rules as a dropdown.
      *
-     * @return \single_select list of courses.
+     * @return \single_select|bool returns the list of courses, or false if the select box
+     *      should not be displayed.
      */
     public function get_user_courses_select() {
-        $courses = get_user_capability_course('tool/monitor:subscribe', null, true, 'fullname');
+        global $DB;
+
+        // If the number of courses on the site exceed the maximum drop down limit do not display the select box.
+        $numcourses = $DB->count_records('course');
+        if ($numcourses > COURSE_MAX_COURSES_PER_DROPDOWN) {
+            return false;
+        }
+
         $options = array(0 => get_string('site'));
-        $systemcontext = \context_system::instance();
-        foreach ($courses as $course) {
-            $options[$course->id] = format_text($course->fullname, array('context' => $systemcontext));
+        if ($courses = get_user_capability_course('tool/monitor:subscribe', null, true, 'fullname')) {
+            foreach ($courses as $course) {
+                $options[$course->id] = format_string($course->fullname, true,
+                    array('context' => \context_course::instance($course->id)));
+            }
         }
         $url = new \moodle_url('/admin/tool/monitor/index.php');
         $select = new \single_select($url, 'courseid', $options, $this->courseid);
index ba2236b..99003d6 100644 (file)
@@ -201,6 +201,8 @@ class subscription_manager {
             }
         }
 
+        $subscriptions->close();
+
         return $success;
     }
 
index fcef71b..5433d09 100644 (file)
@@ -28,6 +28,7 @@ $string['addrule'] = 'Add a new rule';
 $string['allevents'] = 'All events';
 $string['allmodules'] = 'All modules';
 $string['core'] = 'Core';
+$string['context'] = 'Context';
 $string['customisefilters'] = 'Select the frequency of the events';
 $string['customisemessage'] = 'Customise the notification message';
 $string['currentsubscriptions'] = 'Your current subscriptions';
@@ -92,6 +93,8 @@ $string['subcreatesuccess'] = "Subscription successfully created";
 $string['subdeletesuccess'] = "Subscription successfully removed";
 $string['subhelp'] = 'Subscription details';
 $string['subhelp_help'] = 'This subscription listens for when the event \'{$a->eventname}\' has been triggered in \'{$a->moduleinstance}\' {$a->frequency} time(s) in {$a->minutes} minute(s).';
+$string['subscribeto'] = 'Subscribe to rule "{$a}"';
+$string['system'] = "System";
 $string['taskcleanevents'] = 'Removes any unnecessary event monitor events';
 $string['title'] = '{$a->coursename} : {$a->reportname}';
 $string['monitor:managerules'] = 'Manage event monitor rules';
index 80f74d3..33287a8 100644 (file)
@@ -55,7 +55,7 @@ Feature: tool_monitor_rule
       | minutes           | 1                                                 |
       | Message template  | The forum post was created. {modulelink}          |
     And   I press "Save changes"
-    Then  I should see "New rule"
+    Then  "New rule" row "Context" column of "toolmonitorrules_table" table should contain "Course"
     And   I should see "I want a rule to monitor posts created on a forum"
     And   I should see "Forum"
     And   I should see "Post created"
@@ -116,7 +116,7 @@ Feature: tool_monitor_rule
       | minutes           | 1                                                 |
       | Message template  | The forum post was created. {modulelink}          |
     And   I press "Save changes"
-    Then  I should see "New rule"
+    Then  "New rule" row "Context" column of "toolmonitorrules_table" table should contain "System"
     And   I should see "I want a rule to monitor posts created on a forum"
     And   I should see "Forum"
     And   I should see "Post created"
index 4fe3660..f189a2a 100644 (file)
@@ -44,8 +44,8 @@ Feature: tool_monitor_subscriptions
     Given I log in as "teacher1"
     And   I follow "Course 1"
     And   I navigate to "Event monitoring" node in "My profile settings"
-    And   I set the field "courseid" to "Course 1"
-    When  I set the field "cmid" to "All events"
+    And   I set the field "Select a course" to "Course 1"
+    When  I set the field "Subscribe to rule \"New rule course level\"" to "All events"
     Then  I should see "Subscription successfully created"
     And   "#toolmonitorsubs_r0" "css_element" should exist
 
@@ -53,10 +53,10 @@ Feature: tool_monitor_subscriptions
     Given I log in as "teacher1"
     And   I follow "Course 1"
     And   I navigate to "Event monitoring" node in "My profile settings"
-    And   I set the field "courseid" to "Course 1"
-    And   I set the field "cmid" to "All events"
+    And   I set the field "Select a course" to "Course 1"
+    And   I set the field "Subscribe to rule \"New rule course level\"" to "All events"
     And   I should see "Subscription successfully created"
-    When  I click on "Delete subscription" "link"
+    When  I click on "Delete subscription" "link" in the "New rule course level" "table_row"
     And   I should see "Are you sure you want to delete this subscription for the rule \"New rule course level\"?"
     And   I press "Yes"
     Then  I should see "Subscription successfully removed"
@@ -65,19 +65,19 @@ Feature: tool_monitor_subscriptions
   Scenario: Subscribe to a rule on site level
     Given I log in as "admin"
     And   I navigate to "Event monitoring" node in "My profile settings"
-    And   I set the field "courseid" to "Site"
-    When  I set the field "cmid" to "All events"
+    And   I set the field "Select a course" to "Site"
+    When  I set the field "Subscribe to rule \"New rule site level\"" to "All events"
     Then  I should see "Subscription successfully created"
     And   "#toolmonitorsubs_r0" "css_element" should exist
 
   Scenario: Delete a subscription on site level
     Given I log in as "admin"
     And   I navigate to "Event monitoring" node in "My profile settings"
-    And   I set the field "courseid" to "Site"
-    And   I set the field "cmid" to "All events"
+    And   I set the field "Select a course" to "Site"
+    And   I set the field "Subscribe to rule \"New rule site level\"" to "All events"
     And   I should see "Subscription successfully created"
     And   "#toolmonitorsubs_r0" "css_element" should exist
-    When  I click on "Delete subscription" "link"
+    When  I click on "Delete subscription" "link" in the "New rule site level" "table_row"
     And   I should see "Are you sure you want to delete this subscription for the rule \"New rule site level\"?"
     And   I press "Yes"
     Then  I should see "Subscription successfully removed"
@@ -91,15 +91,14 @@ Feature: tool_monitor_subscriptions
     And   I am on homepage
     And   I follow "Course 1"
     And   I navigate to "Event monitoring" node in "My profile settings"
-    And   I set the field "courseid" to "Site"
-    And   I set the field "cmid" to "All events"
+    And   I set the field "Select a course" to "Site"
+    And   I set the field "Subscribe to rule \"New rule site level\"" to "All events"
     And   I should see "Subscription successfully created"
     And   "#toolmonitorsubs_r0" "css_element" should exist
     And   I am on homepage
     And   I trigger cron
     And   I am on homepage
-    And   I expand "My profile" node
-    When  I follow "Messages"
+    When  I navigate to "Messages" node in "My profile"
     And   I follow "Do not reply to this email (1)"
     Then  I should see "The course was viewed."
 
@@ -111,16 +110,15 @@ Feature: tool_monitor_subscriptions
     And   I am on homepage
     And   I follow "Course 1"
     And   I navigate to "Event monitoring" node in "My profile settings"
-    And   I set the field "courseid" to "Course 1"
-    And   I set the field "cmid" to "All events"
+    And   I set the field "Select a course" to "Course 1"
+    And   I set the field "Subscribe to rule \"New rule course level\"" to "All events"
     And   I should see "Subscription successfully created"
     And   "#toolmonitorsubs_r0" "css_element" should exist
     And   I am on homepage
     And   I follow "Course 1"
     And   I trigger cron
     And   I am on homepage
-    And   I expand "My profile" node
-    When  I follow "Messages"
+    When  I navigate to "Messages" node in "My profile"
     And   I follow "Do not reply to this email (1)"
     Then  I should see "The course was viewed."
 
index 26d4086..4e60183 100644 (file)
@@ -270,10 +270,12 @@ class tool_monitor_eventobservers_testcase extends advanced_testcase {
      * Run adhoc tasks.
      */
     protected function run_adhock_tasks() {
+        ob_start();
         while ($task = \core\task\manager::get_next_adhoc_task(time())) {
             $task->execute();
             \core\task\manager::adhoc_task_complete($task);
         }
+        ob_clean(); // Suppress mtrace debugging info.
     }
 
     /**
index 9f55c19..730e18d 100644 (file)
@@ -264,13 +264,12 @@ class tool_monitor_events_testcase extends advanced_testcase {
         $this->assertEquals(context_system::instance(), $event->get_context());
 
         // Now, create a bunch of subscriptions for the rule we created.
+        $subids = array();
         $sub->courseid = $course->id;
         for ($i = 1; $i <= 10; $i++) {
             $sub->userid = $i;
             $subscription = $monitorgenerator->create_subscription($sub);
-            if ($i == 1) {
-                $subscription1 = $subscription;
-            }
+            $subids[$subscription->id] = $subscription;
         }
 
         // Trigger and capture the events.
@@ -281,12 +280,17 @@ class tool_monitor_events_testcase extends advanced_testcase {
         // Check that there were 10 events in total.
         $this->assertCount(10, $events);
 
-        // Get the first event and ensure it is valid (we can assume the rest are the same).
-        $event = reset($events);
-        $this->assertInstanceOf('\tool_monitor\event\subscription_deleted', $event);
-        $this->assertEquals(context_course::instance($course->id), $event->get_context());
-        $this->assertEquals($subscription1->id, $event->objectid);
-        $this->assertEventContextNotUsed($event);
+        // Get all the events and ensure they are valid.
+        foreach ($events as $event) {
+            $this->assertInstanceOf('\tool_monitor\event\subscription_deleted', $event);
+            $this->assertEquals(context_course::instance($course->id), $event->get_context());
+            $this->assertEventContextNotUsed($event);
+            $this->assertArrayHasKey($event->objectid, $subids);
+            unset($subids[$event->objectid]);
+        }
+
+        // We should have found all the subscriptions.
+        $this->assertEmpty($subids);
     }
 
     /**
index 79184dd..a11a2be 100644 (file)
Binary files a/admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown-debug.js and b/admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown-debug.js differ
index c62a013..bf74086 100644 (file)
Binary files a/admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown-min.js and b/admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown-min.js differ
index 79184dd..a11a2be 100644 (file)
Binary files a/admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown.js and b/admin/tool/monitor/yui/build/moodle-tool_monitor-dropdown/moodle-tool_monitor-dropdown.js differ
index ff839b6..1dff1af 100644 (file)
@@ -55,6 +55,9 @@ Y.extend(DropDown, Y.Base, {
     initializer: function() {
         this.plugin = Y.one(SELECTORS.PLUGIN);
         this.eventname = Y.one(SELECTORS.EVENTNAME);
+        var selection = this.eventname.get('value'); // Get selected event name.
+        this.updateEventsList();
+        this.updateSelection(selection);
         this.plugin.on('change', this.updateEventsList, this);
     },
 
@@ -83,6 +86,20 @@ Y.extend(DropDown, Y.Base, {
             }
         }, this);
 
+    },
+
+    /**
+     * Method to update the selected node from the options list.
+     *
+     * @method updateSelection
+     * @param {string} selection The options node value that should be selected.
+     */
+    updateSelection: function(selection) {
+        this.eventname.get('options').each(function(opt) {
+            if (opt.get('value') === selection) {
+                opt.set('selected', 'selected');
+            }
+        }, this);
     }
 }, {
     NAME: 'dropDown',
index 25775cb..accf417 100644 (file)
@@ -35,12 +35,12 @@ function get_selection_data($ufiltering) {
 
     if ($scount) {
         if ($scount < MAX_BULK_USERS) {
-            $in = implode(',', $SESSION->bulk_users);
+            $bulkusers = $SESSION->bulk_users;
         } else {
             $bulkusers = array_slice($SESSION->bulk_users, 0, MAX_BULK_USERS, true);
-            $in = implode(',', $bulkusers);
         }
-        $userlist['susers'] = $DB->get_records_select_menu('user', "id IN ($in)", null, 'fullname', 'id,'.$DB->sql_fullname().' AS fullname');
+        list($in, $inparams) = $DB->get_in_or_equal($bulkusers);
+        $userlist['susers'] = $DB->get_records_select_menu('user', "id $in", $inparams, 'fullname', 'id,'.$DB->sql_fullname().' AS fullname');
     }
 
     return $userlist;
index 2803711..0b014bb 100644 (file)
@@ -41,13 +41,16 @@ class auth_plugin_manual extends auth_plugin_base {
      * The name of the component. Used by the configuration.
      */
     const COMPONENT_NAME = 'auth_manual';
+    const LEGACY_COMPONENT_NAME = 'auth/manual';
 
     /**
      * Constructor.
      */
     function auth_plugin_manual() {
         $this->authtype = 'manual';
-        $this->config = get_config(self::COMPONENT_NAME);
+        $config = get_config(self::COMPONENT_NAME);
+        $legacyconfig = get_config(self::LEGACY_COMPONENT_NAME);
+        $this->config = (object)array_merge((array)$legacyconfig, (array)$config);
     }
 
     /**
index 34ede3b..959c2fb 100644 (file)
@@ -96,7 +96,7 @@ class behat_auth extends behat_base {
         // hamburger.
 
         // However, the user menu *always* needs to be expanded.
-        $xpath ="//div[@class='usermenu']//a[contains(concat(' ', @class, ' '), ' toggle-display ')]";
+        $xpath = "//div[@class='usermenu']//a[contains(concat(' ', @class, ' '), ' toggle-display ')]";
         array_unshift($steps, new When('I click on "'.$xpath.'" "xpath_element"'));
 
         return $steps;
index 332a956..0728dac 100644 (file)
@@ -338,7 +338,7 @@ class cc2moodle {
 
                     $replace_values = array($i,
                                             $i - 1,
-                                            $topic['title'],
+                                            entities::safexml($topic['title']),
                                             $node_node_course_sections_section_mods_mod);
 
                 } else {
index 5265fa0..bbdfd7f 100644 (file)
@@ -1292,7 +1292,9 @@ class moodle1_question_bank_handler extends moodle1_xml_handler {
      * Closes the questions wrapper
      */
     public function on_questions_end() {
-        $this->xmlwriter->end_tag('questions');
+        if ($this->questionswrapperwritten) {
+            $this->xmlwriter->end_tag('questions');
+        }
     }
 
     /**
index d277721..209e992 100644 (file)
@@ -31,7 +31,7 @@ Feature: Award badges
     And I expand all fieldsets
     And I set the field "Phone" to "123456789"
     And I press "Update profile"
-    And I follow "My badges"
+    And I navigate to "My badges" node in "My profile"
     Then I should see "Profile Badge"
     And I should not see "There are no badges available."
 
@@ -64,8 +64,7 @@ Feature: Award badges
     Then I should see "Recipients (2)"
     And I log out
     And I log in as "student"
-    And I expand "My profile" node
-    And I follow "My badges"
+    And I navigate to "My badges" node in "My profile"
     Then I should see "Site Badge"
 
   @javascript
@@ -109,8 +108,7 @@ Feature: Award badges
     And I log out
     And I log in as "student1"
     And I follow "Course 1"
-    And I expand "My profile" node
-    And I follow "My badges"
+    And I navigate to "My badges" node in "My profile"
     Then I should see "Course Badge"
 
   @javascript
@@ -158,14 +156,12 @@ Feature: Award badges
     And I log out
     And I log in as "student1"
     And I follow "Course 1"
-    And I expand "My profile" node
-    And I follow "My badges"
+    And I navigate to "My badges" node in "My profile"
     Then I should see "There are no badges available."
     And I follow "Home"
     And I follow "Course 1"
     And I press "Mark as complete: Test assignment name"
-    And I expand "My profile" node
-    And I follow "My badges"
+    And I navigate to "My badges" node in "My profile"
     Then I should see "Course Badge"
 
   @javascript
@@ -219,8 +215,7 @@ Feature: Award badges
     And I log out
     And I log in as "student1"
     And I follow "Course 1"
-    And I expand "My profile" node
-    And I follow "My badges"
+    And I navigate to "My badges" node in "My profile"
     Then I should see "There are no badges available."
     And I follow "Home"
     And I follow "Course 1"
@@ -237,6 +232,5 @@ Feature: Award badges
     And I am on homepage
     And I log out
     And I log in as "student1"
-    And I expand "My profile" node
-    And I follow "My badges"
+    And I navigate to "My badges" node in "My profile"
     Then I should see "Course Badge"
index ce97602..6a46c36 100644 (file)
@@ -294,12 +294,12 @@ class block_recent_activity extends block_base {
                 if (!isset($modinfo->instances[$modname][$instanceid])) {
                     continue;
                 }
-                $entry['cmid'] = $modinfo->instances[$modname][$instanceid]->id;
                 if ($log->action == 'add mod') {
                     $entry['action'] = 0;
                 } else {
                     $entry['action'] = 1;
                 }
+                $entry['cmid'] = $modinfo->instances[$modname][$instanceid]->id;
             }
             $entries[] = $entry;
         }
index 97f2ca8..a84e3d1 100644 (file)
@@ -128,7 +128,7 @@ class block_site_main_menu extends block_list {
                 } else {
                     $editbuttons = '';
                 }
-                if ($mod->visible || has_capability('moodle/course:viewhiddenactivities', $context)) {
+                if ($mod->visible || has_capability('moodle/course:viewhiddenactivities', $mod->context)) {
                     if ($ismoving) {
                         if ($mod->id == $USER->activitycopy) {
                             continue;
index 5e127d8..6c6971b 100644 (file)
@@ -130,7 +130,7 @@ class block_social_activities extends block_list {
                 } else {
                     $editbuttons = '';
                 }
-                if ($mod->visible || has_capability('moodle/course:viewhiddenactivities', $context)) {
+                if ($mod->visible || has_capability('moodle/course:viewhiddenactivities', $mod->context)) {
                     if ($ismoving) {
                         if ($mod->id == $USER->activitycopy) {
                             continue;
@@ -169,5 +169,3 @@ class block_social_activities extends block_list {
         return $this->content;
     }
 }
-
-
index 4405da4..e4d51ec 100644 (file)
@@ -556,6 +556,9 @@ class cachestore_mongodb extends cache_store implements cache_is_configurable {
         }
 
         $store = new cachestore_mongodb('Test mongodb', $configuration);
+        if (!$store->is_ready()) {
+            return false;
+        }
         $store->initialise($definition);
 
         return $store;
@@ -581,6 +584,9 @@ class cachestore_mongodb extends cache_store implements cache_is_configurable {
         $configuration['usesafe'] = 1;
 
         $store = new cachestore_mongodb('Test mongodb', $configuration);
+        if (!$store->is_ready()) {
+            return false;
+        }
         $store->initialise($definition);
 
         return $store;
index ba0fc08..b435842 100644 (file)
@@ -318,9 +318,9 @@ function calendar_get_mini($courses, $groups, $users, $calmonth = false, $calyea
     }
 
     // Now display all the calendar
-    $daytime = $display->tstart - DAYSECS;
+    $daytime = strtotime('-1 day', $display->tstart);
     for($day = 1; $day <= $display->maxdays; ++$day, ++$dayweek) {
-        $daytime += DAYSECS;
+        $daytime = strtotime('+1 day', $daytime);
         if($dayweek > $display->maxwday) {
             // We need to change week (table row)
             $content .= '</tr><tr>';
@@ -932,8 +932,8 @@ function calendar_top_controls($type, $data) {
         case 'day':
             $days = calendar_get_days();
 
-            $prevtimestamp = $time - DAYSECS;
-            $nexttimestamp = $time + DAYSECS;
+            $prevtimestamp = strtotime('-1 day', $time);
+            $nexttimestamp = strtotime('+1 day', $time);
 
             $prevdate = $calendartype->timestamp_to_date_array($prevtimestamp);
             $nextdate = $calendartype->timestamp_to_date_array($nexttimestamp);
@@ -1679,7 +1679,7 @@ function calendar_format_event_time($event, $now, $linkparams = null, $usecommon
             $timeend = calendar_time_representation($event->timestart + $event->timeduration);
 
             // Set printable representation.
-            if ($now >= $usermidnightstart && $now < ($usermidnightstart + DAYSECS)) {
+            if ($now >= $usermidnightstart && $now < strtotime('+1 day', $usermidnightstart)) {
                 $url = calendar_get_link_href(new moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $endtime);
                 $eventtime = $timestart . ' <strong>&raquo;</strong> ' . html_writer::link($url, $dayend) . $timeend;
             } else {
@@ -2775,7 +2775,7 @@ class calendar_information {
      * @return int tomorrow timestamp
      */
     public function timestamp_tomorrow() {
-        return $this->time + DAYSECS;
+        return strtotime('+1 day', $this->time);
     }
     /**
      * Adds the pretend blocks for the calendar
index 8f0c7a5..91e7a48 100644 (file)
@@ -498,9 +498,9 @@ class core_calendar_renderer extends plugin_renderer_base {
             $weekend = intval($CFG->calendar_weekend);
         }
 
-        $daytime = $display->tstart - DAYSECS;
+        $daytime = strtotime('-1 day', $display->tstart);
         for ($day = 1; $day <= $display->maxdays; ++$day, ++$dayweek) {
-            $daytime = $daytime + DAYSECS;
+            $daytime = strtotime('+1 day', $daytime);
             if($dayweek > $display->maxwday) {
                 // We need to change week (table row)
                 $table->data[] = $row;
index dfe2970..4c9289b 100644 (file)
@@ -69,8 +69,7 @@ class format_singleactivity extends format_base {
     public function extend_course_navigation($navigation, navigation_node $node) {
         // Display orphaned activities for the users who can see them.
         $context = context_course::instance($this->courseid);
-        if (has_all_capabilities(array('moodle/course:viewhiddensections',
-                'moodle/course:viewhiddenactivities'), $context)) {
+        if (has_capability('moodle/course:viewhiddensections', $context)) {
             $modinfo = get_fast_modinfo($this->courseid);
             if (!empty($modinfo->sections[1])) {
                 $section1 = $modinfo->get_section_info(1);
@@ -80,7 +79,9 @@ class format_singleactivity extends format_base {
                 $orphanednode->nodetype = navigation_node::NODETYPE_BRANCH;
                 $orphanednode->add_class('orphaned');
                 foreach ($modinfo->sections[1] as $cmid) {
-                    $this->navigation_add_activity($orphanednode, $modinfo->cms[$cmid]);
+                    if (has_capability('moodle/course:viewhiddenactivities', context_module::instance($cmid))) {
+                        $this->navigation_add_activity($orphanednode, $modinfo->cms[$cmid]);
+                    }
                 }
             }
         }
index 2cca5c7..3247db4 100644 (file)
@@ -78,7 +78,7 @@ if (!empty($add)) {
     $returntomod = optional_param('return', 0, PARAM_BOOL);
     redirect("$CFG->wwwroot/course/modedit.php?update=$update&return=$returntomod&sr=$sectionreturn");
 
-} else if (!empty($duplicate)) {
+} else if (!empty($duplicate) and confirm_sesskey()) {
      $cm     = get_coursemodule_from_id('', $duplicate, 0, true, MUST_EXIST);
      $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST);
 
index ae75273..81eefa6 100644 (file)
 require_once(dirname(dirname(__FILE__)) . '/config.php');
 
 $cmid           = required_param('cmid', PARAM_INT);
-$courseid       = optional_param('course', PARAM_INT);
+$courseid       = required_param('course', PARAM_INT);
 $sectionreturn  = optional_param('sr', null, PARAM_INT);
 
+require_sesskey();
+
 debugging('Please use moodle_url(\'/course/mod.php\', array(\'duplicate\' => $cmid
     , \'id\' => $courseid, \'sesskey\' => sesskey(), \'sr\' => $sectionreturn)))
     instead of new moodle_url(\'/course/modduplicate.php\', array(\'cmid\' => $cmid
index ba7ca0e..574aea4 100644 (file)
@@ -770,8 +770,7 @@ class core_course_renderer extends plugin_renderer_base {
         if ($mod->uservisible) {
             $conditionalhidden = $this->is_cm_conditionally_hidden($mod);
             $accessiblebutdim = (!$mod->visible || $conditionalhidden) &&
-                has_capability('moodle/course:viewhiddenactivities',
-                        context_course::instance($mod->course));
+                has_capability('moodle/course:viewhiddenactivities', $mod->context);
             if ($accessiblebutdim) {
                 $linkclasses .= ' dimmed';
                 $textclasses .= ' dimmed_text';
@@ -828,8 +827,7 @@ class core_course_renderer extends plugin_renderer_base {
         if ($mod->uservisible) {
             $conditionalhidden = $this->is_cm_conditionally_hidden($mod);
             $accessiblebutdim = (!$mod->visible || $conditionalhidden) &&
-                has_capability('moodle/course:viewhiddenactivities',
-                        context_course::instance($mod->course));
+                has_capability('moodle/course:viewhiddenactivities', $mod->context);
             if ($accessiblebutdim) {
                 $textclasses .= ' dimmed_text';
                 if ($conditionalhidden) {
index 756c626..9f70969 100644 (file)
@@ -589,7 +589,8 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $label = $this->getDataGenerator()->create_module('label', array('course' => $course->id,
             'intro' => $labeldescription));
         $labelcm = get_coursemodule_from_instance('label', $label->id);
-        $url = $this->getDataGenerator()->create_module('url', array('course' => $course->id));
+        $url = $this->getDataGenerator()->create_module('url', array('course' => $course->id,
+            'name' => 'URL: % & $ ../'));
 
         // Set the required capabilities by the external function.
         $context = context_course::instance($course->id);
index 5d680b8..b090740 100644 (file)
@@ -467,7 +467,9 @@ class edit_category_form extends moodleform {
                 }
 
                 // Remove fields used by natural weighting if the parent category is not using natural weighting.
-                if ($parent_category->aggregation != GRADE_AGGREGATE_SUM) {
+                // Or if the item is a scale and scales are not used in aggregation.
+                if ($parent_category->aggregation != GRADE_AGGREGATE_SUM
+                        || (empty($CFG->grade_includescalesinaggregation) && $grade_item->gradetype == GRADE_TYPE_SCALE)) {
                     if ($mform->elementExists('grade_item_weightoverride')) {
                         $mform->removeElement('grade_item_weightoverride');
                     }
index c89631b..f8a08c6 100644 (file)
@@ -310,7 +310,9 @@ class edit_item_form extends moodleform {
             }
 
             // Remove fields used by natural weighting if the parent category is not using natural weighting.
-            if ($parent_category->aggregation != GRADE_AGGREGATE_SUM) {
+            // Or if the item is a scale and scales are not used in aggregation.
+            if ($parent_category->aggregation != GRADE_AGGREGATE_SUM
+                    || (empty($CFG->grade_includescalesinaggregation) && $grade_item->gradetype == GRADE_TYPE_SCALE)) {
                 if ($mform->elementExists('weightoverride')) {
                     $mform->removeElement('weightoverride');
                 }
index da33c55..fe90e41 100644 (file)
@@ -113,7 +113,7 @@ class grade_edit_tree {
 
         $object = $element['object'];
         $eid    = $element['eid'];
-        $object->name = $this->gtree->get_element_header($element, true, true, true, true);
+        $object->name = $this->gtree->get_element_header($element, true, true, true, true, true);
         $object->stripped_name = $this->gtree->get_element_header($element, false, false, false);
 
         $is_category_item = false;
@@ -188,7 +188,7 @@ class grade_edit_tree {
             $item = $category->get_grade_item();
 
             // Add aggregation coef input if not a course item and if parent category has correct aggregation type
-            $dimmed = ($item->is_hidden()) ? 'dimmed' : '';
+            $dimmed = ($item->is_hidden()) ? 'dimmed_text' : '';
 
             // Before we print the category's row, we must find out how many rows will appear below it (for the filler cell's rowspan)
             $aggregation_position = grade_get_setting($COURSE->id, 'aggregationposition', $CFG->grade_aggregationposition);
@@ -706,6 +706,7 @@ class grade_edit_tree_column_weight extends grade_edit_tree_column {
     }
 
     public function get_item_cell($item, $params) {
+        global $CFG;
         if (empty($params['element'])) {
             throw new Exception('Array key (element) missing from 2nd param of grade_edit_tree_column_weightorextracredit::get_item_cell($item, $params)');
         }
@@ -715,7 +716,8 @@ class grade_edit_tree_column_weight extends grade_edit_tree_column {
 
         if (!in_array($object->itemtype, array('courseitem', 'categoryitem', 'category'))
                 && !in_array($object->gradetype, array(GRADE_TYPE_NONE, GRADE_TYPE_TEXT))
-                && (!$object->is_outcome_item() || $object->load_parent_category()->aggregateoutcomes)) {
+                && (!$object->is_outcome_item() || $object->load_parent_category()->aggregateoutcomes)
+                && ($object->gradetype != GRADE_TYPE_SCALE || !empty($CFG->grade_includescalesinaggregation))) {
             $itemcell->text = grade_edit_tree::get_weight_input($item);
         }
 
@@ -837,7 +839,7 @@ class grade_edit_tree_column_select extends grade_edit_tree_column {
         $selectnone = new action_link(new moodle_url('#'), get_string('none'), new component_action('click', 'togglecheckboxes', array('eid' => $params['eid'], 'check' => false)));
 
         $categorycell = parent::get_category_cell($category, $levelclass, $params);
-        $categorycell->text = $OUTPUT->render($selectall) . '<br />' . $OUTPUT->render($selectnone);
+        $categorycell->text = $OUTPUT->render($selectall) . ' / ' . $OUTPUT->render($selectnone);
         return $categorycell;
     }
 
index 5536aca..8199efa 100644 (file)
@@ -645,7 +645,8 @@ class gradingform_guide_controller extends gradingform_controller {
         $returnvalue['maxscore'] = $maxscore;
         $returnvalue['minscore'] = 0;
         if (!empty($this->moduleinstance->grade)) {
-            $returnvalue['modulegrade'] = $this->moduleinstance->grade;
+            $graderange = make_grades_menu($this->moduleinstance->grade);
+            $returnvalue['modulegrade'] = count($graderange) - 1;
         }
         return $returnvalue;
     }
index 40aa1c5..3470a69 100644 (file)
@@ -1334,32 +1334,34 @@ class grade_structure {
      * @param bool  $icon Whether or not to display an icon with this header
      * @param bool  $spacerifnone return spacer if no icon found
      * @param bool  $withdescription Show description if defined by this item.
+     * @param bool  $fulltotal If the item is a category total, returns $categoryname."total"
+     *                         instead of "Category total" or "Course total"
      *
      * @return string header
      */
-    public function get_element_header(&$element, $withlink=false, $icon=true, $spacerifnone=false, $withdescription=false) {
+    public function get_element_header(&$element, $withlink = false, $icon = true, $spacerifnone = false,
+        $withdescription = false, $fulltotal = false) {
         $header = '';
 
         if ($icon) {
             $header .= $this->get_element_icon($element, $spacerifnone);
         }
 
-        $header .= $element['object']->get_name();
+        $header .= $element['object']->get_name($fulltotal);
 
         if ($element['type'] != 'item' and $element['type'] != 'categoryitem' and
             $element['type'] != 'courseitem') {
             return $header;
         }
 
-        if ($withlink) {
-            $url = $this->get_activity_link($element);
-            if ($url) {
-                $a = new stdClass();
-                $a->name = get_string('modulename', $element['object']->itemmodule);
-                $title = get_string('linktoactivity', 'grades', $a);
+        if ($withlink && $url = $this->get_activity_link($element)) {
+            $a = new stdClass();
+            $a->name = get_string('modulename', $element['object']->itemmodule);
+            $title = get_string('linktoactivity', 'grades', $a);
 
-                $header = html_writer::link($url, $header, array('title' => $title));
-            }
+            $header = html_writer::link($url, $header, array('title' => $title));
+        } else {
+            $header = html_writer::span($header);
         }
 
         if ($withdescription) {
index 26b4933..bdd7727 100644 (file)
@@ -816,7 +816,7 @@ class grade_report_grader extends grade_report {
                         $arrow = $this->get_sort_arrow('move', $sortlink);
                     }
 
-                    $headerlink = $this->gtree->get_element_header($element, true, $showactivityicons, false);
+                    $headerlink = $this->gtree->get_element_header($element, true, $showactivityicons, false, false, true);
 
                     $itemcell = new html_table_cell();
                     $itemcell->attributes['class'] = $type . ' ' . $catlevel . ' highlightable'. ' i'. $element['object']->id;
index 0cb0321..87c8cdb 100644 (file)
@@ -108,9 +108,7 @@ class grade extends tablelike implements selectable_items, filterable_items {
     public function original_definition() {
         $def = array('finalgrade', 'feedback');
 
-        if ($this->requiresextra) {
-            $def[] = 'override';
-        }
+        $def[] = 'override';
 
         $def[] = 'exclude';
 
@@ -162,7 +160,6 @@ class grade extends tablelike implements selectable_items, filterable_items {
     public function original_headers() {
         return array(
             '', // For filter icon.
-            '', // For user picture.
             get_string('firstname') . ' (' . get_string('alternatename') . ') ' . get_string('lastname'),
             get_string('range', 'grades'),
             get_string('grade', 'grades'),
@@ -210,12 +207,31 @@ class grade extends tablelike implements selectable_items, filterable_items {
 
         $line = array(
             $OUTPUT->action_icon($this->format_link('user', $item->id), new pix_icon('t/editstring', $iconstring)),
-            $OUTPUT->user_picture($item),
+            $OUTPUT->user_picture($item, array('visibletoscreenreaders' => false)) .
             html_writer::link($url, $fullname),
             $this->item_range()
         );
+        $lineclasses = array(
+            "action",
+            "user",
+            "range"
+        );
+        $outputline = array();
+        $i = 0;
+        foreach ($line as $key => $value) {
+            $cell = new \html_table_cell($value);
+            if ($isheader = $i == 1) {
+                $cell->header = $isheader;
+                $cell->scope = "row";
+            }
+            if (array_key_exists($key, $lineclasses)) {
+                $cell->attributes['class'] = $lineclasses[$key];
+            }
+            $outputline[] = $cell;
+            $i++;
+        }
 
-        return $this->format_definition($line, $grade);
+        return $this->format_definition($outputline, $grade);
     }
 
     /**
@@ -269,6 +285,15 @@ class grade extends tablelike implements selectable_items, filterable_items {
         return $this->item->get_name();
     }
 
+    /**
+     * Get the summary for this table.
+     *
+     * @return string
+     */
+    public function summary() {
+        return get_string('summarygrade', 'gradereport_singleview');
+    }
+
     /**
      * Process the data from the form.
      *
index 937fad9..c718787 100644 (file)
@@ -59,6 +59,13 @@ abstract class tablelike extends screen {
      */
     public abstract function format_line($item);
 
+    /**
+     * Get the summary for this table.
+     *
+     * @return string
+     */
+    public abstract function summary();
+
     /**
      * Get the table headers
      *
@@ -166,6 +173,11 @@ abstract class tablelike extends screen {
 
         $table->head = $this->headers();
 
+        $summary = $this->summary();
+        if (!empty($summary)) {
+            $table->summary = $summary;
+        }
+
         // To be used for extra formatting.
         $this->index = 0;
         $this->total = count($this->items);
index c588d6e..d98bb1f 100644 (file)
@@ -139,7 +139,6 @@ class user extends tablelike implements selectable_items {
     public function original_headers() {
         return array(
             '', // For filter icon.
-            '', // For activity icon.
             get_string('assessmentname', 'gradereport_singleview'),
             get_string('gradecategory', 'grades'),
             get_string('range', 'grades'),
@@ -192,14 +191,44 @@ class user extends tablelike implements selectable_items {
         $gradetreeitem['object'] = $item;
         $gradetreeitem['userid'] = $this->item->id;
 
-        $itemlabel = $this->structure->get_element_header($gradetreeitem, true, false);
+        $itemlabel = $this->structure->get_element_header($gradetreeitem, true, false, false, false, true);
         $grade->label = $item->get_name();
 
+        $itemlabel = $item->get_name();
+        if (!empty($realmodid)) {
+            $url = new moodle_url('/mod/' . $item->itemmodule . '/view.php', array('id' => $realmodid));
+            $itemlabel = html_writer::link($url, $item->get_name());
+        }
+
         $line = array(
             $OUTPUT->action_icon($this->format_link('grade', $item->id), new pix_icon('t/editstring', $iconstring)),
-            $this->format_icon($item) . $lockicon, $itemlabel, $this->category($item), (new range($item))
+            $this->format_icon($item) . $lockicon . $itemlabel,
+            $this->category($item),
+            new range($item)
         );
-        return $this->format_definition($line, $grade);
+        $lineclasses = array(
+            "action",
+            "gradeitem",
+            "category",
+            "range"
+        );
+
+        $outputline = array();
+        $i = 0;
+        foreach ($line as $key => $value) {
+            $cell = new \html_table_cell($value);
+            if ($isheader = $i == 1) {
+                $cell->header = $isheader;
+                $cell->scope = "row";
+            }
+            if (array_key_exists($key, $lineclasses)) {
+                $cell->attributes['class'] = $lineclasses[$key];
+            }
+            $outputline[] = $cell;
+            $i++;
+        }
+
+        return $this->format_definition($outputline, $grade);
     }
 
     /**
@@ -252,6 +281,15 @@ class user extends tablelike implements selectable_items {
         return fullname($this->item);
     }
 
+    /**
+     * Get the summary for this table.
+     *
+     * @return string
+     */
+    public function summary() {
+        return get_string('summaryuser', 'gradereport_singleview');
+    }
+
     /**
      * Default pager
      *
index 58bf08b..d3d9f7f 100644 (file)
@@ -46,9 +46,13 @@ class finalgrade extends grade_attribute_format implements unique_value, be_disa
      */
     public function get_value() {
         $this->label = $this->grade->grade_item->itemname;
-        // Manual item raw grade support.
-        $val = $this->grade->grade_item->is_manual_item() && (!is_null($this->grade->rawgrade)) ?
-            $this->grade->rawgrade : $this->grade->finalgrade;
+
+        $isoverridden = $this->grade->is_overridden();
+        if (!empty($isoverridden)) {
+            $val = $this->grade->finalgrade;
+        } else {
+            $val = $this->grade->rawgrade;
+        }
 
         if ($this->grade->grade_item->scaleid) {
             return $val ? (int)$val : -1;
@@ -114,8 +118,8 @@ class finalgrade extends grade_attribute_format implements unique_value, be_disa
             return new dropdown_attribute(
                 $this->get_name(),
                 $options,
-                $this->get_value(),
                 $this->get_label(),
+                $this->get_value(),
                 $this->is_disabled(),
                 $this->get_tabindex()
             );
index 2b5bd32..7264b71 100644 (file)
@@ -46,3 +46,5 @@ $string['overridefor'] = 'Override for {$a}';
 $string['overridenone'] = 'Override no grades';
 $string['pluginname'] = 'Single view';
 $string['singleview:view'] = 'View report';
+$string['summarygrade'] = 'A table of users, with columns for range, grade, feedback, and whether to override or exclude a particular grade.';
+$string['summaryuser'] = 'A table of grade items, with columns for grade category, range, grade, feedback, and whether to override or exclude a particular grade.';
index cdfef58..a2a7470 100644 (file)
 .path-grade-report-singleview input[name^="finalgrade"] {
     width: 50px;
 }
+.path-grade-report-singleview .generaltable tbody th {
+  white-space: nowrap;
+}
+.path-grade-report-singleview .generaltable tbody th > * {
+  display: inline-block;
+  vertical-align: middle;
+  margin: 0 2px;
+}
 
 .path-grade-report-singleview #region-main h2, .paging{
    text-align: center;
index 8683961..0c90836 100644 (file)
@@ -15,9 +15,15 @@ Feature: We can use Single view
       | student2 | Student | 2 | student1@asd.com | s2 | holly |
       | student3 | Student | 3 | student1@asd.com | s3 | anna |
       | student4 | Student | 4 | student1@asd.com | s4 | zac |
+    And the following "scales" exist:
+      | name | scale |
+      | Test Scale | Disappointing, Good, Very good, Excellent |
     And the following "grade items" exist:
-      | itemname | course  |
-      | new grade item 1 | C1 |
+      | itemname | course | gradetype | scale |
+      | new grade item 1 | C1 | Scale | Test Scale |
+    And the following "scales" exist:
+      | name       | scale                                     |
+      | Test Scale | Disappointing, Good, Very good, Excellent |
     And the following "course enrolments" exist:
       | user | course | role |
       | teacher1 | C1 | editingteacher |
@@ -35,6 +41,9 @@ Feature: We can use Single view
       | assign | C1 | a2 | Test assignment two | Submit something! | 100 |
       | assign | C1 | a3 | Test assignment three | Submit something! | 150 |
       | assign | C1 | a4 | Test assignment four | Submit nothing! | 150 |
+    And the following "grade items" exist:
+      | itemname | course | gradetype |
+      | Test grade item | C1 | Scale |
     And I log in as "teacher1"
     And I follow "Course 1"
     And I follow "Grades"
@@ -66,6 +75,12 @@ Feature: We can use Single view
         | Test assignment three |
         | 12.05 |
         | Excluded |
+    And I click on "Single view" "link"
+    And I click on "new grade item 1" "option"
+    And I click on "Very good" "option"
+    And I press "Update"
+    And the following should exist in the "generaltable" table:
+        | Grade for james (Student) 1 | "Very good" |
 
   Scenario: Single view links work on grade report.
     Given I follow "Single view for Test assignment one"
index e02c99f..a7f8633 100644 (file)
@@ -385,7 +385,7 @@ class grade_report_user extends grade_report {
         $grade_object = $element['object'];
         $eid = $grade_object->id;
         $element['userid'] = $this->user->id;
-        $fullname = $this->gtree->get_element_header($element, true, true, true, true);
+        $fullname = $this->gtree->get_element_header($element, true, true, true, true, true);
         $data = array();
         $hidden = '';
         $excluded = '';
index 13d1177..2b5dde9 100644 (file)
@@ -225,7 +225,7 @@ Feature: We can understand the gradebook user report
       | Test assignment four | 33.33 % | 10.00 | 1.11 % |
       | Test assignment five | 33.33 % | 70.00 | 7.78 % |
       | Test assignment six | 33.33 % | 30.00 | 3.33 % |
-      | Category totalWeighted mean of grades. | 33.33 % | 36.67 | - |
+      | Sub category totalWeighted mean of grades. | 33.33 % | 36.67 | - |
       | Course total | - | 156.67 | - |
 
   @javascript
@@ -244,5 +244,5 @@ Feature: We can understand the gradebook user report
       | Test assignment four | 33.33 % | 10.00 | 2.00 % |
       | Test assignment five | 33.33 % | 70.00 | 14.00 % |
       | Test assignment six | 33.33 % | 30.00 | 6.00 % |
-      | Category total | 60.00 % | 110.00 | - |
+      | Sub category total | 60.00 % | 110.00 | - |
       | Course total | - | 230.00 | - |
index 94f1cf3..b5884d7 100644 (file)
@@ -255,5 +255,14 @@ Feature: We can use natural aggregation and weights will be normalised to a tota
   And I set the field "Override weight of Test assignment six" to "1"
   And I set the field "Weight of Test assignment six" to "-25"
   And I press "Save changes"
-  Then the field "Weight of Test assignment six" matches value "0.0"
+  And the field "Weight of Test assignment six" matches value "0.0"
   And the field "Weight of Test assignment seven" matches value "100.0"
+  And I reset weights for grade category "Sub category 1"
+  And I set the field "Override weight of Test assignment five" to "1"
+  And I set the field "Override weight of Test assignment six" to "1"
+  And I set the field "Weight of Test assignment five" to "-10"
+  And I set the field "Weight of Test assignment six" to "120"
+  And I press "Save changes"
+  And the field "Weight of Test assignment five" matches value "0.0"
+  And the field "Weight of Test assignment six" matches value "100.0"
+  And the field "Weight of Test assignment seven" matches value "0.0"
index c04f0f8..67e98f4 100644 (file)
@@ -90,13 +90,13 @@ Feature: View gradebook when scales are used
     And the following should exist in the "user-grade" table:
       | Grade item          | Grade | Range | Percentage | Contribution to course total |
       | Test assignment one | C     | F–A   | 50.00 %    | 60.00 %                      |
-      | Category total      | 3.00  | 0–5   | 60.00 %    | -                            |
+      | Sub category 1 total      | 3.00  | 0–5   | 60.00 %    | -                            |
       | Course total        | 3.00  | 0–5   | 60.00 %    | -                            |
     And I set the field "jump" to "Categories and items"
     And the following should exist in the "grade_edit_tree_table" table:
       | Name                | Max grade |
       | Test assignment one | 5.00      |
-      | Category total      | 5.00      |
+      | Sub category 1 total      | 5.00      |
       | Course total        | 5.00      |
     And I log out
     And I log in as "student2"
@@ -105,7 +105,7 @@ Feature: View gradebook when scales are used
     And the following should exist in the "user-grade" table:
       | Grade item          | Grade | Range | Percentage | Contribution to course total |
       | Test assignment one | B     | F–A   | 75.00 %    | 80.00 %                      |
-      | Category total      | 4.00  | 0–5   | 80.00 %    | -                            |
+      | Sub category 1 total      | 4.00  | 0–5   | 80.00 %    | -                            |
       | Course total        | 4.00  | 0–5   | 80.00 %    | -                            |
 
   @javascript
@@ -138,13 +138,13 @@ Feature: View gradebook when scales are used
     And the following should exist in the "user-grade" table:
       | Grade item                   | Grade          | Range | Percentage    | Contribution to course total |
       | Test assignment one          | C              | F–A   | 50.00 %       | <contrib3>                   |
-      | Category total<aggregation>. | 3.00           | 1–5   | 50.00 %       | -                            |
+      | Sub category (<aggregation>) total<aggregation>. | 3.00           | 1–5   | 50.00 %       | -                            |
       | Course total<aggregation>.   | <coursetotal3> | 0–100 | <courseperc3> | -                            |
     And I set the field "jump" to "Categories and items"
     And the following should exist in the "grade_edit_tree_table" table:
       | Name                | Max grade |
       | Test assignment one | A (5)     |
-      | Category total<aggregation>. |           |
+      | Sub category (<aggregation>) total<aggregation>. |           |
       | Course total<aggregation>.   |           |
     And I log out
     And I log in as "student2"
@@ -153,7 +153,7 @@ Feature: View gradebook when scales are used
     And the following should exist in the "user-grade" table:
       | Grade item                   | Grade          | Range | Percentage    | Contribution to course total |
       | Test assignment one          | B              | F–A   | 75.00 %       | <contrib2>                   |
-      | Category total<aggregation>. | 4.00           | 1–5   | 75.00 %       | -                            |
+      | Sub category (<aggregation>) total<aggregation>. | 4.00           | 1–5   | 75.00 %       | -                            |
       | Course total<aggregation>.   | <coursetotal2> | 0–100 | <courseperc2> | -                            |
 
     Examples:
diff --git a/grade/tests/behat/grade_scales_aggregation.feature b/grade/tests/behat/grade_scales_aggregation.feature
new file mode 100644 (file)
index 0000000..7106000
--- /dev/null
@@ -0,0 +1,121 @@
+@core @core_grades
+Feature: Control the aggregation of the scales
+  In order to use control the aggregation of the scales
+  As an admin
+  I can change use administration setting
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+    And the following "users" exist:
+      | username | firstname | lastname | email            | idnumber |
+      | teacher1 | Teacher   | 1        | teacher1@asd.com | t1       |
+      | student1 | Student   | 1        | student1@asd.com | s1       |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | student1 | C1     | student        |
+    And I log in as "admin"
+    And I set the following administration settings values:
+      | grade_aggregations_visible | Mean of grades,Weighted mean of grades,Simple weighted mean of grades,Mean of grades (with extra credits),Median of grades,Lowest grade,Highest grade,Mode of grades,Natural |
+      | grade_report_user_showcontributiontocoursetotal | 1 |
+    And I navigate to "Scales" node in "Site administration > Grades"
+    And I press "Add a new scale"
+    And I set the following fields to these values:
+      | Name  | Letterscale |
+      | Scale | F,D,C,B,A   |
+    And I press "Save changes"
+    And the following "grade items" exist:
+      | itemname | course |
+      | Grade me | C1     |
+    And the following "grade items" exist:
+      | itemname | course | scale       |
+      | Scale me | C1     | Letterscale |
+
+  @javascript
+  Scenario Outline: Scales can be exluded from aggregation
+    Given I set the following administration settings values:
+      | grade_includescalesinaggregation | 0 |
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I turn editing mode on
+    When I give the grade "10" to the user "Student 1" for the grade item "Grade me"
+    And I give the grade "B" to the user "Student 1" for the grade item "Scale me"
+    And I press "Save changes"
+    And I set the following settings for grade item "Course 1":
+      | Aggregation | <aggregation> |
+    And I follow "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    Then the following should exist in the "user-grade" table:
+      | Grade item             | Grade          | Percentage  | Contribution to course total |
+      | Grade me               | 10.00          | 10.00 %     | <gradecontrib>               |
+      | Scale me               | B              | 75.00 %     | <scalecontrib>               |
+      | Course total<totalstr> | <coursetotal>  | <coursepc>  | -                            |
+    And I log out
+    And I log in as "admin"
+    And I set the following administration settings values:
+      | grade_includescalesinaggregation | 1 |
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I follow "User report"
+    And I set the field "Select all or one user" to "Student 1"
+    And the following should exist in the "user-grade" table:
+      | Grade item             | Grade          | Percentage  | Contribution to course total |
+      | Grade me               | 10.00          | 10.00 %     | <gradecontrib2>              |
+      | Scale me               | B              | 75.00 %     | <scalecontrib2>              |
+      | Course total<totalstr> | <coursetotal2> | <coursepc2> | -                            |
+
+    Examples:
+      | aggregation                         | totalstr                             | coursetotal | coursepc | gradecontrib | scalecontrib | coursetotal2 | coursepc2 | gradecontrib2 | scalecontrib2 |
+      | Natural                             |                                      | 10.00       | 10.00 %  | 10.00        | 0.00         | 14.00        | 13.33 %   | 9.52 %        | 3.81 %        |
+      | Mean of grades                      | Mean of grades.                      | 10.00       | 10.00 %  | 10.00        | 0.00         | 42.50        | 42.50 %   | 5.00 %        | 37.50 %       |
+      | Weighted mean of grades             | Weighted mean of grades.             | 10.00       | 10.00 %  | 10.00        | 0.00         | 42.50        | 42.50 %   | 5.00 %        | 37.50 %       |
+      | Simple weighted mean of grades      | Simple weighted mean of grades.      | 10.00       | 10.00 %  | 10.00        | 0.00         | 12.50        | 12.50 %   | 9.62 %        | 2.88 %        |
+      | Mean of grades (with extra credits) | Mean of grades (with extra credits). | 10.00       | 10.00 %  | 10.00        | 0.00         | 42.50        | 42.50 %   | 5.00 %        | 37.50 %       |
+      | Median of grades                    | Median of grades.                    | 10.00       | 10.00 %  | 10.00        | 0.00         | 42.50        | 42.50 %   | 5.00 %        | 37.50 %       |
+      | Lowest grade                        | Lowest grade.                        | 10.00       | 10.00 %  | 10.00        | 0.00         | 10.00        | 10.00 %   | 10.00 %       | 0.00 %        |
+      | Highest grade                       | Highest grade.                       | 10.00       | 10.00 %  | 10.00        | 0.00         | 75.00        | 75.00 %   | 0.00 %        | 75.00 %       |
+      | Mode of grades                      | Mode of grades.                      | 10.00       | 10.00 %  | 10.00        | 0.00         | 75.00        | 75.00 %   | 0.00 %        | 75.00 %       |
+
+  @javascript
+  Scenario: Weights of scales cannot be edited when they are not aggregated
+    Given I set the following administration settings values:
+      | grade_includescalesinaggregation | 0 |
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I turn editing mode on
+    When I set the following settings for grade item "Course 1":
+      | Aggregation | Natural |
+    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I set the field "Override weight of Grade me" to "1"
+    Then the field "Override weight of Grade me" matches value "100.00"
+    And I click on "Edit" "link" in the "Scale me" "table_row"
+    And I click on "Edit settings" "link" in the "Scale me" "table_row"
+    And I follow "Show more..."
+    And I should not see "Weight adjusted"
+    And I should not see "Weight"
+    And I log out
+    And I log in as "admin"
+    And I set the following administration settings values:
+      | grade_includescalesinaggregation | 1 |
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I set the field "Override weight of Grade me" to "1"
+    And the field "Override weight of Grade me" matches value "95.238"
+    And I set the field "Override weight of Scale me" to "1"
+    And the field "Override weight of Scale me" matches value "4.8"
+    And I click on "Edit" "link" in the "Scale me" "table_row"
+    And I click on "Edit settings" "link" in the "Scale me" "table_row"
+    And I follow "Show more..."
+    And I should see "Weight adjusted"
+    And I should see "Weight"
index 5a83d48..200b209 100644 (file)
@@ -71,19 +71,19 @@ Feature: View gradebook when single item scales are used
     And the following should exist in the "user-grade" table:
       | Grade item          | Grade | Range     | Contribution to course total |
       | Test assignment one | Ace!  | Ace!–Ace! | 100.00 %                     |
-      | Category total      | 1.00  | 0–1       | -                            |
+      | Sub category 1 total      | 1.00  | 0–1       | -                            |
       | Course total        | 1.00  | 0–1       | -                            |
     And I set the field "Select all or one user" to "Student 2"
     And the following should exist in the "user-grade" table:
       | Grade item          | Grade | Range     | Contribution to course total |
       | Test assignment one | -     | Ace!–Ace! | -                            |
-      | Category total      | -     | 0–1       | -                            |
+      | Sub category 1 total      | -     | 0–1       | -                            |
       | Course total        | -     | 0–1       | -                            |
     And I set the field "jump" to "Categories and items"
     And the following should exist in the "grade_edit_tree_table" table:
       | Name                | Max grade |
       | Test assignment one | 1.00      |
-      | Category total      | 1.00      |
+      | Sub category 1 total      | 1.00      |
       | Course total        | 1.00      |
 
   @javascript
@@ -113,13 +113,13 @@ Feature: View gradebook when single item scales are used
     And the following should exist in the "user-grade" table:
       | Grade item                        | Grade          | Range       | Contribution to course total |
       | Test assignment one               | Ace!           | Ace!–Ace!   | <contrib1>                   |
-      | Category total<aggregation>.      | <cattotal1>    | 0–100       | -                            |
+      | Sub category (<aggregation>) total<aggregation>.      | <cattotal1>    | 0–100       | -                            |
       | Course total<aggregation>.        | <coursetotal1> | 0–100       | -                            |
     And I set the field "jump" to "Categories and items"
     And the following should exist in the "grade_edit_tree_table" table:
       | Name                         | Max grade |
       | Test assignment one          | Ace! (1)  |
-      | Category total<aggregation>. | 100.00    |
+      | Sub category (<aggregation>) total<aggregation>. | 100.00    |
       | Course total<aggregation>.   | 100.00    |
 
     Examples:
index 17b22ef..62aec64 100644 (file)
@@ -31,6 +31,6 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['language'] = 'Jezik';
-$string['next'] = 'Nastavite';
+$string['next'] = 'Nastavi';
 $string['previous'] = 'Prethodni';
 $string['reload'] = 'Učitaj ponovno';
index 4503afb..c9f4611 100644 (file)
@@ -178,6 +178,7 @@ Moodle community|https://moodle.org
 #####
 Moodle.com|http://moodle.com/
 </pre>';
+$string['configcustomusermenuitems'] = 'You can configure the contents of the user menu (with the exception of the log out link, which is automatically added). Each line is separated by | characters and consists of 1) a string in "langstringname, componentname" form or as plain text, 2) a URL, and 3) an icon either as a pix icon or as a URL.';
 $string['configdbsessions'] = 'If enabled, this setting will use the database to store information about current sessions. Note that changing this setting now will log out all current users (including you). If you are using MySQL please make sure that \'max_allowed_packet\' in my.cnf (or my.ini) is at least 4M. Other session drivers can be configured directly in config.php, see config-dist.php for more information. This option disappears if you specify session driver in config.php file.';
 $string['configdebug'] = 'If you turn this on, then PHP\'s error_reporting will be increased so that more warnings are printed.  This is only useful for developers.';
 $string['configdebugdisplay'] = 'Set to on, the error reporting will go to the HTML page. This is practical, but breaks XHTML, JS, cookies and HTTP headers in general. Set to off, it will send the output to your server logs, allowing better debugging. The PHP setting error_log controls which log this goes to.';
@@ -393,6 +394,7 @@ $string['currenttheme'] = 'Current theme';
 $string['customcheck'] = 'Other checks';
 $string['custommenu'] = 'Custom menu';
 $string['custommenuitems'] = 'Custom menu items';
+$string['customusermenuitems'] = 'User menu items';
 $string['datarootsecurityerror'] = '<p><strong>SECURITY WARNING!</strong></p><p>Your dataroot directory is in the wrong location and is exposed to the web. This means that all your private files are available to anyone in the world, and some of them could be used by a cracker to obtain unauthorised administrative access to your site!</p>
 <p>You <em>must</em> move dataroot directory ({$a}) to a new location that is not within your public web directory, and update the <code>$CFG->dataroot</code> setting in your config.php accordingly.</p>';
 $string['datarootsecuritywarning'] = 'Your site configuration might not be secure. Please make sure that your dataroot directory ({$a}) is not directly accessible via web.';
@@ -505,7 +507,7 @@ $string['errorsetting'] = 'Could not save setting:';
 $string['errorwithsettings'] = 'Some settings were not changed due to an error.';
 $string['everyonewhocan'] = 'Everyone who can \'{$a}\'';
 $string['exceptions'] = 'exceptions';
-$string['execpathnotallowed'] = 'Setting executable paths disabled in config.php';
+$string['execpathnotallowed'] = 'Setting executable and local paths disabled in config.php';
 $string['experimental'] = 'Experimental';
 $string['experimentalsettings'] = 'Experimental settings';
 $string['extendedusernamechars'] = 'Allow extended characters in usernames';
index 5caa783..9d8ee54 100644 (file)
@@ -160,8 +160,8 @@ $string['authenticateduserdescription'] = 'All logged in users.';
 $string['authentication'] = 'Authentication';
 $string['authenticationplugins'] = 'Authentication plugins';
 $string['autosubscribe'] = 'Forum auto-subscribe';
-$string['autosubscribeno'] = 'No: don\'t automatically subscribe me to forums';
-$string['autosubscribeyes'] = 'Yes: when I post, subscribe me to that forum';
+$string['autosubscribeno'] = 'No: don\'t automatically subscribe me to forum discussions';
+$string['autosubscribeyes'] = 'Yes: when I post, subscribe me to that forum discussion';
 $string['availability'] = 'Availability';
 $string['availablecourses'] = 'Available courses';
 $string['back'] = 'Back';
index abd8fdb..15a1802 100644 (file)
@@ -4201,7 +4201,7 @@ function count_role_users($roleid, context $context, $parent = false) {
  *   otherwise use a comma-separated list of the fields you require, not including id
  * @param string $orderby If set, use a comma-separated list of fields from course
  *   table with sql modifiers (DESC) if needed
- * @return array Array of courses, may have zero entries. Or false if query failed.
+ * @return array|bool Array of courses, if none found false is returned.
  */
 function get_user_capability_course($capability, $userid = null, $doanything = true, $fieldsexceptid = '', $orderby = '') {
     global $DB;
index be24d9e..0c6aafe 100644 (file)
@@ -2425,6 +2425,7 @@ class admin_setting_configfile extends admin_setting_configtext {
      * @return string XHTML field
      */
     public function output_html($data, $query='') {
+        global $CFG;
         $default = $this->get_defaultsetting();
 
         if ($data) {
@@ -2436,9 +2437,14 @@ class admin_setting_configfile extends admin_setting_configtext {
         } else {
             $executable = '';
         }
+        $readonly = '';
+        if (!empty($CFG->preventexecpath)) {
+            $this->visiblename .= '<div class="form-overridden">'.get_string('execpathnotallowed', 'admin').'</div>';
+            $readonly = 'readonly="readonly"';
+        }
 
         return format_admin_setting($this, $this->visiblename,
-        '<div class="form-file defaultsnext"><input type="text" size="'.$this->size.'" id="'.$this->get_id().'" name="'.$this->get_full_name().'" value="'.s($data).'" />'.$executable.'</div>',
+        '<div class="form-file defaultsnext"><input '.$readonly.' type="text" size="'.$this->size.'" id="'.$this->get_id().'" name="'.$this->get_full_name().'" value="'.s($data).'" />'.$executable.'</div>',
         $this->description, true, '', $default, $query);
     }
 
@@ -2490,12 +2496,14 @@ class admin_setting_configexecutable extends admin_setting_configfile {
         } else {
             $executable = '';
         }
+        $readonly = '';
         if (!empty($CFG->preventexecpath)) {
             $this->visiblename .= '<div class="form-overridden">'.get_string('execpathnotallowed', 'admin').'</div>';
+            $readonly = 'readonly="readonly"';
         }
 
         return format_admin_setting($this, $this->visiblename,
-        '<div class="form-file defaultsnext"><input type="text" size="'.$this->size.'" id="'.$this->get_id().'" name="'.$this->get_full_name().'" value="'.s($data).'" />'.$executable.'</div>',
+        '<div class="form-file defaultsnext"><input '.$readonly.' type="text" size="'.$this->size.'" id="'.$this->get_id().'" name="'.$this->get_full_name().'" value="'.s($data).'" />'.$executable.'</div>',
         $this->description, true, '', $default, $query);
     }
 }
@@ -2516,6 +2524,7 @@ class admin_setting_configdirectory extends admin_setting_configfile {
      * @return string XHTML
      */
     public function output_html($data, $query='') {
+        global $CFG;
         $default = $this->get_defaultsetting();
 
         if ($data) {
@@ -2527,9 +2536,14 @@ class admin_setting_configdirectory extends admin_setting_configfile {
         } else {
             $executable = '';
         }
+        $readonly = '';
+        if (!empty($CFG->preventexecpath)) {
+            $this->visiblename .= '<div class="form-overridden">'.get_string('execpathnotallowed', 'admin').'</div>';
+            $readonly = 'readonly="readonly"';
+        }
 
         return format_admin_setting($this, $this->visiblename,
-        '<div class="form-file defaultsnext"><input type="text" size="'.$this->size.'" id="'.$this->get_id().'" name="'.$this->get_full_name().'" value="'.s($data).'" />'.$executable.'</div>',
+        '<div class="form-file defaultsnext"><input '.$readonly.' type="text" size="'.$this->size.'" id="'.$this->get_id().'" name="'.$this->get_full_name().'" value="'.s($data).'" />'.$executable.'</div>',
         $this->description, true, '', $default, $query);
     }
 }
index 3553d53..025cb36 100644 (file)
@@ -677,7 +677,7 @@ function badges_notify_badge_award(badge $badge, $userid, $issued, $filepathhash
     $params->username = fullname($userto);
     $params->badgelink = $issuedlink;
     $message = badge_message_from_template($badge->message, $params);
-    $plaintext = format_text_email($message, FORMAT_HTML);
+    $plaintext = html_to_text($message);
 
     // Notify recipient.
     $eventdata = new stdClass();
@@ -688,9 +688,9 @@ function badges_notify_badge_award(badge $badge, $userid, $issued, $filepathhash
     $eventdata->notification      = 1;
     $eventdata->subject           = $badge->messagesubject;
     $eventdata->fullmessage       = $plaintext;
-    $eventdata->fullmessageformat = FORMAT_PLAIN;
+    $eventdata->fullmessageformat = FORMAT_HTML;
     $eventdata->fullmessagehtml   = $message;
-    $eventdata->smallmessage      = $plaintext;
+    $eventdata->smallmessage      = '';
 
     // Attach badge image if possible.
     if (!empty($CFG->allowattachments) && $badge->attachment && is_string($filepathhash)) {
@@ -723,10 +723,10 @@ function badges_notify_badge_award(badge $badge, $userid, $issued, $filepathhash
         $eventdata->userto            = $creator;
         $eventdata->notification      = 1;
         $eventdata->subject           = $creatorsubject;
-        $eventdata->fullmessage       = format_text_email($creatormessage, FORMAT_HTML);
-        $eventdata->fullmessageformat = FORMAT_PLAIN;
+        $eventdata->fullmessage       = html_to_text($creatormessage);
+        $eventdata->fullmessageformat = FORMAT_HTML;
         $eventdata->fullmessagehtml   = $creatormessage;
-        $eventdata->smallmessage      = $creatorsubject;
+        $eventdata->smallmessage      = '';
 
         message_send($eventdata);
         $DB->set_field('badge_issued', 'issuernotified', time(), array('badgeid' => $badge->id, 'userid' => $userid));
index fc0b3be..0a810c2 100644 (file)
@@ -106,7 +106,7 @@ class calendar_event_updated extends base {
         if (empty($this->other['name'])) {
             throw new \coding_exception('The \'name\' value must be set in other.');
         }
-        if (empty($this->other['timestart'])) {
+        if (!isset($this->other['timestart'])) {
             throw new \coding_exception('The \'timestart\' value must be set in other.');
         }
     }
index ab92ee1..d1d4d94 100644 (file)
@@ -53,7 +53,7 @@ abstract class handler {
     /**
      * @var int $defaultexpiration The default expiration time to use when created a new key.
      */
-    private $defaultexpiration = 86400;
+    private $defaultexpiration = WEEKSECS;
 
     /**
      * @var bool $validateaddress Whether to validate the sender address when processing this handler.
index a27147a..9d7b767 100644 (file)
@@ -441,13 +441,10 @@ class mysqli_native_moodle_database extends moodle_database {
         if ($dbhost and !empty($this->dboptions['dbpersist'])) {
             $dbhost = "p:$dbhost";
         }
-        ob_start();
-        $this->mysqli = new mysqli($dbhost, $dbuser, $dbpass, $dbname, $dbport, $dbsocket);
-        $dberr = ob_get_contents();
-        ob_end_clean();
-        $errorno = @$this->mysqli->connect_errno;
+        $this->mysqli = @new mysqli($dbhost, $dbuser, $dbpass, $dbname, $dbport, $dbsocket);
 
-        if ($errorno !== 0) {
+        if ($this->mysqli->connect_errno !== 0) {
+            $dberr = $this->mysqli->connect_error;
             $this->mysqli = null;
             throw new dml_connection_exception($dberr);
         }
index a2229b9..b315481 100644 (file)
@@ -344,6 +344,11 @@ function setup_DB() {
         $DB->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->prefix, $CFG->dboptions);
     } catch (moodle_exception $e) {
         if (empty($CFG->noemailever) and !empty($CFG->emailconnectionerrorsto)) {
+            $body = "Connection error: ".$CFG->wwwroot.
+                "\n\nInfo:".
+                "\n\tError code: ".$e->errorcode.
+                "\n\tDebug info: ".$e->debuginfo.
+                "\n\tServer: ".$_SERVER['SERVER_NAME']." (".$_SERVER['SERVER_ADDR'].")";
             if (file_exists($CFG->dataroot.'/emailcount')){
                 $fp = @fopen($CFG->dataroot.'/emailcount', 'r');
                 $content = @fread($fp, 24);
@@ -352,7 +357,7 @@ function setup_DB() {
                     //email directly rather than using messaging
                     @mail($CFG->emailconnectionerrorsto,
                         'WARNING: Database connection error: '.$CFG->wwwroot,
-                        'Connection error: '.$CFG->wwwroot);
+                        $body);
                     $fp = @fopen($CFG->dataroot.'/emailcount', 'w');
                     @fwrite($fp, time());
                 }
@@ -360,7 +365,7 @@ function setup_DB() {
                //email directly rather than using messaging
                @mail($CFG->emailconnectionerrorsto,
                     'WARNING: Database connection error: '.$CFG->wwwroot,
-                    'Connection error: '.$CFG->wwwroot);
+                    $body);
                $fp = @fopen($CFG->dataroot.'/emailcount', 'w');
                @fwrite($fp, time());
             }
index 9378dfe..3090b0d 100644 (file)
@@ -18,6 +18,7 @@ Feature: Atto accessibility checker
     And I press "Save image"
     And I press "Accessibility checker"
     And I should see "Congratulations, no accessibility problems found!"
+    And I click on ".moodle-dialogue-focused .closebutton" "css_element"
     And I select the text in the "Description" Atto editor
     And I click on "Image" "button"
     And I set the field "Describe this image" to ""
index 7fcd08d..488bae2 100644 (file)
@@ -1953,6 +1953,13 @@ function file_mimetype_in_typegroup($mimetype, $groups) {
  */
 function send_file_not_found() {
     global $CFG, $COURSE;
+
+    // Allow cross-origin requests only for Web Services.
+    // This allow to receive requests done by Web Workers or webapps in different domains.
+    if (WS_SERVER) {
+        header('Access-Control-Allow-Origin: *');
+    }
+
     send_header_404();
     print_error('filenotfound', 'error', $CFG->wwwroot.'/course/view.php?id='.$COURSE->id); //this is not displayed on IIS??
 }
@@ -2476,6 +2483,12 @@ function send_stored_file($stored_file, $lifetime=null, $filter=0, $forcedownloa
         }
     }
 
+    // Allow cross-origin requests only for Web Services.
+    // This allow to receive requests done by Web Workers or webapps in different domains.
+    if (WS_SERVER) {
+        header('Access-Control-Allow-Origin: *');
+    }
+
     if (empty($filter)) {
         // send the contents
         readfile_accel($stored_file, $mimetype, !$dontdie);
index fa4294d..acb5db3 100644 (file)
@@ -2881,7 +2881,7 @@ class MoodleQuickForm_Rule_Required extends HTML_QuickForm_Rule {
         }
         $stripvalues = array(
             '#</?(?!img|canvas|hr).*?>#im', // all tags except img, canvas and hr
-            '#(\xc2|\xa0|\s|&nbsp;)#', //any whitespaces actually
+            '#(\xc2\xa0|\s|&nbsp;)#', // Any whitespaces actually.
         );
         if (!empty($CFG->strictformsrequired)) {
             $value = preg_replace($stripvalues, '', (string)$value);
index 1b0e734..431f0c7 100644 (file)
@@ -1419,6 +1419,7 @@ class grade_category extends grade_object {
      * @return void
      */
     private function auto_update_weights() {
+        global $CFG;
         if ($this->aggregation != GRADE_AGGREGATE_SUM) {
             // This is only required if we are using natural weights.
             return;
@@ -1457,6 +1458,9 @@ class grade_category extends grade_object {
             } else if (!$this->aggregateoutcomes && $gradeitem->is_outcome_item()) {
                 // We will not aggregate outcome items, so we can ignore them.
                 continue;
+            } else if (empty($CFG->grade_includescalesinaggregation) && $gradeitem->gradetype == GRADE_TYPE_SCALE) {
+                // The scales are not included in the aggregation, ignore them.
+                continue;
             }
 
             // Record the ID and the weight for this grade item.
@@ -1499,7 +1503,9 @@ class grade_category extends grade_object {
             if (!$gradeitemdetail['extracredit']) {
                 $normalisetotal += $gradeitemdetail['weight'];
             }
-            if ($gradeitemdetail['weightoverride'] && !$gradeitemdetail['extracredit']) {
+            // The overridden total comprises of items that are set as overridden, that aren't extra credit and have a value
+            // greater than zero.
+            if ($gradeitemdetail['weightoverride'] && !$gradeitemdetail['extracredit'] && $gradeitemdetail['weight'] > 0) {
                 // Add overriden weights up to see if they are greater than 1.
                 $overriddentotal += $gradeitemdetail['weight'];
             }
@@ -1530,6 +1536,9 @@ class grade_category extends grade_object {
             } else if (!$this->aggregateoutcomes && $gradeitem->is_outcome_item()) {
                 // We will not aggregate outcome items, so we can ignore updating their weights.
                 continue;
+            } else if (empty($CFG->grade_includescalesinaggregation) && $gradeitem->gradetype == GRADE_TYPE_SCALE) {
+                // We will not aggregate the scales, so we can ignore upating their weights.
+                continue;
             }
 
             if (!$gradeitem->weightoverride) {
index 360116f..b00833d 100644 (file)
@@ -5919,9 +5919,14 @@ function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '',
 
             $attachmentpath = $attachment;
 
+            // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction).
+            $attachpath = str_replace('\\', '/', $attachmentpath);
+            // Make sure both variables are normalised before comparing.
+            $temppath = str_replace('\\', '/', $CFG->tempdir);
+
             // If the attachment is a full path to a file in the tempdir, use it as is,
             // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons).
-            if (strpos($attachmentpath, $CFG->tempdir) !== 0) {
+            if (strpos($attachpath, $temppath) !== 0) {
                 $attachmentpath = $CFG->dataroot . '/' . $attachmentpath;
             }
 
index aabbdc6..b82f553 100644 (file)
@@ -2886,10 +2886,11 @@ class global_navigation_for_ajax extends global_navigation {
                 require_course_login($course, true, $cm, false, true);
                 $this->page->set_context(context_module::instance($cm->id));
                 $coursenode = $this->load_course($course);
-                if ($course->id != $SITE->id) {
-                    $this->load_course_sections($course, $coursenode, null, $cm);
+                $this->load_course_sections($course, $coursenode, null, $cm);
+                $activitynode = $coursenode->find($cm->id, self::TYPE_ACTIVITY);
+                if ($activitynode) {
+                    $modulenode = $this->load_activity($cm, $course, $activitynode);
                 }
-                $modulenode = $this->load_activity($cm, $course, $coursenode->find($cm->id, self::TYPE_ACTIVITY));
                 break;
             default:
                 throw new Exception('Unknown type');
index b0deb72..d20a2a4 100644 (file)
@@ -3049,8 +3049,14 @@ EOD;
             $idx = 0;
             foreach ($opts->navitems as $key => $value) {
                 $pix = null;
-                if (isset($value->pix)) {
+                if (isset($value->pix) && !empty($value->pix)) {
                     $pix = new pix_icon($value->pix, $value->title, null, array('class' => 'iconsmall'));
+                } else if (isset($value->imgsrc) && !empty($value->imgsrc)) {
+                    $value->title = html_writer::img(
+                        $value->imgsrc,
+                        $value->title,
+                        array('class' => 'iconsmall')
+                    ) . $value->title;
                 }
                 $al = new action_menu_link_secondary(
                     $value->url,
index e55573c..9a59592 100644 (file)
@@ -884,11 +884,19 @@ function setup_get_remote_url() {
         //IIS - needs a lot of tweaking to make it work
         $rurl['fullpath'] = $_SERVER['SCRIPT_NAME'];
 
-        // NOTE: ignore PATH_INFO because it is incorrectly encoded using 8bit filesystem legacy encoding in IIS
-        //       since 2.0 we rely on iis rewrite extenssion like Helicon ISAPI_rewrite
-        //       example rule: RewriteRule ^([^\?]+?\.php)(\/.+)$ $1\?file=$2 [QSA]
+        // NOTE: we should ignore PATH_INFO because it is incorrectly encoded using 8bit filesystem legacy encoding in IIS.
+        //       Since 2.0, we rely on IIS rewrite extensions like Helicon ISAPI_rewrite
+        //         example rule: RewriteRule ^([^\?]+?\.php)(\/.+)$ $1\?file=$2 [QSA]
+        //       OR
+        //       we rely on a proper IIS 6.0+ configuration: the 'FastCGIUtf8ServerVariables' registry key.
+        if (isset($_SERVER['PATH_INFO']) and $_SERVER['PATH_INFO'] !== '') {
+            // Check that PATH_INFO works == must not contain the script name.
+            if (strpos($_SERVER['PATH_INFO'], $_SERVER['SCRIPT_NAME']) === false) {
+                $rurl['fullpath'] .= clean_param(urldecode($_SERVER['PATH_INFO']), PARAM_PATH);
+            }
+        }
 
-        if ($_SERVER['QUERY_STRING'] != '') {
+        if (isset($_SERVER['QUERY_STRING']) and $_SERVER['QUERY_STRING'] !== '') {
             $rurl['fullpath'] .= '?'.$_SERVER['QUERY_STRING'];
         }
         $_SERVER['REQUEST_URI'] = $rurl['fullpath']; // extra IIS compatibility
index 24677af..8a377b7 100644 (file)
@@ -272,7 +272,7 @@ class core_admintree_testcase extends advanced_testcase {
         $setting->write_setting('/mm/nn');
         $this->assertSame('', get_config('abc_cde', 'execpath'));
 
-        // This also (most probably incorrectly) affects admin_setting_configfile.
+        // This also affects admin_setting_configfile and admin_setting_configdirectory.
 
         set_config('preventexecpath', 0);
         set_config('execpath', null, 'abc_cde');
@@ -297,5 +297,27 @@ class core_admintree_testcase extends advanced_testcase {
         $setting->write_setting('/mm/nn');
         $this->assertSame('', get_config('abc_cde', 'execpath'));
 
+        set_config('preventexecpath', 0);
+        set_config('execpath', null, 'abc_cde');
+        $this->assertFalse(get_config('abc_cde', 'execpath'));
+        $setting = new admin_setting_configdirectory('abc_cde/execpath', 'some desc', '', '/xx/yy');
+        $setting->write_setting('/oo/pp');
+        $this->assertSame('/oo/pp', get_config('abc_cde', 'execpath'));
+
+        // Prevent changes.
+        set_config('preventexecpath', 1);
+        $setting->write_setting('/mm/nn');
+        $this->assertSame('/oo/pp', get_config('abc_cde', 'execpath'));
+
+        // Use default in install.
+        set_config('execpath', null, 'abc_cde');
+        $setting->write_setting('/mm/nn');
+        $this->assertSame('/xx/yy', get_config('abc_cde', 'execpath'));
+
+        // Use empty value if no default.
+        $setting = new admin_setting_configdirectory('abc_cde/execpath', 'some desc', '', null);
+        set_config('execpath', null, 'abc_cde');
+        $setting->write_setting('/mm/nn');
+        $this->assertSame('', get_config('abc_cde', 'execpath'));
     }
 }
index 1ad2776..18e5edc 100644 (file)
@@ -51,6 +51,8 @@ class core_formslib_testcase extends advanced_testcase {
         $this->assertTrue($rule->validate("Something\nmore"));
         $this->assertTrue($rule->validate("\nmore"));
         $this->assertTrue($rule->validate(" more "));
+        $this->assertTrue($rule->validate('ш'));
+        $this->assertTrue($rule->validate("の"));
         $this->assertTrue($rule->validate("0"));
         $this->assertTrue($rule->validate(0));
         $this->assertTrue($rule->validate(true));
@@ -88,6 +90,8 @@ class core_formslib_testcase extends advanced_testcase {
         $this->assertTrue($rule->validate("\nmore"));
         $this->assertTrue($rule->validate(" more "));
         $this->assertTrue($rule->validate("0"));
+        $this->assertTrue($rule->validate('ш'));
+        $this->assertTrue($rule->validate("の"));
         $this->assertTrue($rule->validate(0));
         $this->assertTrue($rule->validate(true));
         $this->assertTrue($rule->validate('<p>x</p>'));
index 63e9b8c..ee5b31a 100644 (file)
@@ -1070,9 +1070,11 @@ function get_file_argument() {
 
     // Then try extract file from the slasharguments.
     if (stripos($_SERVER['SERVER_SOFTWARE'], 'iis') !== false) {
-        // NOTE: ISS tends to convert all file paths to single byte DOS encoding,
+        // NOTE: IIS tends to convert all file paths to single byte DOS encoding,
         //       we can not use other methods because they break unicode chars,
-        //       the only way is to use URL rewriting.
+        //       the only ways are to use URL rewriting
+        //       OR
+        //       to properly set the 'FastCGIUtf8ServerVariables' registry key.
         if (isset($_SERVER['PATH_INFO']) and $_SERVER['PATH_INFO'] !== '') {
             // Check that PATH_INFO works == must not contain the script name.
             if (strpos($_SERVER['PATH_INFO'], $SCRIPT) === false) {
index af66f37..d086ba0 100644 (file)
Binary files a/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js and b/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js differ
index 991fbbb..559fab6 100644 (file)
Binary files a/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js and b/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js differ
index af66f37..d086ba0 100644 (file)
Binary files a/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop.js and b/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop.js differ
index 5ab6c80..b3e88a5 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js and b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js differ
index 8b58eea..026db21 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js and b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js differ
index dadf0e8..6b29197 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js and b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js differ
index 00a8e9f..ab52c5f 100644 (file)
@@ -559,6 +559,9 @@ Y.extend(DRAGDROP, Y.Base, {
             M.core.dragdrop.keydraghandle.focus();
             M.core.dragdrop.keydragcontainer = null;
         }
+        if (M.core.dragdrop.dropui) {
+            M.core.dragdrop.dropui.destroy();
+        }
     },
 
     /**
index b7788c2..77553ea 100644 (file)
@@ -69,6 +69,16 @@ Y.extend(DIALOGUE, Y.Panel, {
     _orientationevent : null,
     _calculatedzindex : false,
 
+    /**
+     * The original position of the dialogue before it was reposition to
+     * avoid browser jumping.
+     *
+     * @property _originalPosition
+     * @protected
+     * @type Array
+     */
+    _originalPosition: null,
+
     /**
      * Initialise the dialogue.
      *
@@ -80,7 +90,6 @@ Y.extend(DIALOGUE, Y.Panel, {
         if (this.get('render')) {
             this.render();
         }
-        this.makeResponsive();
         this.after('visibleChange', this.visibilityChanged, this);
         if (this.get('center')) {
             this.centerDialogue();
@@ -103,14 +112,25 @@ Y.extend(DIALOGUE, Y.Panel, {
         }
         // Recalculate the zIndex every time the modal is altered.
         this.on('maskShow', this.applyZIndex);
-        // We must show - after the dialogue has been positioned,
-        // either by centerDialogue or makeResonsive. This is because the show() will trigger
-        // a focus on the dialogue, which will scroll the page. If the dialogue has not
-        // been positioned it will scroll back to the top of the page.
-        if (this.get('visible')) {
-            this.show();
-            this.keyDelegation();
-        }
+
+        this.on('maskShow', function() {
+            // When the mask shows, position the boundingBox at the top-left of the window such that when it is
+            // focused, the position does not change.
+            var w = Y.one(Y.config.win),
+                bb = this.get('boundingBox');
+
+            if (!this.get('center')) {
+                this._originalPosition = bb.getXY();
+            }
+
+            if (bb.getStyle('position') !== 'fixed') {
+                // If the boundingBox has been positioned in a fixed manner, then it will not position correctly to scrollTop.
+                bb.setStyles({
+                    top: w.get('scrollTop'),
+                    left: w.get('scrollLeft')
+                });
+            }
+        }, this);
 
         // Remove the dialogue from the DOM when it is destroyed.
         this.after('destroyedChange', function(){
@@ -228,8 +248,7 @@ Y.extend(DIALOGUE, Y.Panel, {
      * @method makeResponsive
      */
     makeResponsive : function() {
-        var bb = this.get('boundingBox'),
-            content;
+        var bb = this.get('boundingBox');
 
         if (this.shouldResizeFullscreen()) {
             // Make this dialogue fullscreen on a small screen.
@@ -244,15 +263,12 @@ Y.extend(DIALOGUE, Y.Panel, {
                           'height' : null,
                           'right' : null,
                           'bottom' : null});
-
-            content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
         } else {
             if (this.get('responsive')) {
                 // We must reset any of the fullscreen changes.
                 bb.removeClass(DIALOGUE_FULLSCREEN_CLASS)
                     .setStyles({'width' : this.get('width'),
                                 'height' : this.get('height')});
-                content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
             }
         }
     },
@@ -281,6 +297,7 @@ Y.extend(DIALOGUE, Y.Panel, {
         if (hidden) {
             bb.addClass(DIALOGUE_HIDDEN_CLASS);
         }
+        this.makeResponsive();
     },
     /**
      * Return whether this dialogue should be fullscreen or not.
@@ -296,7 +313,7 @@ Y.extend(DIALOGUE, Y.Panel, {
                Math.floor(Y.one(document.body).get('winWidth')) < this.get('responsiveWidth');
     },
 
-    show : function() {
+    show: function() {
         var result = null,
             header = this.headerNode,
             content = this.bodyNode,
@@ -305,6 +322,11 @@ Y.extend(DIALOGUE, Y.Panel, {
 
         result = DIALOGUE.superclass.show.call(this);
 
+        if (!this.get('center') && this._originalPosition) {
+            // Restore the dialogue position to it's location before it was moved at show time.
+            this.get('boundingBox').setXY(this._originalPosition);
+        }
+
         // Lock scroll if the plugin is present.
         if (this.lockScroll) {
             // We need to force the scroll locking for full screen dialogues, even if they have a small vertical size to
index 2abd861..d6fc504 100644 (file)
@@ -92,7 +92,7 @@ class message_output_airnotifier extends message_output {
             }
 
             // Sending the message to the device.
-            $serverurl = $CFG->airnotifierurl . ':' . $CFG->airnotifierport . '/notification/';
+            $serverurl = $CFG->airnotifierurl . ':' . $CFG->airnotifierport . '/api/v2/push/';
             $header = array('Accept: application/json', 'X-AN-APP-NAME: ' . $CFG->airnotifierappname,
                 'X-AN-APP-KEY: ' . $CFG->airnotifieraccesskey);
             $curl = new curl;
index 7d042f4..577da7c 100644 (file)
@@ -52,11 +52,7 @@ class behat_message extends behat_base {
         $steps = array();
         $steps[] = new Given('I am on homepage');
 
-        if ($this->running_javascript()) {
-            $steps[] = new Given('I expand "' . get_string('myprofile') . '" node');
-        }
-
-        $steps[] = new Given('I follow "' . get_string('messages', 'message') . '"');
+        $steps[] = new Given('I navigate to "' . get_string('messages', 'message') . '" node in "' . get_string('myprofile') . '"');
         $steps[] = new Given('I set the field "' . get_string('searchcombined', 'message') .
             '" to "' . $this->escape($userfullname) . '"');
         $steps[] = new Given('I press "' . get_string('searchcombined', 'message') . '"');
index 85bc317..dac1cc6 100644 (file)
@@ -11,8 +11,7 @@ Feature: Block users from contacting me
       | user1 | User | One | one@asd.com |
       | user2 | User | Two | two@asd.com |
     And I log in as "user1"
-    And I expand "My profile" node
-    And I follow "Messages"
+    And I navigate to "Messages" node in "My profile"
     And I set the field "Search people and messages" to "User Two"
     And I press "Search people and messages"
     When I click on "Block contact" "link" in the "User Two" "table_row"
@@ -21,8 +20,7 @@ Feature: Block users from contacting me
     And I should see "User Two"
     And I log out
     And I log in as "user2"
-    And I expand "My profile" node
-    And I follow "Messages"
+    And I navigate to "Messages" node in "My profile"
     And I set the field "Search people and messages" to "User One"
     And I press "Search people and messages"
     And I follow "Send message to User One"
index bf97b30..87b6012 100644 (file)
@@ -22,8 +22,7 @@ Feature: Message history displays correctly
     And I send "Message 10 from user1 to user2" message to "User Two" user
 
   Scenario: View sent messages
-    When I expand "My profile" node
-    And I click on "Messages" "link" in the "Navigation" "block"
+    When I navigate to "Messages" node in "My profile"
     And I set the field "Search people and messages" to "User Two"
     And I press "Search people and messages"
     And I click on "Message history" "link" in the "User Two" "table_row"
@@ -38,8 +37,7 @@ Feature: Message history displays correctly
   Scenario: View received messages
     When I log out
     And I log in as "user2"
-    And I expand "My profile" node
-    And I click on "Messages" "link" in the "Navigation" "block"
+    And I navigate to "Messages" node in "My profile"
     And I follow "User One (10)"
     # Should show all of the user's unread messages.
     Then I should see "Message 1 from user1 to user2"
index 9bb073f..de51944 100644 (file)
@@ -12,8 +12,7 @@ Feature: Manage contacts
     And I log in as "user1"
     And I send "Message 1 from user1 to user2" message to "User Two" user
     And I send "Message 2 from user1 to user2" message to "User Two" user
-    And I expand "My profile" node
-    And I click on "Messages" "link" in the "Navigation" "block"
+    And I navigate to "Messages" node in "My profile"
     And I set the field "Search people and messages" to "User Two"
     And I press "Search people and messages"
     When I click on "Add contact" "link" in the "User Two" "table_row"
index 3de5662..4c151c6 100644 (file)
@@ -30,8 +30,7 @@ Feature: An user can message course participants
     And I press "Preview"
     And I press "Send message"
     And I follow "Home"
-    And I expand "My profile" node
-    And I follow "Messages"
+    And I navigate to "Messages" node in "My profile"
     And I set the field "Message navigation:" to "Recent conversations"
     Then I should see "Here it is, the message content"
     And I should see "Student 1"
index d0c72b6..66d74cc 100644 (file)
@@ -11,8 +11,7 @@ Feature: Users can search their message history
       | user2 | User | Two | two@asd.com |
     And I log in as "user1"
     When I send "Give me your biscuits" message to "User Two" user
-    And I expand "My profile" node
-    And I click on "Messages" "link" in the "Navigation" "block"
+    And I navigate to "Messages" node in "My profile"
     And I set the field "Search people and messages" to "your biscuits"
     And I press "Search people and messages"
     Then I should see "User Two"
index 3dfa071..afa01ef 100644 (file)
@@ -338,6 +338,23 @@ class page_editor {
             $DB->insert_record('assignfeedback_editpdf_cmnt', $comment);
         }
 
+        // Delete the existing stamps and copy the source ones.
+        $fs = get_file_storage();
+        $fs->delete_area_files($assignment->get_context()->id, 'assignfeedback_editpdf', 'stamps', $grade->id);
+        if ($files = $fs->get_area_files($assignment->get_context()->id,
+                                         'assignfeedback_editpdf',
+                                         'stamps',
+                                         $sourceusergrade->id,
+                                         "filename",
+                                         false)) {
+            foreach ($files as $file) {
+                $newrecord = new \stdClass();
+                $newrecord->contextid = $assignment->get_context()->id;
+                $newrecord->itemid = $grade->id;
+                $fs->create_file_from_storedfile($newrecord, $file);
+            }
+        }
+
         return true;
     }
 
index 4a1ee07..b49f680 100644 (file)
@@ -131,8 +131,8 @@ Feature: In an assignment, teacher can annotate PDF files during grading
     And I follow "View/grade all submissions"
     And I click on "Grade" "link" in the "Student 2" "table_row"
     And I follow "Launch PDF editor..."
-    And I click on ".linebutton" "css_element"
-    And I drag ".drawingcanvas" "css_element" and I drop it in ".assignfeedback_editpdf_widget" "css_element"
+    And I click on ".stampbutton" "css_element"
+    And I click on ".drawingcanvas" "css_element"
     And I click on "Close" "button"
     And I press "Save changes"
     And I should see "The grade changes were saved"
index 5770d3a..bdf6665 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js differ
index 868bdaf..68b5228 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js differ
index 5770d3a..bdf6665 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js differ
index 2c41908..12b4237 100644 (file)
@@ -325,6 +325,7 @@ EDITOR.prototype = {
         }
         this.dialogue.centerDialogue();
         this.dialogue.show();
+        drawingcanvas.on('windowresize', this.resize, this);
     },
 
     /**
@@ -831,6 +832,16 @@ EDITOR.prototype = {
         this.currentedit.path = [];
     },
 
+    /**
+     * Resize the dialogue window when the browser is resized.
+     * @public
+     * @method resize
+     */
+    resize : function() {
+        this.dialogue.centerDialogue();
+        return true;
+    },
+
     /**
      * Factory method for creating annotations of the correct subclass.
      * @public
index b740dc9..e8cf2c8 100644 (file)
@@ -8,6 +8,7 @@
         "graphics",
         "json",
         "event-move",
+        "event-resize",
         "querystring-stringify-simple",
         "moodle-core-notification-dialog",
         "moodle-core-notification-exception",
index 97d59e5..14dc5d5 100644 (file)
@@ -728,7 +728,7 @@ class assign {
                 $fileareas = array();
                 $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
                 $fileareas = $plugin->get_file_areas();
-                foreach ($fileareas as $filearea) {
+                foreach ($fileareas as $filearea => $notused) {
                     $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
                 }
 
@@ -743,7 +743,7 @@ class assign {
                 $fileareas = array();
                 $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
                 $fileareas = $plugin->get_file_areas();
-                foreach ($fileareas as $filearea) {
+                foreach ($fileareas as $filearea => $notused) {
                     $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
                 }
 
index 41dfb1f..5f2071c 100644 (file)
 
     echo $OUTPUT->header();
 
-/// Check to see if groups are being used here
-    $currentgroup = groups_get_activity_group($cm);
+    // Check to see if groups are being used here.
+    // We need the most up to date current group value. Make sure it is updated at this point.
+    $currentgroup = groups_get_activity_group($cm, true);
     $groupmode = groups_get_activity_groupmode($cm);
     $canmanageentries = has_capability('mod/data:manageentries', $context);
     // If a student is not part of a group and seperate groups is enabled, we don't
index 1e76e93..01a10bf 100644 (file)
@@ -201,7 +201,7 @@ class mod_forum_post_form extends moodleform {
         } else {
             $submit_string = get_string('posttoforum', 'forum');
         }
-        $this->add_action_buttons(false, $submit_string);
+        $this->add_action_buttons(true, $submit_string);
 
         $mform->addElement('hidden', 'course');
         $mform->setType('course', PARAM_INT);
index c241541..06a9377 100644 (file)
@@ -200,5 +200,29 @@ function xmldb_forum_upgrade($oldversion) {
         upgrade_mod_savepoint(true, 2014081900, 'forum');
     }
 
+    if ($oldversion < 2014103000) {
+        // Find records with multiple userid/postid combinations and find the lowest ID.
+        // Later we will remove all those which don't match this ID.
+        $sql = "
+            SELECT MIN(id) as lowid, userid, postid
+            FROM {forum_read}
+            GROUP BY userid, postid
+            HAVING COUNT(id) > 1";
+
+        if ($duplicatedrows = $DB->get_recordset_sql($sql)) {
+            foreach ($duplicatedrows as $row) {
+                $DB->delete_records_select('forum_read', 'userid = ? AND postid = ? AND id <> ?', array(
+                    $row->userid,
+                    $row->postid,
+                    $row->lowid,
+                ));
+            }
+        }
+        $duplicatedrows->close();
+
+        // Forum savepoint reached.
+        upgrade_mod_savepoint(true, 2014103000, 'forum');
+    }
+
     return true;
 }
index 2c67ca9..30bed3c 100644 (file)
             $forum,
             $discussiongroup,
             $modcontext,
-            'u.id'
+            'u.id',
+            true
         );
 
         // Pre-seed the subscribed_discussion caches.
         $newdiscussion->forum = $forumto->id;
         foreach ($subscriptionchanges as $userid => $preference) {
             if ($preference === \mod_forum\subscriptions::FORUM_DISCUSSION_SUBSCRIBED) {
-                \mod_forum\subscriptions::subscribe_user_to_discussion($userid, $newdiscussion, $destinationctx);
+                // Users must have viewdiscussion to a discussion.
+                if (has_capability('mod/forum:viewdiscussion', $destinationctx, $userid)) {
+                    \mod_forum\subscriptions::subscribe_user_to_discussion($userid, $newdiscussion, $destinationctx);
+                }
             } else {
                 \mod_forum\subscriptions::unsubscribe_user_from_discussion($userid, $newdiscussion, $destinationctx);
             }
 
     echo $OUTPUT->header();
 
-    $headingvalue = format_string($forum->name);
+    echo $OUTPUT->heading(format_string($forum->name), 2);
+    echo $OUTPUT->heading(format_string($discussion->name), 3, 'discussionname');
+
     if ((!isguestuser() && isloggedin()) && has_capability('mod/forum:viewdiscussion', $modcontext)) {
         // Discussion subscription.
         if (\mod_forum\subscriptions::is_subscribable($forum)) {
-            $headingvalue .= '&nbsp;';
-            $headingvalue .= html_writer::tag('span', forum_get_discussion_subscription_icon($forum, $post->discussion), array(
-                'class' => 'discussionsubscription',
-            ));
+            echo html_writer::div(
+                forum_get_discussion_subscription_icon($forum, $post->discussion, null, true),
+                'discussionsubscription'
+            );
         }
     }
-    echo $OUTPUT->heading($headingvalue, 2);
 
 
 /// Check to see if groups are being used in this forum
 
     // Output the links to neighbour discussions.
     $neighbours = forum_get_discussion_neighbours($cm, $discussion);
-    echo $renderer->neighbouring_discussion_navigation($neighbours['prev'], $neighbours['next']);
+    $neighbourlinks = $renderer->neighbouring_discussion_navigation($neighbours['prev'], $neighbours['next']);
+    echo $neighbourlinks;
 
 /// Print the controls across the top
     echo '<div class="discussioncontrols clearfix">';
     $canrate = has_capability('mod/forum:rate', $modcontext);
     forum_print_discussion($course, $cm, $forum, $discussion, $post, $displaymode, $canreply, $canrate);
 
+    echo $neighbourlinks;
+
     // Add the subscription toggle JS.
     $PAGE->requires->yui_module('moodle-mod_forum-subscriptiontoggle', 'Y.M.mod_forum.subscriptiontoggle.init');
 
index 5989935..f326af2 100644 (file)
@@ -472,9 +472,13 @@ class mod_forum_external extends external_api {
             }
 
             // Function forum_get_all_discussion_posts adds postread field.
-            if (!isset($post->postread)) {
+            // Note that the value returned can be a boolean or an integer. The WS expects a boolean.
+            if (empty($post->postread)) {
                 $posts[$pid]->postread = false;
+            } else {
+                $posts[$pid]->postread = true;
             }
+
             $posts[$pid]->canreply = $canreply;
             if (!empty($posts[$pid]->children)) {
                 $posts[$pid]->children = array_keys($posts[$pid]->children);
index b06f87e..f0ea4a8 100644 (file)
@@ -268,7 +268,7 @@ if ($generalforums) {
         }
 
         if ($can_subscribe) {
-            if (\mod_forum\subscriptions::subscription_disabled($forum)) {
+            if (!\mod_forum\subscriptions::subscription_disabled($forum)) {
                 $row[] = forum_get_subscribe_link($forum, $context, array('subscribed' => $stryes,
                         'unsubscribed' => $strno, 'forcesubscribed' => $stryes,
                         'cantsubscribe' => '-'), false, false, true);
index 177939a..85ce9cd 100644 (file)
@@ -80,7 +80,7 @@ $string['cannotviewpostyet'] = 'You cannot read other students questions in this
 $string['cannotviewusersposts'] = 'There are no posts made by this user that you are able to view.';
 $string['cleanreadtime'] = 'Mark old posts as read hour';
 $string['clicktounsubscribe'] = 'You are subscribed to this discussion. Click to unsubscribe.';
-$string['clicktosubscribe'] = 'Not subscribed. Click to subscribe.';
+$string['clicktosubscribe'] = 'You are not subscribed to this discussion. Click to subscribe.';
 $string['completiondiscussions'] = 'Student must create discussions:';
 $string['completiondiscussionsgroup'] = 'Require discussions';
 $string['completiondiscussionshelp'] = 'requiring discussions to complete';
@@ -343,6 +343,7 @@ $string['nopostsmadebyuser'] = '{$a} has made no posts';
 $string['nopostsmadebyyou'] = 'You haven\'t made any posts';
 $string['noquestions'] = 'There are no questions yet in this forum';
 $string['nosubscribers'] = 'There are no subscribers yet for this forum';
+$string['notsubscribed'] = 'Not subscribed';
 $string['notexists'] = 'Discussion no longer exists';
 $string['nothingnew'] = 'Nothing new for {$a}';
 $string['notingroup'] = 'Sorry, but you need to be part of a group to see this forum.';
@@ -489,7 +490,9 @@ $string['unreadpostsnumber'] = '{$a} unread posts';
 $string['unreadpostsone'] = '1 unread post';
 $string['unsubscribe'] = 'Unsubscribe from this forum';
 $string['unsubscribeall'] = 'Unsubscribe from all forums';
-$string['unsubscribeallconfirm'] = 'You are subscribed to {$a} forums now. Do you really want to unsubscribe from all forums and disable forum auto-subscribe?';
+$string['unsubscribeallconfirm'] = 'You are currently subscribed to {$a->forums} forums, and {$a->discussions} discussions. Do you really want to unsubscribe from all forums and discussions, and disable discussion auto-subscription?';
+$string['unsubscribeallconfirmforums'] = 'You are currently subscribed to {$a->forums} forums. Do you really want to unsubscribe from all forums and disable discussion auto-subscription?';
+$string['unsubscribeallconfirmdiscussions'] = 'You are currently subscribed to {$a->discussions} discussions. Do you really want to unsubscribe from all discussions and disable discussion auto-subscription?';
 $string['unsubscribealldone'] = 'All optional forum subscriptions were removed. You will still receive notifications from forums with forced subscription. To manage forum notifications go to Messaging in My Profile Settings.';
 $string['unsubscribeallempty'] = 'You are not subscribed to any forums. To disable all notifications from this server go to Messaging in My Profile Settings.';
 $string['unsubscribed'] = 'Unsubscribed';
index 8848e70..54fcac2 100644 (file)
@@ -3768,7 +3768,7 @@ function forum_print_discussion_header(&$post, $forum, $group=-1, $datestring=""
  * @param int $discussionid The discussion to create an icon for.
  * @return string The generated markup.
  */
-function forum_get_discussion_subscription_icon($forum, $discussionid, $returnurl = null) {
+function forum_get_discussion_subscription_icon($forum, $discussionid, $returnurl = null, $includetext = false) {
     global $USER, $OUTPUT, $PAGE;
 
     if ($returnurl === null && $PAGE->url) {
@@ -3791,6 +3791,7 @@ function forum_get_discussion_subscription_icon($forum, $discussionid, $returnur
                 'class' => 'discussiontoggle iconsmall',
                 'data-forumid' => $forum->id,
                 'data-discussionid' => $discussionid,
+                'data-includetext' => $includetext,
         ));
     } else {
         $o .= html_writer::link($subscriptionlink,
@@ -3800,9 +3801,14 @@ function forum_get_discussion_subscription_icon($forum, $discussionid, $returnur
                 'class' => 'discussiontoggle iconsmall',
                 'data-forumid' => $forum->id,
                 'data-discussionid' => $discussionid,
+                'data-includetext' => $includetext,
         ));
     }
 
+    if ($includetext) {
+        $o .= $subscriptionstatus ? get_string('subscribed', 'mod_forum') : get_string('notsubscribed', 'mod_forum');
+    }
+
     return $o;
 }
 
@@ -5956,57 +5962,58 @@ function forum_tp_mark_posts_read($user, $postids) {
         return $status;
     }
 
-    list($usql, $params) = $DB->get_in_or_equal($postids);
-    $params[] = $user->id;
-
-    $sql = "SELECT id
-              FROM {forum_read}
-             WHERE postid $usql AND userid = ?";
-    if ($existing = $DB->get_records_sql($sql, $params)) {
-        $existing = array_keys($existing);
-    } else {
-        $existing = array();
-    }
-
-    $new = array_diff($postids, $existing);
-
-    if ($new) {
-        list($usql, $new_params) = $DB->get_in_or_equal($new);
-        $params = array($user->id, $now, $now, $user->id);
-        $params = array_merge($params, $new_params);
-        $params[] = $cutoffdate;
-
-        if ($CFG->forum_allowforcedreadtracking) {
-            $trackingsql = "AND (f.trackingtype = ".FORUM_TRACKING_FORCED."
-                            OR (f.trackingtype = ".FORUM_TRACKING_OPTIONAL." AND tf.id IS NULL))";
-        } else {
-            $trackingsql = "AND ((f.trackingtype = ".FORUM_TRACKING_OPTIONAL."  OR f.trackingtype = ".FORUM_TRACKING_FORCED.")
-                                AND tf.id IS NULL)";
-        }
-
-        $sql = "INSERT INTO {forum_read} (userid, postid, discussionid, forumid, firstread, lastread)
+    list($usql, $postidparams) = $DB->get_in_or_equal($postids, SQL_PARAMS_NAMED, 'postid');
 
-                SELECT ?, p.id, p.discussion, d.forum, ?, ?
-                  FROM {forum_posts} p
-                       JOIN {forum_discussions} d       ON d.id = p.discussion
-                       JOIN {forum} f                   ON f.id = d.forum
-                       LEFT JOIN {forum_track_prefs} tf ON (tf.userid = ? AND tf.forumid = f.id)
-                 WHERE p.id $usql
-                       AND p.modified >= ?
-                       $trackingsql";
-        $status = $DB->execute($sql, $params) && $status;
-    }
-
-    if ($existing) {
-        list($usql, $new_params) = $DB->get_in_or_equal($existing);
-        $params = array($now, $user->id);
-        $params = array_merge($params, $new_params);
+    $insertparams = array(
+        'userid1' => $user->id,
+        'userid2' => $user->id,
+        'userid3' => $user->id,
+        'firstread' => $now,
+        'lastread' => $now,
+        'cutoffdate' => $cutoffdate,
+    );
+    $params = array_merge($postidparams, $insertparams);
 
-        $sql = "UPDATE {forum_read}
-                   SET lastread = ?
-                 WHERE userid = ? AND postid $usql";
-        $status = $DB->execute($sql, $params) && $status;
-    }
+    if ($CFG->forum_allowforcedreadtracking) {
+        $trackingsql = "AND (f.trackingtype = ".FORUM_TRACKING_FORCED."
+                        OR (f.trackingtype = ".FORUM_TRACKING_OPTIONAL." AND tf.id IS NULL))";
+    } else {
+        $trackingsql = "AND ((f.trackingtype = ".FORUM_TRACKING_OPTIONAL."  OR f.trackingtype = ".FORUM_TRACKING_FORCED.")
+                            AND tf.id IS NULL)";
+    }
+
+    // First insert any new entries.
+    $sql = "INSERT INTO {forum_read} (userid, postid, discussionid, forumid, firstread, lastread)
+
+            SELECT :userid1, p.id, p.discussion, d.forum, :firstread, :lastread
+                FROM {forum_posts} p
+                    JOIN {forum_discussions} d       ON d.id = p.discussion
+                    JOIN {forum} f                   ON f.id = d.forum
+                    LEFT JOIN {forum_track_prefs} tf ON (tf.userid = :userid2 AND tf.forumid = f.id)
+                    LEFT JOIN {forum_read} fr        ON (
+                            fr.userid = :userid3
+                        AND fr.postid = p.id
+                        AND fr.discussionid = d.id
+                        AND fr.forumid = f.id
+                    )
+                WHERE p.id $usql
+                    AND p.modified >= :cutoffdate
+                    $trackingsql
+                    AND fr.id IS NULL";
+
+    $status = $DB->execute($sql, $params) && $status;
+
+    // Then update all records.
+    $updateparams = array(
+        'userid' => $user->id,
+        'lastread' => $now,
+    );
+    $params = array_merge($postidparams, $updateparams);
+    $status = $DB->set_field_select('forum_read', 'lastread', $now, '
+                userid      =  :userid
+            AND lastread    <> :lastread
+            AND postid      ' . $usql,
+            $params) && $status;
 
     return $status;
 }
index 23862e8..3464d6b 100644 (file)
@@ -107,6 +107,8 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
         print_error("invalidcoursemodule");
     }
 
+    // Retrieve the contexts.
+    $modcontext    = context_module::instance($cm->id);
     $coursecontext = context_course::instance($course->id);
 
     if (! forum_user_can_post_discussion($forum, $groupid, -1, $cm)) {
@@ -122,7 +124,7 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
         print_error('nopostforum', 'forum');
     }
 
-    if (!$cm->visible and !has_capability('moodle/course:viewhiddenactivities', $coursecontext)) {
+    if (!$cm->visible and !has_capability('moodle/course:viewhiddenactivities', $modcontext)) {
         print_error("activityiscurrentlyhidden");
     }
 
@@ -176,8 +178,9 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
     // Ensure lang, theme, etc. is set up properly. MDL-6926
     $PAGE->set_cm($cm, $course, $forum);
 
-    $coursecontext = context_course::instance($course->id);
+    // Retrieve the contexts.
     $modcontext    = context_module::instance($cm->id);
+    $coursecontext = context_course::instance($course->id);
 
     if (! forum_user_can_post($forum, $discussion, $USER, $cm, $course, $modcontext)) {
         if (!isguestuser()) {
@@ -206,7 +209,7 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
         }
     }
 
-    if (!$cm->visible and !has_capability('moodle/course:viewhiddenactivities', $coursecontext)) {
+    if (!$cm->visible and !has_capability('moodle/course:viewhiddenactivities', $modcontext)) {
         print_error("activityiscurrentlyhidden");
     }
 
@@ -613,9 +616,8 @@ $postid = empty($post->id) ? null : $post->id;
 $draftid_editor = file_get_submitted_draft_itemid('message');
 $currenttext = file_prepare_draft_area($draftid_editor, $modcontext->id, 'mod_forum', 'post', $postid, mod_forum_post_form::editor_options($modcontext, $postid), $post->message);
 
-// Always suggest that the user be subscribed to a discussion that they're posting in unless they've already posted, in
-// which case use their existing preference.
-$discussionsubscribe = true;
+// Respect the user's discussion autosubscribe preference unless they have already posted - in which case, use that preference.
+$discussionsubscribe = $USER->autosubscribe;
 if (isset($discussion) && forum_user_has_posted($forum->id, $discussion->id, $USER->id)) {
     $discussionsubscribe = \mod_forum\subscriptions::is_subscribed($USER->id, $forum, $discussion->id, $cm);
 }
@@ -655,7 +657,14 @@ $mform_post->set_data(array(        'attachments'=>$draftitemid,
                                     array('discussion'=>$discussion->id):
                                     array()));
 
-if ($fromform = $mform_post->get_data()) {
+if ($mform_post->is_cancelled()) {
+    if (!isset($discussion->id) || $forum->type === 'qanda') {
+        // Q and A forums don't have a discussion page, so treat them like a new thread..
+        redirect(new moodle_url('/mod/forum/view.php', array('f' => $forum->id)));
+    } else {
+        redirect(new moodle_url('/mod/forum/discuss.php', array('d' => $discussion->id)));
+    }
+} else if ($fromform = $mform_post->get_data()) {
 
     if (empty($SESSION->fromurl)) {
         $errordestination = "$CFG->wwwroot/mod/forum/view.php?f=$forum->id";
index f8793a7..4ef2c36 100644 (file)
 .dir-rtl .forumpost .row .content-mask,
 .dir-rtl .forumpost .row .options {margin-right:43px;margin-left:0;}
 .dir-rtl .forumpost .row .left {float:right;}
-.dir-rtl.path-mod-forum .indent {margin-right:30px;margin-left:0;}
+.dir-rtl.path-mod-forum .indent {
+    margin-right: 3%;
+    margin-left: 0;
+}
 
 .path-mod-forum .forumolddiscuss,
 #page-mod-forum-search .c0 {text-align:right;}
-.path-mod-forum .indent {margin-left: 30px;}
+.path-mod-forum .indent {
+    margin-left: 3%;
+}
 .path-mod-forum .forumheaderlist {width: 100%;border-width:1px;border-style:solid;border-collapse:separate;margin-top: 10px;}
 .path-mod-forum .forumheaderlist td {border-width:1px 0px 0px 1px;border-style:solid;}
 .path-mod-forum .forumheaderlist th.header.replies .iconsmall { margin: 0 .3em;}
@@ -142,3 +147,17 @@ span.unread {
     display: inline-block;
     width: 12px;
 }
+
+.path-mod-forum .discussionsubscription {
+    margin-top: -10px;
+    text-align: right;
+    margin-bottom: 10px;
+}
+
+.path-mod-forum .discussionsubscription > a {
+    padding: 0 4px;
+}
+
+.dir-rtl .path-mod-forum .discussionsubscription {
+    text-align: left;
+}
index d2a867a..cbee96e 100644 (file)
@@ -29,6 +29,7 @@ require_once($CFG->dirroot . '/mod/forum/lib.php');
 $forumid        = required_param('forumid', PARAM_INT);             // The forum to subscribe or unsubscribe.
 $discussionid   = optional_param('discussionid', null, PARAM_INT);  // The discussionid to subscribe.
 $sesskey        = optional_param('sesskey', null, PARAM_RAW);
+$includetext    = optional_param('includetext', false, PARAM_BOOL);
 
 $forum          = $DB->get_record('forum', array('id' => $forumid), '*', MUST_EXIST);
 $course         = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST);
@@ -37,6 +38,7 @@ $cm             = get_coursemodule_from_instance('forum', $forum->id, $course->i
 $context        = context_module::instance($cm->id);
 
 require_login($course, false, $cm);
+require_capability('mod/forum:viewdiscussion', $context);
 
 $return = new stdClass();
 
@@ -55,6 +57,6 @@ if (\mod_forum\subscriptions::is_subscribed($USER->id, $forum, $discussion->id,
 }
 
 // Now return the updated subscription icon.
-$return->icon = forum_get_discussion_subscription_icon($forum, $discussion->id);
+$return->icon = forum_get_discussion_subscription_icon($forum, $discussion->id, null, $includetext);
 echo json_encode($return);
 die;
index cfdacb8..f8f3077 100644 (file)
@@ -35,23 +35,23 @@ Feature: A user can control their own subscription preferences for a discussion
     And I follow "Course 1"
     And I follow "Test forum name"
     Then I should see "Subscribe to this forum"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
-    And I click on "Not subscribed. Click to subscribe." "link" in the "Test post subject one" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
+    And I click on "You are not subscribed to this discussion. Click to subscribe." "link" in the "Test post subject one" "table_row"
     And I follow "Continue"
     And I should see "Subscribe to this forum"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject one" "table_row"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
     And I click on "You are subscribed to this discussion. Click to unsubscribe." "link" in the "Test post subject one" "table_row"
     And I follow "Continue"
     And I should see "Subscribe to this forum"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
-    And I click on "Not subscribed. Click to subscribe." "link" in the "Test post subject one" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
+    And I click on "You are not subscribed to this discussion. Click to subscribe." "link" in the "Test post subject one" "table_row"
     And I follow "Continue"
     And I should see "Subscribe to this forum"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject one" "table_row"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
     And I follow "Subscribe to this forum"
     And I follow "Continue"
     And I should see "Unsubscribe from this forum"
@@ -60,8 +60,8 @@ Feature: A user can control their own subscription preferences for a discussion
     And I follow "Unsubscribe from this forum"
     And I follow "Continue"
     And I should see "Subscribe to this forum"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
 
   Scenario: An automatic subscription forum can have discussions unsubscribed from
     Given I add a "Forum" to section "1" and I fill the form with:
@@ -85,9 +85,9 @@ Feature: A user can control their own subscription preferences for a discussion
     And I click on "You are subscribed to this discussion. Click to unsubscribe." "link" in the "Test post subject one" "table_row"
     And I follow "Continue"
     And I should see "Unsubscribe from this forum"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject two" "table_row"
-    And I click on "Not subscribed. Click to subscribe." "link" in the "Test post subject one" "table_row"
+    And I click on "You are not subscribed to this discussion. Click to subscribe." "link" in the "Test post subject one" "table_row"
     And I follow "Continue"
     And I should see "Unsubscribe from this forum"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject one" "table_row"
@@ -95,13 +95,13 @@ Feature: A user can control their own subscription preferences for a discussion
     And I click on "You are subscribed to this discussion. Click to unsubscribe." "link" in the "Test post subject one" "table_row"
     And I follow "Continue"
     And I should see "Unsubscribe from this forum"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject two" "table_row"
     And I follow "Unsubscribe from this forum"
     And I follow "Continue"
     And I should see "Subscribe to this forum"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
     And I follow "Subscribe to this forum"
     And I follow "Continue"
     And I should see "Unsubscribe from this forum"
@@ -125,13 +125,13 @@ Feature: A user can control their own subscription preferences for a discussion
     And I follow "Course 1"
     And I follow "Test forum name"
     And I should see "Subscribe to this forum"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
-    And I click on "Not subscribed. Click to subscribe." "link" in the "Test post subject one" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
+    And I click on "You are not subscribed to this discussion. Click to subscribe." "link" in the "Test post subject one" "table_row"
     And I follow "Continue"
     And I should see "Subscribe to this forum"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject one" "table_row"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
     And I log out
     And I log in as "admin"
     And I follow "Course 1"
@@ -151,7 +151,7 @@ Feature: A user can control their own subscription preferences for a discussion
     And I follow "Continue"
     Then I should see "Subscribe to this forum"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject one" "table_row"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
 
   Scenario: A user does not lose their preferences when a forum is switch from optional to automatic
     Given I add a "Forum" to section "1" and I fill the form with:
@@ -170,13 +170,13 @@ Feature: A user can control their own subscription preferences for a discussion
     And I follow "Course 1"
     And I follow "Test forum name"
     And I should see "Subscribe to this forum"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
-    And I click on "Not subscribed. Click to subscribe." "link" in the "Test post subject one" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject one" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
+    And I click on "You are not subscribed to this discussion. Click to subscribe." "link" in the "Test post subject one" "table_row"
     And I follow "Continue"
     And I should see "Subscribe to this forum"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject one" "table_row"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
     And I log out
     And I log in as "admin"
     And I follow "Course 1"
@@ -196,7 +196,7 @@ Feature: A user can control their own subscription preferences for a discussion
     And I follow "Continue"
     Then I should see "Subscribe to this forum"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject one" "table_row"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
 
   Scenario: An optional forum prompts a user to subscribe to a discussion when posting unless they have already chosen not to subscribe
     Given I add a "Forum" to section "1" and I fill the form with:
@@ -225,7 +225,7 @@ Feature: A user can control their own subscription preferences for a discussion
       | Discussion subscription | I don't want email copies of posts to this discussion |
     And I follow "Test forum name"
     Then "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject one" "table_row"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
     And I follow "Test post subject one"
     And I follow "Reply"
     And the field "Discussion subscription" matches value "Send me email copies of posts to this discussion"
@@ -261,7 +261,7 @@ Feature: A user can control their own subscription preferences for a discussion
       | Discussion subscription | I don't want email copies of posts to this discussion |
     And I follow "Test forum name"
     Then "You are subscribed to this discussion. Click to unsubscribe." "link" should exist in the "Test post subject one" "table_row"
-    And "Not subscribed. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should exist in the "Test post subject two" "table_row"
     And I follow "Test post subject one"
     And I follow "Reply"
     And the field "Discussion subscription" matches value "Send me email copies of posts to this discussion"
@@ -282,10 +282,10 @@ Feature: A user can control their own subscription preferences for a discussion
    And I log out
    When I log in as "guest"
    And I follow "Test forum name"
-   Then "Not subscribed. Click to subscribe." "link" should not exist in the "Test post subject one" "table_row"
+   Then "You are not subscribed to this discussion. Click to subscribe." "link" should not exist in the "Test post subject one" "table_row"
    And "You are subscribed to this discussion. Click to unsubscribe." "link" should not exist in the "Test post subject one" "table_row"
    And I follow "Test post subject one"
-   And "Not subscribed. Click to subscribe." "link" should not exist
+   And "You are not subscribed to this discussion. Click to subscribe." "link" should not exist
    And "You are subscribed to this discussion. Click to unsubscribe." "link" should not exist
 
  Scenario: A user who is not logged in should not be able to subscribe to a discussion
@@ -299,8 +299,8 @@ Feature: A user can control their own subscription preferences for a discussion
      | Message | Test post message one |
    And I log out
    When I follow "Test forum name"
-   Then "Not subscribed. Click to subscribe." "link" should not exist in the "Test post subject one" "table_row"
+   Then "You are not subscribed to this discussion. Click to subscribe." "link" should not exist in the "Test post subject one" "table_row"
    And "You are subscribed to this discussion. Click to unsubscribe." "link" should not exist in the "Test post subject one" "table_row"
    And I follow "Test post subject one"
-   And "Not subscribed. Click to subscribe." "link" should not exist
+   And "You are not subscribed to this discussion. Click to subscribe." "link" should not exist
    And "You are subscribed to this discussion. Click to unsubscribe." "link" should not exist
index fa351e4..cc7f766 100644 (file)
@@ -34,7 +34,7 @@ Feature: A user can control their own subscription preferences for a forum
     Then I should not see "Subscribe to this forum"
     And I should not see "Unsubscribe from this forum"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should not exist in the "Test post subject" "table_row"
-    And "Not subscribed. Click to subscribe." "link" should not exist in the "Test post subject" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should not exist in the "Test post subject" "table_row"
 
   Scenario: A forced subscription forum cannot be subscribed to
     Given I add a "Forum" to section "1" and I fill the form with:
@@ -52,7 +52,7 @@ Feature: A user can control their own subscription preferences for a forum
     Then I should not see "Subscribe to this forum"
     And I should not see "Unsubscribe from this forum"
     And "You are subscribed to this discussion. Click to unsubscribe." "link" should not exist in the "Test post subject" "table_row"
-    And "Not subscribed. Click to subscribe." "link" should not exist in the "Test post subject" "table_row"
+    And "You are not subscribed to this discussion. Click to subscribe." "link" should not exist in the "Test post subject" "table_row"
 
   Scenario: An optional forum can be subscribed to
     Given I add a "Forum" to section "1" and I fill the form with:
index acecc23..0130c0b 100644 (file)
@@ -52,6 +52,7 @@ if (data_submitted() and $confirm and confirm_sesskey()) {
     foreach($forums as $forum) {
         \mod_forum\subscriptions::unsubscribe_user($USER->id, $forum, context_module::instance($forum->cm), true);
     }
+    $DB->delete_records('forum_discussion_subs', array('userid' => $USER->id));
     $DB->set_field('user', 'autosubscribe', 0, array('id'=>$USER->id));
 
     echo $OUTPUT->box(get_string('unsubscribealldone', 'forum'));
@@ -60,10 +61,18 @@ if (data_submitted() and $confirm and confirm_sesskey()) {
     die;
 
 } else {
-    $a = count(\mod_forum\subscriptions::get_unsubscribable_forums());
+    $count = new stdClass();
+    $count->forums = count(\mod_forum\subscriptions::get_unsubscribable_forums());
+    $count->discussions = $DB->count_records('forum_discussion_subs', array('userid' => $USER->id));
 
-    if ($a) {
-        $msg = get_string('unsubscribeallconfirm', 'forum', $a);
+    if ($count->forums || $count->discussions) {
+        if ($count->forums && $count->discussions) {
+            $msg = get_string('unsubscribeallconfirm', 'forum', $count);
+        } else if ($count->forums) {
+            $msg = get_string('unsubscribeallconfirmforums', 'forum', $count);
+        } else if ($count->discussions) {
+            $msg = get_string('unsubscribeallconfirmdiscussions', 'forum', $count);
+        }
         echo $OUTPUT->confirm($msg, new moodle_url('unsubscribeall.php', array('confirm'=>1)), $return);
         echo $OUTPUT->footer();
         die;
index fc107f3..5cbe482 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014082101;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2014103000;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2014050800;       // Requires this Moodle version
 $plugin->component = 'mod_forum';      // Full name of the plugin (used for diagnostics)
index 879a241..44313f0 100644 (file)
Binary files a/mod/forum/yui/build/moodle-mod_forum-subscriptiontoggle/moodle-mod_forum-subscriptiontoggle-debug.js and b/mod/forum/yui/build/moodle-mod_forum-subscriptiontoggle/moodle-mod_forum-subscriptiontoggle-debug.js differ
index 19bf4f9..b920e9f 100644 (file)
Binary files a/mod/forum/yui/build/moodle-mod_forum-subscriptiontoggle/moodle-mod_forum-subscriptiontoggle-min.js and b/mod/forum/yui/build/moodle-mod_forum-subscriptiontoggle/moodle-mod_forum-subscriptiontoggle-min.js differ
index a45deb0..6669223 100644 (file)
Binary files a/mod/forum/yui/build/moodle-mod_forum-subscriptiontoggle/moodle-mod_forum-subscriptiontoggle.js and b/mod/forum/yui/build/moodle-mod_forum-subscriptiontoggle/moodle-mod_forum-subscriptiontoggle.js differ
index 2c6eb5a..d269baa 100644 (file)
@@ -46,7 +46,8 @@ Y.extend(SubscriptionToggle, Y.Base, {
             data: {
                 sesskey: M.cfg.sesskey,
                 forumid: clickedLink.getData('forumid'),
-                discussionid: clickedLink.getData('discussionid')
+                discussionid: clickedLink.getData('discussionid'),
+                includetext: clickedLink.getData('includetext')
             },
             context: this,
             'arguments': {
index 3d5bb58..c432477 100644 (file)
@@ -75,7 +75,8 @@ if ($show_rss = (isset($CFG->enablerssfeeds) && isset($CFG->glossary_enablerssfe
 $currentsection = "";
 
 foreach ($glossarys as $glossary) {
-    if (!$glossary->visible && has_capability('moodle/course:viewhiddenactivities', $context)) {
+    if (!$glossary->visible && has_capability('moodle/course:viewhiddenactivities',
+            context_module::instance($glossary->coursemodule))) {
         // Show dimmed if the mod is hidden.
         $link = "<a class=\"dimmed\" href=\"view.php?id=$glossary->coursemodule\">".format_string($glossary->name,true)."</a>";
     } else if ($glossary->visible) {
index 11e58a4..94331dc 100644 (file)
@@ -182,7 +182,7 @@ switch ($mode) {
 
         $pages = $lesson->load_all_pages();
         foreach ($pages as $key=>$page) {
-            if ($page->qtype !== LESSON_PAGE_ESSAY) {
+            if ($page->qtype != LESSON_PAGE_ESSAY) {
                 unset($pages[$key]);
             }
         }
@@ -265,7 +265,7 @@ switch ($mode) {
         // Get lesson pages that are essay
         $pages = $lesson->load_all_pages();
         foreach ($pages as $key=>$page) {
-            if ($page->qtype !== LESSON_PAGE_ESSAY) {
+            if ($page->qtype != LESSON_PAGE_ESSAY) {
                 unset($pages[$key]);
             }
         }
index b045b31..299b90a 100644 (file)
@@ -83,7 +83,8 @@ class edit_renderer extends \plugin_renderer_base {
         $output .= $this->end_section_list();
 
         // Inialise the JavaScript.
-        $this->initialise_editing_javascript($quizobj->get_course(), $quizobj->get_quiz());
+        $this->initialise_editing_javascript($quizobj->get_course(), $quizobj->get_quiz(),
+                $structure, $contexts, $pagevars, $pageurl);
 
         // Include the contents of any other popups required.
         if ($structure->can_be_edited()) {
@@ -361,6 +362,38 @@ class edit_renderer extends \plugin_renderer_base {
     public function question_row(structure $structure, $question, $contexts, $pagevars, $pageurl) {
         $output = '';
 
+        $output .= $this->page_row($structure, $question, $contexts, $pagevars, $pageurl);
+
+        // Page split/join icon.
+        $joinhtml = '';
+        if ($structure->can_be_edited() && !$structure->is_last_slot_in_quiz($question->slot)) {
+            $joinhtml = $this->page_split_join_button($structure->get_quiz(),
+                    $question, !$structure->is_last_slot_on_page($question->slot));
+        }
+
+        // Question HTML.
+        $questionhtml = $this->question($structure, $question, $pageurl);
+        $questionclasses = 'activity ' . $question->qtype . ' qtype_' . $question->qtype . ' slot';
+
+        $output .= html_writer::tag('li', $questionhtml . $joinhtml,
+                array('class' => $questionclasses, 'id' => 'slot-' . $question->slotid));
+
+        return $output;
+    }
+
+    /**
+     * Displays one question with the surrounding controls.
+     *
+     * @param structure $structure object containing the structure of the quiz.
+     * @param \stdClass $question data from the question and quiz_slots tables.
+     * @param \question_edit_contexts $contexts the relevant question bank contexts.
+     * @param array $pagevars the variables from {@link \question_edit_setup()}.
+     * @param \moodle_url $pageurl the canonical URL of this page.
+     * @return string HTML to output.
+     */
+    public function page_row(structure $structure, $question, $contexts, $pagevars, $pageurl) {
+        $output = '';
+
         // Put page in a span for easier styling.
         $page = html_writer::tag('span', get_string('page') . ' ' . $question->page,
                 array('class' => 'text'));
@@ -378,20 +411,6 @@ class edit_renderer extends \plugin_renderer_base {
                     array('class' => 'pagenumber activity yui3-dd-drop page', 'id' => 'page-' . $question->page));
         }
 
-        // Page split/join icon.
-        $joinhtml = '';
-        if ($structure->can_be_edited() && !$structure->is_last_slot_in_quiz($question->slot)) {
-            $joinhtml = $this->page_split_join_button($structure->get_quiz(),
-                    $question, !$structure->is_last_slot_on_page($question->slot));
-        }
-
-        // Question HTML.
-        $questionhtml = $this->question($structure, $question, $pageurl);
-        $questionclasses = 'activity ' . $question->qtype . ' qtype_' . $question->qtype . ' slot';
-
-        $output .= html_writer::tag('li', $questionhtml . $joinhtml,
-                array('class' => $questionclasses, 'id' => 'slot-' . $question->slotid));
-
         return $output;
     }
 
@@ -413,6 +432,7 @@ class edit_renderer extends \plugin_renderer_base {
         }
         $menu = new \action_menu();
         $menu->set_alignment(\action_menu::TR, \action_menu::BR);
+        $menu->set_constraint('.mod-quiz-edit-content');
         $trigger = html_writer::tag('span', get_string('add', 'quiz'), array('class' => 'add-menu'));
         $menu->set_menu_trigger($trigger);
 
@@ -644,8 +664,8 @@ class edit_renderer extends \plugin_renderer_base {
      *
      * @param \stdClass $quiz the quiz settings from the database.
      * @param \stdClass $question data from the question and quiz_slots tables.
-     * @param string $insertpagebreak if true, show an insert page break icon.
-     *      Else show a join pages icon.
+     * @param bool $insertpagebreak if true, show an insert page break icon.
+     *      else show a join pages icon.
      * @return string HTML to output.
      */
     public function page_split_join_button($quiz, $question, $insertpagebreak) {
@@ -653,13 +673,13 @@ class edit_renderer extends \plugin_renderer_base {
                     'slot' => $question->slot, 'repag' => $insertpagebreak ? 2 : 1, 'sesskey' => sesskey()));
 
         if ($insertpagebreak) {
-            $title = get_string('splitpages', 'quiz');
+            $title = get_string('addpagebreak', 'quiz');
             $image = $this->pix_icon('e/insert_page_break', $title);
-            $action = 'unlinkpage';
+            $action = 'addpagebreak';
         } else {
-            $title = get_string('joinpages', 'quiz');
+            $title = get_string('removepagebreak', 'quiz');
             $image = $this->pix_icon('e/remove_page_break', $title);
-            $action = 'linkpage';
+            $action = 'removepagebreak';
         }
 
         // Disable the link if quiz has attempts.
@@ -668,7 +688,7 @@ class edit_renderer extends \plugin_renderer_base {
             $disabled = "disabled";
         }
         return html_writer::span($this->action_link($url, $image, null, array('title' => $title,
-                    'class' => 'page_split_join', 'disabled' => $disabled, 'data-action' => $action)),
+                    'class' => 'page_split_join cm-edit-action', 'disabled' => $disabled, 'data-action' => $action)),
                 'page_split_join_wrapper');
     }
 
@@ -837,17 +857,23 @@ class edit_renderer extends \plugin_renderer_base {
      *
      * @param \stdClass $course the course settings from the database.
      * @param \stdClass $quiz the quiz settings from the database.
+     * @param structure $structure object containing the structure of the quiz.
+     * @param \question_edit_contexts $contexts the relevant question bank contexts.
+     * @param array $pagevars the variables from {@link \question_edit_setup()}.
+     * @param \moodle_url $pageurl the canonical URL of this page.
      * @return bool Always returns true
      */
-    protected function initialise_editing_javascript($course, $quiz) {
+    protected function initialise_editing_javascript($course, $quiz, structure $structure,
+            \question_edit_contexts $contexts, array $pagevars, \moodle_url $pageurl) {
 
         $config = new \stdClass();
         $config->resourceurl = '/mod/quiz/edit_rest.php';
         $config->sectionurl = '/mod/quiz/edit_rest.php';
         $config->pageparams = array();
         $config->questiondecimalpoints = $quiz->questiondecimalpoints;
+        $config->pagehtml = $this->new_page_template($structure, $contexts, $pagevars, $pageurl);
+        $config->addpageiconhtml = $this->add_page_icon_template($structure, $quiz);
 
-        // Include toolboxes.
         $this->page->requires->yui_module('moodle-mod_quiz-toolboxes',
                 'M.mod_quiz.init_resource_toolbox',
                 array(array(
@@ -857,6 +883,9 @@ class edit_renderer extends \plugin_renderer_base {
                         'config' => $config,
                 ))
         );
+        unset($config->pagehtml);
+        unset($config->addpageiconhtml);
+
         $this->page->requires->yui_module('moodle-mod_quiz-toolboxes',
                 'M.mod_quiz.init_section_toolbox',
                 array(array(
@@ -868,7 +897,6 @@ class edit_renderer extends \plugin_renderer_base {
                 ))
         );
 
-        // Include course dragdrop.
         $this->page->requires->yui_module('moodle-mod_quiz-dragdrop', 'M.mod_quiz.init_section_dragdrop',
                 array(array(
                         'courseid' => $course->id,
@@ -900,15 +928,20 @@ class edit_renderer extends \plugin_renderer_base {
                 'movecontent',
                 'moveleft',
                 'movesection',
+                'page',
+                'question',
                 'selectall',
                 'show',
                 'tocontent',
         ), 'moodle');
 
         $this->page->requires->strings_for_js(array(
+                'addpagebreak',
                 'confirmremovequestion',
                 'dragtoafter',
                 'dragtostart',
+                'numquestionsx',
+                'removepagebreak',
         ), 'quiz');
 
         foreach (\question_bank::get_all_qtypes() as $qtype => $notused) {
@@ -918,6 +951,61 @@ class edit_renderer extends \plugin_renderer_base {
         return true;
     }
 
+    /**
+     * HTML for a page, with ids stripped, so it can be used as a javascript template.
+     *
+     * @param structure $structure object containing the structure of the quiz.
+     * @param \question_edit_contexts $contexts the relevant question bank contexts.
+     * @param array $pagevars the variables from {@link \question_edit_setup()}.
+     * @param \moodle_url $pageurl the canonical URL of this page.
+     * @return string HTML for a new page.
+     */
+    protected function new_page_template(structure $structure,
+            \question_edit_contexts $contexts, array $pagevars, \moodle_url $pageurl) {
+        if (!$structure->has_questions()) {
+            return '';
+        }
+
+        $question = $structure->get_question_in_slot(1);
+        $pagehtml = $this->page_row($structure, $question, $contexts, $pagevars, $pageurl);
+
+        // Normalise the page number.
+        $pagenumber = $question->page;
+        $strcontexts = array();
+        $strcontexts[] = 'page-';
+        $strcontexts[] = get_string('page') . ' ';
+        $strcontexts[] = 'addonpage%3D';
+        $strcontexts[] = 'addonpage=';
+        $strcontexts[] = 'addonpage="';
+        $strcontexts[] = get_string('addquestionfrombanktopage', 'quiz', '');
+        $strcontexts[] = 'data-addonpage%3D';
+        $strcontexts[] = 'action-menu-';
+
+        foreach ($strcontexts as $strcontext) {
+            $pagehtml = str_replace($strcontext . $pagenumber, $strcontext . '%%PAGENUMBER%%', $pagehtml);
+        }
+
+        return $pagehtml;
+    }
+
+    /**
+     * HTML for a page, with ids stripped, so it can be used as a javascript template.
+     *
+     * @param structure $structure object containing the structure of the quiz.
+     * @param \stdClass $quiz the quiz settings.
+     * @return string HTML for a new icon
+     */
+    protected function add_page_icon_template(structure $structure, $quiz) {
+
+        if (!$structure->has_questions()) {
+            return '';
+        }
+
+        $question = $structure->get_question_in_slot(1);
+        $html = $this->page_split_join_button($quiz, $question, true);
+        return str_replace('&amp;slot=1&amp;', '&amp;slot=%%SLOT%%&amp;', $html);
+    }
+
     /**
      * Return the contents of the question bank, to be displayed in the question-bank pop-up.
      *
index c81f7ae..ac6405b 100644 (file)
@@ -613,6 +613,8 @@ class structure {
             question_delete_question($slot->questionid);
         }
 
+        unset($this->questions[$slot->questionid]);
+
         $this->refresh_page_numbers_and_update_db($quiz);
 
         $trans->allow_commit();
index 55b09fa..9bfd958 100644 (file)
@@ -105,7 +105,7 @@ switch($requestmethod) {
                         echo json_encode(array('instancemaxmark' => quiz_format_question_grade($quiz, $maxmark),
                                 'newsummarks' => quiz_format_grade($quiz, $quiz->sumgrades)));
                         break;
-                    case 'linkslottopage':
+                    case 'updatepagebreak':
                         require_capability('mod/quiz:manage', $modcontext);
                         $slots = $structure->update_page_break($quiz, $id, $value);
                         $json = array();
@@ -133,7 +133,8 @@ switch($requestmethod) {
                 $structure->remove_slot($quiz, $slot->slot);
                 quiz_delete_previews($quiz);
                 quiz_update_sumgrades($quiz);
-                echo json_encode(array('newsummarks' => quiz_format_grade($quiz, $quiz->sumgrades)));
+                echo json_encode(array('newsummarks' => quiz_format_grade($quiz, $quiz->sumgrades),
+                            'deleted' => true, 'newnumquestions' => $structure->get_question_count()));
                 break;
         }
         break;
index 76143fb..9b94095 100644 (file)
@@ -41,6 +41,7 @@ $string['addnewgroupoverride'] = 'Add group override';
 $string['addnewpagesafterselected'] = 'Add new pages after selected questions';
 $string['addnewquestionsqbank'] = 'Add questions to the category {$a->catname}: {$a->link}';
 $string['addnewuseroverride'] = 'Add user override';
+$string['addpagebreak'] = 'Add page break';
 $string['addpagehere'] = 'Add page here';
 $string['addquestion'] = 'Add question';
 $string['addquestionfrombanktopage'] = 'Add from the question bank to page {$a}';
@@ -431,7 +432,6 @@ $string['invalidquizid'] = 'Invalid quiz ID';
 $string['invalidsource'] = 'The source is not accepted as valid.';
 $string['invalidsourcetype'] = 'Invalid source type.';
 $string['invalidstateid'] = 'Invalid state id';
-$string['joinpages'] = 'Remove page break';
 $string['lastanswer'] = 'Your last answer was';
 $string['layout'] = 'Layout';
 $string['layoutasshown'] = 'Page layout as shown.';
@@ -694,6 +694,7 @@ $string['regradingquiz'] = 'Regrading quiz "{$a}"';
 $string['remove'] = 'Remove';
 $string['removeallquizattempts'] = 'Delete all quiz attempts';
 $string['removeemptypage'] = 'Remove empty page';
+$string['removepagebreak'] = 'Remove page break';
 $string['removeselected'] = 'Remove selected';
 $string['rename'] = 'Rename';
 $string['renderingserverconnectfailed'] = 'The server {$a} failed to process an RQP request. Check that the URL is correct.';
@@ -833,7 +834,6 @@ $string['sortsubmit'] = 'Sort questions';
 $string['sorttypealpha'] = 'Sort by type, name';
 $string['specificapathnotonquestion'] = 'The specified file path is not on the specified question';
 $string['specificquestionnotonquiz'] = 'Specified question is not on the specified quiz';
-$string['splitpages'] = 'Add page break';
 $string['startagain'] = 'Start again';
 $string['startattempt'] = 'Start attempt';
 $string['startedon'] = 'Started on';
@@ -912,7 +912,7 @@ $string['xhtml'] = 'XHTML';
 $string['youneedtoenrol'] = 'You need to enrol in this course before you can attempt this quiz';
 $string['yourfinalgradeis'] = 'Your final grade for this quiz is {$a}.';
 
-// Deprecated since Moodle 2.8
+// Deprecated since Moodle 2.8.
 
 $string['categories'] = 'Categories';
 $string['category'] = 'Category';
index 7931e2b..d574af7 100644 (file)
@@ -222,7 +222,7 @@ M.mod_quiz.secure_window = {
         Y.delegate('mousedown',   M.mod_quiz.secure_window.prevent_mouse, 'body', '*');
         Y.delegate('mouseup',     M.mod_quiz.secure_window.prevent_mouse, 'body', '*');
         Y.delegate('dragstart',   M.mod_quiz.secure_window.prevent, document, '*');
-        Y.delegate('selectstart', M.mod_quiz.secure_window.prevent, document, '*');
+        Y.delegate('selectstart', M.mod_quiz.secure_window.prevent_selection, document, '*');
         Y.delegate('cut',         M.mod_quiz.secure_window.prevent, document, '*');
         Y.delegate('copy',        M.mod_quiz.secure_window.prevent, document, '*');
         Y.delegate('paste',       M.mod_quiz.secure_window.prevent, document, '*');
@@ -246,6 +246,21 @@ M.mod_quiz.secure_window = {
         setTimeout(M.mod_quiz.secure_window.clear_status, 10);
     },
 
+    is_content_editable: function(n) {
+        if (n.test('[contenteditable=true]')) {
+            return true;
+        }
+        n = n.get('parentNode');
+        if (n === null) {
+            return false;
+        }
+        return M.mod_quiz.secure_window.is_content_editable(n);
+    },
+
+    prevent_selection: function(e) {
+        return false;
+    },
+
     prevent: function(e) {
         alert(M.str.quiz.functiondisabledbysecuremode);
         e.halt();
@@ -256,6 +271,10 @@ M.mod_quiz.secure_window = {
             // Left click on a button or similar. No worries.
             return;
         }
+        if (e.button == 1 && M.mod_quiz.secure_window.is_content_editable(e.target)) {
+            // Left click in Atto or similar.
+            return;
+        }
         e.halt();
     },
 
index 9a00d14..511d477 100644 (file)
@@ -913,3 +913,8 @@ table#categoryquestions {
 .questionbank .categoryselectallcontainer {
     background-color: #FFF;
 }
+
+/* Base theme needs extra support. */
+#page-mod-quiz-edit ul.slots li.section ul.section {
+    list-style: none;
+}
index 5886d45..0b43058 100644 (file)
@@ -128,6 +128,22 @@ class behat_mod_quiz extends behat_question_base {
         );
     }
 
+    /**
+     * Check whether a particular question is not on a particular page of the quiz on the Edit quiz page.
+     * @Given /^I should not see "(?P<question_name>(?:[^"]|\\")*)" on quiz page "(?P<page_number>\d+)"$/
+     * @param string $questionname the name of the question we are looking for.
+     * @param number $pagenumber the page it should be found on.
+     * @return array of steps.
+     */
+    public function i_should_not_see_on_quiz_page($questionname, $pagenumber) {
+        $xpath = "//li[contains(., '" . $this->escape($questionname) .
+                "')][./preceding-sibling::li[contains(@class, 'pagenumber')][1][contains(., 'Page " .
+                $pagenumber . "')]]";
+        return array(
+            new Given('"' . $xpath . '" "xpath_element" should not exist'),
+        );
+    }
+
     /**
      * Check whether one question comes before another on the Edit quiz page.
      * The two questions must be on the same page.
@@ -153,13 +169,24 @@ class behat_mod_quiz extends behat_question_base {
      * @return array of steps.
      */
     public function should_have_number_on_the_edit_quiz_page($questionname, $number) {
-        $xpath = "//li[contains(@class, ' slot ') and contains(., '" . $this->escape($questionname) .
-                "')]//span[@class = 'slotnumber' and normalize-space(text()) = '" . $this->escape($number) . "']";
+        $xpath = "//li[contains(@class, 'slot') and contains(., '" . $this->escape($questionname) .
+                "')]//span[contains(@class, 'slotnumber') and normalize-space(text()) = '" . $this->escape($number) . "']";
         return array(
             new Given('"' . $xpath . '" "xpath_element" should exist'),
         );
     }
 
+    /**
+     * Get the xpath for a partcular add/remove page-break icon.
+     * @param string $addorremoves 'Add' or 'Remove'.
+     * @param string $questionname the name of the question before the icon.
+     * @return string the requried xpath.
+     */
+    protected function get_xpath_page_break_icon_after_question($addorremoves, $questionname) {
+        return "//li[contains(@class, 'slot') and contains(., '" . $this->escape($questionname) .
+                "')]//a[contains(@class, 'page_split_join') and @title = '" . $addorremoves . " page break']";
+    }
+
     /**
      * Click the add or remove page-break icon after a particular question.
      * @When /^I click on the "(Add|Remove)" page break icon after question "(?P<question_name>(?:[^"]|\\")*)"$/
@@ -168,8 +195,50 @@ class behat_mod_quiz extends behat_question_base {
      * @return array of steps.
      */
     public function i_click_on_the_page_break_icon_after_question($addorremoves, $questionname) {
-        $xpath = "//li[contains(@class, ' slot ') and contains(., '" . $this->escape($questionname) .
-                "')]//a[@class = 'page_split_join' and @title = '" . $addorremoves . " page break']";
+        $xpath = $this->get_xpath_page_break_icon_after_question($addorremoves, $questionname);
+        return array(
+            new Given('I click on "' . $xpath . '" "xpath_element"'),
+        );
+    }
+
+    /**
+     * Assert the add or remove page-break icon after a particular question exists.
+     * @When /^the "(Add|Remove)" page break icon after question "(?P<question_name>(?:[^"]|\\")*)" should exist$/
+     * @param string $addorremoves 'Add' or 'Remove'.
+     * @param string $questionname the name of the question before the icon to click.
+     * @return array of steps.
+     */
+    public function the_page_break_icon_after_question_should_exist($addorremoves, $questionname) {
+        $xpath = $this->get_xpath_page_break_icon_after_question($addorremoves, $questionname);
+        return array(
+            new Given('"' . $xpath . '" "xpath_element" should exist'),
+        );
+    }
+
+    /**
+     * Assert the add or remove page-break icon after a particular question does not exist.
+     * @When /^the "(Add|Remove)" page break icon after question "(?P<question_name>(?:[^"]|\\")*)" should not exist$/
+     * @param string $addorremoves 'Add' or 'Remove'.
+     * @param string $questionname the name of the question before the icon to click.
+     * @return array of steps.
+     */
+    public function the_page_break_icon_after_question_should_not_exist($addorremoves, $questionname) {
+        $xpath = $this->get_xpath_page_break_icon_after_question($addorremoves, $questionname);
+        return array(
+            new Given('"' . $xpath . '" "xpath_element" should not exist'),
+        );
+    }
+
+    /**
+     * Check the add or remove page-break link after a particular question contains the given parameters in its url.
+     * @When /^the "(Add|Remove)" page break link after question "(?P<question_name>(?:[^"]|\\")*) should contain:"$/
+     * @param string $addorremoves 'Add' or 'Remove'.
+     * @param string $questionname the name of the question before the icon to click.
+     * @param TableNode $paramdata with data for checking the page break url
+     * @return array of steps.
+     */
+    public function the_page_break_link_after_question_should_contain($addorremoves, $questionname, $paramdata) {
+        $xpath = $this->get_xpath_page_break_icon_after_question($addorremoves, $questionname);
         return array(
             new Given('I click on "' . $xpath . '" "xpath_element"'),
         );
@@ -210,4 +279,21 @@ class behat_mod_quiz extends behat_question_base {
                 'and I drop it in "' . $destinationxpath . '" "xpath_element"'),
         );
     }
+
+    /**
+     * Delete a question on the Edit quiz page by first clicking on the Delete icon,
+     * then clicking one of the "After ..." links.
+     * @When /^I delete "(?P<question_name>(?:[^"]|\\")*)" in the quiz by clicking the delete icon$/
+     * @param string $questionname the name of the question we are looking for.
+     * @return array of steps.
+     */
+    public function i_delete_question_by_clicking_the_delete_icon($questionname) {
+        $slotxpath = "//li[contains(@class, ' slot ') and contains(., '" . $this->escape($questionname) .
+                "')]";
+        $deletexpath = "//a[contains(@class, 'editing_delete')]";
+        return array(
+            new Given('I click on "' . $slotxpath . $deletexpath . '" "xpath_element"'),
+            new Given('I click on "Yes" "button" in the "Confirm" "dialogue"'),
+        );
+    }
 }
diff --git a/mod/quiz/tests/behat/editing_click_delete_icon.feature b/mod/quiz/tests/behat/editing_click_delete_icon.feature
new file mode 100644 (file)
index 0000000..a2b47ae
--- /dev/null
@@ -0,0 +1,121 @@
+@mod @mod_quiz
+Feature: Edit quiz page - delete
+  In order to change the layout of a quiz I built
+  As a teacher
+  I need to be able to delete questions.
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email               |
+      | teacher1 | T1        | Teacher1 | teacher1@moodle.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+    And the following "activities" exist:
+      | activity   | name   | course | idnumber |
+      | quiz       | Quiz 1 | C1     | quiz1    |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+
+  @javascript
+  Scenario: Delete questions by clicking on the delete icon.
+    And I add a "True/False" question to the "Quiz 1" quiz with:
+      | Question name | Question A |
+      | Question text | Answer me  |
+    And I add a "True/False" question to the "Quiz 1" quiz with:
+      | Question name | Question B   |
+      | Question text | Answer again |
+    And I add a "True/False" question to the "Quiz 1" quiz with:
+      | Question name | Question C |
+      | Question text | And again  |
+    And I click on the "Add" page break icon after question "Question B"
+
+    # Confirm the starting point.
+    Then I should see "Question A" on quiz page "1"
+    And I should see "Question B" on quiz page "1"
+    And I should see "Question C" on quiz page "2"
+    And I should see "Total of marks: 3.00"
+    And I should see "Questions: 3"
+    And I should see "This quiz is open"
+
+    # Delete last question in last page. Page contains multiple questions
+    When I delete "Question C" in the quiz by clicking the delete icon
+    Then I should see "Question A" on quiz page "1"
+    And I should see "Question B" on quiz page "1"
+    And I should not see "Question C" on quiz page "2"
+    And I should see "Total of marks: 2.00"
+    And I should see "Questions: 2"
+
+    # Delete last question in last page. The page contains multiple questions and there are multiple pages.
+    When I click on the "Add" page break icon after question "Question A"
+    Then I should see "Question B" on quiz page "2"
+    And the "Remove" page break icon after question "Question A" should exist
+    And I delete "Question B" in the quiz by clicking the delete icon
+    Then I should see "Question A" on quiz page "1"
+    And I should not see "Page 2"
+    And I should not see "Question B" on quiz page "2"
+    And the "Remove" page break icon after question "Question A" should not exist
+    And I should see "Total of marks: 1.00"
+
+    # Delete last remaining question in the last remaining page.
+    And I delete "Question A" in the quiz by clicking the delete icon
+    Then I should not see "Question A" on quiz page "1"
+    And I should not see "Page 1"
+    And I should see "Total of marks: 0.00"
+
+  @javascript @edit_quiz_delete_start
+  Scenario: Delete questions from the start of the list.
+    # Add more questions.
+    When I add a "Description" question to the "Quiz 1" quiz with:
+    | Question name | Question A |
+    | Question text | Answer A   |
+
+    And I add a "True/False" question to the "Quiz 1" quiz with:
+    | Question name | Question B |
+    | Question text | Answer B   |
+
+    And I add a "Description" question to the "Quiz 1" quiz with:
+    | Question name | Question C |
+    | Question text | Answer C   |
+
+    And I add a "True/False" question to the "Quiz 1" quiz with:
+    | Question name | Question D |
+    | Question text | Answer D   |
+
+    And I add a "True/False" question to the "Quiz 1" quiz with:
+    | Question name | Question E |
+    | Question text | Answer E   |
+
+    Then "Question A" should have number "i" on the edit quiz page
+    And "Question B" should have number "1" on the edit quiz page
+    And "Question C" should have number "i" on the edit quiz page
+    And "Question D" should have number "2" on the edit quiz page
+    And "Question E" should have number "3" on the edit quiz page
+
+    # Delete from first question in the last remaining page. Are the page breaks updated?
+    When I delete "Question A" in the quiz by clicking the delete icon
+    Then "Question B" should have number "1" on the edit quiz page
+    And "Question C" should have number "i" on the edit quiz page
+    And "Question D" should have number "2" on the edit quiz page
+    And "Question E" should have number "3" on the edit quiz page
+
+    When I click on the "Add" page break icon after question "Question C"
+    Then I should see "Page 1"
+    And I should see "Question B" on quiz page "1"
+    And I should see "Question C" on quiz page "1"
+    Then I should see "Page 2"
+    And I should see "Question D" on quiz page "2"
+    And I should see "Question E" on quiz page "2"
+
+    # Test reorder of pages
+    When I click on the "Add" page break icon after question "Question B"
+    Then I should see "Page 1"
+    And I should see "Question B" on quiz page "1"
+    Then I should see "Page 2"
+    And I should see "Question C" on quiz page "2"
+    Then I should see "Page 3"
+    And I should see "Question D" on quiz page "3"
+    And I should see "Question E" on quiz page "3"
index b1d586c..10ddabd 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-debug.js and b/mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-debug.js differ
index bdc0126..bbf82b0 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-min.js and b/mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-min.js differ
index 76c7621..58da56f 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop.js and b/mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop.js differ
index a035d44..8e0c43c 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-quizbase/moodle-mod_quiz-quizbase-debug.js and b/mod/quiz/yui/build/moodle-mod_quiz-quizbase/moodle-mod_quiz-quizbase-debug.js differ
index eebd343..bb24c94 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-quizbase/moodle-mod_quiz-quizbase.js and b/mod/quiz/yui/build/moodle-mod_quiz-quizbase/moodle-mod_quiz-quizbase.js differ
index aece0a6..46637e9 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js and b/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js differ
index 4dc2ceb..ce8eba7 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js and b/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js differ
index aece0a6..46637e9 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js and b/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js differ
index 7882b6f..d71f038 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page-debug.js and b/mod/quiz/yui/build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page-debug.js differ
index 24de48e..2e23af2 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page-min.js and b/mod/quiz/yui/build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page-min.js differ
index 7882b6f..d71f038 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page.js and b/mod/quiz/yui/build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page.js differ
index 38e3e0a..9992cdf 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-debug.js and b/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-debug.js differ
index ff750af..2c13207 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-min.js and b/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-min.js differ
index 38e3e0a..9992cdf 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot.js and b/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot.js differ
index f4fdd46..74d284e 100644 (file)
@@ -14,7 +14,6 @@ Y.extend(DRAGRESOURCE, M.core.dragdrop, {
         this.groups = ['resource'];
         this.samenodeclass = CSS.ACTIVITY;
         this.parentnodeclass = CSS.SECTION;
-        //this.resourcedraghandle = this.get_drag_handle(M.util.get_string('movecoursemodule', 'moodle'), CSS.EDITINGMOVE, CSS.ICONCLASS, true);
         this.resourcedraghandle = this.get_drag_handle(M.str.moodle.move, CSS.EDITINGMOVE, CSS.ICONCLASS, true);
 
         this.samenodelabel = {
@@ -168,12 +167,11 @@ Y.extend(DRAGRESOURCE, M.core.dragdrop, {
                     var responsetext = Y.JSON.parse(response.responseText);
                     var params = {element: dragnode, visible: responsetext.visible};
                     M.mod_quiz.quizbase.invoke_function('set_visibility_resource_ui', params);
-                    Y.Moodle.mod_quiz.util.slot.reorder_slots();
                     this.unlock_drag_handle(drag, CSS.EDITINGMOVE);
                     window.setTimeout(function() {
                         spinner.hide();
                     }, 250);
-                    window.location.reload(true);
+                    M.mod_quiz.resource_toolbox.reorganise_edit_page();
                 },
                 failure: function(tid, response) {
                     this.ajax_failure(response);
index f80f8ed..b1998ac 100644 (file)
@@ -10,7 +10,9 @@
             "moodle-core-dragdrop",
             "moodle-core-notification",
             "moodle-mod_quiz-quizbase",
-            "moodle-mod_quiz-util",
+            "moodle-mod_quiz-util-base",
+            "moodle-mod_quiz-util-page",
+            "moodle-mod_quiz-util-slot",
             "moodle-course-util"
         ]
     }
index db05ce1..06a0f85 100644 (file)
@@ -77,9 +77,9 @@ M.mod_quiz.edit.swap_sections = function(Y, node1, node2) {
         SECTIONADDMENUS : 'section_add_menus'
     };
 
-    var sectionlist = Y.Node.all('.'+CSS.COURSECONTENT+' '+M.mod_quiz.edit.get_section_selector(Y));
+    var sectionlist = Y.Node.all('.' + CSS.COURSECONTENT + ' ' + M.mod_quiz.edit.get_section_selector(Y));
     // Swap menus.
-    sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.'+CSS.SECTIONADDMENUS));
+    sectionlist.item(node1).one('.' + CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.' + CSS.SECTIONADDMENUS));
 };
 
 /**
@@ -115,7 +115,7 @@ M.mod_quiz.edit.process_sections = function(Y, sectionlist, response, sectionfro
 
         for (var i = sectionfrom; i <= sectionto; i++) {
             // Update section title.
-            sectionlist.item(i).one('.'+CSS.SECTIONNAME).setContent(response.sectiontitles[i]);
+            sectionlist.item(i).one('.' + CSS.SECTIONNAME).setContent(response.sectiontitles[i]);
 
             // Update move icon.
             ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE);
index ba27dfd..fce93b1 100644 (file)
@@ -101,10 +101,10 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
                 // The user is deleting the activity.
                 this.delete_with_confirmation(ev, node, activity, action);
                 break;
-            case 'linkpage':
-            case 'unlinkpage':
-                // The user is linking or unlinking pages.
-                this.link_page(ev, node, activity, action);
+            case 'addpagebreak':
+            case 'removepagebreak':
+                // The user is adding or removing a page break.
+                this.update_page_break(ev, node, activity, action);
                 break;
             default:
                 // Nothing to do here!
@@ -140,10 +140,10 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
      * @chainable
      */
     delete_with_confirmation: function(ev, button, activity) {
-        // Prevent the default button action
+        // Prevent the default button action.
         ev.preventDefault();
 
-        // Get the element we're working on
+        // Get the element we're working on.
         var element   = activity,
             // Create confirm string (different if element has or does not have name)
             confirmstring = '',
@@ -160,19 +160,24 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
         // If it is confirmed.
         confirm.on('complete-yes', function() {
 
-            // Actually remove the element.
-            element.remove();
-            Y.Moodle.mod_quiz.util.slot.reorder_slots();
+            var spinner = this.add_spinner(element);
             var data = {
                 'class': 'resource',
                 'action': 'DELETE',
                 'id': Y.Moodle.mod_quiz.util.slot.getId(element)
             };
-            this.send_request(data);
-            if (M.core.actionmenu && M.core.actionmenu.instance) {
-                M.core.actionmenu.instance.hideMenu();
-            }
-            window.location.reload(true);
+            this.send_request(data, spinner, function(response) {
+                if (response.deleted) {
+                    // Actually remove the element.
+                    Y.Moodle.mod_quiz.util.slot.remove(element);
+                    this.reorganise_edit_page();
+                    if (M.core.actionmenu && M.core.actionmenu.instance) {
+                        M.core.actionmenu.instance.hideMenu();
+                    }
+                } else {
+                    window.location.reload(true);
+                }
+            });
 
         }, this);
 
@@ -215,12 +220,12 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
                 M.core.actionmenu.instance.hideMenu();
             }
 
-            // Try to retrieve the existing string from the server
+            // Try to retrieve the existing string from the server.
             if (response.instancemaxmark) {
                 maxmarktext = response.instancemaxmark;
             }
 
-            // Create the editor and submit button
+            // Create the editor and submit button.
             var editform = Y.Node.create('<form action="#" />');
             var editinstructions = Y.Node.create('<span class="' + CSS.EDITINSTRUCTIONS + '" id="id_editinstructions" />')
                 .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle'));
@@ -232,7 +237,7 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
                 'size' : parseInt(this.get('config').questiondecimalpoints, 10) + 2
             });
 
-            // Clear the existing content and put the editor in
+            // Clear the existing content and put the editor in.
             editform.appendChild(editor);
             editform.setData('anchor', anchor);
             instance.insert(editinstructions, 'before');
@@ -247,7 +252,7 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
             // We hide various components whilst editing:
             activity.addClass(CSS.EDITINGMAXMARK);
 
-            // Focus and select the editor text
+            // Focus and select the editor text.
             editor.focus().select();
 
             // Cancel the edit if we lose focus or the escape key is pressed.
@@ -272,7 +277,7 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
      * @param {String} originalmaxmark The original maxmark the activity or resource had.
      */
     edit_maxmark_submit : function(ev, activity, originalmaxmark) {
-        // We don't actually want to submit anything
+        // We don't actually want to submit anything.
         ev.preventDefault();
         var newmaxmark = Y.Lang.trim(activity.one(SELECTOR.ACTIVITYFORM + ' ' + SELECTOR.ACTIVITYMAXMARK).get('value'));
         var spinner = this.add_spinner(activity);
@@ -350,97 +355,59 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
      * the other slots
      *
      * @protected
-     * @method link_page
+     * @method update_page_break
      * @param {EventFacade} ev The event that was fired.
      * @param {Node} button The button that triggered this action.
      * @param {Node} activity The activity node that this action will be performed on.
      * @chainable
      */
-    link_page: function(ev, button, activity, action) {
+    update_page_break: function(ev, button, activity, action) {
         // Prevent the default button action
         ev.preventDefault();
 
-        activity = activity.next('li.activity.slot');
-        var spinner = this.add_spinner(activity),
+        nextactivity = activity.next('li.activity.slot');
+        var spinner = this.add_spinner(nextactivity),
             slotid = 0;
-        var value = action === 'linkpage' ? 1:2;
+        var value = action === 'removepagebreak' ? 1 : 2;
 
         var data = {
             'class': 'resource',
-            'field': 'linkslottopage',
+            'field': 'updatepagebreak',
             'id':    slotid,
             'value': value
         };
 
-        slotid = Y.Moodle.mod_quiz.util.slot.getId(activity);
+        slotid = Y.Moodle.mod_quiz.util.slot.getId(nextactivity);
         if (slotid) {
             data.id = Number(slotid);
         }
         this.send_request(data, spinner, function(response) {
-            window.location.reload(true);
-//            if (response.slots) {
-//                this.repaginate_slots(response.slots);
-//            }
+            if (response.slots) {
+                if (action === 'addpagebreak') {
+                    Y.Moodle.mod_quiz.util.page.add(activity);
+                } else {
+                    var page = activity.next(Y.Moodle.mod_quiz.util.page.SELECTORS.PAGE);
+                    Y.Moodle.mod_quiz.util.page.remove(page, true);
+                }
+                this.reorganise_edit_page();
+            } else {
+                window.location.reload(true);
+            }
         });
 
         return this;
     },
-    repaginate_slots: function(slots) {
-        this.slots = slots;
-        var section = Y.one(SELECTOR.PAGECONTENT + ' ' + SELECTOR.SECTIONUL),
-            activities = section.all(SELECTOR.ACTIVITYLI);
-        activities.each(function(node) {
-
-            // What element is it? page/slot/link
-            // what is the current slot?
-            var type;
-            var slot;
-            if(node.hasClass(CSS.PAGE)){
-                type = this.NODE_PAGE;
-                slot = node.next(SELECTOR.SLOTLI);
-            } else if (node.hasClass(CSS.SLOT)){
-                type = this.NODE_SLOT;
-                slot = node;
-            } else if (node.hasClass(CSS.JOIN)){
-                type = this.NODE_JOIN;
-                slot = node.previous(SELECTOR.SLOTLI);
-            }
 
-            // getSlotnumber() Should be a method of util.slot
-            var slotnumber = Number(Y.Moodle.mod_quiz.util.slot.getNumber(slot));
-            if(!type){
-                // Nothing we can do.
-                return;
-            }
-
-            // Is it correct?
-            if(!this.slots.hasOwnProperty(slotnumber)){
-                // An error. We should handle this.
-                return;
-            }
-
-            var slotdata = this.slots[slotnumber];
-
-            if(type === this.NODE_PAGE){
-                // Get page number
-                var pagenumber = Y.Moodle.mod_quiz.util.page.getNumber(node);
-                // Is the page number correct?
-                if (slotdata.page === pagenumber) {
-                    console.log('slotdata.page == pagenumber return');
-                    return;
-                }
-
-                if (pagenumber < slotdata.page) {
-                    // Remove page node.
-                    node.remove();
-                }
-                else {
-                    // Add page node.
-                    console.log('pagenumber > slotdata.page update page number');
-                }
-
-            }
-        }, this);
+    /**
+     * Reorganise the UI after every edit action.
+     *
+     * @protected
+     * @method reorganise_edit_page
+     */
+    reorganise_edit_page: function() {
+        Y.Moodle.mod_quiz.util.slot.reorderSlots();
+        Y.Moodle.mod_quiz.util.slot.reorderPageBreaks();
+        Y.Moodle.mod_quiz.util.page.reorderPages();
     },
 
     NAME : 'mod_quiz-resource-toolbox',
index 2e1eb75..d87f4f7 100644 (file)
@@ -51,6 +51,7 @@
         INSTANCEMAXMARK : 'span.instancemaxmark',
         MODINDENTDIV : '.mod-indent',
         MODINDENTOUTER : '.mod-indent-outer',
+        NUMQUESTIONS : '.numberofquestions',
         PAGECONTENT : 'div#page-content',
         PAGELI : 'li.page',
         SECTIONUL : 'ul.section',
@@ -124,6 +125,9 @@ Y.extend(TOOLBOX, Y.Base, {
                     if (responsetext.newsummarks) {
                         Y.one(SELECTOR.SUMMARKS).setHTML(responsetext.newsummarks);
                     }
+                    if (responsetext.newnumquestions) {
+                        Y.one(SELECTOR.NUMQUESTIONS).setHTML(M.util.get_string('numquestionsx', 'quiz', responsetext.newnumquestions));
+                    }
                     if (success_callback) {
                         Y.bind(success_callback, this, responsetext)();
                     }
index 39f49db..b20596f 100644 (file)
@@ -14,13 +14,23 @@ Y.namespace('Moodle.mod_quiz.util.page');
  * @static
  */
 Y.Moodle.mod_quiz.util.page = {
+    CSS: {
+        PAGE : 'page'
+    },
     CONSTANTS: {
-        PAGEIDPREFIX : 'page-',
-        PAGENUMBERPREFIX : 'Page '
+        ACTIONMENUIDPREFIX: 'action-menu-',
+        ACTIONMENUBARIDSUFFIX: '-menubar',
+        ACTIONMENUMENUIDSUFFIX: '-menu',
+        PAGEIDPREFIX: 'page-',
+        PAGENUMBERPREFIX: M.util.get_string('page', 'moodle') + ' '
     },
     SELECTORS: {
+        ACTIONMENU: 'div.moodle-actionmenu',
+        ACTIONMENUBAR: 'ul.menubar',
+        ACTIONMENUMENU: 'ul.menu',
         PAGE: 'li.page',
-        INSTANCENAME: '.instancename'
+        INSTANCENAME: '.instancename',
+        NUMBER: 'span.text'
     },
 
     /**
@@ -35,7 +45,18 @@ Y.Moodle.mod_quiz.util.page = {
     },
 
     /**
-     * Determines the page ID for the provided page.
+     * Retrieve the page item from one of it's previous siblings.
+     *
+     * @method getPageFromSlot
+     * @param pagecomponent {Node} The component Node.
+     * @return {Node|null} The Page Node.
+     */
+    getPageFromSlot: function(slot) {
+        return Y.one(slot).previous(this.SELECTORS.PAGE);
+    },
+
+    /**
+     * Returns the page ID for the provided page.
      *
      * @method getId
      * @param page {Node} The page to find an ID for.
@@ -54,6 +75,18 @@ Y.Moodle.mod_quiz.util.page = {
         return false;
     },
 
+    /**
+     * Updates the page id for the provided page.
+     *
+     * @method setId
+     * @param page {Node} The page to update the number for.
+     * @param id int The id value.
+     * @return void
+     */
+    setId: function(page, id) {
+        page.set('id', this.CONSTANTS.PAGEIDPREFIX + id);
+    },
+
     /**
      * Determines the page name for the provided page.
      *
@@ -77,8 +110,8 @@ Y.Moodle.mod_quiz.util.page = {
      * @return {Number|false} The number of the page in question or false if no number was found.
      */
     getNumber: function(page) {
-        // We perform a simple substitution operation to get the ID.
-        var number = page.get('text').replace(
+        // We perform a simple substitution operation to get the number.
+        var number = page.one(this.SELECTORS.NUMBER).get('text').replace(
                 this.CONSTANTS.PAGENUMBERPREFIX, '');
 
         // Attempt to validate the ID.
@@ -87,5 +120,205 @@ Y.Moodle.mod_quiz.util.page = {
             return number;
         }
         return false;
+    },
+
+    /**
+     * Updates the page number for the provided page.
+     *
+     * @method setNumber
+     * @param page {Node} The page to update the number for.
+     * @return void
+     */
+    setNumber: function(page, number) {
+        page.one(this.SELECTORS.NUMBER).set('text', this.CONSTANTS.PAGENUMBERPREFIX + number);
+    },
+
+    /**
+     * Returns a list of all page elements.
+     *
+     * @method getPages
+     * @return {node[]} An array containing page nodes.
+     */
+    getPages: function() {
+        return Y.all(Y.Moodle.mod_quiz.util.slot.SELECTORS.PAGECONTENT + ' ' + Y.Moodle.mod_quiz.util.slot.SELECTORS.SECTIONUL + ' ' + this.SELECTORS.PAGE);
+    },
+
+    /**
+     * Is the given element a page element?
+     *
+     * @method isPage
+     * @param page Page node
+     * @return boolean
+     */
+    isPage: function(page) {
+        if (!page) {
+            return false;
+        }
+        return page.hasClass(this.CSS.PAGE);
+    },
+
+    /**
+     * Does the page have atleast one slot?
+     *
+     * @method isEmpty
+     * @param page Page node
+     * @return boolean
+     */
+    isEmpty: function(page) {
+        var activity = page.next('li.activity');
+        if (!activity) {
+            return true;
+        }
+        return !activity.hasClass('slot');
+    },
+
+    /**
+     * Add a page and related elements to the list of slots.
+     *
+     * @method add
+     * @param beforenode Int | Node | HTMLElement | String to add
+     * @return page Page node
+     */
+    add: function(beforenode) {
+        var pagenumber = this.getNumber(this.getPageFromSlot(beforenode)) + 1;
+        var pagehtml = M.mod_quiz.resource_toolbox.get('config').pagehtml;
+
+        // Normalise the page number.
+        pagehtml = pagehtml.replace(/%%PAGENUMBER%%/g, pagenumber);
+
+        // Create the page node.
+        var page = Y.Node.create(pagehtml);
+
+        // Assign is as a drop target.
+        YUI().use('dd-drop', function(Y) {
+            var drop = new Y.DD.Drop({
+                node: page,
+                groups: M.mod_quiz.dragres.groups
+            });
+            page.drop = drop;
+        });
+
+        // Insert in the correct place.
+        beforenode.insert(page, 'after');
+
+        // Enhance the add menu to make if fully visible and clickable.
+        M.core.actionmenu.newDOMNode(page);
+        return page;
+    },
+
+    /**
+     * Remove a page and related elements from the list of slots.
+     *
+     * @method remove
+     * @param page Page node
+     * @return void
+     */
+    remove: function(page, keeppagebreak) {
+        // Remove page break from previous slot.
+        var previousslot = page.previous(Y.Moodle.mod_quiz.util.slot.SELECTORS.SLOT);
+        if (!keeppagebreak && previousslot) {
+            Y.Moodle.mod_quiz.util.slot.removePageBreak(previousslot);
+        }
+        page.remove();
+    },
+
+    /**
+     * Reset the order of the numbers given to each page.
+     *
+     * @method reorderPages
+     * @return void
+     */
+    reorderPages: function() {
+        // Get list of page nodes.
+        var pages = this.getPages(), currentpagenumber = 0;
+        // Loop through pages incrementing the number each time.
+        pages.each(function(page) {
+            // Is the page empty?
+            if (this.isEmpty(page)) {
+                var keeppagebreak = page.next('li.slot') ? true : false;
+                this.remove(page, keeppagebreak);
+                return;
+            }
+
+            currentpagenumber++;
+            // Set page number.
+            this.setNumber(page, currentpagenumber);
+            this.setId(page, currentpagenumber);
+        }, this);
+
+        // Reorder action menus
+        this.reorderActionMenus();
+    },
+
+    /**
+     * Reset the order of the numbers given to each action menu.
+     *
+     * @method reorderActionMenus
+     * @return void
+     */
+    reorderActionMenus: function() {
+        // Get list of action menu nodes.
+        var actionmenus = this.getActionMenus();
+        // Loop through pages incrementing the number each time.
+        actionmenus.each(function(actionmenu, key) {
+            var previousActionMenu = actionmenus.item(key - 1);
+                previousActionMenunumber = 0;
+            if (previousActionMenu) {
+                previousActionMenunumber = this.getActionMenuId(previousActionMenu);
+            }
+            var id = previousActionMenunumber + 1;
+
+            // Set menu id.
+            this.setActionMenuId(actionmenu, id);
+
+            // Update action-menu-1-menubar
+            var menubar = actionmenu.one(this.SELECTORS.ACTIONMENUBAR);
+            menubar.set('id', this.CONSTANTS.ACTIONMENUIDPREFIX + id + this.CONSTANTS.ACTIONMENUBARIDSUFFIX);
+            // Update action-menu-1-menu
+            var menumenu = actionmenu.one(this.SELECTORS.ACTIONMENUMENU);
+            menumenu.set('id', this.CONSTANTS.ACTIONMENUIDPREFIX + id + this.CONSTANTS.ACTIONMENUMENUIDSUFFIX);
+        }, this);
+    },
+
+    /**
+     * Returns a list of all page elements.
+     *
+     * @method getActionMenus
+     * @return {node[]} An array containing page nodes.
+     */
+    getActionMenus: function() {
+        return Y.all(Y.Moodle.mod_quiz.util.slot.SELECTORS.PAGECONTENT + ' ' + Y.Moodle.mod_quiz.util.slot.SELECTORS.SECTIONUL + ' ' + this.SELECTORS.ACTIONMENU);
+    },
+
+    /**
+     * Returns the ID for the provided action menu.
+     *
+     * @method getId
+     * @param actionmenu {Node} The actionmenu to find an ID for.
+     * @return {Number|false} The ID of the actionmenu in question or false if no ID was found.
+     */
+    getActionMenuId: function(actionmenu) {
+        // We perform a simple substitution operation to get the ID.
+        var id = actionmenu.get('id').replace(
+                this.CONSTANTS.ACTIONMENUIDPREFIX, '');
+
+        // Attempt to validate the ID.
+        id = parseInt(id, 10);
+        if (typeof id === 'number' && isFinite(id)) {
+            return id;
+        }
+        return false;
+    },
+
+    /**
+     * Updates the page id for the provided page.
+     *
+     * @method setId
+     * @param page {Node} The page to update the number for.
+     * @param id int The id value.
+     * @return void
+     */
+    setActionMenuId: function(actionmenu, id) {
+        actionmenu.set('id', this.CONSTANTS.ACTIONMENUIDPREFIX + id);
     }
 };
index 4fc216e..38b37fb 100644 (file)
@@ -14,14 +14,22 @@ Y.namespace('Moodle.mod_quiz.util.slot');
  * @static
  */
 Y.Moodle.mod_quiz.util.slot = {
+    CSS: {
+        SLOT : 'slot',
+        QUESTIONTYPEDESCRIPTION : 'qtype_description'
+    },
     CONSTANTS: {
-        SLOTIDPREFIX : 'slot-'
+        SLOTIDPREFIX : 'slot-',
+        QUESTION : M.util.get_string('question', 'moodle')
     },
     SELECTORS: {
         SLOT: 'li.slot',
         INSTANCENAME: '.instancename',
         NUMBER: 'span.slotnumber',
         PAGECONTENT : 'div#page-content',