Merge branch 'MDL-49058-master' of git://github.com/jethac/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 10 Feb 2015 12:09:38 +0000 (13:09 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 10 Feb 2015 12:09:38 +0000 (13:09 +0100)
218 files changed:
admin/settings/plugins.php
admin/tool/filetypes/edit_form.php
admin/tool/filetypes/tests/behat/add_filetypes.feature
admin/tool/generator/classes/course_backend.php
admin/tool/generator/lang/en/tool_generator.php
admin/tool/generator/tests/maketestcourse_test.php
admin/webservice/forms.php
availability/tests/behat/display_availability.feature
backup/util/ui/tests/behat/duplicate_activities.feature
backup/util/ui/tests/behat/restore_moodle2_courses.feature
badges/badge.php
badges/mybadges.php
blocks/navigation/renderer.php
blocks/navigation/styles.css
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js
blocks/navigation/yui/src/navigation/js/navigation.js
blocks/online_users/block_online_users.php
blocks/tests/behat/hidden_block_region.feature [new file with mode: 0644]
blocks/tests/behat/hide_blocks.feature [new file with mode: 0644]
cache/classes/helper.php
cache/classes/loaders.php
cache/upgrade.txt
calendar/lib.php
calendar/renderer.php
calendar/view.php
course/changenumsections.php
course/editsection.php
course/externallib.php
course/format/lib.php
course/format/renderer.php
course/format/topics/lang/en/format_topics.php
course/format/topics/lib.php
course/format/topics/renderer.php
course/format/topics/tests/behat/edit_delete_sections.feature [new file with mode: 0644]
course/format/topics/tests/format_topics_test.php [new file with mode: 0644]
course/format/upgrade.txt
course/format/weeks/lang/en/format_weeks.php
course/format/weeks/lib.php
course/format/weeks/tests/behat/edit_delete_sections.feature [new file with mode: 0644]
course/format/weeks/tests/format_weeks_test.php [new file with mode: 0644]
course/lib.php
course/tests/behat/course_controls.feature
course/tests/behat/move_activities.feature
course/tests/behat/move_sections.feature
course/tests/behat/paged_course_navigation.feature
course/tests/courselib_test.php
course/tests/externallib_test.php
enrol/externallib.php
enrol/manual/db/services.php
enrol/manual/externallib.php
enrol/manual/lib.php
enrol/manual/yui/quickenrolment/quickenrolment.js
enrol/renderer.php
file.php
files/externallib.php
files/tests/externallib_test.php
filter/mathjaxloader/filter.php
grade/export/grade_export_form.php
grade/export/lib.php
grade/export/ods/dump.php
grade/export/ods/export.php
grade/export/txt/dump.php
grade/export/txt/export.php
grade/export/xls/dump.php
grade/export/xls/export.php
grade/export/xml/dump.php
grade/export/xml/export.php
grade/externallib.php
grade/lib.php
grade/report/grader/lang/en/gradereport_grader.php
grade/report/grader/lib.php
grade/report/singleview/classes/local/screen/grade.php
grade/report/singleview/classes/local/screen/screen.php
grade/report/singleview/classes/local/screen/select.php
grade/report/singleview/classes/local/screen/selectable_items.php
grade/report/singleview/classes/local/screen/tablelike.php
grade/report/singleview/classes/local/screen/user.php
grade/report/singleview/index.php
grade/report/singleview/lang/en/gradereport_singleview.php
grade/report/singleview/lib.php
grade/report/singleview/styles.css
grade/report/singleview/tests/behat/singleview.feature
grade/report/upgrade.txt
group/externallib.php
install.php
install/lang/eu/install.php
install/lang/fi_co/langconfig.php [new file with mode: 0644]
install/lang/sk/install.php
install/lang/uk/admin.php
lang/en/enrol.php
lang/en/error.php
lang/en/grades.php
lang/en/message.php
lang/en/moodle.php
lib/adminlib.php
lib/ajax/blocks.php
lib/badgeslib.php
lib/behat/behat_base.php
lib/behat/classes/behat_selectors.php
lib/blocklib.php
lib/classes/message/manager.php
lib/classes/message/message.php [new file with mode: 0644]
lib/db/services.php
lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-debug.js
lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-min.js
lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button.js
lib/editor/atto/plugins/link/yui/src/button/js/button.js
lib/externallib.php
lib/messagelib.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/pear/HTML/QuickForm/Renderer/Tableless.php
lib/pear/README_MOODLE.txt
lib/testing/lib.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_general.php
lib/tests/html_writer_test.php
lib/tests/message_test.php [new file with mode: 0644]
lib/tests/questionlib_test.php
lib/timezone.txt
lib/upgrade.txt
message/ajax.php [new file with mode: 0644]
message/externallib.php
message/lib.php
message/output/airnotifier/message_output_airnotifier.php
message/tests/behat/send_message.feature [new file with mode: 0644]
message/tests/externallib_test.php
message/yui/build/moodle-core_message-messenger/moodle-core_message-messenger-debug.js [new file with mode: 0644]
message/yui/build/moodle-core_message-messenger/moodle-core_message-messenger-min.js [new file with mode: 0644]
message/yui/build/moodle-core_message-messenger/moodle-core_message-messenger.js [new file with mode: 0644]
message/yui/src/messenger/build.json [new file with mode: 0644]
message/yui/src/messenger/js/constants.js [new file with mode: 0644]
message/yui/src/messenger/js/manager.js [new file with mode: 0644]
message/yui/src/messenger/js/sendmessage.js [new file with mode: 0644]
message/yui/src/messenger/meta/messenger.json [new file with mode: 0644]
mod/assign/feedback/comments/locallib.php
mod/assign/gradingtable.php
mod/assign/tests/behat/display_grade.feature [new file with mode: 0644]
mod/assign/upgrade.txt
mod/book/settings.php
mod/book/tests/behat/create_chapters.feature
mod/folder/settings.php
mod/forum/lib.php
mod/imscp/settings.php
mod/lesson/backup/moodle2/backup_lesson_stepslib.php
mod/lesson/backup/moodle2/restore_lesson_stepslib.php
mod/lesson/essay.php
mod/lesson/essay_form.php
mod/lesson/lang/en/lesson.php
mod/lesson/lib.php
mod/lesson/locallib.php
mod/lesson/pagetypes/essay.php
mod/lesson/pagetypes/matching.php
mod/lesson/pagetypes/multichoice.php
mod/lesson/pagetypes/truefalse.php
mod/lesson/reformat.php
mod/lesson/report.php
mod/lesson/settings.php
mod/lesson/tests/behat/lesson_essay_question.feature
mod/lesson/tests/behat/questions_images.feature
mod/lesson/tests/behat/teacher_grade_essays.feature
mod/lesson/tests/pagetypes_test.php [new file with mode: 0644]
mod/lesson/upgrade.txt
mod/page/settings.php
mod/resource/settings.php
mod/resource/view.php
mod/survey/lang/en/survey.php
mod/survey/view.php
mod/url/settings.php
mod/workshop/allocation/manual/tests/behat/behat_workshopallocation_manual.php
notes/externallib.php
question/export.php
question/format/gift/tests/behat/import_export.feature
question/format/xml/tests/behat/import_export.feature
report/log/graph.php
repository/filesystem/lib.php
repository/s3/lang/en/repository_s3.php
repository/s3/lib.php
tag/lib.php
tag/tests/taglib_test.php
theme/base/style/core.css
theme/base/style/message.css
theme/bootstrapbase/config.php
theme/bootstrapbase/layout/columns2.php
theme/bootstrapbase/layout/columns3.php
theme/bootstrapbase/layout/secure.php
theme/bootstrapbase/less/bootstrap/mixins.less
theme/bootstrapbase/less/bootstrap/navbar.less
theme/bootstrapbase/less/moodle/admin.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/debug.less
theme/bootstrapbase/less/moodle/expendable.less
theme/bootstrapbase/less/moodle/message.less
theme/bootstrapbase/readme_moodle.txt
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/yui/build/moodle-theme_bootstrapbase-bootstrap/moodle-theme_bootstrapbase-bootstrap-debug.js
theme/bootstrapbase/yui/build/moodle-theme_bootstrapbase-bootstrap/moodle-theme_bootstrapbase-bootstrap-min.js
theme/bootstrapbase/yui/build/moodle-theme_bootstrapbase-bootstrap/moodle-theme_bootstrapbase-bootstrap.js
theme/bootstrapbase/yui/src/bootstrap/js/bootstrap.js
theme/clean/config.php
theme/clean/layout/columns2.php
theme/clean/layout/columns3.php
theme/clean/layout/secure.php
theme/font.php
theme/more/config.php
theme/upgrade.txt
user/externallib.php
user/profile.php
user/profile/lib.php
user/view.php
version.php
webservice/externallib.php
webservice/renderer.php
webservice/upgrade.txt

index 7a1551a..f0455b7 100644 (file)
@@ -282,9 +282,6 @@ if ($hassiteconfig) {
     $ADMIN->add('webservicesettings', new admin_externalpage('webservicedocumentation', new lang_string('wsdocapi', 'webservice'), "$CFG->wwwroot/$CFG->admin/webservice/documentation.php", 'moodle/site:config', false));
     /// manage service
     $temp = new admin_settingpage('externalservices', new lang_string('externalservices', 'webservice'));
-    $temp->add(new admin_setting_enablemobileservice('enablemobilewebservice',
-            new lang_string('enablemobilewebservice', 'admin'),
-            new lang_string('configenablemobilewebservice', 'admin', $enablemobiledoclink), 0));
     $temp->add(new admin_setting_heading('manageserviceshelpexplaination', new lang_string('information', 'webservice'), new lang_string('servicehelpexplanation', 'webservice')));
     $temp->add(new admin_setting_manageexternalservices());
     $ADMIN->add('webservicesettings', $temp);
index 5d16177..f2c60aa 100644 (file)
@@ -94,6 +94,29 @@ class tool_filetypes_form extends moodleform {
         parent::set_data($data);
     }
 
+    public function get_data() {
+        $data = parent::get_data();
+
+        // Update the data to handle the descriptiontype dropdown. (The type
+        // is not explicitly stored, we just set or unset relevant fields.)
+        if ($data) {
+            switch ($data->descriptiontype) {
+                case 'lang' :
+                    unset($data->description);
+                    break;
+                case 'custom' :
+                    unset($data->corestring);
+                    break;
+                default:
+                    unset($data->description);
+                    unset($data->corestring);
+                    break;
+            }
+            unset($data->descriptiontype);
+        }
+        return $data;
+    }
+
     public function validation($data, $files) {
         $errors = parent::validation($data, $files);
 
@@ -110,6 +133,20 @@ class tool_filetypes_form extends moodleform {
             $errors['defaulticon'] = get_string('error_defaulticon', 'tool_filetypes', $extension);
         }
 
+        // If you choose 'lang' or 'custom' descriptiontype, you must fill something in the field.
+        switch ($data['descriptiontype']) {
+            case 'lang' :
+                if (!trim($data['corestring'])) {
+                    $errors['corestring'] = get_string('required');
+                }
+                break;
+            case 'custom' :
+                if (!trim($data['description'])) {
+                    $errors['description'] = get_string('required');
+                }
+                break;
+        }
+
         return $errors;
     }
 }
index 18d043e..ca52f01 100644 (file)
@@ -36,6 +36,35 @@ Feature: Add customised file types
     And I press "Save changes"
     And I should see "frog" in the "application/x-7z-compressed" "table_row"
 
+  Scenario: Change the text option (was buggy)
+    Given I log in as "admin"
+    And I navigate to "File types" node in "Site administration > Server"
+    When I click on "Edit 7z" "link"
+    And I set the following fields to these values:
+      | Description type   | Custom description specified in this form |
+      | Custom description | New description for 7z                    |
+    And I press "Save changes"
+    Then I should see "New description" in the "application/x-7z-compressed" "table_row"
+    And I click on "Edit 7z" "link"
+    And I set the field "Description type" to "Default"
+    And I press "Save changes"
+    And I should not see "New description" in the "application/x-7z-compressed" "table_row"
+
+  Scenario: Try to select a text option without entering a value.
+    Given I log in as "admin"
+    And I navigate to "File types" node in "Site administration > Server"
+    When I click on "Edit dmg" "link"
+    And I set the field "Description type" to "Custom description"
+    And I press "Save changes"
+    Then I should see "Required"
+    And I set the field "Description type" to "Alternative language string"
+    And I press "Save changes"
+    And I should see "Required"
+    And I set the field "Description type" to "Default"
+    And I press "Save changes"
+    # Check we're back on the main page now.
+    And "dmg" "table_row" should exist
+
   Scenario: Delete an existing file type
     Given I log in as "admin"
     And I navigate to "File types" node in "Site administration > Server"
@@ -76,7 +105,7 @@ Feature: Add customised file types
     And I press "Yes"
     Then "//img[contains(@src, 'archive')]" "xpath_element" should exist in the "7z" "table_row"
 
-  @javascript
+  @javascript @_file_upload
   Scenario: Create a resource activity which contains a customised file type
     Given the following "courses" exist:
       | fullname | shortname |
index 554530e..20a0186 100644 (file)
@@ -308,8 +308,7 @@ class tool_generator_course_backend extends tool_generator_backend {
             $username = 'tool_generator_' . $textnumber;
 
             // Create user account.
-            $record = array('firstname' => get_string('firstname', 'tool_generator'),
-                    'lastname' => $number, 'username' => $username);
+            $record = array('username' => $username, 'idnumber' => $number);
 
             // We add a user password if it has been specified.
             if (!empty($CFG->tool_generator_users_password)) {
index edaab03..86247ec 100644 (file)
@@ -62,7 +62,6 @@ $string['error_nonexistingcourse'] = 'The specified course does not exist';
 $string['error_nopageinstances'] = 'The selected course does not contain page module instances';
 $string['error_notdebugging'] = 'Not available on this server because debugging is not set to DEVELOPER';
 $string['error_nouserspassword'] = 'You need to set $CFG->tool_generator_users_password in config.php to generate the test plan';
-$string['firstname'] = 'Test course user';
 $string['fullname'] = 'Test course: {$a->size}';
 $string['maketestcourse'] = 'Make test course';
 $string['maketestplan'] = 'Make JMeter test plan';
index a5150b1..c762103 100644 (file)
@@ -136,7 +136,7 @@ class tool_generator_maketestcourse_testcase extends advanced_testcase {
         $lastusernumber = 0;
         $discussionstarters = array();
         foreach ($discussions as $discussion) {
-            $usernumber = intval($discussion->lastname);
+            $usernumber = core_user::get_user($discussion->userid, 'id, idnumber')->idnumber;
 
             // Checks that the users are odd numbers.
             $this->assertEquals(1, $usernumber % 2);
index 41c15c2..9465bf4 100644 (file)
@@ -190,7 +190,12 @@ class external_service_functions_form extends moodleform {
         foreach ($functions as $functionid => $functionname) {
             //retrieve full function information (including the description)
             $function = external_function_info($functionname);
-            $functions[$functionid] = $function->name . ':' . $function->description;
+            if (empty($function->deprecated)) {
+                $functions[$functionid] = $function->name . ':' . $function->description;
+            } else {
+                // Exclude the deprecated ones.
+                unset($functions[$functionid]);
+            }
         }
 
         $mform->addElement('searchableselector', 'fids', get_string('name'),
index 1efef1d..9043836 100644 (file)
@@ -109,17 +109,17 @@ Feature: display_availability
 
     # Page 1 display still there but should be dimmed and not a link.
     Then I should see "Page 1" in the "#section-1 .dimmed_text" "css_element"
-    And ".activityinstance a" "css_element" should not exist in the "#section-1" "css_element"
+    And ".activityinstance a" "css_element" should not exist in the "Topic 1" "section"
 
     # Date display should be present.
-    And I should see "Available until" in the "#section-1" "css_element"
+    And I should see "Available until" in the "Topic 1" "section"
 
     # Page 2 display not there at all
     And I should not see "Page 2" in the "region-main" "region"
 
     # Page 3 display and link
     And I should see "Page 3" in the "region-main" "region"
-    And ".activityinstance a" "css_element" should exist in the "#section-3" "css_element"
+    And ".activityinstance a" "css_element" should exist in the "Topic 3" "section"
 
   @javascript
   Scenario: Section availability display
index d839beb..37537ec 100644 (file)
@@ -34,6 +34,6 @@ Feature: Duplicate activities
       | Name | Duplicated database name |
       | Description | Duplicated database description |
     And I press "Save and return to course"
-    Then I should see "Original database name" in the "#section-1" "css_element"
-    And I should see "Duplicated database name" in the "#section-1" "css_element"
+    Then I should see "Original database name" in the "Topic 1" "section"
+    And I should see "Duplicated database name" in the "Topic 1" "section"
     And "Original database name" "link" should appear before "Duplicated database name" "link"
index 934e011..ec07143 100644 (file)
@@ -126,5 +126,5 @@ Feature: Restore Moodle 2 course backups
     And section "3" should be hidden
     And section "7" should be hidden
     And section "15" should be visible
-    And I should see "Test URL name" in the "#section-3" "css_element"
-    And I should see "Test forum name" in the "#section-1" "css_element"
+    And I should see "Test URL name" in the "Topic 3" "section"
+    And I should see "Test forum name" in the "Topic 1" "section"
index 29f8cb4..68cf109 100644 (file)
@@ -26,6 +26,7 @@
 
 require_once(dirname(dirname(__FILE__)) . '/config.php');
 require_once($CFG->libdir . '/badgeslib.php');
+require_once($CFG->libdir . '/filelib.php');
 
 $id = required_param('hash', PARAM_ALPHANUM);
 $bake = optional_param('bake', 0, PARAM_BOOL);
@@ -37,12 +38,10 @@ $badge = new issued_badge($id);
 
 if ($bake && ($badge->recipient->id == $USER->id)) {
     $name = str_replace(' ', '_', $badge->badgeclass['name']) . '.png';
-    ob_start();
-    $file = badges_bake($id, $badge->badgeid);
-    header('Content-Type: image/png');
-    header('Content-Disposition: attachment; filename="'. $name .'"');
-    readfile($file);
-    ob_flush();
+    $filehash = badges_bake($id, $badge->badgeid, $USER->id, true);
+    $fs = get_file_storage();
+    $file = $fs->get_file_by_hash($filehash);
+    send_stored_file($file, 0, 0, true, array('filename' => $name));
 }
 
 $PAGE->set_url('/badges/badge.php', array('hash' => $id));
index 3f15a93..a1454da 100644 (file)
@@ -26,6 +26,7 @@
 
 require_once(dirname(dirname(__FILE__)) . '/config.php');
 require_once($CFG->libdir . '/badgeslib.php');
+require_once($CFG->libdir . '/filelib.php');
 
 $page        = optional_param('page', 0, PARAM_INT);
 $search      = optional_param('search', '', PARAM_CLEAN);
@@ -71,17 +72,13 @@ if ($hide) {
     require_sesskey();
     $badge = new badge($download);
     $name = str_replace(' ', '_', $badge->name) . '.png';
-    ob_start();
-    $file = badges_bake($hash, $download);
-    header('Content-Type: image/png');
-    header('Content-Disposition: attachment; filename="'. $name .'"');
-    readfile($file);
-    ob_flush();
+    $filehash = badges_bake($hash, $download, $USER->id, true);
+    $fs = get_file_storage();
+    $file = $fs->get_file_by_hash($filehash);
+    send_stored_file($file, 0, 0, true, array('filename' => $name));
 } else if ($downloadall) {
     require_sesskey();
-    ob_start();
     badges_download($USER->id);
-    ob_flush();
 }
 
 $context = context_user::instance($USER->id);
index c92ef41..74cc088 100644 (file)
@@ -51,7 +51,7 @@ class block_navigation_renderer extends plugin_renderer_base {
     /**
      * Produces a navigation node for the navigation tree
      *
-     * @param array $items
+     * @param navigation_node[] $items
      * @param array $attrs
      * @param int $expansionlimit
      * @param array $options
@@ -59,13 +59,12 @@ class block_navigation_renderer extends plugin_renderer_base {
      * @return string
      */
     protected function navigation_node($items, $attrs=array(), $expansionlimit=null, array $options = array(), $depth=1) {
-
-        // exit if empty, we don't want an empty ul element
-        if (count($items)==0) {
+        // Exit if empty, we don't want an empty ul element.
+        if (count($items) === 0) {
             return '';
         }
 
-        // array of nested li elements
+        // Turn our navigation items into list items.
         $lis = array();
         foreach ($items as $item) {
             if (!$item->display && !$item->contains_active_node()) {
@@ -86,10 +85,13 @@ class block_navigation_renderer extends plugin_renderer_base {
 
             if ($hasicon) {
                 $icon = $this->output->render($item->icon);
+                // Because an icon is being used we're going to wrap the actual content in a span.
+                // This will allow designers to create columns for the content, as we've done in styles.css.
+                $content = $icon . html_writer::span($content, 'item-content-wrap');
             } else {
                 $icon = '';
             }
-            $content = $icon.$content; // use CSS for spacing of icons
+
             if ($item->helpbutton !== null) {
                 $content = trim($item->helpbutton).html_writer::tag('span', $content, array('class'=>'clearhelpbutton'));
             }
@@ -116,13 +118,16 @@ class block_navigation_renderer extends plugin_renderer_base {
                 $link->text = $icon.$link->text;
                 $link->attributes = array_merge($link->attributes, $attributes);
                 $content = $this->output->render($link);
-                $linkrendered = true;
             } else if ($item->action instanceof moodle_url) {
                 $content = html_writer::link($item->action, $content, $attributes);
             }
 
-            // this applies to the li item which contains all child lists too
+            // This applies to the li item which contains all child lists too.
             $liclasses = array($item->get_css_type(), 'depth_'.$depth);
+
+            // Class attribute on the div item which only contains the item content.
+            $divclasses = array('tree_item');
+
             $liexpandable = array();
             if ($item->has_children() && (!$item->forceopen || $item->collapse)) {
                 $liclasses[] = 'collapsed';
@@ -130,30 +135,30 @@ class block_navigation_renderer extends plugin_renderer_base {
             if ($isbranch) {
                 $liclasses[] = 'contains_branch';
                 $liexpandable = array('aria-expanded' => in_array('collapsed', $liclasses) ? "false" : "true");
-            } else if ($hasicon) {
-                $liclasses[] = 'item_with_icon';
-            }
-            if ($item->isactive === true) {
-                $liclasses[] = 'current_branch';
-            }
-            $liattr = array('class' => join(' ',$liclasses)) + $liexpandable;
-            // class attribute on the div item which only contains the item content
-            $divclasses = array('tree_item');
-            if ($isbranch) {
                 $divclasses[] = 'branch';
             } else {
                 $divclasses[] = 'leaf';
             }
             if ($hasicon) {
+                // Add this class if the item has an icon, whether it is a branch or not.
+                $liclasses[] = 'item_with_icon';
                 $divclasses[] = 'hasicon';
             }
+            if ($item->isactive === true) {
+                $liclasses[] = 'current_branch';
+            }
             if (!empty($item->classes) && count($item->classes)>0) {
                 $divclasses[] = join(' ', $item->classes);
             }
+
+            // Now build attribute arrays.
+            $liattr = array('class' => join(' ', $liclasses)) + $liexpandable;
             $divattr = array('class'=>join(' ', $divclasses));
             if (!empty($item->id)) {
                 $divattr['id'] = $item->id;
             }
+
+            // Create the structure.
             $content = html_writer::tag('p', $content, $divattr);
             if ($isexpandable) {
                 $content .= $this->navigation_node($item->children, array(), $expansionlimit, $options, $depth+1);
@@ -165,11 +170,14 @@ class block_navigation_renderer extends plugin_renderer_base {
             $lis[] = $content;
         }
 
-        if (count($lis)) {
-            return html_writer::tag('ul', implode("\n", $lis), $attrs);
-        } else {
+        if (count($lis) === 0) {
+            // There is still a chance, despite having items, that nothing had content and no list items were created.
             return '';
         }
+
+        // We used to separate using new lines, however we don't do that now, instead we'll save a few chars.
+        // The source is complex already anyway.
+        return html_writer::tag('ul', implode('', $lis), $attrs);
     }
 
 }
index c25945c..c35c764 100644 (file)
-/** General display rules **/
-.block_navigation .block_tree {margin:5px;padding-left:0px;overflow:visible;}
-.block_navigation .block_tree li {margin:3px;list-style: none;padding:0;}
-.block_navigation .block_tree li.item_with_icon > p {position:relative; padding-left: 21px;}
-.block_navigation .block_tree li.item_with_icon > p img,
-.block_navigation .block_tree .type_activity > p.tree_item.active_tree_node img,
-.block_navigation .block_tree li > p.hasicon img {vertical-align:middle;position:absolute;left:0;top:-1px;width:16px;height:16px;}
-.block_navigation .block_tree li.item_with_icon.contains_branch > p img {left:16px;}
-.block_navigation .block_tree .type_activity > p.branch.hasicon,
-.block_navigation .block_tree .type_activity > p.emptybranch.hasicon,
-.block_navigation .block_tree li.item_with_icon.contains_branch > .tree_item {padding-left:37px;}
-
-.block_navigation .block_tree li ul {padding-left:0;margin:0;}
-.block_navigation .block_tree li.depth_2 ul {padding-left:16px;margin:0;}
-.block_navigation .block_tree .type_activity > p.tree_item.branch.hasicon.active_tree_node,
-.block_navigation .block_tree .tree_item {padding-left: 21px;margin:3px 0px;text-align:left;}
-
-.block_navigation .block_tree .tree_item.branch {background-image: url([[pix:t/expanded]]);background-position: 0 0;background-repeat: no-repeat;}
-.block_navigation .block_tree .tree_item.branch.navigation_node {background-image:none;padding-left:0;}
-.block_navigation .block_tree .type_activity > .tree_item.emptybranch,
-.block_navigation .block_tree .type_activity > .tree_item.branch {background-image:none;position:relative;}
-.block_navigation .block_tree .type_activity > .tree_item.hasicon.emptybranch img,
-.block_navigation .block_tree .type_activity > .tree_item.branch img {left: 16px;}
-.block_navigation .block_tree .root_node.leaf {padding-left:0px;}
-.block_navigation .block_tree .active_tree_node {font-weight:bold;}
-.block_navigation .block_tree .depth_1.current_branch ul {font-weight:normal;}
-
-.dock .block_navigation .tree_item {white-space: nowrap;}
-
-.jsenabled .block_navigation .block_tree .tree_item.branch {cursor:pointer;}
-.jsenabled .block_navigation .block_tree .tree_item.emptybranch {background-image: url([[pix:t/collapsed_empty]]);background-position: 0 0;background-repeat: no-repeat;}
-.jsenabled .block_navigation .block_tree .collapsed ul {display: none;}
-.jsenabled .block_navigation .block_tree .type_activity > .tree_item.branch {background-image: url([[pix:t/expanded]]);}
-.jsenabled .block_navigation .block_tree .collapsed .tree_item.branch {background-image: url([[pix:t/collapsed]]);}
-.jsenabled .block_navigation .block_tree .tree_item.branch.loadingbranch {background-image:url([[pix:i/loading_small]]);}
-
-/** JavaScript state rules **/
-.jsenabled .block_navigation.dock_on_load,
-.block_navigation .block_tree_box .requiresjs {display:none;}
-.jsenabled .block_navigation .block_tree_box .requiresjs {display:inline;}
-
-/** Internet explorer specific rules **/
-.ie6 .block_navigation .block_tree .tree_item {width:100%;}
-
-/** Overide for RTL layout **/
-.dir-rtl .block_navigation .block_tree li.depth_2 ul {padding-left: 0; padding-right: 16px;}
-.dir-rtl .block_navigation .block_tree .type_activity > p.tree_item.branch.hasicon.active_tree_node,
-.dir-rtl .block_navigation .block_tree .tree_item {padding-right: 21px;text-align:right;}
-
-.dir-rtl .block_navigation .block_tree .tree_item.branch {background-position: center right;}
-
-.dir-rtl .block_navigation .block_tree,
-.dir-rtl .block_navigation .block_tree li ul,
-.dir-rtl .block_navigation .block_tree .navigation_node.tree_item.branch,
-.dir-rtl .block_navigation .block_tree .root_node.leaf {padding-right:0;}
-
-.dir-rtl .block_navigation .block_tree li.item_with_icon > p img,
-.dir-rtl .block_navigation .block_tree .type_activity > p.tree_item.active_tree_node img,
-.dir-rtl .block_navigation .block_tree li > p.hasicon img {left:auto; right:0;}
-.dir-rtl .block_navigation .block_tree li.item_with_icon.contains_branch > p img {left: auto; right:16px;}
-.dir-rtl .block_navigation .block_tree .type_activity > p.branch.hasicon,
-.dir-rtl .block_navigation .block_tree li.item_with_icon.contains_branch > .tree_item {padding-right:37px; padding-left: 0;}
-.dir-rtl .block_navigation .block_tree .type_activity > .tree_item.branch img {right: 16px; left: auto;}
-
-.jsenabled.dir-rtl .block_navigation .block_tree .tree_item.emptybranch {background-image: url([[pix:t/collapsed_empty_rtl]]);background-position: center right;}
-.jsenabled.dir-rtl .block_navigation .block_tree .collapsed .tree_item.branch {background-image: url([[pix:t/collapsed_rtl]]);}
+.block_navigation .block_tree {
+    margin: 0;
+    list-style: none;
+}
+
+.block_navigation .block_tree .depth_1 > .tree_item.branch,
+.block_navigation .block_tree .depth_1 > .tree_item.emptybranch {
+    padding-left: 0;
+    background-image: none;
+}
+
+.block_navigation .block_tree .tree_item {
+    margin: 3px 0;
+    background-position: 0 50%;
+    background-repeat: no-repeat;
+}
+
+.block_navigation .block_tree .tree_item.branch {
+    padding-left: 21px;
+    cursor: pointer;
+    background-image: url('[[pix:t/expanded]]');
+}
+
+.block_navigation .block_tree .tree_item.emptybranch {
+    padding-left: 21px;
+    background-image: url('[[pix:t/collapsed_empty]]');
+}
+
+.block_navigation .block_tree .tree_item.loadingbranch {
+    background-image: url('[[pix:i/loading_small]]');
+}
+
+.block_navigation .block_tree .tree_item img {
+    width: 16px;
+    height: 16px;
+    margin-top: 3px;
+    margin-right: 5px;
+    vertical-align: top;
+}
+
+.block_navigation .block_tree .tree_item.active_tree_node {
+    font-weight: bold;
+}
+
+.block_navigation .block_tree .tree_item.hasicon {
+    white-space: nowrap;
+}
+
+.block_navigation .block_tree .tree_item.hasicon .item-content-wrap {
+    display: inline-block;
+    white-space: normal;
+}
+
+.block_navigation .block_tree ul {
+    margin: 0;
+}
+
+.block_navigation .block_tree ul ul {
+    margin: 0 0 0 16px;
+    list-style: none;
+}
+
+.jsenabled .block_navigation .block_tree li.collapsed ul {
+    display: none;
+}
+
+.jsenabled .block_navigation .block_tree li.collapsed .tree_item.branch {
+    background-image: url('[[pix:t/collapsed]]');
+}
+
+.jsenabled .block_navigation.dock_on_load {
+    display: none;
+}
+
+.dir-rtl .block_navigation .block_tree .depth_1 .tree_item {
+    padding-left: 0;
+}
+
+.dir-rtl .block_navigation .block_tree .tree_item {
+    background-position: 100% 50%;
+}
+
+.dir-rtl .block_navigation .block_tree .tree_item.branch {
+    padding-right: 21px;
+    padding-left: 0;
+}
+
+.dir-rtl .block_navigation .block_tree .tree_item.emptybranch {
+    padding-right: 21px;
+    padding-left: 0;
+    background-image: url('[[pix:t/collapsed_empty_rtl]]');
+}
+
+.dir-rtl .block_navigation .block_tree .tree_item img {
+    margin-right: 0;
+    margin-left: 5px;
+}
+
+.dir-rtl .block_navigation .block_tree ul {
+    margin: 0 16px 0 0;
+}
+
+.dir-rtl.jsenabled .block_navigation .block_tree .collapsed .tree_item.branch {
+    background-image: url('[[pix:t/collapsed_rtl]]');
+}
index 2073700..2d58702 100644 (file)
Binary files a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js and b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js differ
index 962ebaa..7c67d98 100644 (file)
Binary files a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js and b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js differ
index 0f46ccd..b661bfd 100644 (file)
Binary files a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js and b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js differ
index e237023..3678bff 100644 (file)
@@ -481,6 +481,9 @@ BRANCH.prototype = {
      * This function creates a DOM structure for the branch and then injects
      * it into the navigation tree at the correct point.
      *
+     * It is important that this is kept in check with block_navigation_renderer::navigation_node as that produces
+     * the same thing as this but on the php side.
+     *
      * @method draw
      * @chainable
      * @param {Node} element
@@ -492,6 +495,7 @@ BRANCH.prototype = {
         var branchli = Y.Node.create('<li></li>');
         var link = this.get('link');
         var branchp = Y.Node.create('<p class="tree_item"></p>').setAttribute('id', this.get('id'));
+        var name;
         if (!link) {
             //add tab focus if not link (so still one focus per menu node).
             // it was suggested to have 2 foci. one for the node and one for the link in MDL-27428.
@@ -506,10 +510,11 @@ BRANCH.prototype = {
         // Prepare the icon, should be an object representing a pix_icon
         var branchicon = false;
         var icon = this.get('icon');
-        if (icon && (!isbranch || this.get('type') === NODETYPE.ACTIVITY)) {
+        if (icon && (!isbranch || this.get('type') === NODETYPE.ACTIVITY || this.get('type') === NODETYPE.RESOURCE)) {
             branchicon = Y.Node.create('<img alt="" />');
             branchicon.setAttribute('src', M.util.image_url(icon.pix, icon.component));
             branchli.addClass('item_with_icon');
+            branchp.addClass('hasicon');
             if (icon.alt) {
                 branchicon.setAttribute('alt', icon.alt);
             }
@@ -527,8 +532,11 @@ BRANCH.prototype = {
             var branchspan = Y.Node.create('<span></span>');
             if (branchicon) {
                 branchspan.appendChild(branchicon);
+                name = '<span class="item-content-wrap">' + this.get('name') + '</span>';
+            } else {
+                name = this.get('name');
             }
-            branchspan.append(this.get('name'));
+            branchspan.append(name);
             if (this.get('hidden')) {
                 branchspan.addClass('dimmed_text');
             }
@@ -537,8 +545,11 @@ BRANCH.prototype = {
             var branchlink = Y.Node.create('<a title="'+this.get('title')+'" href="'+link+'"></a>');
             if (branchicon) {
                 branchlink.appendChild(branchicon);
+                name = '<span class="item-content-wrap">' + this.get('name') + '</span>';
+            } else {
+                name = this.get('name');
             }
-            branchlink.append(this.get('name'));
+            branchlink.append(name);
             if (this.get('hidden')) {
                 branchlink.addClass('dimmed');
             }
index 9682f4f..c47d460 100644 (file)
@@ -37,7 +37,7 @@ class block_online_users extends block_base {
     }
 
     function get_content() {
-        global $USER, $CFG, $DB, $OUTPUT;
+        global $USER, $CFG, $DB, $OUTPUT, $PAGE;
 
         if ($this->content !== NULL) {
             return $this->content;
@@ -167,6 +167,7 @@ class block_online_users extends block_base {
             if (isloggedin() && has_capability('moodle/site:sendmessage', $this->page->context)
                            && !empty($CFG->messaging) && !isguestuser()) {
                 $canshowicon = true;
+                message_messenger_requirejs();
             } else {
                 $canshowicon = false;
             }
@@ -185,7 +186,11 @@ class block_online_users extends block_base {
                 }
                 if ($canshowicon and ($USER->id != $user->id) and !isguestuser($user)) {  // Only when logged in and messaging active etc
                     $anchortagcontents = '<img class="iconsmall" src="'.$OUTPUT->pix_url('t/message') . '" alt="'. get_string('messageselectadd') .'" />';
-                    $anchortag = '<a href="'.$CFG->wwwroot.'/message/index.php?id='.$user->id.'" title="'.get_string('messageselectadd').'">'.$anchortagcontents .'</a>';
+                    $anchorurl = new moodle_url('/message/index.php', array('id' => $user->id));
+                    $anchortag = html_writer::link($anchorurl, $anchortagcontents, array_merge(
+                      message_messenger_sendmessage_link_params($user),
+                      array('title' => get_string('messageselectadd'))
+                    ));
 
                     $this->content->text .= '<div class="message">'.$anchortag.'</div>';
                 }
diff --git a/blocks/tests/behat/hidden_block_region.feature b/blocks/tests/behat/hidden_block_region.feature
new file mode 100644 (file)
index 0000000..f08b349
--- /dev/null
@@ -0,0 +1,49 @@
+@core @core_block
+Feature: Show hidden blocks in a docked block region when editing
+  In order to edit blocks in a hidden region
+  As a teacher
+  I need to be able to see the blocks when editing is on
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | admin | C1 | editingteacher |
+    And I log in as "admin"
+    And I follow "Course 1"
+    And I turn editing mode on
+    # Hide all the blocks in the non-default region
+    And I configure the "Search forums" block
+    And I set the following fields to these values:
+      | Visible | No |
+    And I click on "Save changes" "button"
+    And I configure the "Latest news" block
+    And I set the following fields to these values:
+      | Visible | No |
+    And I click on "Save changes" "button"
+    And I configure the "Upcoming events" block
+    And I set the following fields to these values:
+      | Visible | No |
+    And I click on "Save changes" "button"
+    And I configure the "Recent activity" block
+    And I set the following fields to these values:
+      | Visible | No |
+    When I click on "Save changes" "button"
+    # Editing is on so they should be visible
+    Then I should see "Search forums"
+    And I should see "Latest news"
+    And I should see "Upcoming events"
+    And I should see "Recent activity"
+    And I turn editing mode off
+    # Editing is off, so they should no longer be visible
+    And I should not see "Search forums"
+    And I should not see "Latest news"
+    And I should not see "Upcoming events"
+    And I should not see "Recent activity"
+
+  @javascript
+  Scenario: Check that a region with only hidden blocks is not docked in editing mode (javascript enabled)
+
+  Scenario: Check that a region with only hidden blocks is not docked in editing mode (javascript disabled)
diff --git a/blocks/tests/behat/hide_blocks.feature b/blocks/tests/behat/hide_blocks.feature
new file mode 100644 (file)
index 0000000..72822ba
--- /dev/null
@@ -0,0 +1,28 @@
+@core @core_block
+Feature: Block visibility
+  In order to configure blocks visibility
+  As a teacher
+  I need to show and hide blocks on a page
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And I log in as "admin"
+    And I am on homepage
+    And I follow "Course 1"
+    And I follow "Turn editing on"
+
+  @javascript
+  Scenario: Hiding all blocks on the page should remove the column they're in
+    Given I open the "Search forums" blocks action menu
+    And I click on "Hide Search forums block" "link" in the "Search forums" "block"
+    And I open the "Latest news" blocks action menu
+    And I click on "Hide Latest news block" "link" in the "Latest news" "block"
+    And I open the "Upcoming events" blocks action menu
+    And I click on "Hide Upcoming events block" "link" in the "Upcoming events" "block"
+    And I open the "Recent activity" blocks action menu
+    When I click on "Hide Recent activity block" "link" in the "Recent activity" "block"
+    Then ".empty-region-side-post" "css_element" should not exist in the "body" "css_element"
+    And I follow "Turn editing off"
+    And ".empty-region-side-post" "css_element" should exist in the "body" "css_element"
index 4f0586b..e272ee4 100644 (file)
@@ -346,9 +346,10 @@ class cache_helper {
     /**
      * Ensure that the stats array is ready to collect information for the given store and definition.
      * @param string $store
-     * @param string $definition
+     * @param string $definition A string that identifies the definition.
+     * @param int $mode One of cache_store::MODE_*. Since 2.9.
      */
-    protected static function ensure_ready_for_stats($store, $definition) {
+    protected static function ensure_ready_for_stats($store, $definition, $mode = cache_store::MODE_APPLICATION) {
         // This function is performance-sensitive, so exit as quickly as possible
         // if we do not need to do anything.
         if (isset(self::$stats[$definition][$store])) {
@@ -356,14 +357,17 @@ class cache_helper {
         }
         if (!array_key_exists($definition, self::$stats)) {
             self::$stats[$definition] = array(
-                $store => array(
-                    'hits' => 0,
-                    'misses' => 0,
-                    'sets' => 0,
+                'mode' => $mode,
+                'stores' => array(
+                    $store => array(
+                        'hits' => 0,
+                        'misses' => 0,
+                        'sets' => 0,
+                    )
                 )
             );
         } else if (!array_key_exists($store, self::$stats[$definition])) {
-            self::$stats[$definition][$store] = array(
+            self::$stats[$definition]['stores'][$store] = array(
                 'hits' => 0,
                 'misses' => 0,
                 'sets' => 0,
@@ -371,43 +375,80 @@ class cache_helper {
         }
     }
 
+    /**
+     * Returns a string to describe the definition.
+     *
+     * This method supports the definition as a string due to legacy requirements.
+     * It is backwards compatible when a string is passed but is not accurate.
+     *
+     * @since 2.9
+     * @param cache_definition|string $definition
+     * @return string
+     */
+    protected static function get_definition_stat_id_and_mode($definition) {
+        if (!($definition instanceof cache_definition)) {
+            // All core calls to this method have been updated, this is the legacy state.
+            // We'll use application as the default as that is the most common, really this is not accurate of course but
+            // at this point we can only guess and as it only affects calls to cache stat outside of core (of which there should
+            // be none) I think that is fine.
+            debugging('Please update you cache stat calls to pass the definition rather than just its ID.', DEBUG_DEVELOPER);
+            return array((string)$definition, cache_store::MODE_APPLICATION);
+        }
+        return array($definition->get_id(), $definition->get_mode());
+    }
+
     /**
      * Record a cache hit in the stats for the given store and definition.
      *
+     * In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
+     * cache_definition instance. It is preferable to pass a cache definition instance.
+     *
      * @internal
-     * @param string $store
-     * @param string $definition
+     * @param cache_definition $store
+     * @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
+     *      actual cache_definition object now.
      * @param int $hits The number of hits to record (by default 1)
      */
     public static function record_cache_hit($store, $definition, $hits = 1) {
-        self::ensure_ready_for_stats($store, $definition);
-        self::$stats[$definition][$store]['hits'] += $hits;
+        list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
+        self::ensure_ready_for_stats($store, $definitionstr, $mode);
+        self::$stats[$definitionstr]['stores'][$store]['hits'] += $hits;
     }
 
     /**
      * Record a cache miss in the stats for the given store and definition.
      *
+     * In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
+     * cache_definition instance. It is preferable to pass a cache definition instance.
+     *
      * @internal
      * @param string $store
-     * @param string $definition
+     * @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
+     *      actual cache_definition object now.
      * @param int $misses The number of misses to record (by default 1)
      */
     public static function record_cache_miss($store, $definition, $misses = 1) {
-        self::ensure_ready_for_stats($store, $definition);
-        self::$stats[$definition][$store]['misses'] += $misses;
+        list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
+        self::ensure_ready_for_stats($store, $definitionstr, $mode);
+        self::$stats[$definitionstr]['stores'][$store]['misses'] += $misses;
     }
 
     /**
      * Record a cache set in the stats for the given store and definition.
      *
+     * In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
+     * cache_definition instance. It is preferable to pass a cache definition instance.
+     *
      * @internal
      * @param string $store
-     * @param string $definition
+     * @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
+     *      actual cache_definition object now.
      * @param int $sets The number of sets to record (by default 1)
      */
     public static function record_cache_set($store, $definition, $sets = 1) {
-        self::ensure_ready_for_stats($store, $definition);
-        self::$stats[$definition][$store]['sets'] += $sets;
+        list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
+        self::ensure_ready_for_stats($store, $definitionstr, $mode);
+        self::$stats[$definitionstr]['stores'][$store]['sets'] += $sets;
     }
 
     /**
index 6cd588a..9d623e2 100644 (file)
@@ -317,7 +317,7 @@ class cache implements cache_loader {
         $setaftervalidation = false;
         if ($result === false) {
             if ($this->perfdebug) {
-                cache_helper::record_cache_miss($this->storetype, $this->definition->get_id());
+                cache_helper::record_cache_miss($this->storetype, $this->definition);
             }
             if ($this->loader !== false) {
                 // We must pass the original (unparsed) key to the next loader in the chain.
@@ -329,7 +329,7 @@ class cache implements cache_loader {
             }
             $setaftervalidation = ($result !== false);
         } else if ($this->perfdebug) {
-            cache_helper::record_cache_hit($this->storetype, $this->definition->get_id());
+            cache_helper::record_cache_hit($this->storetype, $this->definition);
         }
         // 5. Validate strictness.
         if ($strictness === MUST_EXIST && $result === false) {
@@ -473,8 +473,8 @@ class cache implements cache_loader {
                     $hits++;
                 }
             }
-            cache_helper::record_cache_hit($this->storetype, $this->definition->get_id(), $hits);
-            cache_helper::record_cache_miss($this->storetype, $this->definition->get_id(), $misses);
+            cache_helper::record_cache_hit($this->storetype, $this->definition, $hits);
+            cache_helper::record_cache_miss($this->storetype, $this->definition, $misses);
         }
 
         // Return the result. Phew!
@@ -500,7 +500,7 @@ class cache implements cache_loader {
      */
     public function set($key, $data) {
         if ($this->perfdebug) {
-            cache_helper::record_cache_set($this->storetype, $this->definition->get_id());
+            cache_helper::record_cache_set($this->storetype, $this->definition);
         }
         if ($this->loader !== false) {
             // We have a loader available set it there as well.
@@ -649,7 +649,7 @@ class cache implements cache_loader {
         }
         $successfullyset = $this->store->set_many($data);
         if ($this->perfdebug && $successfullyset) {
-            cache_helper::record_cache_set($this->storetype, $this->definition->get_id(), $successfullyset);
+            cache_helper::record_cache_set($this->storetype, $this->definition, $successfullyset);
         }
         return $successfullyset;
     }
@@ -1039,7 +1039,7 @@ class cache implements cache_loader {
         }
         if ($result) {
             if ($this->perfdebug) {
-                cache_helper::record_cache_hit('** static acceleration **', $this->definition->get_id());
+                cache_helper::record_cache_hit('** static acceleration **', $this->definition);
             }
             if ($this->staticaccelerationsize > 1 && $this->staticaccelerationcount > 1) {
                 // Check to see if this is the last item on the static acceleration keys array.
@@ -1053,7 +1053,7 @@ class cache implements cache_loader {
             return $result;
         } else {
             if ($this->perfdebug) {
-                cache_helper::record_cache_miss('** static acceleration **', $this->definition->get_id());
+                cache_helper::record_cache_miss('** static acceleration **', $this->definition);
             }
             return false;
         }
@@ -1779,7 +1779,7 @@ class cache_session extends cache {
         // 4. Load if from the loader/datasource if we don't already have it.
         if ($result === false) {
             if ($this->perfdebug) {
-                cache_helper::record_cache_miss($this->storetype, $this->get_definition()->get_id());
+                cache_helper::record_cache_miss($this->storetype, $this->get_definition());
             }
             if ($this->get_loader() !== false) {
                 // We must pass the original (unparsed) key to the next loader in the chain.
@@ -1794,7 +1794,7 @@ class cache_session extends cache {
                 $this->set($key, $result);
             }
         } else if ($this->perfdebug) {
-            cache_helper::record_cache_hit($this->storetype, $this->get_definition()->get_id());
+            cache_helper::record_cache_hit($this->storetype, $this->get_definition());
         }
         // 5. Validate strictness.
         if ($strictness === MUST_EXIST && $result === false) {
@@ -1838,7 +1838,7 @@ class cache_session extends cache {
             $loader->set($key, $data);
         }
         if ($this->perfdebug) {
-            cache_helper::record_cache_set($this->storetype, $this->get_definition()->get_id());
+            cache_helper::record_cache_set($this->storetype, $this->get_definition());
         }
         if (is_object($data) && $data instanceof cacheable_object) {
             $data = new cache_cached_object($data);
@@ -1962,8 +1962,8 @@ class cache_session extends cache {
                     $hits++;
                 }
             }
-            cache_helper::record_cache_hit($this->storetype, $this->get_definition()->get_id(), $hits);
-            cache_helper::record_cache_miss($this->storetype, $this->get_definition()->get_id(), $misses);
+            cache_helper::record_cache_hit($this->storetype, $this->get_definition(), $hits);
+            cache_helper::record_cache_miss($this->storetype, $this->get_definition(), $misses);
         }
         return $return;
 
@@ -2040,7 +2040,7 @@ class cache_session extends cache {
         }
         $successfullyset = $this->get_store()->set_many($data);
         if ($this->perfdebug && $successfullyset) {
-            cache_helper::record_cache_set($this->storetype, $definitionid, $successfullyset);
+            cache_helper::record_cache_set($this->storetype, $this->get_definition(), $successfullyset);
         }
         return $successfullyset;
     }
index 760438e..1c1b133 100644 (file)
@@ -12,6 +12,7 @@ Information provided here is intended especially for developers.
    - cache::make Argument 4 (final arg) is now unused.
 * cache_config_phpunittest has been renamed to cache_config_testing
 * New method cache_store::ready_to_be_used_for_testing() that returns true|false if the store is suitable and ready for use as the primary store during unit and acceptance tests.
+* cache_helper::get_stats structure we changed to include the cache mode.
 
 === 2.7 ===
 * cache_store::is_ready is no longer abstract, calling cache_store::are_requirements_met by default.
index 39704f0..4882914 100644 (file)
@@ -991,7 +991,7 @@ function calendar_filter_controls_element(moodle_url $url, $type) {
         $str = get_string('show'.$typeforhumans.'events', 'calendar');
     }
     $content = html_writer::start_tag('li', array('class' => 'calendar_event'));
-    $content .= html_writer::start_tag('a', array('href' => $url));
+    $content .= html_writer::start_tag('a', array('href' => $url, 'rel' => 'nofollow'));
     $content .= html_writer::tag('span', $icon, array('class' => $class));
     $content .= html_writer::tag('span', $str, array('class' => 'eventname'));
     $content .= html_writer::end_tag('a');
index bc9d7d5..46e6c56 100644 (file)
@@ -176,7 +176,7 @@ class core_calendar_renderer extends plugin_renderer_base {
 
         if (empty($events)) {
             // There is nothing to display today.
-            $output .= $this->output->heading(get_string('daywithnoevents', 'calendar'), 3);
+            $output .= html_writer::span(get_string('daywithnoevents', 'calendar'), 'calendar-information calendar-no-results');
         } else {
             $output .= html_writer::start_tag('div', array('class' => 'eventlist'));
             $underway = array();
@@ -194,7 +194,8 @@ class core_calendar_renderer extends plugin_renderer_base {
 
             // Then, show a list of all events that just span this day
             if (!empty($underway)) {
-                $output .= $this->output->heading(get_string('spanningevents', 'calendar'), 3);
+                $output .= html_writer::span(get_string('spanningevents', 'calendar'),
+                    'calendar-information calendar-span-multiple-days');
                 foreach ($underway as $event) {
                     $event->time = calendar_format_event_time($event, time(), null, false, $calendar->timestamp_today());
                     $output .= $this->event($event);
@@ -536,7 +537,7 @@ class core_calendar_renderer extends plugin_renderer_base {
             }
             $output .= html_writer::end_tag('div');
         } else {
-            $output .= $this->output->heading(get_string('noupcomingevents', 'calendar'));
+            $output .= html_writer::span(get_string('noupcomingevents', 'calendar'), 'calendar-information calendar-no-results');
         }
 
         return $output;
index 469cbf9..5121a98 100644 (file)
@@ -124,6 +124,7 @@ $calendar->add_sidecalendar_blocks($renderer, true, $view);
 echo $OUTPUT->header();
 echo $renderer->start_layout();
 echo html_writer::start_tag('div', array('class'=>'heightcontainer'));
+echo $OUTPUT->heading(get_string('calendar', 'calendar'));
 
 switch($view) {
     case 'day':
@@ -169,4 +170,4 @@ if (!empty($CFG->enablecalendarexport)) {
 echo $OUTPUT->container_end();
 echo html_writer::end_tag('div');
 echo $renderer->complete_layout();
-echo $OUTPUT->footer();
\ No newline at end of file
+echo $OUTPUT->footer();
index deed367..beeaea9 100644 (file)
@@ -52,8 +52,8 @@ if (isset($courseformatoptions['numsections'])) {
     // Don't go less than 0, intentionally redirect silently (for the case of
     // double clicks).
     if ($courseformatoptions['numsections'] >= 0) {
-        course_get_format($course)->update_course_format_options(
-                array('numsections' => $courseformatoptions['numsections']));
+        update_course((object)array('id' => $course->id,
+            'numsections' => $courseformatoptions['numsections']));
     }
 }
 
index 6b8ce0a..b1e01db 100644 (file)
@@ -29,6 +29,7 @@ require_once($CFG->libdir . '/formslib.php');
 
 $id = required_param('id', PARAM_INT);    // course_sections.id
 $sectionreturn = optional_param('sr', 0, PARAM_INT);
+$deletesection = optional_param('delete', 0, PARAM_BOOL);
 
 $PAGE->set_url('/course/editsection.php', array('id'=>$id, 'sr'=> $sectionreturn));
 
@@ -43,6 +44,41 @@ require_capability('moodle/course:update', $context);
 // Get section_info object with all availability options.
 $sectioninfo = get_fast_modinfo($course)->get_section_info($sectionnum);
 
+// Deleting the section.
+if ($deletesection) {
+    $cancelurl = course_get_url($course, $sectioninfo, array('sr' => $sectionreturn));
+    if (course_can_delete_section($course, $sectioninfo)) {
+        $confirm = optional_param('confirm', false, PARAM_BOOL) && confirm_sesskey();
+        if ($confirm) {
+            course_delete_section($course, $sectioninfo, true);
+            $courseurl = course_get_url($course, 0, array('sr' => $sectionreturn));
+            redirect($courseurl);
+        } else {
+            if (get_string_manager()->string_exists('deletesection', 'format_' . $course->format)) {
+                $strdelete = get_string('deletesection', 'format_' . $course->format);
+            } else {
+                $strdelete = get_string('deletesection');
+            }
+            $PAGE->navbar->add($strdelete);
+            $PAGE->set_title($strdelete);
+            $PAGE->set_heading($course->fullname);
+            echo $OUTPUT->header();
+            echo $OUTPUT->box_start('noticebox');
+            $optionsyes = array('id' => $id, 'confirm' => 1, 'delete' => 1, 'sesskey' => sesskey());
+            $deleteurl = new moodle_url('/course/editsection.php', $optionsyes);
+            $formcontinue = new single_button($deleteurl, get_string('yes'));
+            $formcancel = new single_button($cancelurl, get_string('no'), 'get');
+            echo $OUTPUT->confirm(get_string('confirmdeletesection', '',
+                get_section_name($course, $sectioninfo)), $formcontinue, $formcancel);
+            echo $OUTPUT->box_end();
+            echo $OUTPUT->footer();
+            exit;
+        }
+    } else {
+        notice(get_string('nopermissions', 'error', get_string('deletesection')), $cancelurl);
+    }
+}
+
 $editoroptions = array('context'=>$context ,'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes'=>$CFG->maxbytes, 'trusttext'=>false, 'noclean'=>true);
 $mform = course_get_format($course->id)->editsection_form($PAGE->url,
         array('cs' => $sectioninfo, 'editoroptions' => $editoroptions));
index 71c24a0..ff3dbfa 100644 (file)
@@ -217,6 +217,7 @@ class core_course_external extends external_api {
                                     'id' => new external_value(PARAM_INT, 'activity id'),
                                     'url' => new external_value(PARAM_URL, 'activity url', VALUE_OPTIONAL),
                                     'name' => new external_value(PARAM_RAW, 'activity module name'),
+                                    'instance' => new external_value(PARAM_INT, 'instance id', VALUE_OPTIONAL),
                                     'description' => new external_value(PARAM_RAW, 'activity description', VALUE_OPTIONAL),
                                     'visible' => new external_value(PARAM_INT, 'is the module visible', VALUE_OPTIONAL),
                                     'modicon' => new external_value(PARAM_URL, 'activity icon url'),
@@ -838,27 +839,50 @@ class core_course_external extends external_api {
         // Parameter validation.
         $params = self::validate_parameters(self::delete_courses_parameters(), array('courseids'=>$courseids));
 
-        $transaction = $DB->start_delegated_transaction();
+        $warnings = array();
 
         foreach ($params['courseids'] as $courseid) {
-            $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST);
+            $course = $DB->get_record('course', array('id' => $courseid));
+
+            if ($course === false) {
+                $warnings[] = array(
+                                'item' => 'course',
+                                'itemid' => $courseid,
+                                'warningcode' => 'unknowncourseidnumber',
+                                'message' => 'Unknown course ID ' . $courseid
+                            );
+                continue;
+            }
 
             // Check if the context is valid.
             $coursecontext = context_course::instance($course->id);
             self::validate_context($coursecontext);
 
-            // Check if the current user has enought permissions.
+            // Check if the current user has permission.
             if (!can_delete_course($courseid)) {
-                throw new moodle_exception('cannotdeletecategorycourse', 'error',
-                    '', format_string($course->fullname)." (id: $courseid)");
+                $warnings[] = array(
+                                'item' => 'course',
+                                'itemid' => $courseid,
+                                'warningcode' => 'cannotdeletecourse',
+                                'message' => 'You do not have the permission to delete this course' . $courseid
+                            );
+                continue;
             }
 
-            delete_course($course, false);
+            if (delete_course($course, false) === false) {
+                $warnings[] = array(
+                                'item' => 'course',
+                                'itemid' => $courseid,
+                                'warningcode' => 'cannotdeletecategorycourse',
+                                'message' => 'Course ' . $courseid . ' failed to be deleted'
+                            );
+                continue;
+            }
         }
 
-        $transaction->allow_commit();
+        fix_course_sortorder();
 
-        return null;
+        return array('warnings' => $warnings);
     }
 
     /**
@@ -868,7 +892,11 @@ class core_course_external extends external_api {
      * @since Moodle 2.2
      */
     public static function delete_courses_returns() {
-        return null;
+        return new external_single_structure(
+            array(
+                'warnings' => new external_warnings()
+            )
+        );
     }
 
     /**
@@ -1028,7 +1056,7 @@ class core_course_external extends external_api {
 
         // Check if we need to unzip the file because the backup temp dir does not contains backup files.
         if (!file_exists($backupbasepath . "/moodle_backup.xml")) {
-            $file->extract_to_pathname(get_file_packer(), $backupbasepath);
+            $file->extract_to_pathname(get_file_packer('application/vnd.moodle.backup'), $backupbasepath);
         }
 
         // Create new course.
@@ -1955,6 +1983,15 @@ class moodle_course_external extends external_api {
         return core_course_external::get_courses_returns();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function get_courses_is_deprecated() {
+        return true;
+    }
+
     /**
      * Returns description of method parameters
      *
@@ -1992,4 +2029,12 @@ class moodle_course_external extends external_api {
         return core_course_external::create_courses_returns();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function create_courses_is_deprecated() {
+        return true;
+    }
 }
index eaeaff7..51709b9 100644 (file)
@@ -935,6 +935,85 @@ abstract class format_base {
      */
     public function section_get_available_hook(section_info $section, &$available, &$availableinfo) {
     }
+
+    /**
+     * Whether this format allows to delete sections
+     *
+     * If format supports deleting sections it is also recommended to define language string
+     * 'deletesection' inside the format.
+     *
+     * Do not call this function directly, instead use {@link course_can_delete_section()}
+     *
+     * @param int|stdClass|section_info $section
+     * @return bool
+     */
+    public function can_delete_section($section) {
+        return false;
+    }
+
+    /**
+     * Deletes a section
+     *
+     * Do not call this function directly, instead call {@link course_delete_section()}
+     *
+     * @param int|stdClass|section_info $section
+     * @param bool $forcedeleteifnotempty if set to false section will not be deleted if it has modules in it.
+     * @return bool whether section was deleted
+     */
+    public function delete_section($section, $forcedeleteifnotempty = false) {
+        global $DB;
+        if (!$this->uses_sections()) {
+            // Not possible to delete section if sections are not used.
+            return false;
+        }
+        if (!is_object($section)) {
+            $section = $DB->get_record('course_sections', array('course' => $this->get_courseid(), 'section' => $section),
+                'id,section,sequence');
+        }
+        if (!$section || !$section->section) {
+            // Not possible to delete 0-section.
+            return false;
+        }
+
+        if (!$forcedeleteifnotempty && !empty($section->sequence)) {
+            return false;
+        }
+
+        $course = $this->get_course();
+
+        // Remove the marker if it points to this section.
+        if ($section->section == $course->marker) {
+            course_set_marker($course->id, 0);
+        }
+
+        $lastsection = $DB->get_field_sql('SELECT max(section) from {course_sections}
+                            WHERE course = ?', array($course->id));
+
+        // Find out if we need to descrease the 'numsections' property later.
+        $courseformathasnumsections = array_key_exists('numsections',
+            $this->get_format_options());
+        $decreasenumsections = $courseformathasnumsections && ($section->section <= $course->numsections);
+
+        // Move the section to the end.
+        move_section_to($course, $section->section, $lastsection, true);
+
+        // Delete all modules from the section.
+        foreach (preg_split('/,/', $section->sequence, -1, PREG_SPLIT_NO_EMPTY) as $cmid) {
+            course_delete_module($cmid);
+        }
+
+        // Delete section and it's format options.
+        $DB->delete_records('course_format_options', array('sectionid' => $section->id));
+        $DB->delete_records('course_sections', array('id' => $section->id));
+        rebuild_course_cache($course->id, true);
+
+        // Descrease 'numsections' if needed.
+        if ($decreasenumsections) {
+            $this->update_course_format_options(array('numsections' => $course->numsections - 1));
+        }
+
+        return true;
+    }
 }
 
 /**
index d9afaa0..af8533c 100644 (file)
@@ -226,6 +226,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         }
 
         $coursecontext = context_course::instance($course->id);
+        $isstealth = isset($course->numsections) && ($section->section > $course->numsections);
 
         if ($onsectionpage) {
             $baseurl = course_get_url($course, $section->section);
@@ -237,7 +238,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         $controls = array();
 
         $url = clone($baseurl);
-        if (has_capability('moodle/course:sectionvisibility', $coursecontext)) {
+        if (!$isstealth && has_capability('moodle/course:sectionvisibility', $coursecontext)) {
             if ($section->visible) { // Show the hide/show eye.
                 $strhidefromothers = get_string('hidefromothers', 'format_'.$course->format);
                 $url->param('hide', $section->section);
@@ -255,7 +256,21 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
             }
         }
 
-        if (!$onsectionpage && has_capability('moodle/course:movesections', $coursecontext)) {
+        if (course_can_delete_section($course, $section)) {
+            if (get_string_manager()->string_exists('deletesection', 'format_'.$course->format)) {
+                $strdelete = get_string('deletesection', 'format_'.$course->format);
+            } else {
+                $strdelete = get_string('deletesection');
+            }
+            $url = new moodle_url('/course/editsection.php', array('id' => $section->id,
+                'sr' => $onsectionpage ? $section->section : 0, 'delete' => 1));
+            $controls[] = html_writer::link($url,
+                html_writer::empty_tag('img', array('src' => $this->output->pix_url('t/delete'),
+                    'class' => 'icon delete', 'alt' => $strdelete)),
+                array('title' => $strdelete));
+        }
+
+        if (!$isstealth && !$onsectionpage && has_capability('moodle/course:movesections', $coursecontext)) {
             $url = clone($baseurl);
             if ($section->section > 1) { // Add a arrow to move section up.
                 $url->param('section', $section->section);
@@ -530,7 +545,10 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         $o = '';
         $o.= html_writer::start_tag('li', array('id' => 'section-'.$sectionno, 'class' => 'section main clearfix orphaned hidden'));
         $o.= html_writer::tag('div', '', array('class' => 'left side'));
-        $o.= html_writer::tag('div', '', array('class' => 'right side'));
+        $course = course_get_format($this->page->course)->get_course();
+        $section = course_get_format($this->page->course)->get_section($sectionno);
+        $rightcontent = $this->section_right_content($section, $course, false);
+        $o.= html_writer::tag('div', $rightcontent, array('class' => 'right side'));
         $o.= html_writer::start_tag('div', array('class' => 'content'));
         $o.= $this->output->heading(get_string('orphanedactivitiesinsectionno', '', $sectionno), 3, 'sectionname');
         return $o;
index f9766d9..37e2f11 100644 (file)
@@ -24,6 +24,7 @@
  */
 
 $string['currentsection'] = 'This topic';
+$string['deletesection'] = 'Delete topic';
 $string['sectionname'] = 'Topic';
 $string['pluginname'] = 'Topics format';
 $string['section0name'] = 'General';
index cb0a7ed..034b58e 100644 (file)
@@ -315,8 +315,8 @@ class format_topics extends format_base {
      */
     public function update_course_format_options($data, $oldcourse = null) {
         global $DB;
+        $data = (array)$data;
         if ($oldcourse !== null) {
-            $data = (array)$data;
             $oldcourse = (array)$oldcourse;
             $options = $this->course_format_options();
             foreach ($options as $key => $unused) {
@@ -337,6 +337,30 @@ class format_topics extends format_base {
                 }
             }
         }
-        return $this->update_format_options($data);
+        $changed = $this->update_format_options($data);
+        if ($changed && array_key_exists('numsections', $data)) {
+            // If the numsections was decreased, try to completely delete the orphaned sections (unless they are not empty).
+            $numsections = (int)$data['numsections'];
+            $maxsection = $DB->get_field_sql('SELECT max(section) from {course_sections}
+                        WHERE course = ?', array($this->courseid));
+            for ($sectionnum = $maxsection; $sectionnum > $numsections; $sectionnum--) {
+                if (!$this->delete_section($sectionnum, false)) {
+                    break;
+                }
+            }
+        }
+        return $changed;
+    }
+
+    /**
+     * Whether this format allows to delete sections
+     *
+     * Do not call this function directly, instead use {@link course_can_delete_section()}
+     *
+     * @param int|stdClass|section_info $section
+     * @return bool
+     */
+    public function can_delete_section($section) {
+        return true;
     }
 }
index ffc7be6..6e2b43b 100644 (file)
@@ -97,8 +97,9 @@ class format_topics_renderer extends format_section_renderer_base {
         }
         $url->param('sesskey', sesskey());
 
+        $isstealth = $section->section > $course->numsections;
         $controls = array();
-        if (has_capability('moodle/course:setcurrentsection', $coursecontext)) {
+        if (!$isstealth && has_capability('moodle/course:setcurrentsection', $coursecontext)) {
             if ($course->marker == $section->section) {  // Show the "light globe" on/off.
                 $url->param('marker', 0);
                 $controls[] = html_writer::link($url,
diff --git a/course/format/topics/tests/behat/edit_delete_sections.feature b/course/format/topics/tests/behat/edit_delete_sections.feature
new file mode 100644 (file)
index 0000000..81e8a25
--- /dev/null
@@ -0,0 +1,85 @@
+@format @format_topics
+Feature: Sections can be edited and deleted in topics format
+  In order to rearrange my course contents
+  As a teacher
+  I need to edit and Delete topics
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email            |
+      | teacher1 | Teacher   | 1        | teacher1@asd.com |
+    And the following "courses" exist:
+      | fullname | shortname | format | coursedisplay | numsections |
+      | Course 1 | C1        | topics | 0             | 5           |
+    And the following "activities" exist:
+      | activity   | name                   | intro                         | course | idnumber    | section |
+      | assign     | Test assignment name   | Test assignment description   | C1     | assign1     | 0       |
+      | book       | Test book name         | Test book description         | C1     | book1       | 1       |
+      | chat       | Test chat name         | Test chat description         | C1     | chat1       | 4       |
+      | choice     | Test choice name       | Test choice description       | C1     | choice1     | 5       |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+
+  Scenario: Edit section summary in topics format
+    When I click on "Edit summary" "link" in the "li#section-2" "css_element"
+    And I set the following fields to these values:
+      | Summary | Welcome to section 2 |
+    And I press "Save changes"
+    Then I should see "Welcome to section 2" in the "li#section-2" "css_element"
+
+  Scenario: Edit section default name in topics format
+    When I click on "Edit summary" "link" in the "li#section-2" "css_element"
+    And I set the following fields to these values:
+      | Use default section name | 0                        |
+      | name                     | This is the second topic |
+    And I press "Save changes"
+    Then I should see "This is the second topic" in the "li#section-2" "css_element"
+    And I should not see "Topic 2" in the "li#section-2" "css_element"
+
+  Scenario: Deleting the last section in topics format
+    When I click on "Delete topic" "link" in the "li#section-5" "css_element"
+    Then I should see "Are you absolutely sure you want to delete \"Topic 5\"? All activities will be also deleted"
+    And I press "Yes"
+    And I should not see "Topic 5"
+    And I navigate to "Edit settings" node in "Course administration"
+    And I expand all fieldsets
+    And the field "Number of sections" matches value "4"
+
+  Scenario: Deleting the middle section in topics format
+    When I click on "Delete topic" "link" in the "li#section-4" "css_element"
+    And I press "Yes"
+    Then I should not see "Topic 5"
+    And I should not see "Test chat name"
+    And I should see "Test choice name" in the "li#section-4" "css_element"
+    And I navigate to "Edit settings" node in "Course administration"
+    And I expand all fieldsets
+    And the field "Number of sections" matches value "4"
+
+  Scenario: Deleting the orphaned section in topics format
+    When I follow "Reduce the number of sections"
+    Then I should see "Orphaned activities (section 5)" in the "li#section-5" "css_element"
+    And I click on "Delete topic" "link" in the "li#section-5" "css_element"
+    And I press "Yes"
+    And I should not see "Topic 5"
+    And I should not see "Orphaned activities"
+    And "li#section-5" "css_element" should not exist
+    And I navigate to "Edit settings" node in "Course administration"
+    And I expand all fieldsets
+    And the field "Number of sections" matches value "4"
+
+  Scenario: Deleting a section when orphaned section is present in topics format
+    When I follow "Reduce the number of sections"
+    Then I should see "Orphaned activities (section 5)" in the "li#section-5" "css_element"
+    And "li#section-5.orphaned" "css_element" should exist
+    And "li#section-4.orphaned" "css_element" should not exist
+    And I click on "Delete topic" "link" in the "li#section-1" "css_element"
+    And I press "Yes"
+    And I should not see "Test book name"
+    And I should see "Orphaned activities (section 4)" in the "li#section-4" "css_element"
+    And "li#section-5" "css_element" should not exist
+    And "li#section-4.orphaned" "css_element" should exist
+    And "li#section-3.orphaned" "css_element" should not exist
diff --git a/course/format/topics/tests/format_topics_test.php b/course/format/topics/tests/format_topics_test.php
new file mode 100644 (file)
index 0000000..09dea51
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * format_topics related unit tests
+ *
+ * @package    format_topics
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/course/lib.php');
+
+/**
+ * format_topics related unit tests
+ *
+ * @package    format_topics
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class format_topics_testcase extends advanced_testcase {
+
+    public function test_update_course_numsections() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $generator = $this->getDataGenerator();
+
+        $course = $generator->create_course(array('numsections' => 10, 'format' => 'topics'),
+            array('createsections' => true));
+        $generator->create_module('assign', array('course' => $course, 'section' => 7));
+
+        $this->setAdminUser();
+
+        $this->assertEquals(11, $DB->count_records('course_sections', array('course' => $course->id)));
+
+        // Change the numsections to 8, last two sections did not have any activities, they should be deleted.
+        update_course((object)array('id' => $course->id, 'numsections' => 8));
+        $this->assertEquals(9, $DB->count_records('course_sections', array('course' => $course->id)));
+        $this->assertEquals(9, count(get_fast_modinfo($course)->get_section_info_all()));
+
+        // Change the numsections to 5, section 8 should be deleted but section 7 should remain as it has activities.
+        update_course((object)array('id' => $course->id, 'numsections' => 6));
+        $this->assertEquals(8, $DB->count_records('course_sections', array('course' => $course->id)));
+        $this->assertEquals(8, count(get_fast_modinfo($course)->get_section_info_all()));
+        $this->assertEquals(6, course_get_format($course)->get_course()->numsections);
+    }
+}
index 7916854..6919320 100644 (file)
@@ -2,6 +2,11 @@ This files describes API changes for course formats
 
 Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
 
+=== 2.9 ===
+* Course formats may support deleting sections, see MDL-10405 for more details.
+  format_section_renderer_base::section_edit_controls() is now also called for
+  stealth sections and it also returns "delete" control.
+
 === 2.8 ===
 * The activity chooser now uses M.course.format.get_sectionwrapperclass()
   to determine the section selector, rather than a hard-coded `li.section`.
index fd6c24d..86e24b7 100644 (file)
@@ -24,6 +24,7 @@
  */
 
 $string['currentsection'] = 'This week';
+$string['deletesection'] = 'Delete week';
 $string['sectionname'] = 'Week';
 $string['pluginname'] = 'Weekly format';
 $string['section0name'] = 'General';
index 66152d2..59a58b3 100644 (file)
@@ -324,8 +324,8 @@ class format_weeks extends format_base {
      */
     public function update_course_format_options($data, $oldcourse = null) {
         global $DB;
+        $data = (array)$data;
         if ($oldcourse !== null) {
-            $data = (array)$data;
             $oldcourse = (array)$oldcourse;
             $options = $this->course_format_options();
             foreach ($options as $key => $unused) {
@@ -346,7 +346,19 @@ class format_weeks extends format_base {
                 }
             }
         }
-        return $this->update_format_options($data);
+        $changed = $this->update_format_options($data);
+        if ($changed && array_key_exists('numsections', $data)) {
+            // If the numsections was decreased, try to completely delete the orphaned sections (unless they are not empty).
+            $numsections = (int)$data['numsections'];
+            $maxsection = $DB->get_field_sql('SELECT max(section) from {course_sections}
+                        WHERE course = ?', array($this->courseid));
+            for ($sectionnum = $maxsection; $sectionnum > $numsections; $sectionnum--) {
+                if (!$this->delete_section($sectionnum, false)) {
+                    break;
+                }
+            }
+        }
+        return $changed;
     }
 
     /**
@@ -393,4 +405,16 @@ class format_weeks extends format_base {
         $dates = $this->get_section_dates($section);
         return (($timenow >= $dates->start) && ($timenow < $dates->end));
     }
+
+    /**
+     * Whether this format allows to delete sections
+     *
+     * Do not call this function directly, instead use {@link course_can_delete_section()}
+     *
+     * @param int|stdClass|section_info $section
+     * @return bool
+     */
+    public function can_delete_section($section) {
+        return true;
+    }
 }
diff --git a/course/format/weeks/tests/behat/edit_delete_sections.feature b/course/format/weeks/tests/behat/edit_delete_sections.feature
new file mode 100644 (file)
index 0000000..ae1524c
--- /dev/null
@@ -0,0 +1,88 @@
+@format @format_weeks
+Feature: Sections can be edited and deleted in weeks format
+  In order to rearrange my course contents
+  As a teacher
+  I need to edit and Delete weeks
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email            |
+      | teacher1 | Teacher   | 1        | teacher1@asd.com |
+    And the following "courses" exist:
+      | fullname | shortname | format | coursedisplay | numsections | startdate |
+      | Course 1 | C1        | weeks  | 0             | 5           | 957139200 |
+    And the following "activities" exist:
+      | activity   | name                   | intro                         | course | idnumber    | section |
+      | assign     | Test assignment name   | Test assignment description   | C1     | assign1     | 0       |
+      | book       | Test book name         | Test book description         | C1     | book1       | 1       |
+      | chat       | Test chat name         | Test chat description         | C1     | chat1       | 4       |
+      | choice     | Test choice name       | Test choice description       | C1     | choice1     | 5       |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+
+  Scenario: Edit section summary in weeks format
+    When I click on "Edit summary" "link" in the "li#section-2" "css_element"
+    And I set the following fields to these values:
+      | Summary | Welcome to section 2 |
+    And I press "Save changes"
+    Then I should see "Welcome to section 2" in the "li#section-2" "css_element"
+
+  Scenario: Edit section default name in weeks format
+    Given I should see "8 May - 14 May" in the "li#section-2" "css_element"
+    When I click on "Edit summary" "link" in the "li#section-2" "css_element"
+    And I set the following fields to these values:
+      | Use default section name | 0                       |
+      | name                     | This is the second week |
+    And I press "Save changes"
+    Then I should see "This is the second week" in the "li#section-2" "css_element"
+    And I should not see "8 May - 14 May" in the "li#section-2" "css_element"
+
+  Scenario: Deleting the last section in weeks format
+    Given I should see "29 May - 4 June" in the "li#section-5" "css_element"
+    When I click on "Delete week" "link" in the "li#section-5" "css_element"
+    Then I should see "Are you absolutely sure you want to delete \"29 May - 4 June\"? All activities will be also deleted"
+    And I press "Yes"
+    And I should not see "29 May - 4 June"
+    And I navigate to "Edit settings" node in "Course administration"
+    And I expand all fieldsets
+    And the field "Number of sections" matches value "4"
+
+  Scenario: Deleting the middle section in weeks format
+    Given I should see "29 May - 4 June" in the "li#section-5" "css_element"
+    When I click on "Delete week" "link" in the "li#section-4" "css_element"
+    And I press "Yes"
+    Then I should not see "29 May - 4 June"
+    And I should not see "Test chat name"
+    And I should see "Test choice name" in the "li#section-4" "css_element"
+    And I navigate to "Edit settings" node in "Course administration"
+    And I expand all fieldsets
+    And the field "Number of sections" matches value "4"
+
+  Scenario: Deleting the orphaned section in weeks format
+    When I follow "Reduce the number of sections"
+    Then I should see "Orphaned activities (section 5)" in the "li#section-5" "css_element"
+    And I click on "Delete week" "link" in the "li#section-5" "css_element"
+    And I press "Yes"
+    And I should not see "29 May - 4 June"
+    And I should not see "Orphaned activities"
+    And "li#section-5" "css_element" should not exist
+    And I navigate to "Edit settings" node in "Course administration"
+    And I expand all fieldsets
+    And the field "Number of sections" matches value "4"
+
+  Scenario: Deleting a section when orphaned section is present in weeks format
+    When I follow "Reduce the number of sections"
+    Then I should see "Orphaned activities (section 5)" in the "li#section-5" "css_element"
+    And "li#section-5.orphaned" "css_element" should exist
+    And "li#section-4.orphaned" "css_element" should not exist
+    And I click on "Delete week" "link" in the "li#section-1" "css_element"
+    And I press "Yes"
+    And I should not see "Test book name"
+    And I should see "Orphaned activities (section 4)" in the "li#section-4" "css_element"
+    And "li#section-5" "css_element" should not exist
+    And "li#section-4.orphaned" "css_element" should exist
+    And "li#section-3.orphaned" "css_element" should not exist
diff --git a/course/format/weeks/tests/format_weeks_test.php b/course/format/weeks/tests/format_weeks_test.php
new file mode 100644 (file)
index 0000000..a388b59
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * format_weeks related unit tests
+ *
+ * @package    format_weeks
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/course/lib.php');
+
+/**
+ * format_weeks related unit tests
+ *
+ * @package    format_weeks
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class format_weeks_testcase extends advanced_testcase {
+
+    public function test_update_course_numsections() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $generator = $this->getDataGenerator();
+
+        $course = $generator->create_course(array('numsections' => 10, 'format' => 'weeks'),
+            array('createsections' => true));
+        $generator->create_module('assign', array('course' => $course, 'section' => 7));
+
+        $this->setAdminUser();
+
+        $this->assertEquals(11, $DB->count_records('course_sections', array('course' => $course->id)));
+
+        // Change the numsections to 8, last two sections did not have any activities, they should be deleted.
+        update_course((object)array('id' => $course->id, 'numsections' => 8));
+        $this->assertEquals(9, $DB->count_records('course_sections', array('course' => $course->id)));
+        $this->assertEquals(9, count(get_fast_modinfo($course)->get_section_info_all()));
+
+        // Change the numsections to 5, section 8 should be deleted but section 7 should remain as it has activities.
+        update_course((object)array('id' => $course->id, 'numsections' => 6));
+        $this->assertEquals(8, $DB->count_records('course_sections', array('course' => $course->id)));
+        $this->assertEquals(8, count(get_fast_modinfo($course)->get_section_info_all()));
+        $this->assertEquals(6, course_get_format($course)->get_course()->numsections);
+    }
+}
index b8212d5..0dabb51 100644 (file)
@@ -1771,9 +1771,10 @@ function delete_mod_from_section($modid, $sectionid) {
  * @param object $course
  * @param int $section Section number (not id!!!)
  * @param int $destination
+ * @param bool $ignorenumsections
  * @return boolean Result
  */
-function move_section_to($course, $section, $destination) {
+function move_section_to($course, $section, $destination, $ignorenumsections = false) {
 /// Moves a whole course section up and down within the course
     global $USER, $DB;
 
@@ -1783,7 +1784,7 @@ function move_section_to($course, $section, $destination) {
 
     // compartibility with course formats using field 'numsections'
     $courseformatoptions = course_get_format($course)->get_format_options();
-    if ((array_key_exists('numsections', $courseformatoptions) &&
+    if ((!$ignorenumsections && array_key_exists('numsections', $courseformatoptions) &&
             ($destination > $courseformatoptions['numsections'])) || ($destination < 1)) {
         return false;
     }
@@ -1825,6 +1826,57 @@ function move_section_to($course, $section, $destination) {
     return true;
 }
 
+/**
+ * This method will delete a course section and may delete all modules inside it.
+ *
+ * No permissions are checked here, use {@link course_can_delete_section()} to
+ * check if section can actually be deleted.
+ *
+ * @param int|stdClass $course
+ * @param int|stdClass|section_info $section
+ * @param bool $forcedeleteifnotempty if set to false section will not be deleted if it has modules in it.
+ * @return bool whether section was deleted
+ */
+function course_delete_section($course, $section, $forcedeleteifnotempty = true) {
+    return course_get_format($course)->delete_section($section, $forcedeleteifnotempty);
+}
+
+/**
+ * Checks if the current user can delete a section (if course format allows it and user has proper permissions).
+ *
+ * @param int|stdClass $course
+ * @param int|stdClass|section_info $section
+ * @return bool
+ */
+function course_can_delete_section($course, $section) {
+    if (is_object($section)) {
+        $section = $section->section;
+    }
+    if (!$section) {
+        // Not possible to delete 0-section.
+        return false;
+    }
+    // Course format should allow to delete sections.
+    if (!course_get_format($course)->can_delete_section($section)) {
+        return false;
+    }
+    // Make sure user has capability to update course and move sections.
+    $context = context_course::instance(is_object($course) ? $course->id : $course);
+    if (!has_all_capabilities(array('moodle/course:movesections', 'moodle/course:update'), $context)) {
+        return false;
+    }
+    // Make sure user has capability to delete each activity in this section.
+    $modinfo = get_fast_modinfo($course);
+    if (!empty($modinfo->sections[$section])) {
+        foreach ($modinfo->sections[$section] as $cmid) {
+            if (!has_capability('moodle/course:manageactivities', context_module::instance($cmid))) {
+                return false;
+            }
+        }
+    }
+    return true;
+}
+
 /**
  * Reordering algorithm for course sections. Given an array of section->section indexed by section->id,
  * an original position number and a target position number, rebuilds the array so that the
index c3a965e..d1e4def 100644 (file)
@@ -42,18 +42,18 @@ Feature: Course activity controls works as expected
     And I click on "Actions" "link" in the "Recent activity" "block"
     And I click on "Delete Recent activity block" "link"
     And I press "Yes"
-    And "#section-2" "css_element" <should_see_other_sections> exist
+    And <belowpage> "section" <should_see_other_sections> exist
     And I add a "Forum" to section "1" and I fill the form with:
       | Forum name | Test forum name 1 |
       | Description | Test forum description 1 |
     And I add a "Forum" to section "1" and I fill the form with:
       | Forum name | Test forum name 2 |
       | Description | Test forum description 2 |
-    And "#section-2" "css_element" <should_see_other_sections> exist
+    And <belowpage> "section" <should_see_other_sections> exist
     And I indent right "Test forum name 1" activity
-    And "#section-2" "css_element" <should_see_other_sections> exist
+    And <belowpage> "section" <should_see_other_sections> exist
     And I indent left "Test forum name 1" activity
-    And "#section-2" "css_element" <should_see_other_sections> exist
+    And <belowpage> "section" <should_see_other_sections> exist
     And I open "Test forum name 1" actions menu
     And I click on "Edit settings" "link" in the "Test forum name 1" activity
     And I should see "Updating Forum"
@@ -63,38 +63,38 @@ Feature: Course activity controls works as expected
       | Description | Just to check that I can edit the description |
       | Display description on course page | 1 |
     And I click on "Cancel" "button"
-    And "#section-2" "css_element" <should_see_other_sections> exist
+    And <belowpage> "section" <should_see_other_sections> exist
     And I open "Test forum name 1" actions menu
     And I click on "Hide" "link" in the "Test forum name 1" activity
-    And "#section-2" "css_element" <should_see_other_sections> exist
+    And <belowpage> "section" <should_see_other_sections> exist
     And I delete "Test forum name 1" activity
     And I should not see "Test forum name 1" in the "#region-main" "css_element"
     And I duplicate "Test forum name 2" activity editing the new copy with:
       | Forum name | Edited test forum name 2 |
-    And "#section-2" "css_element" <should_see_other_sections> exist
+    And <belowpage> "section" <should_see_other_sections> exist
     And I should see "Test forum name 2"
     And I should see "Edited test forum name 2"
     And I hide section "1"
-    And "#section-2" "css_element" <should_see_other_sections> exist
+    And <belowpage> "section" <should_see_other_sections> exist
     And section "1" should be hidden
     And all activities in section "1" should be hidden
     And I show section "1"
-    And "#section-2" "css_element" <should_see_other_sections> exist
+    And <belowpage> "section" <should_see_other_sections> exist
     And section "1" should be visible
     And I add the "Section links" block
-    And "#section-2" "css_element" <should_see_other_sections> exist
+    And <belowpage> "section" <should_see_other_sections> exist
     And I should see "1 2 3 4 5" in the "Section links" "block"
     And I click on "2" "link" in the "Section links" "block"
     And I <should_see_other_sections_following_block_sections_links> see "Test forum name 2"
 
     Examples:
-      | courseformat | coursedisplay | targetpage              | should_see_other_sections | should_see_other_sections_following_block_sections_links |
-      | topics       | 0             | "Course 1"              | should                    | should                                                   |
-      | topics       | 1             | "Topic 1"               | should not                | should not                                               |
-      | topics       | 1             | "Course 1"              | should                    | should not                                               |
-      | weeks        | 0             | "Course 1"              | should                    | should                                                   |
-      | weeks        | 1             | "1 January - 7 January" | should not                | should not                                               |
-      | weeks        | 1             | "Course 1"              | should                    | should not                                               |
+      | courseformat | coursedisplay | targetpage              | should_see_other_sections | should_see_other_sections_following_block_sections_links | belowpage                |
+      | topics       | 0             | "Course 1"              | should                    | should                                                   | "Topic 2"                |
+      | topics       | 1             | "Topic 1"               | should not                | should not                                               | "Topic 2"                |
+      | topics       | 1             | "Course 1"              | should                    | should not                                               | "Topic 2"                |
+      | weeks        | 0             | "Course 1"              | should                    | should                                                   | "8 January - 14 January" |
+      | weeks        | 1             | "1 January - 7 January" | should not                | should not                                               | "8 January - 14 January" |
+      | weeks        | 1             | "Course 1"              | should                    | should not                                               | "8 January - 14 January" |
 
   Scenario Outline: General activities course controls using topics and weeks formats, and paged mode and not paged mode works as expected
     Given the following "users" exist:
@@ -122,51 +122,51 @@ Feature: Course activity controls works as expected
     And I click on "Actions" "link" in the "Recent activity" "block"
     And I click on "Delete Recent activity block" "link"
     And I press "Yes"
-    And "#section-2" "css_element" <should_see_other_sections> exist
+    And <belowpage> "section" <should_see_other_sections> exist
     And I add a "Forum" to section "1" and I fill the form with:
       | Forum name | Test forum name 1 |
       | Description | Test forum description 1 |
     And I add a "Forum" to section "1" and I fill the form with:
       | Forum name | Test forum name 2 |
       | Description | Test forum description 2 |
-    And "#section-2" "css_element" <should_see_other_sections> exist
+    And <belowpage> "section" <should_see_other_sections> exist
     And I indent right "Test forum name 1" activity
-    And "#section-2" "css_element" <should_see_other_sections> exist
+    And <belowpage> "section" <should_see_other_sections> exist
     And I indent left "Test forum name 1" activity
-    And "#section-2" "css_element" <should_see_other_sections> exist
+    And <belowpage> "section" <should_see_other_sections> exist
     And I click on "Edit settings" "link" in the "Test forum name 1" activity
     And I should see "Updating Forum"
     And I should see "Display description on course page"
     And I press "Save and return to course"
-    And "#section-2" "css_element" <should_see_other_sections> exist
+    And <belowpage> "section" <should_see_other_sections> exist
     And I click on "Hide" "link" in the "Test forum name 1" activity
-    And "#section-2" "css_element" <should_see_other_sections> exist
+    And <belowpage> "section" <should_see_other_sections> exist
     And I delete "Test forum name 1" activity
-    And "#section-2" "css_element" <should_see_other_sections> exist
+    And <belowpage> "section" <should_see_other_sections> exist
     And I should not see "Test forum name 1" in the "#region-main" "css_element"
     And I duplicate "Test forum name 2" activity editing the new copy with:
       | Forum name | Edited test forum name 2 |
-    And "#section-2" "css_element" <should_see_other_sections> exist
+    And <belowpage> "section" <should_see_other_sections> exist
     And I should see "Test forum name 2"
     And I should see "Edited test forum name 2"
     And I hide section "1"
-    And "#section-2" "css_element" <should_see_other_sections> exist
+    And <belowpage> "section" <should_see_other_sections> exist
     And section "1" should be hidden
     And all activities in section "1" should be hidden
     And I show section "1"
-    And "#section-2" "css_element" <should_see_other_sections> exist
+    And <belowpage> "section" <should_see_other_sections> exist
     And section "1" should be visible
     And I add the "Section links" block
-    And "#section-2" "css_element" <should_see_other_sections> exist
+    And <belowpage> "section" <should_see_other_sections> exist
     And I should see "1 2 3 4 5" in the "Section links" "block"
     And I click on "2" "link" in the "Section links" "block"
     And I <should_see_other_sections_following_block_sections_links> see "Test forum name 2"
 
     Examples:
-      | courseformat | coursedisplay | targetpage              | should_see_other_sections | should_see_other_sections_following_block_sections_links |
-      | topics       | 0             | "Course 1"              | should                    | should                                                   |
-      | topics       | 1             | "Topic 1"               | should not                | should not                                               |
-      | topics       | 1             | "Course 1"              | should                    | should not                                               |
-      | weeks        | 0             | "Course 1"              | should                    | should                                                   |
-      | weeks        | 1             | "1 January - 7 January" | should not                | should not                                               |
-      | weeks        | 1             | "Course 1"              | should                    | should not                                               |
+      | courseformat | coursedisplay | targetpage              | should_see_other_sections | should_see_other_sections_following_block_sections_links | belowpage                |
+      | topics       | 0             | "Course 1"              | should                    | should                                                   | "Topic 2"                |
+      | topics       | 1             | "Topic 1"               | should not                | should not                                               | "Topic 2"                |
+      | topics       | 1             | "Course 1"              | should                    | should not                                               | "Topic 2"                |
+      | weeks        | 0             | "Course 1"              | should                    | should                                                   | "8 January - 14 January" |
+      | weeks        | 1             | "1 January - 7 January" | should not                | should not                                               | "8 January - 14 January" |
+      | weeks        | 1             | "Course 1"              | should                    | should not                                               | "8 January - 14 January" |
index f2cde5b..2729055 100644 (file)
@@ -29,8 +29,8 @@ Feature: Activities can be moved between sections
 
   Scenario: Move activities in a single page course with Javascript disabled
     When I move "Test forum name" activity to section "2"
-    Then I should see "Test forum name" in the "#section-2" "css_element"
-    And I should not see "Test forum name" in the "#section-1" "css_element"
+    Then I should see "Test forum name" in the "Topic 2" "section"
+    And I should not see "Test forum name" in the "Topic 1" "section"
 
   Scenario: Move activities in the course home with Javascript disabled using paged mode
     Given I click on "Edit settings" "link" in the "Administration" "block"
@@ -38,8 +38,8 @@ Feature: Activities can be moved between sections
       | Course layout | Show one section per page |
     And I press "Save and display"
     When I move "Test forum name" activity to section "2"
-    Then I should see "Test forum name" in the "#section-2" "css_element"
-    And I should not see "Test forum name" in the "#section-1" "css_element"
+    Then I should see "Test forum name" in the "Topic 2" "section"
+    And I should not see "Test forum name" in the "Topic 1" "section"
 
   Scenario: Move activities in a course section with Javascript disabled using paged mode
     Given I click on "Edit settings" "link" in the "Administration" "block"
index a4772fe..932ba31 100644 (file)
@@ -23,9 +23,9 @@ Feature: Sections can be moved
       | Forum name | Test forum name |
       | Description | Test forum description |
     When I move down section "1"
-    Then I should see "Test forum name" in the "#section-2" "css_element"
+    Then I should see "Test forum name" in the "Topic 2" "section"
     And I move up section "2"
-    And I should see "Test forum name" in the "#section-1" "css_element"
+    And I should see "Test forum name" in the "Topic 1" "section"
 
   Scenario: Move up and down a section with Javascript disabled in the course home of a course using paged mode
     Given I click on "Edit settings" "link" in the "Administration" "block"
@@ -36,9 +36,9 @@ Feature: Sections can be moved
       | Forum name | Test forum name |
       | Description | Test forum description |
     When I move down section "1"
-    Then I should see "Test forum name" in the "#section-2" "css_element"
+    Then I should see "Test forum name" in the "Topic 2" "section"
     And I move up section "2"
-    And I should see "Test forum name" in the "#section-1" "css_element"
+    And I should see "Test forum name" in the "Topic 1" "section"
 
   Scenario: Sections can not be moved with Javascript disabled in a section page of a course using paged mode
     Given I click on "Edit settings" "link" in the "Administration" "block"
@@ -49,7 +49,7 @@ Feature: Sections can be moved
       | Forum name | Test forum name |
       | Description | Test forum description |
     When I follow "Topic 2"
-    Then "#section-1" "css_element" should not exist
-    And "#section-3" "css_element" should not exist
+    Then "Topic 1" "section" should not exist
+    And "Topic 3" "section" should not exist
     And "Move down" "link" should not exist
     And "Move up" "link" should not exist
index 0371c12..b6d66f9 100644 (file)
@@ -11,11 +11,11 @@ Feature: Course paged mode
       | Course 1 | C1 | 0 | <courseformat> | 1 | 3 |
     And I log in as "admin"
     And I follow "Course 1"
-    Then I click on <section2> "link" in the "#section-2" "css_element"
+    Then I click on <section2> "link" in the <section2> "section"
     And I follow "C1"
-    And I click on <section3> "link" in the "#section-3" "css_element"
+    And I click on <section3> "link" in the <section3> "section"
     And I follow "C1"
-    And I click on <section1> "link" in the "#section-1" "css_element"
+    And I click on <section1> "link" in the <section1> "section"
     And I should see <section1> in the "div.single-section" "css_element"
     And I should see <section2> in the ".single-section span.mdl-right" "css_element"
     And I should not see <prevunexistingsection> in the ".single-section" "css_element"
@@ -44,11 +44,11 @@ Feature: Course paged mode
       | Course 1 | C1 | 0 | <courseformat> | 1 | 3 |
     And I log in as "admin"
     And I follow "Course 1"
-    Then I click on <section2> "link" in the "#section-2" "css_element"
+    Then I click on <section2> "link" in the <section2> "section"
     And I follow "C1"
-    And I click on <section3> "link" in the "#section-3" "css_element"
+    And I click on <section3> "link" in the <section3> "section"
     And I follow "C1"
-    And I click on <section1> "link" in the "#section-1" "css_element"
+    And I click on <section1> "link" in the <section1> "section"
     And I should see <section1> in the "div.single-section" "css_element"
     And I should see <section2> in the ".single-section span.mdl-right" "css_element"
     And I should not see <prevunexistingsection> in the ".single-section" "css_element"
index 9c38afe..a40bea6 100644 (file)
@@ -859,6 +859,130 @@ class core_course_courselib_testcase extends advanced_testcase {
         $this->assertEquals(3, $course->marker);
     }
 
+    public function test_course_can_delete_section() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $generator = $this->getDataGenerator();
+
+        $courseweeks = $generator->create_course(
+            array('numsections' => 5, 'format' => 'weeks'),
+            array('createsections' => true));
+        $assign1 = $generator->create_module('assign', array('course' => $courseweeks, 'section' => 1));
+        $assign2 = $generator->create_module('assign', array('course' => $courseweeks, 'section' => 2));
+
+        $coursetopics = $generator->create_course(
+            array('numsections' => 5, 'format' => 'topics'),
+            array('createsections' => true));
+
+        $coursesingleactivity = $generator->create_course(
+            array('format' => 'singleactivity'),
+            array('createsections' => true));
+
+        // Enrol student and teacher.
+        $roleids = $DB->get_records_menu('role', null, '', 'shortname, id');
+        $student = $generator->create_user();
+        $teacher = $generator->create_user();
+
+        $generator->enrol_user($student->id, $courseweeks->id, $roleids['student']);
+        $generator->enrol_user($teacher->id, $courseweeks->id, $roleids['editingteacher']);
+
+        $generator->enrol_user($student->id, $coursetopics->id, $roleids['student']);
+        $generator->enrol_user($teacher->id, $coursetopics->id, $roleids['editingteacher']);
+
+        $generator->enrol_user($student->id, $coursesingleactivity->id, $roleids['student']);
+        $generator->enrol_user($teacher->id, $coursesingleactivity->id, $roleids['editingteacher']);
+
+        // Teacher should be able to delete sections (except for 0) in topics and weeks format.
+        $this->setUser($teacher);
+
+        // For topics and weeks formats will return false for section 0 and true for any other section.
+        $this->assertFalse(course_can_delete_section($courseweeks, 0));
+        $this->assertTrue(course_can_delete_section($courseweeks, 1));
+
+        $this->assertFalse(course_can_delete_section($coursetopics, 0));
+        $this->assertTrue(course_can_delete_section($coursetopics, 1));
+
+        // For singleactivity course format no section can be deleted.
+        $this->assertFalse(course_can_delete_section($coursesingleactivity, 0));
+        $this->assertFalse(course_can_delete_section($coursesingleactivity, 1));
+
+        // Now let's revoke a capability from teacher to manage activity in section 1.
+        $modulecontext = context_module::instance($assign1->cmid);
+        assign_capability('moodle/course:manageactivities', CAP_PROHIBIT, $roleids['editingteacher'],
+            $modulecontext);
+        $modulecontext->mark_dirty();
+        $this->assertFalse(course_can_delete_section($courseweeks, 1));
+        $this->assertTrue(course_can_delete_section($courseweeks, 2));
+
+        // Student does not have permissions to delete sections.
+        $this->setUser($student);
+        $this->assertFalse(course_can_delete_section($courseweeks, 1));
+        $this->assertFalse(course_can_delete_section($coursetopics, 1));
+        $this->assertFalse(course_can_delete_section($coursesingleactivity, 1));
+    }
+
+    public function test_course_delete_section() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $generator = $this->getDataGenerator();
+
+        $course = $generator->create_course(array('numsections' => 6, 'format' => 'topics'),
+            array('createsections' => true));
+        $assign0 = $generator->create_module('assign', array('course' => $course, 'section' => 0));
+        $assign1 = $generator->create_module('assign', array('course' => $course, 'section' => 1));
+        $assign21 = $generator->create_module('assign', array('course' => $course, 'section' => 2));
+        $assign22 = $generator->create_module('assign', array('course' => $course, 'section' => 2));
+        $assign3 = $generator->create_module('assign', array('course' => $course, 'section' => 3));
+        $assign5 = $generator->create_module('assign', array('course' => $course, 'section' => 5));
+        $assign6 = $generator->create_module('assign', array('course' => $course, 'section' => 6));
+
+        $this->setAdminUser();
+
+        // Attempt to delete non-existing section.
+        $this->assertFalse(course_delete_section($course, 10, false));
+        $this->assertFalse(course_delete_section($course, 9, true));
+
+        // Attempt to delete 0-section.
+        $this->assertFalse(course_delete_section($course, 0, true));
+        $this->assertTrue($DB->record_exists('course_modules', array('id' => $assign0->cmid)));
+
+        // Delete last section.
+        $this->assertTrue(course_delete_section($course, 6, true));
+        $this->assertFalse($DB->record_exists('course_modules', array('id' => $assign6->cmid)));
+        $this->assertEquals(5, course_get_format($course)->get_course()->numsections);
+
+        // Delete empty section.
+        $this->assertTrue(course_delete_section($course, 4, false));
+        $this->assertEquals(4, course_get_format($course)->get_course()->numsections);
+
+        // Delete section in the middle (2).
+        $this->assertFalse(course_delete_section($course, 2, false));
+        $this->assertTrue(course_delete_section($course, 2, true));
+        $this->assertFalse($DB->record_exists('course_modules', array('id' => $assign21->cmid)));
+        $this->assertFalse($DB->record_exists('course_modules', array('id' => $assign22->cmid)));
+        $this->assertEquals(3, course_get_format($course)->get_course()->numsections);
+        $this->assertEquals(array(0 => array($assign0->cmid),
+            1 => array($assign1->cmid),
+            2 => array($assign3->cmid),
+            3 => array($assign5->cmid)), get_fast_modinfo($course)->sections);
+
+        // Make last section orphaned.
+        update_course((object)array('id' => $course->id, 'numsections' => 2));
+        $this->assertEquals(2, course_get_format($course)->get_course()->numsections);
+
+        // Remove orphaned section.
+        $this->assertTrue(course_delete_section($course, 3, true));
+        $this->assertEquals(2, course_get_format($course)->get_course()->numsections);
+
+        // Remove marked section.
+        course_set_marker($course->id, 1);
+        $this->assertTrue(course_get_format($course)->is_section_current(1));
+        $this->assertTrue(course_delete_section($course, 1, true));
+        $this->assertFalse(course_get_format($course)->is_section_current(1));
+    }
+
     public function test_get_course_display_name_for_list() {
         global $CFG;
         $this->resetAfterTest(true);
@@ -1726,7 +1850,7 @@ class core_course_courselib_testcase extends advanced_testcase {
         $bc->execute_plan();
         $results = $bc->get_results();
         $file = $results['backup_destination'];
-        $fp = get_file_packer();
+        $fp = get_file_packer('application/vnd.moodle.backup');
         $filepath = $CFG->dataroot . '/temp/backup/test-restore-course-event';
         $file->extract_to_pathname($fp, $filepath);
         $bc->destroy();
index a4e4234..7adad2c 100644 (file)
@@ -473,17 +473,46 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $course3  = self::getDataGenerator()->create_course();
 
         // Delete courses.
-        core_course_external::delete_courses(array($course1->id, $course2->id));
+        $result = core_course_external::delete_courses(array($course1->id, $course2->id));
+        $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
+        // Check for 0 warnings.
+        $this->assertEquals(0, count($result['warnings']));
 
         // Check $course 1 and 2 are deleted.
         $notdeletedcount = $DB->count_records_select('course',
             'id IN ( ' . $course1->id . ',' . $course2->id . ')');
         $this->assertEquals(0, $notdeletedcount);
 
+        // Try to delete non-existent course.
+        $result = core_course_external::delete_courses(array($course1->id));
+        $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
+        // Check for 1 warnings.
+        $this->assertEquals(1, count($result['warnings']));
+
+        // Try to delete Frontpage course.
+        $result = core_course_external::delete_courses(array(0));
+        $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
+        // Check for 1 warnings.
+        $this->assertEquals(1, count($result['warnings']));
+
+         // Fail when the user has access to course (enrolled) but does not have permission or is not admin.
+        $student1 = self::getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($student1->id,
+                                              $course3->id,
+                                              $studentrole->id);
+        $this->setUser($student1);
+        $result = core_course_external::delete_courses(array($course3->id));
+        $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
+        // Check for 1 warnings.
+        $this->assertEquals(1, count($result['warnings']));
+
          // Fail when the user is not allow to access the course (enrolled) or is not admin.
         $this->setGuestUser();
         $this->setExpectedException('require_login_exception');
-        $createdsubcats = core_course_external::delete_courses(array($course3->id));
+
+        $result = core_course_external::delete_courses(array($course3->id));
+        $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
     }
 
     /**
@@ -611,12 +640,14 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
                 $formattedtext = format_text($cm->content, FORMAT_HTML,
                     array('noclean' => true, 'para' => false, 'filter' => false));
                 $this->assertEquals($formattedtext, $module['description']);
+                $this->assertEquals($forumcm->instance, $module['instance']);
                 $testexecuted = $testexecuted + 1;
             } else if ($module['id'] == $labelcm->id and $module['modname'] == 'label') {
                 $cm = $modinfo->cms[$labelcm->id];
                 $formattedtext = format_text($cm->content, FORMAT_HTML,
                     array('noclean' => true, 'para' => false, 'filter' => false));
                 $this->assertEquals($formattedtext, $module['description']);
+                $this->assertEquals($labelcm->instance, $module['instance']);
                 $testexecuted = $testexecuted + 1;
             }
         }
index 619cea0..711a1e0 100644 (file)
@@ -922,6 +922,15 @@ class moodle_enrol_external extends external_api {
         );
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function get_enrolled_users_is_deprecated() {
+        return true;
+    }
+
     /**
      * Returns description of method parameters
      *
@@ -960,6 +969,14 @@ class moodle_enrol_external extends external_api {
         return core_enrol_external::get_users_courses_returns();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function get_users_courses_is_deprecated() {
+        return true;
+    }
 
     /**
      * Returns description of method parameters
@@ -997,6 +1014,14 @@ class moodle_enrol_external extends external_api {
         return core_role_external::assign_roles_returns();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function role_assign_is_deprecated() {
+        return true;
+    }
 
     /**
      * Returns description of method parameters
@@ -1033,4 +1058,13 @@ class moodle_enrol_external extends external_api {
     public static function role_unassign_returns() {
         return core_role_external::unassign_roles_returns();
     }
+
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function role_unassign_is_deprecated() {
+        return true;
+    }
 }
index 7b08046..87e4e30 100644 (file)
@@ -27,8 +27,8 @@ $functions = array(
 
     // === enrol related functions ===
     'moodle_enrol_manual_enrol_users' => array(
-        'classname'   => 'enrol_manual_external',
-        'methodname'  => 'enrol_users',
+        'classname'   => 'moodle_enrol_manual_external',
+        'methodname'  => 'manual_enrol_users',
         'classpath'   => 'enrol/manual/externallib.php',
         'description' => 'DEPRECATED: this deprecated function will be removed in a future version. This function has be renamed as enrol_manual_enrol_users()',
         'capabilities'=> 'enrol/manual:enrol',
index 7cec282..d24cc1c 100644 (file)
@@ -207,4 +207,12 @@ class moodle_enrol_manual_external extends external_api {
         return enrol_manual_external::enrol_users_returns();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function manual_enrol_users_is_deprecated() {
+        return true;
+    }
 }
index 4b035f3..464d76b 100644 (file)
@@ -263,8 +263,10 @@ class enrol_manual_plugin extends enrol_plugin {
             'enrol',
             'enrolmentoptions',
             'enrolusers',
+            'enrolxusers',
             'errajaxfailedenrol',
             'errajaxsearch',
+            'foundxcohorts',
             'none',
             'usersearch',
             'unlimitedduration',
index f4d1bee..4bc8350 100644 (file)
@@ -451,13 +451,13 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                     .append(create('<div class="'+CSS.DETAILS+'"></div>')
                         .append(create('<div class="'+CSS.COHORTNAME+'">'+cohort.name+'</div>')))
                     .append(create('<div class="'+CSS.OPTIONS+'"></div>')
-                        .append(create('<input type="button" class="'+CSS.ENROL+'" value="'+'Enrol '+cohort.cnt+' users'+'" />'))) // TODO string
+                        .append(create('<input type="button" class="' + CSS.ENROL + '" value="' + M.util.get_string('enrolxusers', 'enrol', cohort.cnt) + '" />')))
                 );
             }
             this.set(UEP.COHORTCOUNT, count);
             if (!args.append) {
                 //var usersstr = (result.response.totalusers == '1')?M.util.get_string('ajaxoneuserfound', 'enrol'):M.util.get_string('ajaxxusersfound','enrol', result.response.totalusers);
-                var cohortsstr = 'Found '+result.response.totalcohorts+' cohorts'; // TODO
+                var cohortsstr = M.util.get_string('foundxcohorts', 'enrol', result.response.totalcohorts);
                 var content = create('<div class="'+CSS.SEARCHRESULTS+'"></div>')
                     .append(create('<div class="'+CSS.TOTALCOHORTS+'">'+cohortsstr+'</div>'))
                     .append(cohorts);
index 2240199..b1cd895 100644 (file)
@@ -759,8 +759,10 @@ class course_enrolment_other_users_table extends course_enrolment_table {
                     'enrol',
                     'enrolmentoptions',
                     'enrolusers',
+                    'enrolxusers',
                     'errajaxfailedenrol',
                     'errajaxsearch',
+                    'foundxcohorts',
                     'none',
                     'usersearch',
                     'unlimitedduration',
index 3dc8449..61e5632 100644 (file)
--- a/file.php
+++ b/file.php
@@ -87,7 +87,8 @@ if (!$file = $fs->get_file_by_hash(sha1($fullpath))) {
     if (strrpos($fullpath, '/') !== strlen($fullpath) -1 ) {
         $fullpath .= '/';
     }
-    if (!$file = $fs->get_file_by_hash(sha1($fullpath.'/.'))) {
+    // Try to fallback to the directory named as the supposed file.
+    if (!$file = $fs->get_file_by_hash(sha1($fullpath.'.'))) {
         send_file_not_found();
     }
 }
index fdf63c5..389cc73 100644 (file)
@@ -74,6 +74,7 @@ class core_files_external extends external_api {
      * @param string $contextlevel The context level for the file location.
      * @param int $instanceid The instance id for where the file is located.
      * @return array
+     * @since Moodle 2.9 Returns additional fields (timecreated, filesize, author, license)
      * @since Moodle 2.2
      */
     public static function get_files($contextid, $component, $filearea, $itemid, $filepath, $filename, $modified = null,
@@ -139,6 +140,7 @@ class core_files_external extends external_api {
 
                 $params = $child->get_params();
                 $timemodified = $child->get_timemodified();
+                $timecreated = $child->get_timecreated();
 
                 if ($child->is_directory()) {
                     if ((is_null($modified)) or ($modified < $timemodified)) {
@@ -151,7 +153,11 @@ class core_files_external extends external_api {
                             'filename'  => $child->get_visible_name(),
                             'url'       => null,
                             'isdir'     => true,
-                            'timemodified' => $timemodified
+                            'timemodified' => $timemodified,
+                            'timecreated' => $timecreated,
+                            'filesize' => 0,
+                            'author' => null,
+                            'license' => null
                            );
                            $list[] = $node;
                     }
@@ -166,7 +172,11 @@ class core_files_external extends external_api {
                             'filename'  => $child->get_visible_name(),
                             'url'       => $child->get_url(),
                             'isdir'     => false,
-                            'timemodified' => $timemodified
+                            'timemodified' => $timemodified,
+                            'timecreated' => $timecreated,
+                            'filesize' => $child->get_filesize(),
+                            'author' => $child->get_author(),
+                            'license' => $child->get_license()
                         );
                            $list[] = $node;
                     }
@@ -181,6 +191,7 @@ class core_files_external extends external_api {
      * Returns description of get_files returns
      *
      * @return external_single_structure
+     * @since Moodle 2.9 Returns additional fields for files (timecreated, filesize, author, license)
      * @since Moodle 2.2
      */
     public static function get_files_returns() {
@@ -210,6 +221,10 @@ class core_files_external extends external_api {
                             'isdir'    => new external_value(PARAM_BOOL, ''),
                             'url'      => new external_value(PARAM_TEXT, ''),
                             'timemodified' => new external_value(PARAM_INT, ''),
+                            'timecreated' => new external_value(PARAM_INT, 'Time created', VALUE_OPTIONAL),
+                            'filesize' => new external_value(PARAM_INT, 'File size', VALUE_OPTIONAL),
+                            'author' => new external_value(PARAM_TEXT, 'File owner', VALUE_OPTIONAL),
+                            'license' => new external_value(PARAM_TEXT, 'File license', VALUE_OPTIONAL),
                         )
                     )
                 )
@@ -404,13 +419,18 @@ class moodle_file_external extends external_api {
      * @param int $itemid
      * @param string $filepath
      * @param string $filename
+     * @param int $modified timestamp to return files changed after this time.
+     * @param string $contextlevel The context level for the file location.
+     * @param int $instanceid The instance id for where the file is located.
      * @return array
      * @since Moodle 2.0
      * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more.
      * @see core_files_external::get_files()
      */
-    public static function get_files($contextid, $component, $filearea, $itemid, $filepath, $filename) {
-        return core_files_external::get_files($contextid, $component, $filearea, $itemid, $filepath, $filename);
+    public static function get_files($contextid, $component, $filearea, $itemid, $filepath, $filename, $modified = null,
+                                     $contextlevel = null, $instanceid = null) {
+        return core_files_external::get_files($contextid, $component, $filearea, $itemid, $filepath, $filename,
+            $modified, $contextlevel, $instanceid);
     }
 
     /**
@@ -425,6 +445,15 @@ class moodle_file_external extends external_api {
         return core_files_external::get_files_returns();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function get_files_is_deprecated() {
+        return true;
+    }
+
     /**
      * Returns description of upload parameters
      *
@@ -447,13 +476,16 @@ class moodle_file_external extends external_api {
      * @param string $filepath
      * @param string $filename
      * @param string $filecontent
+     * @param string $contextlevel Context level (block, course, coursecat, system, user or module)
+     * @param int    $instanceid   Instance id of the item associated with the context level
      * @return array
      * @since Moodle 2.0
      * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more.
      * @see core_files_external::upload()
      */
-    public static function upload($contextid, $component, $filearea, $itemid, $filepath, $filename, $filecontent) {
-        return core_files_external::upload($contextid, $component, $filearea, $itemid, $filepath, $filename, $filecontent);
+    public static function upload($contextid, $component, $filearea, $itemid, $filepath, $filename, $filecontent, $contextlevel, $instanceid) {
+        return core_files_external::upload($contextid, $component, $filearea, $itemid, $filepath, $filename,
+            $filecontent, $contextlevel, $instanceid);
     }
 
     /**
@@ -467,4 +499,14 @@ class moodle_file_external extends external_api {
     public static function upload_returns() {
         return core_files_external::upload_returns();
     }
+
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function upload_is_deprecated() {
+        return true;
+    }
+
 }
index 3922c64..b49ff72 100644 (file)
@@ -244,6 +244,8 @@ class core_files_externallib_testcase extends advanced_testcase {
         // Create a file from the string that we made earlier.
         $file = $fs->create_file_from_string($filerecord, $filecontent);
         $timemodified = $file->get_timemodified();
+        $timecreated = $file->get_timemodified();
+        $filesize = $file->get_filesize();
 
         // Use the web service function to return the information about the file that we just uploaded.
         // The first time is with a valid context ID.
@@ -293,7 +295,12 @@ class core_files_externallib_testcase extends advanced_testcase {
                                         'filename' => 'Simple4.txt',
                                         'url' => 'http://www.example.com/moodle/pluginfile.php/'.$context->id.'/mod_data/content/'.$itemid.'/Simple4.txt',
                                         'isdir' => false,
-                                        'timemodified' => $timemodified);
+                                        'timemodified' => $timemodified,
+                                        'timecreated' => $timecreated,
+                                        'filesize' => $filesize,
+                                        'author' => null,
+                                        'license' => null
+                                        );
         // Make sure that they are the same.
         $this->assertEquals($testdata, $testfilelisting);
 
index 2d6abb2..3726c95 100644 (file)
@@ -143,7 +143,7 @@ class filter_mathjaxloader extends moodle_text_filter {
             $text = str_replace('[tex]', '\\(', $text);
             $text = str_replace('[/tex]', '\\)', $text);
             // E.g. "$$ blah $$".
-            $text = preg_replace('|\$\$[\S\s]\$\$|u', '\\(\1\\)', $text);
+            $text = preg_replace('|\$\$([\S\s]*?)\$\$|u', '\\(\1\\)', $text);
             // E.g. "\[ blah \]".
             $text = str_replace('\\[', '\\(', $text);
             $text = str_replace('\\]', '\\)', $text);
index 138a9a1..65212dd 100644 (file)
@@ -196,6 +196,8 @@ class grade_export_form extends moodleform {
         $submitstring = get_string('download');
         if (empty($features['simpleui'])) {
             $submitstring = get_string('submit');
+        } else if (!empty($CFG->gradepublishing)) {
+            $submitstring = get_string('export', 'grades');
         }
 
         $this->add_action_buttons(false, $submitstring);
index f08b54c..7dd9a84 100644 (file)
@@ -383,16 +383,30 @@ abstract class grade_export {
             $itemidsparam = '-1';
         }
 
-        $params = array('id'                =>$this->course->id,
-                        'groupid'           =>$this->groupid,
-                        'itemids'           =>$itemidsparam,
-                        'export_letters'    =>$this->export_letters,
-                        'export_feedback'   =>$this->export_feedback,
-                        'updatedgradesonly' =>$this->updatedgradesonly,
-                        'displaytype'       =>$this->displaytype,
-                        'decimalpoints'     =>$this->decimalpoints,
-                        'export_onlyactive' =>$this->onlyactive,
-                        'usercustomfields'  =>$this->usercustomfields);
+        // We have a single grade display type constant.
+        if (!is_array($this->displaytype)) {
+            $displaytypes = $this->displaytype;
+        } else {
+            // Implode the grade display types array as moodle_url function doesn't accept arrays.
+            $displaytypes = implode(',', $this->displaytype);
+        }
+
+        if (!empty($this->updatedgradesonly)) {
+            $updatedgradesonly = $this->updatedgradesonly;
+        } else {
+            $updatedgradesonly = 0;
+        }
+        $params = array('id'                => $this->course->id,
+                        'groupid'           => $this->groupid,
+                        'itemids'           => $itemidsparam,
+                        'export_letters'    => $this->export_letters,
+                        'export_feedback'   => $this->export_feedback,
+                        'updatedgradesonly' => $updatedgradesonly,
+                        'decimalpoints'     => $this->decimalpoints,
+                        'export_onlyactive' => $this->onlyactive,
+                        'usercustomfields'  => $this->usercustomfields,
+                        'displaytype'       => $displaytypes,
+                        'key'               => $this->userkey);
 
         return $params;
     }
@@ -436,6 +450,151 @@ abstract class grade_export {
 
         return;
     }
+
+    /**
+     * Generate the export url.
+     *
+     * Get submitted form data and create the url to be used on the grade publish feature.
+     *
+     * @return moodle_url the url of grade publishing export.
+     */
+    public function get_export_url() {
+        return new moodle_url('/grade/export/'.$this->plugin.'/dump.php', $this->get_export_params());
+    }
+
+    /**
+     * Convert the grade display types parameter into the required array to grade exporting class.
+     *
+     * In order to export, the array key must be the display type name and the value must be the grade display type
+     * constant.
+     *
+     * Note: Added support for combined display types constants like the (GRADE_DISPLAY_TYPE_PERCENTAGE_REAL) as
+     *       the $CFG->grade_export_displaytype config is still used on 2.7 in case of missing displaytype url param.
+     *       In these cases, the file will be exported with a column for each display type.
+     *
+     * @param string $displaytypes can be a single or multiple display type constants comma separated.
+     * @return array $types
+     */
+    public static function convert_flat_displaytypes_to_array($displaytypes) {
+        $types = array();
+
+        // We have a single grade display type constant.
+        if (is_int($displaytypes)) {
+            $displaytype = clean_param($displaytypes, PARAM_INT);
+
+            // Let's set a default value, will be replaced below by the grade display type constant.
+            $display[$displaytype] = 1;
+        } else {
+            // Multiple grade display types constants.
+            $display = array_flip(explode(',', $displaytypes));
+        }
+
+        // Now, create the array in the required format by grade exporting class.
+        foreach ($display as $type => $value) {
+            $type = clean_param($type, PARAM_INT);
+            if ($type == GRADE_DISPLAY_TYPE_LETTER) {
+                $types['letter'] = GRADE_DISPLAY_TYPE_LETTER;
+            } else if ($type == GRADE_DISPLAY_TYPE_PERCENTAGE) {
+                $types['percentage'] = GRADE_DISPLAY_TYPE_PERCENTAGE;
+            } else if ($type == GRADE_DISPLAY_TYPE_REAL) {
+                $types['real'] = GRADE_DISPLAY_TYPE_REAL;
+            } else if ($type == GRADE_DISPLAY_TYPE_REAL_PERCENTAGE) {
+                $types['real'] = GRADE_DISPLAY_TYPE_REAL;
+                $types['percentage'] = GRADE_DISPLAY_TYPE_PERCENTAGE;
+            } else if ($type == GRADE_DISPLAY_TYPE_REAL_LETTER) {
+                $types['real'] = GRADE_DISPLAY_TYPE_REAL;
+                $types['letter'] = GRADE_DISPLAY_TYPE_LETTER;
+            } else if ($type == GRADE_DISPLAY_TYPE_LETTER_REAL) {
+                $types['letter'] = GRADE_DISPLAY_TYPE_LETTER;
+                $types['real'] = GRADE_DISPLAY_TYPE_REAL;
+            } else if ($type == GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE) {
+                $types['letter'] = GRADE_DISPLAY_TYPE_LETTER;
+                $types['percentage'] = GRADE_DISPLAY_TYPE_PERCENTAGE;
+            } else if ($type == GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER) {
+                $types['percentage'] = GRADE_DISPLAY_TYPE_PERCENTAGE;
+                $types['letter'] = GRADE_DISPLAY_TYPE_LETTER;
+            } else if ($type == GRADE_DISPLAY_TYPE_PERCENTAGE_REAL) {
+                $types['percentage'] = GRADE_DISPLAY_TYPE_PERCENTAGE;
+                $types['real'] = GRADE_DISPLAY_TYPE_REAL;
+            }
+        }
+        return $types;
+    }
+
+    /**
+     * Convert the item ids parameter into the required array to grade exporting class.
+     *
+     * In order to export, the array key must be the grade item id and all values must be one.
+     *
+     * @param string $itemids can be a single item id or many item ids comma separated.
+     * @return array $items correctly formatted array.
+     */
+    public static function convert_flat_itemids_to_array($itemids) {
+        $items = array();
+
+        // We just have one single item id.
+        if (is_int($itemids)) {
+            $itemid = clean_param($itemids, PARAM_INT);
+            $items[$itemid] = 1;
+        } else {
+            // Few grade items.
+            $items = array_flip(explode(',', $itemids));
+            foreach ($items as $itemid => $value) {
+                $itemid = clean_param($itemid, PARAM_INT);
+                $items[$itemid] = 1;
+            }
+        }
+        return $items;
+    }
+
+    /**
+     * Create the html code of the grade publishing feature.
+     *
+     * @return string $output html code of the grade publishing.
+     */
+    public function get_grade_publishing_url() {
+        $url = $this->get_export_url();
+        $output =  html_writer::start_div();
+        $output .= html_writer::tag('p', get_string('gradepublishinglink', 'grades', html_writer::link($url, $url)));
+        $output .=  html_writer::end_div();
+        return $output;
+    }
+
+    /**
+     * Create a stdClass object from URL parameters to be used by grade_export class.
+     *
+     * @param int $id course id.
+     * @param string $itemids grade items comma separated.
+     * @param bool $exportfeedback export feedback option.
+     * @param bool $onlyactive only enrolled active students.
+     * @param string $displaytype grade display type constants comma separated.
+     * @param int $decimalpoints grade decimal points.
+     * @param null $updatedgradesonly recently updated grades only (Used by XML exporting only).
+     * @param null $separator separator character: tab, comma, colon and semicolon (Used by TXT exporting only).
+     *
+     * @return stdClass $formdata
+     */
+    public static function export_bulk_export_data($id, $itemids, $exportfeedback, $onlyactive, $displaytype,
+                                                   $decimalpoints, $updatedgradesonly = null, $separator = null) {
+
+        $formdata = new \stdClass();
+        $formdata->id = $id;
+        $formdata->itemids = self::convert_flat_itemids_to_array($itemids);
+        $formdata->exportfeedback = $exportfeedback;
+        $formdata->export_onlyactive = $onlyactive;
+        $formdata->display = self::convert_flat_displaytypes_to_array($displaytype);
+        $formdata->decimals = $decimalpoints;
+
+        if (!empty($updatedgradesonly)) {
+            $formdata->updatedgradesonly = $updatedgradesonly;
+        }
+
+        if (!empty($separator)) {
+            $formdata->separator = $separator;
+        }
+
+        return $formdata;
+    }
 }
 
 /**
index 0310ef8..c2e7a34 100644 (file)
 
 define('NO_MOODLE_COOKIES', true); // session not used here
 require_once '../../../config.php';
+require_once($CFG->dirroot.'/grade/export/ods/grade_export_ods.php');
+
+$id                 = required_param('id', PARAM_INT);
+$groupid            = optional_param('groupid', 0, PARAM_INT);
+$itemids            = required_param('itemids', PARAM_RAW);
+$exportfeedback     = optional_param('export_feedback', 0, PARAM_BOOL);
+$displaytype        = optional_param('displaytype', $CFG->grade_export_displaytype, PARAM_RAW);
+$decimalpoints      = optional_param('decimalpoints', $CFG->grade_export_decimalpoints, PARAM_INT);
+$onlyactive         = optional_param('export_onlyactive', 0, PARAM_BOOL);
 
-$id = required_param('id', PARAM_INT); // course id
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
     print_error('nocourseid');
 }
@@ -30,9 +38,19 @@ if (empty($CFG->gradepublishing)) {
 }
 
 $context = context_course::instance($id);
+require_capability('moodle/grade:export', $context);
+require_capability('gradeexport/ods:view', $context);
 require_capability('gradeexport/ods:publish', $context);
 
-// use the same page parameters as export.php and append &key=sdhakjsahdksahdkjsahksadjksahdkjsadhksa
-require 'export.php';
+if (!groups_group_visible($groupid, $COURSE)) {
+    print_error('cannotaccessgroup', 'grades');
+}
+
+// Get all url parameters and create an object to simulate a form submission.
+$formdata = grade_export::export_bulk_export_data($id, $itemids, $exportfeedback, $onlyactive, $displaytype,
+        $decimalpoints);
+
+$export = new grade_export_ods($course, $groupid, $formdata);
+$export->print_grades();
 
 
index 827bd67..031f568 100644 (file)
@@ -20,6 +20,7 @@ require_once $CFG->dirroot.'/grade/export/lib.php';
 require_once 'grade_export_ods.php';
 
 $id                = required_param('id', PARAM_INT); // course id
+$PAGE->set_url('/grade/export/ods/export.php', array('id'=>$id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
     print_error('nocourseid');
@@ -32,6 +33,13 @@ $groupid = groups_get_course_group($course, true);
 require_capability('moodle/grade:export', $context);
 require_capability('gradeexport/ods:view', $context);
 
+// We need to call this method here before any output otherwise the menu won't display.
+// If you use this method without this check, will break the direct grade exporting (without publishing).
+$key = optional_param('key', '', PARAM_RAW);
+if (!empty($CFG->gradepublishing) && !empty($key)) {
+    print_grade_page_head($COURSE->id, 'export', 'ods', get_string('exportto', 'grades') . ' ' . get_string('pluginname', 'gradeexport_ods'));
+}
+
 if (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) {
     if (!groups_is_member($groupid, $USER->id)) {
         print_error('cannotaccessgroup', 'grades');
@@ -39,9 +47,13 @@ if (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('
 }
 $mform = new grade_export_form(null, array('publishing' => true, 'simpleui' => true, 'multipledisplaytypes' => true));
 $data = $mform->get_data();
-
-// print all the exported data here
 $export = new grade_export_ods($course, $groupid, $data);
-$export->print_grades();
-
 
+// If the gradepublishing is enabled and user key is selected print the grade publishing link.
+if (!empty($CFG->gradepublishing) && !empty($key)) {
+    groups_print_course_menu($course, 'index.php?id='.$id);
+    echo $export->get_grade_publishing_url();
+    echo $OUTPUT->footer();
+} else {
+    $export->print_grades();
+}
index 787a28c..5933954 100644 (file)
 
 define('NO_MOODLE_COOKIES', true); // session not used here
 require_once '../../../config.php';
+require_once($CFG->dirroot.'/grade/export/txt/grade_export_txt.php');
+
+$id                 = required_param('id', PARAM_INT);
+$groupid            = optional_param('groupid', 0, PARAM_INT);
+$itemids            = required_param('itemids', PARAM_RAW);
+$exportfeedback     = optional_param('export_feedback', 0, PARAM_BOOL);
+$separator          = optional_param('separator', 'comma', PARAM_ALPHA);
+$displaytype        = optional_param('displaytype', $CFG->grade_export_displaytype, PARAM_RAW);
+$decimalpoints      = optional_param('decimalpoints', $CFG->grade_export_decimalpoints, PARAM_INT);
+$onlyactive         = optional_param('export_onlyactive', 0, PARAM_BOOL);
 
-$id = required_param('id', PARAM_INT); // course id
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
     print_error('nocourseid');
 }
@@ -30,9 +39,19 @@ if (empty($CFG->gradepublishing)) {
 }
 
 $context = context_course::instance($id);
+require_capability('moodle/grade:export', $context);
 require_capability('gradeexport/txt:publish', $context);
+require_capability('gradeexport/txt:view', $context);
+
+if (!groups_group_visible($groupid, $COURSE)) {
+    print_error('cannotaccessgroup', 'grades');
+}
+
+// Get all url parameters and create an object to simulate a form submission.
+$formdata = grade_export::export_bulk_export_data($id, $itemids, $exportfeedback, $onlyactive, $displaytype,
+        $decimalpoints, null, $separator);
 
-// use the same page parameters as export.php and append &key=sdhakjsahdksahdkjsahksadjksahdkjsadhksa
-require 'export.php';
+$export = new grade_export_txt($course, $groupid, $formdata);
+$export->print_grades();
 
 
index bb3a776..d150e1a 100644 (file)
@@ -20,6 +20,7 @@ require_once $CFG->dirroot.'/grade/export/lib.php';
 require_once 'grade_export_txt.php';
 
 $id                = required_param('id', PARAM_INT); // course id
+$PAGE->set_url('/grade/export/txt/export.php', array('id'=>$id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
     print_error('nocourseid');
@@ -32,6 +33,13 @@ $groupid = groups_get_course_group($course, true);
 require_capability('moodle/grade:export', $context);
 require_capability('gradeexport/txt:view', $context);
 
+// We need to call this method here before any print otherwise the menu won't display.
+// If you use this method without this check, will break the direct grade exporting (without publishing).
+$key = optional_param('key', '', PARAM_RAW);
+if (!empty($CFG->gradepublishing) && !empty($key)) {
+    print_grade_page_head($COURSE->id, 'export', 'txt', get_string('exportto', 'grades') . ' ' . get_string('pluginname', 'gradeexport_txt'));
+}
+
 if (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) {
     if (!groups_is_member($groupid, $USER->id)) {
         print_error('cannotaccessgroup', 'grades');
@@ -46,8 +54,13 @@ $params = array(
 );
 $mform = new grade_export_form(null, $params);
 $data = $mform->get_data();
-
-// Print all the exported data here.
 $export = new grade_export_txt($course, $groupid, $data);
-$export->print_grades();
 
+// If the gradepublishing is enabled and user key is selected print the grade publishing link.
+if (!empty($CFG->gradepublishing) && !empty($key)) {
+    groups_print_course_menu($course, 'index.php?id='.$id);
+    echo $export->get_grade_publishing_url();
+    echo $OUTPUT->footer();
+} else {
+    $export->print_grades();
+}
index 654eb68..287c513 100644 (file)
 
 define('NO_MOODLE_COOKIES', true); // session not used here
 require_once '../../../config.php';
+require_once($CFG->dirroot.'/grade/export/xls/grade_export_xls.php');
+
+$id                 = required_param('id', PARAM_INT);
+$groupid            = optional_param('groupid', 0, PARAM_INT);
+$itemids            = required_param('itemids', PARAM_RAW);
+$exportfeedback     = optional_param('export_feedback', 0, PARAM_BOOL);
+$displaytype        = optional_param('displaytype', $CFG->grade_export_displaytype, PARAM_RAW);
+$decimalpoints      = optional_param('decimalpoints', $CFG->grade_export_decimalpoints, PARAM_INT);
+$onlyactive         = optional_param('export_onlyactive', 0, PARAM_BOOL);
 
-$id = required_param('id', PARAM_INT); // course id
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
     print_error('nocourseid');
 }
@@ -30,9 +38,19 @@ if (empty($CFG->gradepublishing)) {
 }
 
 $context = context_course::instance($id);
+require_capability('moodle/grade:export', $context);
+require_capability('gradeexport/xls:view', $context);
 require_capability('gradeexport/xls:publish', $context);
 
-// use the same page parameters as export.php and append &key=sdhakjsahdksahdkjsahksadjksahdkjsadhksa
-require 'export.php';
+if (!groups_group_visible($groupid, $COURSE)) {
+    print_error('cannotaccessgroup', 'grades');
+}
+
+// Get all url parameters and create an object to simulate a form submission.
+$formdata = grade_export::export_bulk_export_data($id, $itemids, $exportfeedback, $onlyactive, $displaytype,
+        $decimalpoints);
+
+$export = new grade_export_xls($course, $groupid, $formdata);
+$export->print_grades();
 
 
index 409ce48..a844172 100644 (file)
@@ -20,6 +20,7 @@ require_once $CFG->dirroot.'/grade/export/lib.php';
 require_once 'grade_export_xls.php';
 
 $id                = required_param('id', PARAM_INT); // course id
+$PAGE->set_url('/grade/export/xls/export.php', array('id'=>$id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
     print_error('nocourseid');
@@ -32,6 +33,13 @@ $groupid = groups_get_course_group($course, true);
 require_capability('moodle/grade:export', $context);
 require_capability('gradeexport/xls:view', $context);
 
+// We need to call this method here before any print otherwise the menu won't display.
+// If you use this method without this check, will break the direct grade exporting (without publishing).
+$key = optional_param('key', '', PARAM_RAW);
+if (!empty($CFG->gradepublishing) && !empty($key)) {
+    print_grade_page_head($COURSE->id, 'export', 'xls', get_string('exportto', 'grades') . ' ' . get_string('pluginname', 'gradeexport_xls'));
+}
+
 if (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) {
     if (!groups_is_member($groupid, $USER->id)) {
         print_error('cannotaccessgroup', 'grades');
@@ -39,9 +47,14 @@ if (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('
 }
 $mform = new grade_export_form(null, array('publishing' => true, 'simpleui' => true, 'multipledisplaytypes' => true));
 $formdata = $mform->get_data();
-
-// print all the exported data here
 $export = new grade_export_xls($course, $groupid, $formdata);
-$export->print_grades();
 
+// If the gradepublishing is enabled and user key is selected print the grade publishing link.
+if (!empty($CFG->gradepublishing) && !empty($key)) {
+    groups_print_course_menu($course, 'index.php?id='.$id);
+    echo $export->get_grade_publishing_url();
+    echo $OUTPUT->footer();
+} else {
+    $export->print_grades();
+}
 
index a640be8..a8ed187 100644 (file)
 
 define('NO_MOODLE_COOKIES', true); // session not used here
 require_once '../../../config.php';
+require_once($CFG->dirroot.'/grade/export/xml/grade_export_xml.php');
+
+$id                 = required_param('id', PARAM_INT);
+$groupid            = optional_param('groupid', 0, PARAM_INT);
+$itemids            = required_param('itemids', PARAM_RAW);
+$exportfeedback     = optional_param('export_feedback', 0, PARAM_BOOL);
+$updatedgradesonly  = optional_param('updatedgradesonly', false, PARAM_BOOL);
+$displaytype        = optional_param('displaytype', $CFG->grade_export_displaytype, PARAM_RAW);
+$decimalpoints      = optional_param('decimalpoints', $CFG->grade_export_decimalpoints, PARAM_INT);
+$onlyactive         = optional_param('export_onlyactive', 0, PARAM_BOOL);
 
-$id = required_param('id', PARAM_INT); // course id
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
     print_error('nocourseid');
 }
@@ -30,9 +39,19 @@ if (empty($CFG->gradepublishing)) {
 }
 
 $context = context_course::instance($id);
+require_capability('moodle/grade:export', $context);
 require_capability('gradeexport/xml:publish', $context);
+require_capability('gradeexport/xml:view', $context);
+
+if (!groups_group_visible($groupid, $COURSE)) {
+    print_error('cannotaccessgroup', 'grades');
+}
+
+// Get all url parameters and create an object to simulate a form submission.
+$formdata = grade_export::export_bulk_export_data($id, $itemids, $exportfeedback, $onlyactive, $displaytype,
+        $decimalpoints, $updatedgradesonly, null);
 
-// use the same page parameters as export.php and append &key=sdhakjsahdksahdkjsahksadjksahdkjsadhksa
-require 'export.php';
+$export = new grade_export_xml($course, $groupid, $formdata);
+$export->print_grades();
 
 
index b138f06..a2f1bb3 100644 (file)
@@ -20,6 +20,7 @@ require_once $CFG->dirroot.'/grade/export/lib.php';
 require_once 'grade_export_xml.php';
 
 $id                = required_param('id', PARAM_INT); // course id
+$PAGE->set_url('/grade/export/xml/export.php', array('id'=>$id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
     print_error('nocourseid');
@@ -32,17 +33,30 @@ $groupid = groups_get_course_group($course, true);
 require_capability('moodle/grade:export', $context);
 require_capability('gradeexport/xml:view', $context);
 
+// We need to call this method here before any print otherwise the menu won't display.
+// If you use this method without this check, will break the direct grade exporting (without publishing).
+$key = optional_param('key', '', PARAM_RAW);
+if (!empty($CFG->gradepublishing) && !empty($key)) {
+    print_grade_page_head($COURSE->id, 'export', 'xml', get_string('exportto', 'grades') . ' ' . get_string('pluginname', 'gradeexport_xml'));
+}
+
 if (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) {
     if (!groups_is_member($groupid, $USER->id)) {
         print_error('cannotaccessgroup', 'grades');
     }
 }
 $mform = new grade_export_form(null, array('publishing' => true, 'simpleui' => true, 'multipledisplaytypes' => false,
-        'idnumberrequired' => true));
+        'idnumberrequired' => true, 'updategradesonly' => true));
 $formdata = $mform->get_data();
-
-// print all the exported data here
 $export = new grade_export_xml($course, $groupid, $formdata);
-$export->print_grades();
+
+// If the gradepublishing is enabled and user key is selected print the grade publishing link.
+if (!empty($CFG->gradepublishing) && !empty($key)) {
+    groups_print_course_menu($course, 'index.php?id='.$id);
+    echo $export->get_grade_publishing_url();
+    echo $OUTPUT->footer();
+} else {
+    $export->print_grades();
+}
 
 
index ef5e698..98deb9b 100644 (file)
@@ -41,11 +41,19 @@ class core_grade_external extends external_api {
     }
 
     public static function get_definitions($cmids, $areaname, $activeonly = false) {
-        return core_grading_external::get_definitions($cmids, $areaname, $activeonly = false);
+        return core_grading_external::get_definitions($cmids, $areaname, $activeonly);
     }
 
     public static function get_definitions_returns() {
         return core_grading_external::get_definitions_returns();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function get_definitions_is_deprecated() {
+        return true;
+    }
 }
index 3470a69..7a6d09b 100644 (file)
@@ -617,7 +617,7 @@ function grade_print_tabs($active_type, $active_plugin, $plugin_info, $return=fa
     $top_row  = array();
     $bottom_row = array();
     $inactive = array($active_plugin);
-    $activated = array();
+    $activated = array($active_type);
 
     $count = 0;
     $active = '';
@@ -661,9 +661,9 @@ function grade_print_tabs($active_type, $active_plugin, $plugin_info, $return=fa
     $tabs[] = $bottom_row;
 
     if ($return) {
-        return print_tabs($tabs, $active_type, $inactive, $activated, true);
+        return print_tabs($tabs, $active_plugin, $inactive, $activated, true);
     } else {
-        print_tabs($tabs, $active_type, $inactive, $activated);
+        print_tabs($tabs, $active_plugin, $inactive, $activated);
     }
 }
 
index 5286ba3..67d8677 100644 (file)
@@ -33,6 +33,7 @@ $string['grader:manage'] = 'Manage the grader report';
 $string['grader:view'] = 'View the grader report';
 $string['pluginname'] = 'Grader report';
 $string['preferences'] = 'Grader report preferences';
+$string['summarygrader'] = 'A table with the names of students in the first column, with assessable activities grouped by course and category across the top.';
 $string['useractivitygrade'] = '{$a} grade';
 $string['useractivityfeedback'] = '{$a} feedback';
 $string['overriddengrade'] = 'Overridden grade';
index bec5d0a..daaeddc 100644 (file)
@@ -412,8 +412,14 @@ class grade_report_grader extends grade_report {
         // Limit to users with a gradeable role.
         list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
 
+        // Check the status of showing only active enrolments.
+        $coursecontext = $this->context->get_course_context(true);
+        $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
+        $showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol);
+        $showonlyactiveenrol = $showonlyactiveenrol || !has_capability('moodle/course:viewsuspendedusers', $coursecontext);
+
         // Limit to users with an active enrollment.
-        list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context);
+        list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context, '', 0, $showonlyactiveenrol);
 
         // Fields we need from the user table.
         $userfields = user_picture::fields('u', get_extra_user_fields($this->context));
@@ -476,30 +482,29 @@ class grade_report_grader extends grade_report {
             $this->userselect = "AND g.userid $usql";
             $this->userselect_params = $uparams;
 
-            // Add a flag to each user indicating whether their enrolment is active.
-            $sql = "SELECT ue.userid
-                      FROM {user_enrolments} ue
-                      JOIN {enrol} e ON e.id = ue.enrolid
-                     WHERE ue.userid $usql
-                           AND ue.status = :uestatus
-                           AND e.status = :estatus
-                           AND e.courseid = :courseid
-                           AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)
-                  GROUP BY ue.userid";
-            $coursecontext = $this->context->get_course_context(true);
-            $time = time();
-            $params = array_merge($uparams, array('estatus' => ENROL_INSTANCE_ENABLED, 'uestatus' => ENROL_USER_ACTIVE,
-                    'courseid' => $coursecontext->instanceid, 'now1' => $time, 'now2' => $time));
-            $useractiveenrolments = $DB->get_records_sql($sql, $params);
-
-            $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
-            $showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol);
-            $showonlyactiveenrol = $showonlyactiveenrol || !has_capability('moodle/course:viewsuspendedusers', $coursecontext);
+            // First flag everyone as not suspended.
             foreach ($this->users as $user) {
-                // If we are showing only active enrolments, then remove suspended users from list.
-                if ($showonlyactiveenrol && !array_key_exists($user->id, $useractiveenrolments)) {
-                    unset($this->users[$user->id]);
-                } else {
+                $this->users[$user->id]->suspendedenrolment = false;
+            }
+
+            // If we want to mix both suspended and not suspended users, let's find out who is suspended.
+            if (!$showonlyactiveenrol) {
+                $sql = "SELECT ue.userid
+                          FROM {user_enrolments} ue
+                          JOIN {enrol} e ON e.id = ue.enrolid
+                         WHERE ue.userid $usql
+                               AND ue.status = :uestatus
+                               AND e.status = :estatus
+                               AND e.courseid = :courseid
+                               AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)
+                      GROUP BY ue.userid";
+
+                $time = time();
+                $params = array_merge($uparams, array('estatus' => ENROL_INSTANCE_ENABLED, 'uestatus' => ENROL_USER_ACTIVE,
+                        'courseid' => $coursecontext->instanceid, 'now1' => $time, 'now2' => $time));
+                $useractiveenrolments = $DB->get_records_sql($sql, $params);
+
+                foreach ($this->users as $user) {
                     $this->users[$user->id]->suspendedenrolment = !array_key_exists($user->id, $useractiveenrolments);
                 }
             }
@@ -600,12 +605,18 @@ class grade_report_grader extends grade_report {
 
         $levels = count($this->gtree->levels) - 1;
 
-        for ($i = 0; $i < $levels; $i++) {
-            $fillercell = new html_table_cell();
-            $fillercell->attributes['class'] = 'fixedcolumn cell topleft';
-            $fillercell->text = ' ';
-            $fillercell->colspan = $colspan;
-            $row = new html_table_row(array($fillercell));
+        $fillercell = new html_table_cell();
+        $fillercell->header = true;
+        $fillercell->attributes['scope'] = 'col';
+        $fillercell->attributes['class'] = 'cell topleft';
+        $fillercell->text = html_writer::span(get_string('participants'), 'accesshide');
+        $fillercell->colspan = $colspan;
+        $fillercell->rowspan = $levels;
+        $row = new html_table_row(array($fillercell));
+        $rows[] = $row;
+
+        for ($i = 1; $i < $levels; $i++) {
+            $row = new html_table_row();
             $rows[] = $row;
         }
 
@@ -644,13 +655,13 @@ class grade_report_grader extends grade_report {
             $userrow->id = 'fixed_user_'.$userid;
 
             $usercell = new html_table_cell();
-            $usercell->attributes['class'] = 'user';
+            $usercell->attributes['class'] = 'header user';
 
             $usercell->header = true;
             $usercell->scope = 'row';
 
             if ($showuserimage) {
-                $usercell->text = $OUTPUT->user_picture($user);
+                $usercell->text = $OUTPUT->user_picture($user, array('visibletoscreenreaders' => false));
             }
 
             $fullname = fullname($user);
@@ -673,7 +684,7 @@ class grade_report_grader extends grade_report {
 
             $userreportcell = new html_table_cell();
             $userreportcell->attributes['class'] = 'userreport';
-            $userreportcell->header = true;
+            $userreportcell->header = false;
             if (has_capability('gradereport/'.$CFG->grade_profilereport.':view', $this->context)) {
                 $a = new stdClass();
                 $a->user = $fullname;
@@ -694,9 +705,8 @@ class grade_report_grader extends grade_report {
 
             foreach ($extrafields as $field) {
                 $fieldcell = new html_table_cell();
-                $fieldcell->attributes['class'] = 'header userfield user' . $field;
-                $fieldcell->header = true;
-                $fieldcell->scope = 'row';
+                $fieldcell->attributes['class'] = 'userfield user' . $field;
+                $fieldcell->header = false;
                 $fieldcell->text = $user->{$field};
                 $userrow->cells[] = $fieldcell;
             }
@@ -785,16 +795,16 @@ class grade_report_grader extends grade_report {
                     $fillercell->attributes['class'] = $type . ' ' . $catlevel;
                     $fillercell->colspan = $colspan;
                     $fillercell->text = '&nbsp;';
-                    $fillercell->header = true;
-                    $fillercell->scope = 'col';
+
+                    // This is a filler cell; don't use a <th>, it'll confuse screen readers.
+                    $fillercell->header = false;
                     $headingrow->cells[] = $fillercell;
                 } else if ($type == 'category') {
                     // Element is a category
                     $categorycell = new html_table_cell();
                     $categorycell->attributes['class'] = 'category ' . $catlevel;
                     $categorycell->colspan = $colspan;
-                    $categorycell->text = shorten_text($element['object']->get_name());
-                    $categorycell->text .= $this->get_collapsing_icon($element);
+                    $categorycell->text = $this->get_course_header($element);
                     $categorycell->header = true;
                     $categorycell->scope = 'col';
 
@@ -832,8 +842,10 @@ class grade_report_grader extends grade_report {
                             'id' => $this->course->id,
                             'item' => 'grade',
                             'itemid' => $element['object']->id));
-                        $singleview = $OUTPUT->action_icon($url, new pix_icon('t/editstring', get_string('singleview', 'grades',
-                                $element['object']->get_name())));
+                        $singleview = $OUTPUT->action_icon(
+                            $url,
+                            new pix_icon('t/editstring', get_string('singleview', 'grades', $element['object']->itemname))
+                        );
                     }
 
                     $itemcell->colspan = $colspan;
@@ -1154,6 +1166,7 @@ class grade_report_grader extends grade_report {
         $fulltable = new html_table();
         $fulltable->attributes['class'] = 'gradereport-grader-table';
         $fulltable->id = 'user-grades';
+        $fulltable->summary = get_string('summarygrader', 'gradereport_grader');
 
         // Extract rows from each side (left and right) and collate them into one row each
         foreach ($leftrows as $key => $row) {
@@ -1495,6 +1508,49 @@ class grade_report_grader extends grade_report {
         return $rows;
     }
 
+    /**
+     * Given element category, create a collapsible icon and
+     * course header.
+     *
+     * @param array $element
+     * @return string HTML
+     */
+    protected function get_course_header($element) {
+        global $OUTPUT;
+
+        $icon = '';
+        // If object is a category, display expand/contract icon.
+        if ($element['type'] == 'category') {
+            // Load language strings.
+            $strswitchminus = $this->get_lang_string('aggregatesonly', 'grades');
+            $strswitchplus  = $this->get_lang_string('gradesonly', 'grades');
+            $strswitchwhole = $this->get_lang_string('fullmode', 'grades');
+
+            $url = new moodle_url($this->gpr->get_return_url(null, array('target' => $element['eid'], 'sesskey' => sesskey())));
+
+            if (in_array($element['object']->id, $this->collapsed['aggregatesonly'])) {
+                $url->param('action', 'switch_plus');
+                $icon = $OUTPUT->action_icon($url, new pix_icon('t/switch_plus', $strswitchplus), null, null);
+                $showing = get_string('showingaggregatesonly', 'grades');
+            } else if (in_array($element['object']->id, $this->collapsed['gradesonly'])) {
+                $url->param('action', 'switch_whole');
+                $icon = $OUTPUT->action_icon($url, new pix_icon('t/switch_whole', $strswitchwhole), null, null);
+                $showing = get_string('showinggradesonly', 'grades');
+            } else {
+                $url->param('action', 'switch_minus');
+                $icon = $OUTPUT->action_icon($url, new pix_icon('t/switch_minus', $strswitchminus), null, null);
+                $showing = get_string('showingfullmode', 'grades');
+            }
+        }
+
+        $name = shorten_text($element['object']->get_name());
+        $courseheader = html_writer::tag('span', $name, array('id' => 'courseheader'));
+        $courseheader .= html_writer::label($showing, 'courseheader', false, array('class' => 'accesshide'));
+        $courseheader .= $icon;
+
+        return $courseheader;
+    }
+
     /**
      * Given a grade_category, grade_item or grade_grade, this function
      * figures out the state of the object and builds then returns a div
@@ -1555,11 +1611,16 @@ class grade_report_grader extends grade_report {
 
     /**
      * Given a category element returns collapsing +/- icon if available
+     *
+     * @deprecated since Moodle 2.9 MDL-46662 - please do not use this function any more.
+     * @todo MDL-49021 This will be deleted in Moodle 3.1
+     * @see grade_report_grader::get_course_header()
      * @param object $element
      * @return string HTML
      */
     protected function get_collapsing_icon($element) {
         global $OUTPUT;
+        debugging('get_collapsing_icon is deprecated, please use get_course_header instead.', DEBUG_DEVELOPER);
 
         $icon = '';
         // If object is a category, display expand/contract icon
index 2905a9a..5fee778 100644 (file)
@@ -71,6 +71,14 @@ class grade extends tablelike implements selectable_items, filterable_items {
                 !($item->is_course_item() || $item->is_category_item());
     }
 
+    /**
+     * Get the label for the select box that chooses items for this page.
+     * @return string
+     */
+    public function select_label() {
+        return get_string('selectuser', 'gradereport_singleview');
+    }
+
     /**
      * Get the description of this page
      * @return string
@@ -278,7 +286,7 @@ class grade extends tablelike implements selectable_items, filterable_items {
      * @return string
      */
     public function heading() {
-        return $this->item->get_name();
+        return get_string('gradeitem', 'gradereport_singleview', $this->item->get_name());
     }
 
     /**
index f31c1d2..8a43bd5 100644 (file)
@@ -200,7 +200,7 @@ abstract class screen {
      * @return string
      */
     public function heading() {
-        return get_string('pluginname', 'gradereport_singleview');
+        return get_string('entrypage', 'gradereport_singleview');
     }
 
     /**
index c90d9ba..c900b33 100644 (file)
@@ -104,10 +104,11 @@ class select extends screen {
 
             $url = new moodle_url('/grade/report/singleview/index.php', $params);
 
-            $select = new \single_select($url, 'itemid', $options);
-            $select->set_label($screen->description());
+            $select = new \single_select($url, 'itemid', $options, '', array('' => $screen->select_label()));
+            $select->set_label($screen->select_label(), array('class'=>'accesshide'));
             $html .= $OUTPUT->render($select);
         }
+        $html = $OUTPUT->container($html, 'selectitems');
 
         if (empty($html)) {
             $OUTPUT->notification(get_string('noscreens', 'gradereport_singleview'));
index 84967fc..37f1ad3 100644 (file)
@@ -40,6 +40,12 @@ interface selectable_items {
      */
     public function description();
 
+    /**
+     * Get the label for the select box that chooses items for this page.
+     * @return string
+     */
+    public function select_label();
+
     /**
      * Get the list of options to show.
      * @return array
index 3b0a4af..ecb6f90 100644 (file)
@@ -183,7 +183,7 @@ abstract class tablelike extends screen {
 
         $underlying = get_class($this);
 
-        $data = new stdClass;
+        $data = new stdClass();
         $data->table = $table;
         $data->instance = $this;
 
@@ -191,14 +191,18 @@ abstract class tablelike extends screen {
         $buttonhtml = implode(' ', $this->buttons());
 
         $buttons = html_writer::tag('div', $buttonhtml, $buttonattr);
+        $selectview = new select($this->courseid, $this->itemid, $this->groupid);
 
         $sessionvalidation = html_writer::empty_tag('input',
             array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
 
-        return html_writer::tag('form',
+        $html = $selectview->html();
+        $html .= html_writer::tag('form',
             $buttons . html_writer::table($table) . $this->bulk_insert() . $buttons . $sessionvalidation,
             array('method' => 'POST')
         );
+        $html .= $selectview->html();
+        return $html;
     }
 
     /**
@@ -222,7 +226,7 @@ abstract class tablelike extends screen {
     public function buttons() {
         $save = html_writer::empty_tag('input', array(
             'type' => 'submit',
-            'value' => get_string('update'),
+            'value' => get_string('save', 'gradereport_singleview'),
         ));
 
         return array($save);
index 6993237..c5490b8 100644 (file)
@@ -52,6 +52,14 @@ class user extends tablelike implements selectable_items {
     /** @var int $requirespaging Do we have more items than the paging limit? */
     private $requirespaging = true;
 
+    /**
+     * Get the label for the select box that chooses items for this page.
+     * @return string
+     */
+    public function select_label() {
+        return get_string('selectgrade', 'gradereport_singleview');
+    }
+
     /**
      * Get the description for the screen.
      *
@@ -83,15 +91,6 @@ class user extends tablelike implements selectable_items {
         return 'grade';
     }
 
-    /**
-     * Should we show the group selector on this screen?
-     *
-     * @return bool
-     */
-    public function display_group_selector() {
-        return false;
-    }
-
     /**
      * Init the screen
      *
@@ -103,9 +102,12 @@ class user extends tablelike implements selectable_items {
         if (!$selfitemisempty) {
             $validusers = $this->load_users();
             if (!isset($validusers[$this->itemid])) {
-                print_error('invaliduserid');
+                // If the passed user id is not valid, show the first user from the list instead.
+                $this->item = reset($validusers);
+                $this->itemid = $this->item->id;
+            } else {
+                $this->item = $validusers[$this->itemid];
             }
-            $this->item = $validusers[$this->itemid];
         }
 
         $params = array('courseid' => $this->courseid);
@@ -282,7 +284,7 @@ class user extends tablelike implements selectable_items {
      * @return string
      */
     public function heading() {
-        return fullname($this->item);
+        return get_string('gradeuser', 'gradereport_singleview', fullname($this->item));
     }
 
     /**
index 42e99bf..b1f5f7b 100644 (file)
@@ -42,6 +42,7 @@ $perpage = optional_param('perpage', 100, PARAM_INT);
 
 $courseparams = array('id' => $courseid);
 $PAGE->set_url(new moodle_url('/grade/report/singleview/index.php', $courseparams));
+$PAGE->set_pagelayout('incourse');
 
 if (!$course = $DB->get_record('course', $courseparams)) {
     print_error('nocourseid');
@@ -138,13 +139,13 @@ if (!empty($options)) {
         $navparams['itemid'] = $reloptionssorting[$i - 1];
         $link = new moodle_url('/grade/report/singleview/index.php', $navparams);
         $navprev = html_writer::link($link, $OUTPUT->larrow() . ' ' . $reloptions[$reloptionssorting[$i - 1]]);
-        $graderleftnav = html_writer::tag('small', $navprev, array('class' => 'itemnav previtem'));
+        $graderleftnav = html_writer::tag('div', $navprev, array('class' => 'itemnav previtem'));
     }
     if ($i < count($reloptionssorting) - 1) {
         $navparams['itemid'] = $reloptionssorting[$i + 1];
         $link = new moodle_url('/grade/report/singleview/index.php', $navparams);
         $navnext = html_writer::link($link, $reloptions[$reloptionssorting[$i + 1]] . ' ' . $OUTPUT->rarrow());
-        $graderrightnav = html_writer::tag('small', $navnext, array('class' => 'itemnav nextitem'));
+        $graderrightnav = html_writer::tag('div', $navnext, array('class' => 'itemnav nextitem'));
     }
 }
 
index 792cc90..1d685b3 100644 (file)
@@ -31,6 +31,7 @@ $string['bulkinsertvalue'] = 'Insert value';
 $string['bulklegend'] = 'Bulk insert';
 $string['bulkperform'] = 'Perform bulk insert';
 $string['bulkfor'] = 'Grades for {$a}';
+$string['entrypage'] = 'Grade user or grade item';
 $string['exclude'] = 'Exclude';
 $string['excludeall'] = 'Exclude all grades';
 $string['excludefor'] = 'Exclude for {$a}';
@@ -39,6 +40,8 @@ $string['eventgradereportviewed'] = 'Grade single view report viewed.';
 $string['feedbackfor'] = 'Feedback for {$a}';
 $string['filtergrades'] = 'Show grades for {$a}.';
 $string['gradefor'] = 'Grade for {$a}';
+$string['gradeitem'] = 'Grade item: {$a}';
+$string['gradeuser'] = 'Grade user: {$a}';
 $string['noscreens'] = 'Could not find a suitable single view screen.';
 $string['gradeitemcannotbeoverridden'] = 'This grade item cannot be overridden.';
 $string['notvalid'] = 'Not a valid Single view screen: {$a}';
@@ -48,7 +51,11 @@ $string['overridefor'] = 'Override for {$a}';
 $string['overridenone'] = 'Do not override any grades';
 $string['pluginname'] = 'Single view';
 $string['savegrades'] = 'Saving grades';
+$string['save'] = 'Save';
 $string['savegradessuccess'] = 'Grades were set for {$a} items';
+$string['selectgrade'] = 'Select grade item...';
+$string['selectuser'] = 'Select user...';
 $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.';
+$string['userselect'] = 'Select activity';
index b40711b..5ecb430 100644 (file)
@@ -107,7 +107,7 @@ class gradereport_singleview extends grade_report {
      */
     public function output() {
         global $OUTPUT;
-        return $OUTPUT->box($this->screen->html());
+        return $OUTPUT->container($this->screen->html(), 'reporttable');
     }
 }
 
index 888b1ce..0003f00 100644 (file)
@@ -1,17 +1,39 @@
-.path-grade-report-singleview div.generalbox {
-  margin: 0 20px 15px 20px;
+.path-grade-report-singleview div.reporttable {
   text-align: center;
 }
 
-.path-grade-report-singleview div.generalbox div.singleselect form div {
+.path-grade-report-singleview div.groupselector,
+.path-grade-report-singleview div.reporttable form div.singleview_buttons,
+.path-grade-report-singleview div.selectitems {
+    display: block;
+    text-align: right;
+    clear: both;
+}
+.dir-rtl.path-grade-report-singleview div.groupselector,
+.dir-rtl.path-grade-report-singleview div.reporttable form div.singleview_buttons,
+.dir-rtl.path-grade-report-singleview div.selectitems {
+    text-align: left;
+}
+
+.path-grade-report-singleview div.singleselect+div.singleselect select,
+.path-grade-report-singleview div.groupselector select {
+    margin-right: 0px;
+}
+dir-rtl.path-grade-report-singleview div.singleselect+div.singleselect select,
+dir-rtl.path-grade-report-singleview div.groupselector select {
+    margin-right: 10px;
+    margin-left: 0px;
+}
+
+.path-grade-report-singleview div.reporttable div.singleselect form div {
   text-align: center;
 }
 
-.path-grade-report-singleview div.generalbox table.generaltable {
+.path-grade-report-singleview div.reporttable table.reporttable {
   margin: 0 auto 15px auto;
 }
 
-.path-grade-report-singleview div.generalbox form div {
+.path-grade-report-singleview div.reporttable form div {
   text-align: center;
 }
 
     padding: 10px 0;
 }
 
-.path-grade-report-singleview div.generalbox h2 {
+.path-grade-report-singleview div.reporttable h2 {
    text-align: center;
 }
 
 .path-grade-report-singleview input[name^="finalgrade"] {
     width: 50px;
 }
-.path-grade-report-singleview .generaltable tbody th {
+.path-grade-report-singleview .reporttable tbody th,
+.path-grade-report-singleview .reporttable tbody td.range {
   white-space: nowrap;
 }
-.path-grade-report-singleview .generaltable tbody th > * {
+.path-grade-report-singleview .reporttable tbody th > * {
   display: inline-block;
   vertical-align: middle;
   margin: 0 2px;
 
 .path-grade-report-singleview #region-main h2, .paging{
    text-align: center;
+   clear: both;
 }
 
+.path-grade-report-singleview .itemnav {
+    font-size: small;
+    display: inline;
+    margin-bottom: 0.5em;
+}
 .path-grade-report-singleview itemnav.previtem {
    float:left;
 }
-.path-grade-report-singleview.dir-rtl small.previtem {
+.path-grade-report-singleview.dir-rtl div.previtem {
    float:right;
 }
-.path-grade-report-singleview small.nextitem {
+.path-grade-report-singleview div.nextitem {
    float:right;
 }
-.path-grade-report-singleview.dir-rtl small.nextitem {
+.path-grade-report-singleview.dir-rtl div.nextitem {
    float:left;
 }
-.path-grade-report-singleview .generaltable {
+.path-grade-report-singleview .reporttable {
     width: 100%;
 }
-.path-grade-report-singleview .generaltable th {
+.path-grade-report-singleview .reporttable th {
     text-align: left;
 }
 
-.dir-rtl.path-grade-report-singleview .generaltable th {
+.dir-rtl.path-grade-report-singleview .reporttable th {
   text-align: right;
 }
 
-.path-grade-report-singleview div.generalbox form div.singleview_bulk {
+.path-grade-report-singleview div.reporttable form div.singleview_bulk {
   display: inline-block;
   text-align: left;
   margin-bottom: 1em;
 }
-.dir-rtl.path-grade-report-singleview div.generalbox form div.singleview_bulk {
+.dir-rtl.path-grade-report-singleview div.reporttable form div.singleview_bulk {
   text-align: right;
 }
 
   margin: 0;
 }
 
+.path-grade-report-singleview .singleselect select,
+.path-grade-report-singleview div.reporttable form .singleview_bulk select,
+.path-grade-report-singleview div.reporttable form .singleview_bulk input {
+  margin-left: 10px;
+  margin-right: 10px;
+}
+
+
 .path-grade-report-singleview .singleview_bulk > fieldset {
   display: block;
 }
 
-.path-grade-report-singleview div.generalbox form .singleview_bulk > div.enable {
+.path-grade-report-singleview div.reporttable form .singleview_bulk > div.enable {
+  margin-bottom: 0.5em;
   text-align: left;
 }
-.dir-rtl.path-grade-report-singleview div.generalbox form .singleview_bulk > div.enable {
+.dir-rtl.path-grade-report-singleview div.reporttable form .singleview_bulk > div.enable {
   text-align: right;
-}
\ No newline at end of file
+}
index acd8ecc..143eeaf 100644 (file)
@@ -57,14 +57,14 @@ Feature: We can use Single view
         | Grade for Test assignment one | 10.00 |
         | Feedback for Test assignment one | test data |
     And I click on "Exclude for Test assignment four" "checkbox"
-    And I press "Update"
+    And I press "Save"
     Then I should see "Grades were set for 2 items"
     And I press "Continue"
     And the field "Exclude for Test assignment four" matches value "1"
     And the field "Grade for Test assignment one" matches value "10.00"
     And I set the following fields to these values:
         | Test grade item | 45 |
-    And I press "Update"
+    And I press "Save"
     Then I should see "Grades were set for 1 items"
     And I press "Continue"
     And the field "Grade for Test grade item" matches value "45.00"
@@ -75,7 +75,7 @@ Feature: We can use Single view
         | Grade for james (Student) 1 | 12.05 |
         | Feedback for james (Student) 1 | test data2 |
     And I click on "Exclude for holly (Student) 2" "checkbox"
-    And I press "Update"
+    And I press "Save"
     Then I should see "Grades were set for 2 items"
     And I press "Continue"
     And the field "Grade for james (Student) 1" matches value "12.05"
@@ -83,7 +83,7 @@ Feature: We can use Single view
     And I click on "Single view" "link"
     And I click on "new grade item 1" "option"
     And I click on "Very good" "option"
-    And I press "Update"
+    And I press "Save"
     Then I should see "Grades were set for 1 items"
     And I press "Continue"
     And the following should exist in the "generaltable" table:
@@ -104,7 +104,7 @@ Feature: We can use Single view
     When I click on "All grades" "option"
     And I set the field "Insert value" to "1.0"
     And I click on "Perform bulk insert" "checkbox"
-    And I press "Update"
+    And I press "Save"
     Then I should see "Grades were set for 9 items"
 
   Scenario: Navigation works in the Single view.
index 8c10d0b..e111f47 100644 (file)
@@ -1,5 +1,7 @@
 This files describes API changes in /grade/report/*,
 information provided here is intended especially for developers.
+=== 2.9 ===
+* Deprecating grade_report_grader:get_collapsing_icon.
 
 === 2.8.2 ===
 * gradereport_singleview::__construct doesn't need groupid parameter anymore, so it was renamed to $unused.
index 7836758..f02b748 100644 (file)
@@ -1205,6 +1205,15 @@ class moodle_group_external extends external_api {
         return core_group_external::create_groups_returns();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function create_groups_is_deprecated() {
+        return true;
+    }
+
     /**
      * Returns description of method parameters
      *
@@ -1242,6 +1251,15 @@ class moodle_group_external extends external_api {
         return core_group_external::get_groups_returns();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function get_groups_is_deprecated() {
+        return true;
+    }
+
     /**
      * Returns description of method parameters
      *
@@ -1279,6 +1297,15 @@ class moodle_group_external extends external_api {
         return core_group_external::get_course_groups_returns();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function get_course_groups_is_deprecated() {
+        return true;
+    }
+
     /**
      * Returns description of method parameters
      *
@@ -1288,7 +1315,7 @@ class moodle_group_external extends external_api {
      * @see core_group_external::delete_group_members_parameters()
      */
     public static function delete_groups_parameters() {
-        return core_group_external::delete_group_members_parameters();
+        return core_group_external::delete_groups_parameters();
     }
 
     /**
@@ -1312,9 +1339,17 @@ class moodle_group_external extends external_api {
      * @see core_group_external::delete_group_members_returns()
      */
     public static function delete_groups_returns() {
-        return core_group_external::delete_group_members_returns();
+        return core_group_external::delete_groups_returns();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function delete_groups_is_deprecated() {
+        return true;
+    }
 
     /**
      * Returns description of method parameters
@@ -1353,6 +1388,14 @@ class moodle_group_external extends external_api {
         return core_group_external::get_group_members_returns();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function get_groupmembers_is_deprecated() {
+        return true;
+    }
 
     /**
      * Returns description of method parameters
@@ -1390,6 +1433,14 @@ class moodle_group_external extends external_api {
         return core_group_external::add_group_members_returns();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function add_groupmembers_is_deprecated() {
+        return true;
+    }
 
     /**
      * Returns description of method parameters
@@ -1427,4 +1478,13 @@ class moodle_group_external extends external_api {
         return core_group_external::delete_group_members_returns();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function delete_groupmembers_is_deprecated() {
+        return true;
+    }
+
 }
index 5416beb..d23f400 100644 (file)
@@ -48,6 +48,7 @@ define('AJAX_SCRIPT', false); // prevents some warnings later
 define('CACHE_DISABLE_ALL', true); // Disables caching.. just in case.
 define('PHPUNIT_TEST', false);
 define('IGNORE_COMPONENT_CACHE', true);
+define('MDL_PERF_TEST', false);
 
 // Servers should define a default timezone in php.ini, but if they don't then make sure something is defined.
 // This is a quick hack.  Ideally we should ask the admin for a value.  See MDL-22625 for more on this.
index 9a14483..7061946 100644 (file)
@@ -71,9 +71,10 @@ $string['pathsunsecuredataroot'] = 'Dataroot-en kokapena ez da segurua';
 $string['pathswrongadmindir'] = 'Kudeaketa direktorioa ez da existitzen';
 $string['phpextension'] = '{$a} PHP luzapena';
 $string['phpversion'] = 'PHP bertsioa';
-$string['phpversionhelp'] = '<p>Moodle-k PHP 4.1.0 edo geroagoko bertsioa behar du.</p>
+$string['phpversionhelp'] = '<p>Moodle-k gutxienez PHP 4.3.0 edo 5.1.0 (5.0.x errore batzuk ditu). </p>
 <p>Zure bertsioa: {$a}</p>
-<p>PHP eguneratu edo PHP bertsio berriagoa duen zerbitzari batera jo</p>';
+<p>PHP eguneratu edo PHP bertsio berriagoa duen zerbitzari batera jo</p>
+<p>5.0.x baduzu, nahi izanez gero, 4.4.x. bertsiora jaitsi dezakezu</p>';
 $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
 $string['welcomep20'] = 'Orri hau ikusten baduzu <strong>{$a->packname} {$a->packversion}</strong> paketea
     zure ordenadorean instalatu ahal izan duzu. Zorionak!';
diff --git a/install/lang/fi_co/langconfig.php b/install/lang/fi_co/langconfig.php
new file mode 100644 (file)
index 0000000..6a36959
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['parentlanguage'] = 'fi';
+$string['thislanguage'] = 'Suomi yrityksille';
index 1bb1c96..dc07ef3 100644 (file)
@@ -35,7 +35,7 @@ $string['availablelangs'] = 'Dostupné jazykové balíčky';
 $string['chooselanguagehead'] = 'Vyberte jazyk';
 $string['chooselanguagesub'] = 'Zvoľte si jazyk pre inštaláciu. Tento jazyk bude tiež použitý ako východzí jazyk portálu, ale môže byť neskôr zmenený.';
 $string['clialreadyinstalled'] = 'Súbor config.php už existuje. Použite admin/cli/upgrade.php ak chcete aktualizovať váš portál.';
-$string['cliinstallheader'] = 'Mooodle {$a} inštalačný program z príkazového riadku';
+$string['cliinstallheader'] = 'Moodle {$a} inštalačný program z príkazového riadku';
 $string['databasehost'] = 'Databázový server';
 $string['databasename'] = 'Názov databázy';
 $string['databasetypehead'] = 'Vyberte driver pre databázu';
index 0d0180b..24e76d7 100644 (file)
@@ -39,4 +39,4 @@ $string['clitypevaluedefault'] = 'тип значення, натисніть En
 $string['cliunknowoption'] = 'Невизначені опції: {$a}. Будь ласка, використайте опцію --help.';
 $string['cliyesnoprompt'] = 'натисніть y (означає так) або n (означає ні)';
 $string['environmentrequireinstall'] = 'повинен бути встановлений і включений';
-$string['environmentrequireversion'] = 'рекомендується версія {$a->needed}, використовується версія {$a->current}';
+$string['environmentrequireversion'] = 'потрібна версія {$a->needed}, ви запускаєте {$a->current}';
index 23da526..8feb0f2 100644 (file)
@@ -60,6 +60,7 @@ $string['enrolnotpermitted'] = 'You do not have permission or are not allowed to
 $string['enrolperiod'] = 'Enrolment duration';
 $string['enrolusage'] = 'Instances / enrolments';
 $string['enrolusers'] = 'Enrol users';
+$string['enrolxusers'] = 'Enrol {$a} users';
 $string['enroltimecreated'] = 'Enrolment created';
 $string['enroltimeend'] = 'Enrolment ends';
 $string['enroltimestart'] = 'Enrolment starts';
@@ -81,6 +82,7 @@ $string['expirynotifyhour'] = 'Hour to send enrolment expiry notifications';
 $string['expirythreshold'] = 'Notification threshold';
 $string['expirythreshold_help'] = 'How long before enrolment expiry should users be notified?';
 $string['finishenrollingusers'] = 'Finish enrolling users';
+$string['foundxcohorts'] = 'Found {$a} cohorts';
 $string['instanceeditselfwarning'] = 'Warning:';
 $string['instanceeditselfwarningtext'] = 'You are enrolled into this course through this enrolment method, changes may affect your access to this course.';
 $string['invalidenrolinstance'] = 'Invalid enrolment instance';
index 4d9f2d5..1acf56b 100644 (file)
@@ -229,6 +229,7 @@ $string['errorcreatingfile'] = 'Error creating file "{$a}"';
 $string['errorcreatingrole'] = 'Error creating role';
 $string['errorfetchingrssfeed'] = 'Error fetching RSS feed.';
 $string['erroronline'] = 'Error on line {$a}';
+$string['erroroutput'] = 'Error output, so disabling automatic redirect.';
 $string['errorparsingxml'] = 'Error parsing XML: {$a->errorstring} at line {$a->errorline}, char {$a->errorchar}';
 $string['errorreadingfile'] = 'Error reading file "{$a}"';
 $string['errorsavingrequest'] = 'An error occurred when trying to save your request.';
index 1ec79b8..e80d92e 100644 (file)
@@ -47,7 +47,7 @@ $string['aggregateonlygraded_help'] = 'An empty grade is a grade which is missin
 This setting determines whether empty grades are not included in the aggregation or are counted as minimal grades, for example 0 for an assignment graded between 0 and 100.';
 $string['aggregateoutcomes'] = 'Include outcomes in aggregation';
 $string['aggregateoutcomes_help'] = 'If enabled, outcomes are included in the aggregation. This may result in an unexpected category total.';
-$string['aggregatesonly'] = 'Aggregates only';
+$string['aggregatesonly'] = 'Change to aggregates only';
 $string['aggregatesum'] = 'Natural';
 $string['aggregateweightedmean'] = 'Weighted mean of grades';
 $string['aggregateweightedmean2'] = 'Simple weighted mean of grades';
@@ -227,7 +227,7 @@ $string['forceon'] = 'Force: On';
 $string['forelementtypes'] = 'for the selected {$a}';
 $string['forstudents'] = 'For students';
 $string['full'] = 'Full';
-$string['fullmode'] = 'Full view';
+$string['fullmode'] = 'Change to full view';
 $string['generalsettings'] = 'General settings';
 $string['grade'] = 'Grade';
 $string['gradeadministration'] = 'Grade administration';
@@ -305,13 +305,14 @@ $string['gradepointmax_validateerror'] = 'This setting must be an integer betwee
 $string['gradepreferences'] = 'Grade preferences';
 $string['gradepreferenceshelp'] = 'Grade preferences Help';
 $string['gradepublishing'] = 'Enable publishing';
+$string['gradepublishinglink'] = 'Download: {$a}';
 $string['gradepublishing_help'] = 'Enable publishing in exports and imports: Exported grades can be accessed by accessing a URL, without having to log on to a Moodle site. Grades can be imported by accessing such a URL (which means that a Moodle site can import grades published by another site). By default only administrators may use this feature, please educate users before adding required capabilities to other roles (dangers of bookmark sharing and download accelerators, IP restrictions, etc.).';
 $string['gradereport'] = 'Grade report';
 $string['graderreport'] = 'Grader report';
 $string['grades'] = 'Grades';
 $string['gradesforuser'] = 'Grades for {$a->user}';
 $string['singleview'] = 'Single view for {$a}';
-$string['gradesonly'] = 'Grades only';
+$string['gradesonly'] = 'Change to grades only';
 $string['gradessettings'] = 'Grade settings';
 $string['gradetype'] = 'Grade type';
 $string['gradetype_help'] = 'There are 4 grade types:
@@ -614,6 +615,9 @@ $string['showfeedback'] = 'Show feedback';
 $string['showfeedback_help'] = 'Show the feedback column?';
 $string['showgrade'] = 'Show grades';
 $string['showgrade_help'] = 'Show the grade column?';
+$string['showingaggregatesonly'] = 'Showing aggregates only';
+$string['showingfullmode'] = 'Showing full view';
+$string['showinggradesonly'] = 'Showing grades only';
 $string['showlettergrade'] = 'Show letter grades';
 $string['showlettergrade_help'] = 'Show the letter grade column?';
 $string['showrange'] = 'Show ranges';
index 2e93263..dacd551 100644 (file)
@@ -52,6 +52,7 @@ $string['emailtagline'] = 'This is a copy of a message sent to you at "{$a->site
 $string['emptysearchstring'] = 'You must search for something';
 $string['enabled'] = 'Enabled';
 $string['errorcallingprocessor'] = 'Error calling defined output';
+$string['errorwhilesendingmessage'] = 'An error occured while sending the message, please try again later.';
 $string['errortranslatingdefault'] = 'Error translating default setting provided by plugin, using system defaults instead.';
 $string['eventmessagecontactadded'] = 'Message contact added';
 $string['eventmessagecontactblocked'] = 'Message contact blocked';
@@ -85,7 +86,9 @@ $string['message'] = 'Message';
 $string['messagehistory'] = 'Message history';
 $string['messagehistoryfull'] = 'All messages';
 $string['messagenavigation'] = 'Message navigation:';
+$string['messagetosend'] = 'Message to send';
 $string['messages'] = 'Messages';
+$string['messagesent'] = 'Message sent';
 $string['messaging'] = 'Messaging';
 $string['messagingblockednoncontact'] = '{$a} will not be able to reply as you have blocked non-contacts';
 $string['messagingdisabled'] = 'Messaging is disabled on this site, emails will be sent instead';
@@ -125,6 +128,7 @@ $string['searchmessages'] = 'Search messages';
 $string['searchcombined'] = 'Search people and messages';
 $string['sendingvia'] = 'Sending "{$a->provider}" via "{$a->processor}"';
 $string['sendingviawhen'] = 'Sending "{$a->provider}" via "{$a->processor}" when {$a->state}';
+$string['sendingmessage'] = 'Sending message';
 $string['sendmessage'] = 'Send message';
 $string['sendmessageto'] = 'Send message to {$a}';
 $string['sendmessagetopopup'] = 'Send message to {$a} - new window';
@@ -143,3 +147,4 @@ $string['unreadnewmessage'] = 'New message from {$a}';
 $string['userisblockingyou'] = 'This user has blocked you from sending messages to them';
 $string['userisblockingyounoncontact'] = '{$a} only accepts messages from their contacts.';
 $string['userssearchresults'] = 'Users found: {$a}';
+$string['viewconversation'] = 'View conversation';
index 1bb70c9..b9a5d7b 100644 (file)
@@ -270,6 +270,7 @@ $string['complete'] = 'Complete';
 $string['completereport'] = 'Complete report';
 $string['configuration'] = 'Configuration';
 $string['confirm'] = 'Confirm';
+$string['confirmdeletesection'] = 'Are you absolutely sure you want to delete "{$a}"? All activities will be also deleted';
 $string['confirmed'] = 'Your registration has been confirmed';
 $string['confirmednot'] = 'Your registration has not yet been confirmed!';
 $string['confirmcheckfull'] = 'Are you absolutely sure you want to confirm {$a} ?';
@@ -480,6 +481,7 @@ $string['deletechecktypename'] = 'Are you sure that you want to delete the {$a->
 $string['deletecheckfiles'] = 'Are you absolutely sure you want to delete these files?';
 $string['deletecheckfull'] = 'Are you absolutely sure you want to completely delete {$a} ?';
 $string['deletecheckwarning'] = 'You are about to delete these files';
+$string['deletesection'] = 'Delete section';
 $string['deleteselected'] = 'Delete selected';
 $string['deleteselectedkey'] = 'Delete selected key';
 $string['deletingcourse'] = 'Deleting {$a}';
index dee44e0..37da3db 100644 (file)
@@ -3231,17 +3231,22 @@ class admin_setting_configtime extends admin_setting {
             $defaultinfo = NULL;
         }
 
-        $return = '<div class="form-time defaultsnext">'.
-            '<select id="'.$this->get_id().'h" name="'.$this->get_full_name().'[h]">';
+        $return  = '<div class="form-time defaultsnext">';
+        $return .= '<label class="accesshide" for="' . $this->get_id() . 'h">' . get_string('hours') . '</label>';
+        $return .= '<select id="' . $this->get_id() . 'h" name="' . $this->get_full_name() . '[h]">';
         for ($i = 0; $i < 24; $i++) {
-            $return .= '<option value="'.$i.'"'.($i == $data['h'] ? ' selected="selected"' : '').'>'.$i.'</option>';
+            $return .= '<option value="' . $i . '"' . ($i == $data['h'] ? ' selected="selected"' : '') . '>' . $i . '</option>';
         }
-        $return .= '</select>:<select id="'.$this->get_id().'m" name="'.$this->get_full_name().'[m]">';
+        $return .= '</select>:';
+        $return .= '<label class="accesshide" for="' . $this->get_id() . 'm">' . get_string('minutes') . '</label>';
+        $return .= '<select id="' . $this->get_id() . 'm" name="' . $this->get_full_name() . '[m]">';
         for ($i = 0; $i < 60; $i += 5) {
-            $return .= '<option value="'.$i.'"'.($i == $data['m'] ? ' selected="selected"' : '').'>'.$i.'</option>';
+            $return .= '<option value="' . $i . '"' . ($i == $data['m'] ? ' selected="selected"' : '') . '>' . $i . '</option>';
         }
-        $return .= '</select></div>';
-        return format_admin_setting($this, $this->visiblename, $return, $this->description, false, '', $defaultinfo, $query);
+        $return .= '</select>';
+        $return .= '</div>';
+        return format_admin_setting($this, $this->visiblename, $return, $this->description,
+            $this->get_id() . 'h', '', $defaultinfo, $query);
     }
 
 }
@@ -7842,11 +7847,11 @@ class admin_setting_webservicesoverview extends admin_setting {
                 get_string('enablemobilewebservice', 'admin'),
                 get_string('configenablemobilewebservice',
                         'admin', ''), 0); //we don't want to display it but to know the ws mobile status
-        $manageserviceurl = new moodle_url("/admin/settings.php?section=externalservices");
+        $manageserviceurl = new moodle_url("/admin/settings.php?section=mobile");
         $wsmobileparam = new stdClass();
         $wsmobileparam->enablemobileservice = get_string('enablemobilewebservice', 'admin');
         $wsmobileparam->manageservicelink = html_writer::link($manageserviceurl,
-                get_string('externalservices', 'webservice'));
+                get_string('mobile', 'admin'));
         $mobilestatus = $enablemobile->get_setting()?get_string('mobilewsenabled', 'webservice'):get_string('mobilewsdisabled', 'webservice');
         $wsmobileparam->wsmobilestatus = html_writer::tag('strong', $mobilestatus);
         $return .= $OUTPUT->heading(get_string('enablemobilewebservice', 'admin'), 3, 'main');
index 545b82f..1cfd346 100644 (file)
@@ -28,7 +28,7 @@ require_once(dirname(__FILE__) . '/../../config.php');
 // Initialise ALL common incoming parameters here, up front.
 $courseid = required_param('courseid', PARAM_INT);
 $pagelayout = required_param('pagelayout', PARAM_ALPHAEXT);
-$pagetype = required_param('pagetype', PARAM_ALPHAEXT);
+$pagetype = required_param('pagetype', PARAM_ALPHANUMEXT);
 $contextid = required_param('contextid', PARAM_INT);
 $subpage = optional_param('subpage', '', PARAM_ALPHANUMEXT);
 $cmid = optional_param('cmid', null, PARAM_INT);
index e3729a2..80b57ce 100644 (file)
@@ -1126,7 +1126,8 @@ function badges_download($userid) {
     if ($zipper->archive_to_pathname($filelist, $tempzip)) {
         send_temp_file($tempzip, 'badges.zip');
     } else {
-        debugging("Problems with archiving the files.");
+        debugging("Problems with archiving the files.", DEBUG_DEVELOPER);
+        die;
     }
 }
 
index 8c60cec..c2dac9a 100644 (file)
@@ -302,6 +302,11 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
             $loops = $timeout;
         }
 
+        // DOM will never change on non-javascript case; do not wait or try again.
+        if (!$this->running_javascript()) {
+            $loops = 1;
+        }
+
         for ($i = 0; $i < $loops; $i++) {
             // We catch the exception thrown by the step definition to execute it again.
             try {
@@ -320,10 +325,12 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
                 continue;
             }
 
-            if ($microsleep) {
-                usleep(100000);
-            } else {
-                sleep(1);
+            if ($this->running_javascript()) {
+                if ($microsleep) {
+                    usleep(100000);
+                } else {
+                    sleep(1);
+                }
             }
         }
 
index 613b7a9..cd518cf 100644 (file)
@@ -41,6 +41,8 @@ class behat_selectors {
     protected static $allowedtextselectors = array(
         'dialogue' => 'dialogue',
         'block' => 'block',
+        'section' => 'section',
+        'activity' => 'activity',
         'region' => 'region',
         'table_row' => 'table_row',
         'list_item' => 'list_item',
@@ -56,6 +58,8 @@ class behat_selectors {
     protected static $allowedselectors = array(
         'dialogue' => 'dialogue',
         'block' => 'block',
+        'section' => 'section',
+        'activity' => 'activity',
         'region' => 'region',
         'table_row' => 'table_row',
         'list_item' => 'list_item',
@@ -102,6 +106,16 @@ XPATH
     (contains(concat(' ', normalize-space(@class), ' '), concat(' ', %locator%, ' ')) or
      descendant::h2[normalize-space(.) = %locator%] or
      @aria-label = %locator%)]
+XPATH
+        , 'section' => <<<XPATH
+//li[contains(concat(' ', normalize-space(@class), ' '), ' section ')][./descendant::*[self::h3]
+    [normalize-space(.) = %locator%][contains(concat(' ', normalize-space(@class), ' '), ' sectionname ') or
+    contains(concat(' ', normalize-space(@class), ' '), ' section-title ')]] |
+//div[contains(concat(' ', normalize-space(@class), ' '), ' sitetopic ')]
+    [./descendant::*[self::h2][normalize-space(.) = %locator%] or %locator% = 'frontpage']
+XPATH
+        , 'activity' => <<<XPATH
+//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][normalize-space(.) = %locator% ]
 XPATH
         , 'region' => <<<XPATH
 //*[self::div | self::section | self::aside | self::header | self::footer][./@id = %locator%]
index f75455a..630d592 100644 (file)
@@ -486,6 +486,11 @@ class block_manager {
             return false;
         }
 
+        // Block regions should not be docked during editing when all the blocks are hidden.
+        if ($this->page->user_is_editing() && $this->page->user_can_edit_blocks()) {
+            return false;
+        }
+
         $this->check_is_loaded();
         $this->ensure_content_created($region, $output);
         if (!$this->region_has_content($region, $output)) {
@@ -493,7 +498,7 @@ class block_manager {
             return false;
         }
         foreach ($this->visibleblockcontent[$region] as $instance) {
-            if (!empty($instance->content) && !get_user_preferences('docked_block_instance_'.$instance->blockinstanceid, 0)) {
+            if (!get_user_preferences('docked_block_instance_'.$instance->blockinstanceid, 0)) {
                 return false;
             }
         }
index c016bd5..234cc93 100644 (file)
@@ -50,13 +50,19 @@ class manager {
      *
      * NOTE: to be used from message_send() only.
      *
-     * @param \stdClass $eventdata fully prepared event data for processors
+     * @param \stdClass|\core\message\message $eventdata fully prepared event data for processors
      * @param \stdClass $savemessage the message saved in 'message' table
      * @param array $processorlist list of processors for target user
      * @return int $messageid the id from 'message' or 'message_read' table (false is not returned)
      */
-    public static function send_message(\stdClass $eventdata, \stdClass $savemessage, array $processorlist) {
+    public static function send_message($eventdata, \stdClass $savemessage, array $processorlist) {
         global $CFG;
+
+        if (!($eventdata instanceof \stdClass) && !($eventdata instanceof message)) {
+            // Not a valid object.
+            throw new \coding_exception('Message should be of type stdClass or \core\message\message');
+        }
+
         require_once($CFG->dirroot.'/message/lib.php'); // This is most probably already included from messagelib.php file.
 
         if (empty($processorlist)) {
@@ -85,12 +91,13 @@ class manager {
     /**
      * Send message to message processors.
      *
-     * @param \stdClass $eventdata
+     * @param \stdClass|\core\message\message $eventdata
      * @param \stdClass $savemessage
      * @param array $processorlist
      * @return int $messageid
      */
-    protected static function send_message_to_processors(\stdClass $eventdata, \stdClass $savemessage, array $processorlist) {
+    protected static function send_message_to_processors($eventdata, \stdClass $savemessage, array
+    $processorlist) {
         global $CFG, $DB;
 
         // We cannot communicate with external systems in DB transactions,
@@ -114,7 +121,9 @@ class manager {
 
         $failed = false;
         foreach ($processorlist as $procname) {
-            if (!$processors[$procname]->object->send_message($eventdata)) {
+            // Let new messaging class add custom content based on the processor.
+            $proceventdata = ($eventdata instanceof message) ? $eventdata->get_eventobject_for_processor($procname) : $eventdata;
+            if (!$processors[$procname]->object->send_message($proceventdata)) {
                 debugging('Error calling message processor ' . $procname);
                 $failed = true;
                 // Previously the $messageid = false here was overridden
diff --git a/lib/classes/message/message.php b/lib/classes/message/message.php
new file mode 100644 (file)
index 0000000..393d0e7
--- /dev/null
@@ -0,0 +1,252 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * New messaging class.
+ *
+ * @package   core_message
+ * @since     Moodle 2.9
+ * @copyright 2015 onwards Ankit Agarwal
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\message;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * New messaging class.
+ *
+ * Required parameters of the $eventdata object:
+ *  component string Component name. must exist in message_providers
+ *  name string Message type name. must exist in message_providers
+ *  userfrom object|int The user sending the message
+ *  userto object|int The message recipient
+ *  subject string The message subject
+ *  fullmessage string The full message in a given format
+ *  fullmessageformat int The format if the full message (FORMAT_MOODLE, FORMAT_HTML, ..)
+ *  fullmessagehtml string The full version (the message processor will choose with one to use)
+ *  smallmessage string The small version of the message
+ *
+ * Optional parameters of the $eventdata object:
+ *  notification bool Should the message be considered as a notification rather than a personal message
+ *  contexturl string If this is a notification then you can specify a url to view the event.
+ *                    For example the forum post the user is being notified of.
+ *  contexturlname string The display text for contexturl.
+ *  replyto string An email address which can be used to send an reply.
+ *  attachment stored_file File instance that needs to be sent as attachment.
+ *  attachname string Name of the attachment.
+ *
+ * @package   core_message
+ * @since     Moodle 2.9
+ * @copyright 2015 onwards Ankit Agarwal
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class message {
+    /** @var string Component name. */
+    private $component;
+
+    /** @var string Name. */
+    private $name;
+
+    /** @var object|int The user who is sending this message. */
+    private $userfrom;
+
+    /** @var object|int The user who is receiving from which is sending this message. */
+    private $userto;
+
+    /** @var string Subject of the message. */
+    private $subject;
+
+    /** @var string Complete message. */
+    private $fullmessage;
+
+    /** @var int Message format. */
+    private $fullmessageformat;
+
+    /** @var string Complete message in html format. */
+    private $fullmessagehtml;
+
+    /** @var  string Smaller version of the message. */
+    private $smallmessage;
+
+    /** @var  int Is it a notification? */
+    private $notification;
+
+    /** @var  string context url. */
+    private $contexturl;
+
+    /** @var  string context name. */
+    private $contexturlname;
+
+    /** @var  string An email address which can be used to send an reply. */
+    private $replyto;
+
+    /** @var  int Used internally to store the id of the row representing this message in DB. */
+    private $savedmessageid;
+
+    /** @var  \stored_file  File to be attached to the message. Note:- not all processors support this.*/
+    private $attachment;
+
+    /** @var  string Name of the attachment. Note:- not all processors support this.*/
+    private $attachname;
+
+    /** @var array a list of properties that is allowed for each message. */
+    private $properties = array('component', 'name', 'userfrom', 'userto', 'subject', 'fullmessage', 'fullmessageformat',
+                                'fullmessagehtml', 'smallmessage', 'notification', 'contexturl', 'contexturlname', 'savedmessageid',
+                                'replyto', 'attachment', 'attachname');
+
+    /** @var array property to store any additional message processor specific content */
+    private $additionalcontent = array();
+
+    /**
+     * Fullmessagehtml content including any processor specific content.
+     *
+     * @param string $processorname Name of the processor.
+     *
+     * @return mixed|string
+     */
+    protected function get_fullmessagehtml($processorname = '') {
+        if (!empty($processorname) && isset($this->additionalcontent[$processorname])) {
+            return $this->get_message_with_additional_content($processorname, 'fullmessagehtml');
+        } else {
+            return $this->fullmessagehtml;
+        }
+    }
+
+    /**
+     * Fullmessage content including any processor specific content.
+     *
+     * @param string $processorname Name of the processor.
+     *
+     * @return mixed|string
+     */
+    protected function get_fullmessage($processorname = '') {
+        if (!empty($processorname) && isset($this->additionalcontent[$processorname])) {
+            return $this->get_message_with_additional_content($processorname, 'fullmessage');
+        } else {
+            return $this->fullmessage;
+        }
+    }
+
+    /**
+     * Smallmessage content including any processor specific content.
+     *
+     * @param string $processorname Name of the processor.
+     *
+     * @return mixed|string
+     */
+    protected function get_smallmessage($processorname = '') {
+        if (!empty($processorname) && isset($this->additionalcontent[$processorname])) {
+            return $this->get_message_with_additional_content($processorname, 'smallmessage');
+        } else {
+            return $this->smallmessage;
+        }
+    }
+
+    /**
+     * Helper method used to get message content added with processor specific content.
+     *
+     * @param string $processorname Name of the processor.
+     * @param string $messagetype one of 'fullmessagehtml', 'fullmessage', 'smallmessage'.
+     *
+     * @return mixed|string
+     */
+    protected function get_message_with_additional_content($processorname, $messagetype) {
+        $message = $this->$messagetype;
+        if (isset($this->additionalcontent[$processorname]['*'])) {
+            // Content that needs to be added to all format.
+            $pattern = $this->additionalcontent[$processorname]['*'];
+            $message = empty($pattern['header']) ? $message : $pattern['header'] . $message;
+            $message = empty($pattern['footer']) ? $message : $message . $pattern['footer'];
+        }
+
+        if (isset($this->additionalcontent[$processorname][$messagetype])) {
+            // Content that needs to be added to the specific given format.
+            $pattern = $this->additionalcontent[$processorname][$messagetype];
+            $message = empty($pattern['header']) ? $message : $pattern['header'] . $message;
+            $message = empty($pattern['footer']) ? $message : $message . $pattern['footer'];
+        }
+
+        return $message;
+    }
+
+    /**
+     * Magic getter method.
+     *
+     * @param string $prop name of property to get.
+     *
+     * @return mixed
+     * @throws \coding_exception
+     */
+    public function __get($prop) {
+        if (in_array($prop, $this->properties)) {
+            return $this->$prop;
+        }
+        throw new \coding_exception("Invalid property $prop specified");
+    }
+
+    /**
+     * Magic setter method.
+     *
+     * @param string $prop name of property to set.
+     * @param mixed $value value to assign to the property.
+     *
+     * @return mixed
+     * @throws \coding_exception
+     */
+    public function __set($prop, $value) {
+        if (in_array($prop, $this->properties)) {
+            return $this->$prop = $value;
+        }
+        throw new \coding_exception("Invalid property $prop specified");
+    }
+
+    /**
+     * This method lets you define content that would be added to the message only for specific message processors.
+     *
+     * Example of $content:-
+     * array('fullmessagehtml' => array('header' => 'header content', 'footer' => 'footer content'),
+     *       'smallmessage' => array('header' => 'header content for small message', 'footer' => 'footer content'),
+     *       '*' => array('header' => 'header content for all types', 'footer' => 'footer content')
+     * )
+     *
+     * @param string $processorname name of the processor.
+     * @param array $content content to add in the above defined format.
+     */
+    public function set_additional_content($processorname, $content) {
+        $this->additionalcontent[$processorname] = $content;
+    }
+
+    /**
+     * Get a event object for a specific processor in stdClass format.
+     *
+     * @param string $processorname Name of the processor.
+     *
+     * @return \stdClass event object in stdClass format.
+     */
+    public function get_eventobject_for_processor($processorname) {
+        // This is done for Backwards compatibility. We should consider throwing notices here in future versions and requesting
+        // them to use proper api.
+
+        $eventdata = new \stdClass();
+        foreach ($this->properties as $prop) {
+            $func = "get_$prop";
+            $eventdata->$prop = method_exists($this, $func) ? $this->$func($processorname) : $this->$prop;
+        }
+        return $eventdata;
+    }
+}
\ No newline at end of file
index c9318ff..2130148 100644 (file)
@@ -122,10 +122,10 @@ $functions = array(
     // === group related functions ===
 
     'moodle_group_create_groups' => array(
-        'classname'   => 'core_group_external',
+        'classname'   => 'moodle_group_external',
         'methodname'  => 'create_groups',
         'classpath'   => 'group/externallib.php',
-        'description' => 'DEPRECATED: this deprecated function will be removed in a future version. This function has be renamed as core_group_create_groups(). ',
+        'description' => 'DEPRECATED: this deprecated function will be removed in a future version. This function has been renamed as core_group_create_groups(). ',
         'type'        => 'write',
         'capabilities'=> 'moodle/course:managegroups',
     ),
@@ -140,10 +140,10 @@ $functions = array(
     ),
 
     'moodle_group_get_groups' => array(
-        'classname'   => 'core_group_external',
+        'classname'   => 'moodle_group_external',
         'methodname'  => 'get_groups',
         'classpath'   => 'group/externallib.php',
-        'description' => 'DEPRECATED: this deprecated function will be removed in a future version. This function has be renamed as core_group_get_groups()',
+        'description' => 'DEPRECATED: this deprecated function will be removed in a future version. This function has been renamed as core_group_get_groups()',
         'type'        => 'read',
         'capabilities'=> 'moodle/course:managegroups',
     ),
@@ -158,10 +158,10 @@ $functions = array(
     ),
 
     'moodle_group_get_course_groups' => array(
-        'classname'   => 'core_group_external',
+        'classname'   => 'moodle_group_external',
         'methodname'  => 'get_course_groups',
         'classpath'   => 'group/externallib.php',
-        'description' => 'DEPRECATED: this deprecated function will be removed in a future version. This function has be renamed as core_group_get_course_groups()',
+        'description' => 'DEPRECATED: this deprecated function will be removed in a future version. This function has been renamed as core_group_get_course_groups()',
         'type'        => 'read',
         'capabilities'=> 'moodle/course:managegroups',
     ),
@@ -176,10 +176,10 @@ $functions = array(
     ),
 
     'moodle_group_delete_groups' => array(
-        'classname'   => 'core_group_external',
+&