Merge branch 'MDL-47918-master' of git://github.com/andrewnicols/moodle
authorDan Poltawski <dan@moodle.com>
Thu, 30 Oct 2014 14:31:38 +0000 (14:31 +0000)
committerDan Poltawski <dan@moodle.com>
Thu, 30 Oct 2014 14:31:38 +0000 (14:31 +0000)
114 files changed:
admin/tool/monitor/classes/notification_task.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/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
backup/cc/cc2moodle.php
backup/converter/moodle1/handlerlib.php
badges/tests/behat/award_badge.feature
blocks/recent_activity/block_recent_activity.php
calendar/lib.php
calendar/renderer.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/lang/en/gradereport_singleview.php
grade/report/singleview/styles.css
grade/report/user/lib.php
grade/tests/behat/grade_calculated_weights.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/adminlib.php
lib/badgeslib.php
lib/dml/mysqli_native_moodle_database.php
lib/dmllib.php
lib/grade/grade_category.php
lib/moodlelib.php
lib/tests/admintree_test.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/src/dragdrop/js/dragdrop.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/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/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/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/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/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
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
version.php

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 debe732..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;
     }
 
index ba2236b..99003d6 100644 (file)
@@ -201,6 +201,8 @@ class subscription_manager {
             }
         }
 
+        $subscriptions->close();
+
         return $success;
     }
 
index a01dac4..5433d09 100644 (file)
@@ -93,6 +93,7 @@ $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}';
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 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 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 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 d62fa81..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;
@@ -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);
         }
 
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 ad20cdd..87c8cdb 100644 (file)
@@ -160,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'),
@@ -208,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);
     }
 
     /**
@@ -267,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..74743c5 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,38 @@ 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();
 
         $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 +275,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 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 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 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 1fcbdda..c9f4611 100644 (file)
@@ -507,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 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 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 21e496b..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.
@@ -1532,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 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 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 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 42ea9e8..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 . '/api/v2/push';
+            $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 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 49e4c53..30bed3c 100644 (file)
 
     // 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 3452e4b..54fcac2 100644 (file)
@@ -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,6 +3801,7 @@ function forum_get_discussion_subscription_icon($forum, $discussionid, $returnur
                 'class' => 'discussiontoggle iconsmall',
                 'data-forumid' => $forum->id,
                 'data-discussionid' => $discussionid,
+                'data-includetext' => $includetext,
         ));
     }
 
@@ -5960,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;
+    list($usql, $postidparams) = $DB->get_in_or_equal($postids, SQL_PARAMS_NAMED, 'postid');
 
-    $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)
-
-                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 381e462..3464d6b 100644 (file)
@@ -616,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);
 }
@@ -658,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 20815ba..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;}
index 28b7c6b..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);
@@ -56,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, null, true);
+$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 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 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..b123e19 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;
     }
 
@@ -644,8 +663,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 +672,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 +687,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 +856,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 +882,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 +896,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 +927,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 +950,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 eea3854..d574af7 100644 (file)
@@ -254,7 +254,7 @@ M.mod_quiz.secure_window = {
         if (n === null) {
             return false;
         }
-        return M.mod_quiz.is_content_editable(n);
+        return M.mod_quiz.secure_window.is_content_editable(n);
     },
 
     prevent_selection: function(e) {
@@ -271,7 +271,7 @@ M.mod_quiz.secure_window = {
             // Left click on a button or similar. No worries.
             return;
         }
-        if (e.button == 1 && M.mod_quiz.is_content_editable(e.target)) {
+        if (e.button == 1 && M.mod_quiz.secure_window.is_content_editable(e.target)) {
             // Left click in Atto or similar.
             return;
         }
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',
+        PAGEBREAK : 'span.page_split_join_wrapper',
+        ICON : 'img.smallicon',
+        QUESTIONTYPEDESCRIPTION : '.qtype_description',
         SECTIONUL : 'ul.section'
     },
 
@@ -79,7 +87,12 @@ Y.Moodle.mod_quiz.util.slot = {
      * @return {Number|false} The number of the slot in question or false if no number was found.
      */
     getNumber: function(slot) {
-        var number = slot.one(this.SELECTORS.NUMBER).get('text');
+        if (!slot) {
+            return false;
+        }
+        // We perform a simple substitution operation to get the number.
+        var number = slot.one(this.SELECTORS.NUMBER).get('text').replace(
+                        this.CONSTANTS.QUESTION, '');
         // Attempt to validate the ID.
         number = parseInt(number, 10);
         if (typeof number === 'number' && isFinite(number)) {
@@ -96,7 +109,8 @@ Y.Moodle.mod_quiz.util.slot = {
      * @return void
      */
     setNumber: function(slot, number) {
-        slot.one(this.SELECTORS.NUMBER).set('text', number);
+        var numbernode = slot.one(this.SELECTORS.NUMBER);
+        numbernode.setHTML('<span class="accesshide">' + this.CONSTANTS.QUESTION + '</span> ' + number);
     },
 
     /**
@@ -110,7 +124,19 @@ Y.Moodle.mod_quiz.util.slot = {
     },
 
     /**
-     * Returns the previous slot to the give slot.
+     * Returns a list of all slot elements on the page that have numbers. Excudes description questions.
+     *
+     * @method getSlots
+     * @return {node[]} An array containing slot nodes.
+     */
+    getNumberedSlots: function() {
+        var selector = this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL;
+            selector += ' ' + this.SELECTORS.SLOT + ':not(' + this.SELECTORS.QUESTIONTYPEDESCRIPTION + ')';
+        return Y.all(selector);
+    },
+
+    /**
+     * Returns the previous slot to the given slot.
      *
      * @method getPrevious
      * @param slot Slot node
@@ -120,26 +146,190 @@ Y.Moodle.mod_quiz.util.slot = {
         return slot.previous(this.SELECTORS.SLOT);
     },
 
+    /**
+     * Returns the previous numbered slot to the given slot.
+     *
+     * Ignores slots containing description question types.
+     *
+     * @method getPrevious
+     * @param slot Slot node
+     * @return {node|false} The previous slot node or false.
+     */
+    getPreviousNumbered: function(slot) {
+        return slot.previous(this.SELECTORS.SLOT + ':not(' + this.SELECTORS.QUESTIONTYPEDESCRIPTION + ')');
+    },
+
     /**
      * Reset the order of the numbers given to each slot.
      *
-     * @method reorder_slots
+     * @method reorderSlots
      * @return void
      */
-    reorder_slots: function() {
+    reorderSlots: function() {
         // Get list of slot nodes.
         var slots = this.getSlots();
         // Loop through slots incrementing the number each time.
         slots.each(function(slot) {
-            var previousSlot = this.getPrevious(slot),
-                previousslotnumber = 0;
-            if(previousSlot){
+
+            if (!Y.Moodle.mod_quiz.util.page.getPageFromSlot(slot)) {
+                // Move the next page to the front.
+                var nextpage = slot.next(Y.Moodle.mod_quiz.util.page.SELECTORS.PAGE);
+                slot.swap(nextpage);
+            }
+
+            var previousSlot = this.getPreviousNumbered(slot);
+            previousslotnumber = 0;
+            if (slot.hasClass(this.CSS.QUESTIONTYPEDESCRIPTION)) {
+                return;
+            }
+
+            if (previousSlot) {
                 previousslotnumber = this.getNumber(previousSlot);
             }
 
             // Set slot number.
             this.setNumber(slot, previousslotnumber + 1);
         }, this);
+    },
+
+    /**
+     * Remove a slot and related elements from the list of slots.
+     *
+     * @method remove
+     * @param slot Slot node
+     * @return void
+     */
+    remove: function(slot) {
+        var page = Y.Moodle.mod_quiz.util.page.getPageFromSlot(slot);
+        slot.remove();
+        // Is the page empty.
+        if (!Y.Moodle.mod_quiz.util.page.isEmpty(page)) {
+            return;
+        }
+        // If so remove it. Including add menu and page break.
+        Y.Moodle.mod_quiz.util.page.remove(page);
+    },
+
+    /**
+     * Returns a list of all page break elements on the page.
+     *
+     * @method getPageBreaks
+     * @return {node[]} An array containing page break nodes.
+     */
+    getPageBreaks: function() {
+        var selector = this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL;
+            selector += ' ' + this.SELECTORS.SLOT + this.SELECTORS.PAGEBREAK;
+        return Y.all(selector);
+    },
 
+    /**
+     * Retrieve the page break element item from the given slot.
+     *
+     * @method getPageBreak
+     * @param slot Slot node
+     * @return {Node|null} The Page Break Node.
+     */
+    getPageBreak: function(slot) {
+        return Y.one(slot).one(this.SELECTORS.PAGEBREAK);
+    },
+
+    /**
+     * Add a page break and related elements to the list of slots.
+     *
+     * @method addPageBreak
+     * @param beforenode Int | Node | HTMLElement | String to add
+     * @return pagebreak PageBreak node
+     */
+    addPageBreak: function(slot) {
+        var nodetext = M.mod_quiz.resource_toolbox.get('config').addpageiconhtml;
+        nodetext = nodetext.replace('%%SLOT%%', this.getNumber(slot));
+        var pagebreak = Y.Node.create(nodetext);
+        slot.one('div').insert(pagebreak, 'after');
+        return pagebreak;
+    },
+
+    /**
+     * Remove a pagebreak from the given slot.
+     *
+     * @method removePageBreak
+     * @param slot Slot node
+     * @return boolean
+     */
+    removePageBreak: function(slot) {
+        var pagebreak = this.getPageBreak(slot);
+        if (!pagebreak) {
+            return false;
+        }
+        pagebreak.remove();
+        return true;
+    },
+
+    /**
+     * Reorder each pagebreak by iterating through each related slot.
+     *
+     * @method reorderPageBreaks
+     * @return void
+     */
+    reorderPageBreaks: function() {
+        // Get list of slot nodes.
+        var slots = this.getSlots(), slotnumber = 0;
+        // Loop through slots incrementing the number each time.
+        slots.each (function(slot, key) {
+            slotnumber++;
+            var pagebreak = this.getPageBreak(slot);
+            // Last slot won't have a page break.
+            if (!pagebreak && key === slots.size() - 1) {
+                return;
+            }
+
+            // No pagebreak and not last slot. Add one.
+            if (!pagebreak && key !== slots.size() - 1) {
+                pagebreak = this.addPageBreak(slot);
+            }
+
+            // Remove last page break if there is one.
+            if (pagebreak && key === slots.size() - 1) {
+                this.removePageBreak(slot);
+            }
+
+            // Get page break anchor element.
+            var pagebreaklink = pagebreak.get('childNodes').item(0);
+
+            // Get the correct title.
+            var action = '', iconname = '';
+            if (Y.Moodle.mod_quiz.util.page.isPage(slot.next('li.activity'))) {
+                action = 'removepagebreak';
+                iconname = 'e/remove_page_break';
+            } else {
+                action = 'addpagebreak';
+                iconname = 'e/insert_page_break';
+            }
+
+            // Update the link and image titles
+            pagebreaklink.set('title', M.util.get_string(action, 'quiz'));
+            pagebreaklink.setData('action', action);
+            // Update the image title.
+            var icon = pagebreaklink.one(this.SELECTORS.ICON);
+            icon.set('title', M.util.get_string(action, 'quiz'));
+            icon.set('alt', M.util.get_string(action, 'quiz'));
+
+            // Update the image src.
+            icon.set('src', M.util.image_url(iconname));
+
+            // Get anchor url parameters as an associative array.
+            var params = Y.QueryString.parse(pagebreaklink.get('href'));
+            // Update slot number.
+            params.slot = slotnumber;
+            // Create the new url.
+            var newurl = '';
+            for (var index in params) {
+                if (newurl.length) {
+                    newurl += "&";
+                }
+                newurl += index + "=" + params[index];
+            }
+            // Update the anchor.
+            pagebreaklink.set('href', newurl);
+        }, this);
     }
 };
index e92e8ee..34b8418 100644 (file)
@@ -90,7 +90,7 @@ foreach ($scorms as $scorm) {
         $trackedusers = scorm_get_count_users($scorm->id, $scorm->groupingid);
         if ($trackedusers > 0) {
             $reportshow = html_writer::link('report.php?id='.$scorm->coursemodule,
-                                                get_string('viewallreports', 'scorm', $trackedusers)).html_writer::end_div();
+                                                get_string('viewallreports', 'scorm', $trackedusers));
         } else {
             $reportshow = get_string('noreports', 'scorm');
         }
index dd5d0e0..b9ea023 100644 (file)
@@ -110,7 +110,6 @@ if ($uselegacyreader) {
     $limittime = '';
     if (!empty($minloginternalreader)) {
         $limittime = ' AND time < :timeto ';
-        $params['timeto'] = $minloginternalreader;
     }
     // Check if we need to show the last access.
     $sqllasttime = '';
@@ -129,6 +128,9 @@ if ($uselegacyreader) {
                AND m.visible = :visible $limittime
           GROUP BY cm.id";
     $params = array('courseid' => $course->id, 'action' => 'view%', 'visible' => 1);
+    if (!empty($minloginternalreader)) {
+        $params['timeto'] = $minloginternalreader;
+    }
     $views = $DB->get_records_sql($sql, $params);
 }
 
@@ -156,6 +158,9 @@ if ($useinternalreader) {
         foreach ($v as $key => $value) {
             if (isset($views[$key]) && !empty($views[$key]->numviews)) {
                 $views[$key]->numviews += $value->numviews;
+                if ($value->lasttime > $views[$key]->lasttime) {
+                    $views[$key]->lasttime = $value->lasttime;
+                }
             } else {
                 $views[$key] = $value;
             }
index e301bec..600cb3e 100644 (file)
@@ -85,7 +85,6 @@ function report_outline_get_common_log_variables() {
             // If legacy reader is preferred reader.
             if ($readerpluginname == 'logstore_legacy') {
                 $uselegacyreader = true;
-                break;
             }
 
             // If sql_internal_reader is preferred reader.
@@ -93,7 +92,6 @@ function report_outline_get_common_log_variables() {
                 $useinternalreader = true;
                 $logtable = $reader->get_internal_log_table_name();
                 $minloginternalreader = $DB->get_field_sql('SELECT min(timecreated) FROM {' . $logtable . '}');
-                break;
             }
         }
     }
index b4463e1..8505e33 100644 (file)
@@ -57,7 +57,8 @@ if (!empty($userid)) {
     if (!report_stats_can_access_user_report($user, $course, true)) {
         require_capability('report/stats:view', $coursecontext);
     }
-
+} else if ($mode === STATS_MODE_DETAILED) {
+    print_error('invaliduser');
 } else {
     require_capability('report/stats:view', $coursecontext);
 }
index 1a671c2..e2de2e2 100644 (file)
@@ -161,7 +161,11 @@ function report_stats_report($course, $report, $mode, $user, $roleid, $time) {
     echo '</div>';
     echo '</form>';
 
-    if (!empty($report) && !empty($time)) {
+    // Display the report if:
+    //  - A report has been selected.
+    //  - A time frame has been provided
+    //  - If the mode is not detailed OR a valid user has been selected.
+    if (!empty($report) && !empty($time) && ($mode !== STATS_MODE_DETAILED || !empty($userid))) {
         if ($report == STATS_REPORT_LOGINS && $course->id != SITEID) {
             print_error('reportnotavailable');
         }
index e8c756e..6453d4d 100644 (file)
@@ -10,8 +10,7 @@ Feature: Recent files repository lists the recently used files
       | fullname | shortname | category |
       | Course 1 | C1 | 0 |
     And I log in as "admin"
-    And I expand "My profile" node
-    And I follow "My private files"
+    And I navigate to "My private files" node in "My profile"
     And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager
     And I upload "lib/tests/fixtures/upload_users.csv" file to "Files" filemanager
     And I press "Save changes"
index 8f7292c..c2cab33 100644 (file)
@@ -10,8 +10,7 @@ Feature: A selected file can be cancelled
       | fullname | shortname | category |
       | Course 1 | C1 | 0 |
     And I log in as "admin"
-    And I expand "My profile" node
-    And I follow "My private files"
+    And I navigate to "My private files" node in "My profile"
     And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager
     And I press "Save changes"
     And I am on homepage
index 988b49d..daacbab 100644 (file)
@@ -16,8 +16,7 @@ Feature: Create shortcuts
       | user | course | role |
       | teacher1 | C1 | editingteacher |
     When I log in as "teacher1"
-    And I expand "My profile" node
-    And I follow "My private files"
+    And I navigate to "My private files" node in "My profile"
     And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager
     Then I should see "empty.txt" in the "div.fp-content" "css_element"
     And I press "Save changes"
index 221523b..f094488 100644 (file)
@@ -7,8 +7,7 @@ Feature: Delete files and folders from the file manager
   @javascript @_bug_phantomjs
   Scenario: Delete a file and a folder
     Given I log in as "admin"
-    And I expand "My profile" node
-    And I follow "My private files"
+    And I navigate to "My private files" node in "My profile"
     And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager
     And I create "Delete me" folder in "Files" filemanager
     And I press "Save changes"
index cebfc33..d512d50 100644 (file)
@@ -16,8 +16,7 @@ Feature: Overwrite file feature
       | user | course | role |
       | teacher1 | C1 | editingteacher |
     When I log in as "teacher1"
-    And I expand "My profile" node
-    And I follow "My private files"
+    And I navigate to "My private files" node in "My profile"
     And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager
     Then I should see "1" elements in "Files" filemanager
     And I upload and overwrite "lib/tests/fixtures/empty.txt" file to "Files" filemanager
index b86f0c5..66a0b4c 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2014102800.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2014102800.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '2.8beta+ (Build: 20141024)'; // Human-friendly version name
+$release  = '2.8beta+ (Build: 20141028)'; // Human-friendly version name
 
 $branch   = '28';                       // This version's branch.
 $maturity = MATURITY_BETA;             // This version's maturity level.