Merge branch 'wip-mdl-51101' of git://github.com/rajeshtaneja/moodle
authorDavid Monllao <davidm@moodle.com>
Mon, 14 Sep 2015 08:18:01 +0000 (16:18 +0800)
committerDavid Monllao <davidm@moodle.com>
Mon, 14 Sep 2015 08:18:01 +0000 (16:18 +0800)
185 files changed:
admin/roles/classes/capability_table_with_risks.php
admin/roles/classes/define_role_table_advanced.php
admin/roles/classes/define_role_table_basic.php
admin/roles/classes/override_permissions_table_advanced.php
admin/roles/classes/view_role_definition_table.php
admin/tool/behat/tests/behat/data_generators.feature
admin/tool/templatelibrary/amd/build/display.min.js
admin/tool/templatelibrary/amd/build/search.min.js
admin/tool/templatelibrary/amd/src/display.js
admin/tool/templatelibrary/amd/src/search.js
admin/tool/templatelibrary/classes/external.php
admin/tool/templatelibrary/db/services.php
auth/email/auth.php
blocks/tags/block_tags.php
blocks/tags/coursetags.js [deleted file]
blocks/tags/lang/en/block_tags.php
blocks/tags/lang/en/deprecated.txt [new file with mode: 0644]
blocks/tags/styles.css [deleted file]
blocks/tags/tests/behat/coursetags.feature [deleted file]
blog/edit.php
completion/classes/external.php
completion/cron.php
course/edit.php
course/edit_form.php
course/lib.php
course/tags.php [new file with mode: 0644]
course/tags_form.php [new file with mode: 0644]
course/tests/behat/coursetags.feature [new file with mode: 0644]
enrol/meta/locallib.php
enrol/meta/tests/plugin_test.php
grade/edit/tree/index.php
grade/edit/tree/lib.php
grade/lib.php
grade/tests/behat/behat_grade.php
grade/tests/behat/grade_aggregation.feature
grade/tests/behat/grade_aggregation_changes.feature
grade/tests/behat/grade_average.feature
grade/tests/behat/grade_calculated_grade_items.feature
grade/tests/behat/grade_calculated_grade_items_20150627.feature
grade/tests/behat/grade_calculated_weights.feature
grade/tests/behat/grade_contribution_with_extra_credit.feature
grade/tests/behat/grade_mingrade.feature
grade/tests/behat/grade_minmax.feature
grade/tests/behat/grade_natural_exclude_empty.feature
grade/tests/behat/grade_natural_exclude_empty_20150619.feature
grade/tests/behat/grade_natural_normalisation.feature
grade/tests/behat/grade_natural_normalisation_20150619.feature
grade/tests/behat/grade_scales.feature
grade/tests/behat/grade_scales_aggregation.feature
grade/tests/behat/grade_single_item_scales.feature
grade/tests/behat/grade_view.feature
grade/tests/edittreelib_test.php
group/overview.php
install/lang/xct/langconfig.php [new file with mode: 0644]
lang/en/admin.php
lang/en/blog.php
lang/en/deprecated.txt
lang/en/error.php
lang/en/grades.php
lang/en/role.php
lang/en/tag.php
lang/en/webservice.php
lib/ajax/service-nologin.php [moved from blocks/tags/settings.php with 64% similarity]
lib/ajax/service.php
lib/amd/build/ajax.min.js
lib/amd/build/str.min.js
lib/amd/build/templates.min.js
lib/amd/src/ajax.js
lib/amd/src/str.js
lib/amd/src/templates.js
lib/classes/output/external.php
lib/classes/task/completion_daily_task.php [moved from lib/classes/task/completion_cron_task.php with 77% similarity]
lib/classes/task/completion_regular_task.php [new file with mode: 0644]
lib/db/access.php
lib/db/install.xml
lib/db/services.php
lib/db/tasks.php
lib/db/upgrade.php
lib/db/upgradelib.php
lib/deprecatedlib.php
lib/dml/mssql_native_moodle_database.php
lib/dml/sqlsrv_native_moodle_database.php
lib/editor/atto/lib.php
lib/editor/tinymce/lib.php
lib/external/externallib.php
lib/externallib.php
lib/form/editor.php
lib/formslib.php
lib/grade/grade_category.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputrenderers.php
lib/templates/columns-2to1.mustache [new file with mode: 0644]
lib/tests/behat/behat_general.php
lib/tests/moodlelib_test.php
lib/tests/other/completion.manualtest.txt
lib/tests/upgradelib_test.php
lib/upgrade.txt
lib/upgradelib.php
lib/weblib.php
lib/yui/build/moodle-core-event/moodle-core-event-debug.js
lib/yui/build/moodle-core-event/moodle-core-event-min.js
lib/yui/build/moodle-core-event/moodle-core-event.js
lib/yui/src/event/js/event.js
login/signup_form.php
mod/assign/db/upgrade.php
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/tests/lib_test.php
mod/book/classes/external.php [new file with mode: 0644]
mod/book/db/services.php [new file with mode: 0644]
mod/book/lib.php
mod/book/tests/externallib_test.php [new file with mode: 0644]
mod/book/tests/lib_test.php
mod/book/version.php
mod/book/view.php
mod/chat/chat_ajax.php
mod/chat/classes/external.php [new file with mode: 0644]
mod/chat/db/services.php [new file with mode: 0644]
mod/chat/lib.php
mod/chat/tests/externallib_test.php [new file with mode: 0644]
mod/chat/version.php
mod/chat/view.php
mod/choice/classes/external.php [new file with mode: 0644]
mod/choice/db/services.php [new file with mode: 0644]
mod/choice/lib.php
mod/choice/tests/externallib_test.php [new file with mode: 0644]
mod/choice/tests/lib_test.php [new file with mode: 0644]
mod/choice/version.php
mod/choice/view.php
mod/data/lib.php
mod/data/tests/lib_test.php
mod/forum/db/services.php
mod/forum/externallib.php
mod/forum/index.php
mod/forum/lib.php
mod/forum/markposts.php
mod/forum/post.php
mod/forum/settracking.php
mod/forum/tests/externallib_test.php
mod/forum/tests/lib_test.php
mod/forum/version.php
mod/imscp/classes/external.php [new file with mode: 0644]
mod/imscp/db/services.php [new file with mode: 0644]
mod/imscp/lib.php
mod/imscp/tests/externallib_test.php [new file with mode: 0644]
mod/imscp/tests/lib_test.php
mod/imscp/version.php
mod/imscp/view.php
mod/lesson/continue.php
mod/lesson/lang/en/lesson.php
mod/lesson/locallib.php
mod/lesson/tests/behat/lesson_navigation.feature
mod/lesson/tests/behat/lesson_question_attempts.feature [new file with mode: 0644]
mod/lesson/view.php
mod/quiz/tests/behat/manually_mark_question.feature
mod/upgrade.txt
mod/wiki/pagelib.php
notes/externallib.php
pix/f/FileTypesIcons-LICENSE.txt
pix/f/Oxygen-LICENSE.txt
rating/classes/external.php
rating/index.php
rating/tests/externallib_test.php
tag/classes/external.php
tag/coursetags_add.php [deleted file]
tag/coursetags_edit.php [deleted file]
tag/coursetags_more.php [deleted file]
tag/coursetagslib.php [deleted file]
tag/index.php
tag/tag.js
tag/tests/events_test.php
tag/upgrade.txt
theme/base/config.php
theme/base/style/core.css
theme/base/style/course.css
theme/base/style/templates.css [new file with mode: 0644]
theme/bootstrapbase/less/moodle.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/less/moodle/grade.less
theme/bootstrapbase/less/moodle/templates.less [new file with mode: 0644]
theme/bootstrapbase/style/moodle.css
version.php
webservice/renderer.php

index b0480b0..acf7257 100644 (file)
@@ -163,9 +163,8 @@ abstract class core_role_capability_table_with_risks extends core_role_capabilit
     protected abstract function add_permission_cells($capability);
 
     protected function add_row_cells($capability) {
-        $this->add_permission_cells($capability);
+        $cells = $this->add_permission_cells($capability);
         // One cell for each possible risk.
-        $cells = '';
         foreach ($this->allrisks as $riskname => $risk) {
             $cells .= '<td class="risk ' . str_replace('risk', '', $riskname) . '">';
             if ($risk & (int)$capability->riskbitmask) {
index 8b4cc51..06f565d 100644 (file)
@@ -628,6 +628,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
 
     protected function add_permission_cells($capability) {
         // One cell for each possible permission.
+        $content = '';
         foreach ($this->displaypermissions as $perm => $permname) {
             $strperm = $this->strperms[$permname];
             $extraclass = '';
@@ -638,11 +639,12 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
             if ($this->permissions[$capability->name] == $perm) {
                 $checked = 'checked="checked" ';
             }
-            echo '<td class="' . $permname . $extraclass . '">';
-            echo '<label><input type="radio" name="' . $capability->name .
+            $content .= '<td class="' . $permname . $extraclass . '">';
+            $content .= '<label><input type="radio" name="' . $capability->name .
                 '" value="' . $perm . '" ' . $checked . '/> ';
-            echo '<span class="note">' . $strperm . '</span>';
-            echo '</label></td>';
+            $content .= '<span class="note">' . $strperm . '</span>';
+            $content .= '</label></td>';
         }
+        return $content;
     }
 }
index 14393e7..0c3d692 100644 (file)
@@ -45,19 +45,20 @@ class core_role_define_role_table_basic extends core_role_define_role_table_adva
         $perm = $this->permissions[$capability->name];
         $permname = $this->allpermissions[$perm];
         $defaultperm = $this->allpermissions[$this->parentpermissions[$capability->name]];
-        echo '<td class="' . $permname . '">';
+        $content = '<td class="' . $permname . '">';
         if ($perm == CAP_ALLOW || $perm == CAP_INHERIT) {
             $checked = '';
             if ($perm == CAP_ALLOW) {
                 $checked = 'checked="checked" ';
             }
-            echo '<input type="hidden" name="' . $capability->name . '" value="' . CAP_INHERIT . '" />';
-            echo '<label><input type="checkbox" name="' . $capability->name .
+            $content .= '<input type="hidden" name="' . $capability->name . '" value="' . CAP_INHERIT . '" />';
+            $content .= '<label><input type="checkbox" name="' . $capability->name .
                 '" value="' . CAP_ALLOW . '" ' . $checked . '/> ' . $this->strallow . '</label>';
         } else {
-            echo '<input type="hidden" name="' . $capability->name . '" value="' . $perm . '" />';
-            echo $this->strperms[$permname] . '<span class="note">' . $this->stradvmessage . '</span>';
+            $content .= '<input type="hidden" name="' . $capability->name . '" value="' . $perm . '" />';
+            $content .= $this->strperms[$permname] . '<span class="note">' . $this->stradvmessage . '</span>';
         }
-        echo '</td>';
+        $content .= '</td>';
+        return $content;
     }
 }
index 7da1239..a872b96 100644 (file)
@@ -73,6 +73,7 @@ class core_role_override_permissions_table_advanced extends core_role_capability
         }
 
         // One cell for each possible permission.
+        $content = '';
         foreach ($this->displaypermissions as $perm => $permname) {
             $strperm = $this->strperms[$permname];
             $extraclass = '';
@@ -83,8 +84,8 @@ class core_role_override_permissions_table_advanced extends core_role_capability
             if ($this->permissions[$capability->name] == $perm) {
                 $checked = 'checked="checked" ';
             }
-            echo '<td class="' . $permname . $extraclass . '">';
-            echo '<label><input type="radio" name="' . $capability->name .
+            $content .= '<td class="' . $permname . $extraclass . '">';
+            $content .= '<label><input type="radio" name="' . $capability->name .
                 '" value="' . $perm . '" ' . $checked . $disabled . '/> ';
             if ($perm == CAP_INHERIT) {
                 $inherited = $this->parentpermissions[$capability->name];
@@ -95,8 +96,9 @@ class core_role_override_permissions_table_advanced extends core_role_capability
                 }
                 $strperm .= ' (' . $inherited . ')';
             }
-            echo '<span class="note">' . $strperm . '</span>';
-            echo '</label></td>';
+            $content .= '<span class="note">' . $strperm . '</span>';
+            $content .= '</label></td>';
         }
+        return $content;
     }
 }
index a39ff0c..0859969 100644 (file)
@@ -129,7 +129,7 @@ class core_role_view_role_definition_table extends core_role_define_role_table_a
         } else {
             $default = "&#xa0;";
         }
-        echo '<td class="' . $permname . '">' . $this->strperms[$permname] . '<span class="note">' .
+        return '<td class="' . $permname . '">' . $this->strperms[$permname] . '<span class="note">' .
             $default . '</span></td>';
 
     }
index 6a15dd4..9853ea5 100644 (file)
@@ -351,7 +351,7 @@ Feature: Set up contextual data for tests
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
     And I expand "Setup" node
-    And I follow "Categories and items"
+    And I follow "Gradebook setup"
     Then I should see "Test Grade Item 1"
     And I follow "Edit   Test Grade Item 1"
     And I expand all fieldsets
@@ -434,7 +434,7 @@ Feature: Set up contextual data for tests
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
     And I expand "Setup" node
-    And I follow "Categories and items"
+    And I follow "Gradebook setup"
     Then I should see "Test Outcome Grade Item 1"
     And I follow "Edit   Test Outcome Grade Item 1"
     And the field "Outcome" matches value "Grade outcome 1"
index a5bf8ab..49c0afe 100644 (file)
Binary files a/admin/tool/templatelibrary/amd/build/display.min.js and b/admin/tool/templatelibrary/amd/build/display.min.js differ
index 062e585..869d27b 100644 (file)
Binary files a/admin/tool/templatelibrary/amd/build/search.min.js and b/admin/tool/templatelibrary/amd/build/search.min.js differ
index 1585a18..bb2f77a 100644 (file)
@@ -133,7 +133,7 @@ define(['jquery', 'core/ajax', 'core/log', 'core/notification', 'core/templates'
                     component: component,
                     template: name
             }
-        }]);
+        }], true, false);
 
         // When returns a new promise that is resolved when all the passed in promises are resolved.
         // The arguments to the done become the values of each resolved promise.
index d4eefe6..efefb26 100644 (file)
@@ -53,7 +53,7 @@ define(['jquery', 'core/ajax', 'core/log', 'core/notification', 'core/templates'
               args: { component: componentStr, search: searchStr },
               done: reloadListTemplate,
               fail: notification.exception }
-        ]);
+        ], true, false);
     };
 
     var throttle = null;
index 100172b..be583a2 100644 (file)
@@ -63,14 +63,6 @@ class external extends external_api {
         return new external_function_parameters($params);
     }
 
-    /**
-     * Expose to AJAX
-     * @return boolean
-     */
-    public static function list_templates_is_allowed_from_ajax() {
-        return true;
-    }
-
     /**
      * Loads the list of templates.
      * @param string $component Limit the search to a component.
@@ -108,16 +100,6 @@ class external extends external_api {
             );
     }
 
-    /**
-     * Can this function be called directly from ajax?
-     *
-     * @return boolean
-     * @since Moodle 2.9
-     */
-    public static function load_canonical_template_is_allowed_from_ajax() {
-        return true;
-    }
-
     /**
      * Return a mustache template.
      * Note - this function differs from the function core_output_load_template
index c1a9479..9dda647 100644 (file)
@@ -32,12 +32,16 @@ $functions = array(
         'description' => 'List/search templates by component.',
         'type'        => 'read',
         'capabilities'=> '',
+        'ajax'        => true,
+        'loginrequired' => false,
     ),
     'tool_templatelibrary_load_canonical_template' => array(
         'classname'   => 'tool_templatelibrary\external',
         'methodname'  => 'load_canonical_template',
         'description' => 'Load a canonical template by name (not the theme overidden one).',
-        'type'        => 'read'
+        'type'        => 'read',
+        'ajax'        => true,
+        'loginrequired' => false,
     ),
 
 );
index 3f037d7..970a16f 100644 (file)
@@ -240,7 +240,7 @@ class auth_plugin_email extends auth_plugin_base {
      */
     function is_captcha_enabled() {
         global $CFG;
-        return isset($CFG->recaptchapublickey) && isset($CFG->recaptchaprivatekey) && get_config("auth/{$this->authtype}", 'recaptcha');
+        return $this->config->recaptcha;
     }
 
 }
index 66a4be8..ff9e88b 100644 (file)
@@ -95,106 +95,7 @@ class block_tags extends block_base {
 
         require_once($CFG->dirroot.'/tag/locallib.php');
 
-        if (empty($CFG->block_tags_showcoursetags) or !$CFG->block_tags_showcoursetags) {
-
-            $this->content->text = tag_print_cloud(null, $this->config->numberoftags, true);
-
-        } else {
-            // Start of show course tags section.
-            require_once($CFG->dirroot.'/tag/coursetagslib.php');
-
-            // Page awareness.
-            $tagtype = 'all';
-            if ($SCRIPT == '/my/index.php') {
-                $tagtype = 'my';
-            } else if (isset($this->page->course->id)) {
-                if ($this->page->course->id != SITEID) {
-                    $tagtype = 'course';
-                }
-            }
-
-            // DB hits to get groups of marked up tags (if available).
-            // TODO check whether time limited personal tags are required.
-            $content = '';
-            $moretags = new moodle_url('/tag/coursetags_more.php', array('show'=>$tagtype));
-            if ($tagtype == 'all') {
-                $tags = coursetag_get_tags(0, 0, $this->config->tagtype, $this->config->numberoftags);
-            } else if ($tagtype == 'course') {
-                $tags = coursetag_get_tags($this->page->course->id, 0, $this->config->tagtype, $this->config->numberoftags);
-                $moretags->param('courseid', $this->page->course->id);
-            } else if ($tagtype == 'my') {
-                $tags = coursetag_get_tags(0, $USER->id, $this->config->tagtype, $this->config->numberoftags);
-            }
-            $tagcloud = tag_print_cloud($tags, 150, true);
-            if (!$tagcloud) {
-                $tagcloud = get_string('notagsyet', 'block_tags');
-            }
-
-            // Prepare the divs that display the groups of tags.
-            $content = get_string($tagtype."tags", 'block_tags').
-                    '<div class="coursetag_list">'.$tagcloud.'</div>
-                    <div class="coursetag_morelink">
-                        <a href="'.$moretags->out().'" title="'.get_string('moretags', 'block_tags').'">'
-                        .get_string('more', 'block_tags').'</a>
-                    </div>';
-            // Add javascript.
-            coursetag_get_jscript();
-
-            // Add the divs (containing the tags) to the block's content.
-            $this->content->text .= $content;
-
-            // Add the input form section (allowing a user to tag the current course) and navigation, or login message.
-            if (isloggedin() && !isguestuser()) {
-                // Only show the input form on course pages for those allowed (or not barred).
-                if ($tagtype == 'course' &&
-                                has_capability('moodle/tag:create', context_course::instance($this->page->course->id))) {
-                    $buttonadd = get_string('add', 'block_tags');
-                    $arrowtitle = get_string('arrowtitle', 'block_tags');
-                    $edittags = get_string('edittags', 'block_tags');
-                    $sesskey = sesskey();
-                    $arrowright = $OUTPUT->pix_url('t/arrow_left');
-                    $redirect = $this->page->url->out();
-                    $this->content->footer .= <<<EOT
-                        <hr />
-                        <form action="{$CFG->wwwroot}/tag/coursetags_add.php" method="post" id="coursetag"
-                                onsubmit="return ctags_checkinput(this.coursetag_new_tag.value)">
-                            <div style="display: none;">
-                                <input type="hidden" name="entryid" value="$COURSE->id" />
-                                <input type="hidden" name="userid" value="$USER->id" />
-                                <input type="hidden" name="sesskey" value="$sesskey" />
-                                <input type="hidden" name="returnurl" value="$redirect" />
-                                </div>
-                            <div class="coursetag_form_wrapper">
-                                <div class="coursetag_form_positioner">
-                                    <div class="coursetag_form_input1">
-                                        <input type="text" name="coursetag_sug_keyword" class="coursetag_form_input1a" disabled="disabled" />
-                                    </div>
-                                    <div class="coursetag_form_input2">
-                                        <input type="text" name="coursetag_new_tag" id="coursetag_new_tag"
-                                        class="coursetag_form_input2a" onfocus="ctags_getKeywords()" onkeyup="ctags_getKeywords()" maxlength="50" />
-                                    </div>
-                                    <div class="coursetag_form_input3" id="coursetag_sug_btn">
-                                        <a title="$arrowtitle">
-                                            <img src="$arrowright" width="10" height="10" alt="enter" onclick="ctags_setKeywords()" />
-                                        </a>
-                                    </div>
-                                </div>
-                                <div style="display: inline;">
-                                    <button type="submit">$buttonadd</button>
-                                    <a href="$CFG->wwwroot/tag/coursetags_edit.php?courseid=$COURSE->id" title="$edittags">$edittags</a>
-                                </div>
-                            </div>
-                        </form>
-EOT;
-                }
-            } else {
-                // If not logged in.
-                $this->content->footer = '<hr />'.get_string('please', 'block_tags').'
-                    <a href="'.get_login_url().'">'.get_string('login', 'block_tags').'
-                        </a> '.get_string('tagunits', 'block_tags');
-            }
-        }
-        // End of show course tags section.
+        $this->content->text = tag_print_cloud(null, $this->config->numberoftags, true);
 
         return $this->content;
     }
diff --git a/blocks/tags/coursetags.js b/blocks/tags/coursetags.js
deleted file mode 100644 (file)
index 783146f..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * coursetags.js
- * @author j.beedell@open.ac.uk July07
- *
- * getKeywords modified from an original script (Auto Complete Textfield)
- * from The JavaScript Source http://javascript.internet.com
- * originally created by: Timothy Groves http://www.brandspankingnew.net/
- */
-
-
-function ctags_show_div(mydiv) {
-    for(x in coursetagdivs) {
-        if(mydiv == coursetagdivs[x]) {
-            document.getElementById(coursetagdivs[x]).style.display="block";
-        } else {
-            document.getElementById(coursetagdivs[x]).style.display="none";
-        }
-    }
-    return false;
-}
-
-var sug = "";
-var sug_disp = "";
-
-function ctags_getKeywords() {
-  /*
-  // This 'workaround' removing the xhtml strict form autocomplete="off" needs to
-  // be added to the body onload() script to work - but decided not to include
-  // (having the browser list might help with screen readers more than this script)
-  // document.forms['coursetag'].setAttribute("autocomplete", "off");
-  */
-  var input = document.forms['coursetag'].coursetag_new_tag.value;
-  var len = input.length;
-  sug_disp = ""; sug = "";
-
-  if (input.length) {
-    for (ele in coursetag_tags)
-    {
-      if (coursetag_tags[ele].substr(0,len).toLowerCase() == input.toLowerCase())
-      {
-        sug_disp = input + coursetag_tags[ele].substr(len);
-        sug = coursetag_tags[ele];
-        break;
-      }
-    }
-  }
-  document.forms['coursetag'].coursetag_sug_keyword.value = sug_disp;
-  if (!sug.length || input == sug_disp) {
-    document.getElementById('coursetag_sug_btn').style.display = "none";
-  } else {
-    document.getElementById('coursetag_sug_btn').style.display = "block";
-  }
-}
-
-function ctags_setKeywords() {
-  document.forms['coursetag'].coursetag_new_tag.value = sug;
-  ctags_hideSug();
-}
-
-function ctags_hideSug() {
-  document.forms['coursetag'].coursetag_sug_keyword.value = "";
-  document.getElementById('coursetag_sug_btn').style.display = "none";
-}
index c63f492..edf9669 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['configtitle'] = 'Block title';
+$string['disabledtags'] = 'Tags are disabled';
+$string['defaultdisplay'] = 'Tag type to display';
+$string['pluginname'] = 'Tags';
+$string['tags:addinstance'] = 'Add a new tags block';
+$string['tags:myaddinstance'] = 'Add a new tags block to Dashboard';
+
+// Deprecated since 3.0
+
 $string['add'] = 'Add';
 $string['alltags'] = 'All tags:';
 $string['arrowtitle'] = 'Click here to enter the suggested text (grey letters).';
-$string['configtitle'] = 'Block title';
 $string['coursetags'] = 'Course tags:';
-$string['disabledtags'] = 'Tags are disabled';
-$string['defaultdisplay'] = 'Tag type to display';
 $string['edit'] = 'edit...';
 $string['editdeletemytag'] = 'Delete tag from this course:';
 $string['editmytags'] = 'My tags - shortcuts to all your tagged courses.';
@@ -65,13 +71,10 @@ $string['mycoursetags'] = 'My course tags:';
 $string['mytags'] = 'My tags:';
 $string['notagsyet'] = 'No tags yet';
 $string['please'] = 'Please';
-$string['pluginname'] = 'Tags';
 $string['select'] = 'Select...';
 $string['showcoursetags'] = 'Show course tags';
 $string['showcoursetagsdef'] = 'Display the course tagging features in the tags block, allowing students to tag courses.';
 $string['suggestedtagthisunit'] = 'Suggested tag to this course:';
 $string['tags'] = 'tags';
-$string['tags:addinstance'] = 'Add a new tags block';
-$string['tags:myaddinstance'] = 'Add a new tags block to Dashboard';
 $string['tagthisunit'] = 'Tag this course:';
 $string['tagunits'] = 'to tag your favourite courses';
diff --git a/blocks/tags/lang/en/deprecated.txt b/blocks/tags/lang/en/deprecated.txt
new file mode 100644 (file)
index 0000000..7c83a9d
--- /dev/null
@@ -0,0 +1,42 @@
+add,block_tags
+alltags,block_tags
+arrowtitle,block_tags
+coursetags,block_tags
+edit,block_tags
+editdeletemytag,block_tags
+editmytags,block_tags
+editmytagsfor,block_tags
+editnopersonaltags,block_tags
+edittags,block_tags
+edittagthisunit,block_tags
+editthiscoursetags,block_tags
+edittitle,block_tags
+entries,block_tags
+entry,block_tags
+jserror1,block_tags
+jserror2,block_tags
+login,block_tags
+more,block_tags
+moreorder,block_tags
+moreorderalpha,block_tags
+moreorderdate,block_tags
+moreorderpop,block_tags
+moreshow,block_tags
+moreshowalltags,block_tags
+moreshowcommtags,block_tags
+moreshowcoursetags,block_tags
+moreshowmytags,block_tags
+moreshowofficialtags,block_tags
+moretags,block_tags
+moretitle,block_tags
+morewelcome,block_tags
+mytags,block_tags
+notagsyet,block_tags
+please,block_tags
+select,block_tags
+showcoursetags,block_tags
+showcoursetagsdef,block_tags
+suggestedtagthisunit,block_tags
+tags,block_tags
+tagthisunit,block_tags
+tagunits,block_tags
diff --git a/blocks/tags/styles.css b/blocks/tags/styles.css
deleted file mode 100644 (file)
index 59b1afc..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-.block_tags {}
-.block_tags #coursetag {}
-.block_tags #coursetag .coursetag_form_wrapper {}
-.block_tags #coursetag .coursetag_form_wrapper .coursetag_form_positioner {position: relative;}
-.block_tags #coursetag .coursetag_form_wrapper .coursetag_form_positioner .coursetag_form_input1 {position: relative;top: 0;left: 0;z-index: 1;width:100%;}
-.block_tags #coursetag .coursetag_form_wrapper .coursetag_form_positioner .coursetag_form_input2 {position: absolute;top: 0;left: 0;z-index: 2;width:100%;}
-.block_tags #coursetag .coursetag_form_wrapper .coursetag_form_positioner .coursetag_form_input3 {position: absolute;top: 3px;left: 12.8em;display: none;}
-.block_tags #coursetag .coursetag_form_wrapper .coursetag_form_positioner .coursetag_form_input1a {background-color: white; border: 1px solid #999;width: 12em;padding: 2px;}
-.block_tags #coursetag .coursetag_form_wrapper .coursetag_form_positioner .coursetag_form_input2a {background-color: transparent; border: 1px solid #999;width: 12em;color: #669954;padding: 2px;}
-.block_tags .coursetag_morelink {}
-.block_tags .coursetag_list {}
\ No newline at end of file
diff --git a/blocks/tags/tests/behat/coursetags.feature b/blocks/tags/tests/behat/coursetags.feature
deleted file mode 100644 (file)
index c5f9c55..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-@block @block_tags @core_tag
-Feature: Block tags displaying course tags
-  In order to tag courses
-  As a user
-  I need to be able to use the block tags
-
-  Background:
-    Given the following "users" exist:
-      | username | firstname | lastname | email |
-      | teacher1 | Teacher | 1 | teacher1@example.com |
-      | student1 | Student | 1 | student1@example.com |
-      | student2 | Student | 2 | student2@example.com |
-    And the following "courses" exist:
-      | fullname  | shortname |
-      | Course 1  | c1        |
-    And the following "tags" exist:
-      | name         | tagtype  |
-      | Neverusedtag | official |
-    And the following "course enrolments" exist:
-      | user     | course | role           |
-      | teacher1 | c1     | editingteacher |
-      | student1 | c1     | student        |
-      | student2 | c1     | student        |
-    And I log in as "admin"
-    And I set the following administration settings values:
-      | Show course tags | 1 |
-    And I log out
-
-  Scenario: Add Tags block to tag courses in a course
-    When I log in as "teacher1"
-    And I follow "Course 1"
-    And I turn editing mode on
-    And I add the "Tags" block
-    And I log out
-    And I log in as "student1"
-    And I follow "Course 1"
-    And I should not see "Neverusedtag" in the "Tags" "block"
-    And I click on "more..." "link" in the "Tags" "block"
-    And I should not see "Neverusedtag"
-    And I follow "c1"
-    And I set the field "coursetag_new_tag" to "Dogs, Mice"
-    And I press "Add"
-    And I should see "Dogs" in the "Tags" "block"
-    And I should see "Mice" in the "Tags" "block"
-    And I log out
-    And I log in as "student2"
-    And I follow "Course 1"
-    And I should see "Dogs" in the "Tags" "block"
-    And I set the field "coursetag_new_tag" to "Cats, Dogs"
-    And I press "Add"
-    And I should see "Dogs" in the "Tags" "block"
-    And I should see "Cats" in the "Tags" "block"
-    And I click on "more..." "link" in the "Tags" "block"
-    And "Cats" "link" should appear before "Dogs" "link"
-    And "Dogs" "link" should appear before "Mice" "link"
-    And I follow "My tags"
-    And I should see "Dogs"
-    And I should see "Cats"
-    And I should not see "Mice"
-    And I follow "All tags"
-    And I follow "Popularity"
-    And "Mice" "link" should appear before "Dogs" "link"
-    And I should not see "Neverusedtag"
-    And I log out
index 24612dd..d82b244 100644 (file)
@@ -134,6 +134,9 @@ if ($action === 'delete') {
         $PAGE->set_heading($SITE->fullname);
         echo $OUTPUT->header();
 
+        // Output edit mode title.
+        echo $OUTPUT->heading($strblogs . ': ' . get_string('deleteentry', 'blog'), 2);
+
         // Output the entry.
         $entry->prepare_render();
         echo $output->render($entry);
@@ -146,10 +149,12 @@ if ($action === 'delete') {
         die;
     }
 } else if ($action == 'add') {
-    $PAGE->set_title("$SITE->shortname: $strblogs: " . get_string('addnewentry', 'blog'));
+    $editmodetitle = $strblogs . ': ' . get_string('addnewentry', 'blog');
+    $PAGE->set_title("$SITE->shortname: $editmodetitle");
     $PAGE->set_heading(fullname($USER));
 } else if ($action == 'edit') {
-    $PAGE->set_title("$SITE->shortname: $strblogs: " . get_string('editentry', 'blog'));
+    $editmodetitle = $strblogs . ': ' . get_string('editentry', 'blog');
+    $PAGE->set_title("$SITE->shortname: $editmodetitle");
     $PAGE->set_heading(fullname($USER));
 }
 
@@ -270,6 +275,10 @@ $entry->modid = $modid;
 $entry->courseid = $courseid;
 
 echo $OUTPUT->header();
+// Output title for editing mode.
+if (isset($editmodetitle)) {
+    echo $OUTPUT->heading($editmodetitle, 2);
+}
 $blogeditform->display();
 echo $OUTPUT->footer();
 
index d464901..dc57b66 100644 (file)
@@ -365,7 +365,7 @@ class core_completion_external extends external_api {
                                          'type' => new external_value(PARAM_TEXT, 'Type description'),
                                          'criteria' => new external_value(PARAM_RAW, 'Criteria description'),
                                          'requirement' => new external_value(PARAM_TEXT, 'Requirement description'),
-                                         'status' => new external_value(PARAM_TEXT, 'Status description'),
+                                         'status' => new external_value(PARAM_RAW, 'Status description, can be anything'),
                                          ), 'details'),
                                  ), 'Completions'
                             ), ''
index 23e682c..f56e5dd 100644 (file)
@@ -15,7 +15,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Cron job for reviewing and aggregating course completion criteria
+ * Code used by scheduled tasks for reviewing and aggregating course completion criteria.
  *
  * @package core_completion
  * @category completion
 defined('MOODLE_INTERNAL') || die();
 require_once($CFG->libdir.'/completionlib.php');
 
-/**
- * Update user's course completion statuses
- *
- * First update all criteria completions, then aggregate all criteria completions
- * and update overall course completions
- */
-function completion_cron() {
-
-    completion_cron_mark_started();
-
-    completion_cron_criteria();
-
-    completion_cron_completions();
-}
-
 /**
  * Mark users as started if the config option is set
  *
index 6f41d19..0189fae 100644 (file)
@@ -122,6 +122,12 @@ if (!empty($course)) {
         $course->{'role_'.$alias->roleid} = $alias->name;
     }
 
+    // Populate course tags.
+    if (!empty($CFG->usetags)) {
+        include_once($CFG->dirroot.'/tag/lib.php');
+        $course->tags = tag_get_tags_array('course', $course->id);
+    }
+
 } else {
     // Editor should respect category context if course context is not set.
     $editoroptions['context'] = $catcontext;
index 54359aa..3faea92 100644 (file)
@@ -301,6 +301,13 @@ class course_edit_form extends moodleform {
             }
         }
 
+        if (!empty($CFG->usetags) &&
+                ((empty($course->id) && guess_if_creator_will_have_course_capability('moodle/course:tag', $categorycontext))
+                || (!empty($course->id) && has_capability('moodle/course:tag', $coursecontext)))) {
+            $mform->addElement('header', 'tagshdr', get_string('tags', 'tag'));
+            $mform->addElement('tags', 'tags', get_string('tags'));
+        }
+
         // When two elements we need a group.
         $buttonarray = array();
         if ($returnto !== 0) {
index 7c89127..5cbc191 100644 (file)
@@ -2547,7 +2547,8 @@ function course_overviewfiles_options($course) {
  * @return object new course instance
  */
 function create_course($data, $editoroptions = NULL) {
-    global $DB;
+    global $DB, $CFG;
+    require_once($CFG->dirroot.'/tag/lib.php');
 
     //check the categoryid - must be given for all new courses
     $category = $DB->get_record('course_categories', array('id'=>$data->category), '*', MUST_EXIST);
@@ -2623,6 +2624,11 @@ function create_course($data, $editoroptions = NULL) {
     // set up enrolments
     enrol_course_updated(true, $course, $data);
 
+    // Update course tags.
+    if ($CFG->usetags && isset($data->tags)) {
+        tag_set('course', $course->id, $data->tags, 'core', context_course::instance($course->id)->id);
+    }
+
     // Trigger a course created event.
     $event = \core\event\course_created::create(array(
         'objectid' => $course->id,
@@ -2646,7 +2652,8 @@ function create_course($data, $editoroptions = NULL) {
  * @return void
  */
 function update_course($data, $editoroptions = NULL) {
-    global $DB;
+    global $DB, $CFG;
+    require_once($CFG->dirroot.'/tag/lib.php');
 
     $data->timemodified = time();
 
@@ -2733,6 +2740,11 @@ function update_course($data, $editoroptions = NULL) {
     // update enrol settings
     enrol_course_updated(false, $course, $data);
 
+    // Update course tags.
+    if ($CFG->usetags && isset($data->tags)) {
+        tag_set('course', $course->id, $data->tags, 'core', context_course::instance($course->id)->id);
+    }
+
     // Trigger a course updated event.
     $event = \core\event\course_updated::create(array(
         'objectid' => $course->id,
diff --git a/course/tags.php b/course/tags.php
new file mode 100644 (file)
index 0000000..8e81fb1
--- /dev/null
@@ -0,0 +1,68 @@
+<?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/>.
+
+/**
+ * Edit course tags
+ *
+ * @package    core_course
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once("../config.php");
+require_once($CFG->dirroot . '/tag/lib.php');
+require_once($CFG->dirroot . '/course/tags_form.php');
+
+$id = required_param('id', PARAM_INT); // Course id.
+$returnurl = optional_param('return', null, PARAM_LOCALURL);
+$course = get_course($id);
+
+require_login();
+
+// Check capabilities but do not call require_login($course) - the user does not have to be enrolled.
+$context = context_course::instance($course->id);
+if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $context)) {
+    print_error('coursehidden', '', $CFG->wwwroot .'/');
+}
+require_capability('moodle/course:tag', $context);
+if (empty($CFG->usetags)) {
+    print_error('tagsaredisabled', 'tag');
+}
+
+$PAGE->set_course($course);
+$PAGE->set_pagelayout('incourse');
+$PAGE->set_url('/course/tags.php', array('id' => $course->id));
+$PAGE->set_title(get_string('coursetags', 'tag'));
+$PAGE->set_heading($course->fullname);
+
+$form = new coursetags_form();
+$data = array('id' => $course->id, 'tags' => tag_get_tags_array('course', $course->id));
+$form->set_data($data);
+
+$redirecturl = $returnurl ? new moodle_url($returnurl) : course_get_url($course);
+if ($form->is_cancelled()) {
+    redirect($redirecturl);
+} else if ($data = $form->get_data()) {
+    tag_set('course', $course->id, $data->tags, 'core', context_course::instance($course->id)->id);
+    redirect($redirecturl);
+}
+
+echo $OUTPUT->header();
+echo $OUTPUT->heading(get_string('coursetags', 'tag'));
+
+$form->display();
+
+echo $OUTPUT->footer();
diff --git a/course/tags_form.php b/course/tags_form.php
new file mode 100644 (file)
index 0000000..08cacc0
--- /dev/null
@@ -0,0 +1,52 @@
+<?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/>.
+
+/**
+ * Edit course tags form
+ *
+ * @package    core_course
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Edit course tags form
+ *
+ * @package    core_course
+ * @copyright  2015 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class coursetags_form extends moodleform {
+
+    /**
+     * Form definition
+     */
+    public function definition() {
+        $mform    = $this->_form;
+
+        $mform->addElement('tags', 'tags', get_string('tags'));
+
+        $mform->addElement('hidden', 'id', null);
+        $mform->setType('id', PARAM_INT);
+
+        $this->add_action_buttons();
+
+    }
+}
diff --git a/course/tests/behat/coursetags.feature b/course/tests/behat/coursetags.feature
new file mode 100644 (file)
index 0000000..425efa0
--- /dev/null
@@ -0,0 +1,98 @@
+@core @core_course @core_tag
+Feature: Tagging courses
+  In order to search courses
+  As a teacher
+  I need to be able to tag courses
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | teacher2 | Teacher | 2 | teacher2@example.com |
+      | user1    | User    | 1 | user1@example.com |
+    And the following "courses" exist:
+      | fullname  | shortname |
+      | Course 1  | c1        |
+      | Course 2  | c2        |
+    And the following "tags" exist:
+      | name         | tagtype  |
+      | Neverusedtag | official |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | c1     | editingteacher |
+      | teacher2 | c1     | teacher        |
+      | teacher1 | c2     | editingteacher |
+      | teacher2 | c2     | teacher        |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I click on "Edit settings" "link" in the "Administration" "block"
+    And I set the following fields to these values:
+      | Other tags (enter tags separated by commas) | Mathematics |
+    And I press "Save and display"
+    And I log out
+
+  Scenario: Set course tags using the course edit form
+    When I log in as "teacher1"
+    And I follow "Course 1"
+    And "Course tags" "link" should not exist in the "Administration" "block"
+    And I click on "Edit settings" "link" in the "Administration" "block"
+    And the field "Other tags (enter tags separated by commas)" matches value "Mathematics"
+    And I set the following fields to these values:
+      | Other tags (enter tags separated by commas) | Mathematics, Algebra |
+    And I press "Save and display"
+    And I click on "Dashboard" "link" in the "Navigation" "block"
+    And I follow "Course 2"
+    And I click on "Edit settings" "link" in the "Administration" "block"
+    And I set the following fields to these values:
+      | Other tags (enter tags separated by commas) | Mathematics, Geometry |
+    And I press "Save and display"
+    And I log out
+    And I log in as "user1"
+    And I navigate to "Tags" node in "Site pages"
+    And I follow "Mathematics"
+    Then I should see "Course 1"
+    And I should see "Course 2"
+    And I follow "Tags"
+    And I follow "Algebra"
+    And I should see "Course 1"
+    And I should not see "Course 2"
+    And I follow "Tags"
+    And I follow "Geometry"
+    And I should not see "Course 1"
+    And I should see "Course 2"
+    And I log out
+
+  Scenario: User can set course tags using separate form
+    Given I log in as "admin"
+    And I set the following system permissions of "Non-editing teacher" role:
+      | moodle/course:tag | Allow |
+    And I log out
+    When I log in as "teacher2"
+    And I follow "Course 1"
+    And "Edit settings" "link" should not exist in the "Administration" "block"
+    And I click on "Course tags" "link" in the "Administration" "block"
+    And the field "Other tags (enter tags separated by commas)" matches value "Mathematics"
+    And I set the following fields to these values:
+      | Other tags (enter tags separated by commas) | Mathematics, Algebra |
+    And I press "Save changes"
+    And I click on "Dashboard" "link" in the "Navigation" "block"
+    And I follow "Course 2"
+    And I click on "Course tags" "link" in the "Administration" "block"
+    And I set the following fields to these values:
+      | Other tags (enter tags separated by commas) | Mathematics, Geometry |
+    And I press "Save changes"
+    And I log out
+    And I log in as "user1"
+    And I navigate to "Tags" node in "Site pages"
+    And I follow "Mathematics"
+    Then I should see "Course 1"
+    And I should see "Course 2"
+    And I follow "Tags"
+    And I follow "Algebra"
+    And I should see "Course 1"
+    And I should not see "Course 2"
+    And I follow "Tags"
+    And I follow "Geometry"
+    And I should not see "Course 1"
+    And I should see "Course 2"
+    And I log out
index 08c50c8..a7af91c 100644 (file)
@@ -95,7 +95,7 @@ class enrol_meta_handler {
         list($enabled, $params) = $DB->get_in_or_equal(explode(',', $CFG->enrol_plugins_enabled), SQL_PARAMS_NAMED, 'e');
         $params['userid'] = $userid;
         $params['parentcourse'] = $instance->customint1;
-        $sql = "SELECT ue.*
+        $sql = "SELECT ue.*, e.status AS enrolstatus
                   FROM {user_enrolments} ue
                   JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol <> 'meta' AND e.courseid = :parentcourse AND e.enrol $enabled)
                  WHERE ue.userid = :userid";
@@ -142,23 +142,33 @@ class enrol_meta_handler {
             return;
         }
 
-        // is parent enrol active? (we ignore enrol starts and ends, sorry it would be too complex)
+        // Is parent enrol active? Find minimum timestart and maximum timeend of all active enrolments.
         $parentstatus = ENROL_USER_SUSPENDED;
+        $parenttimeend = null;
+        $parenttimestart = null;
         foreach ($parentues as $pue) {
-            if ($pue->status == ENROL_USER_ACTIVE) {
+            if ($pue->status == ENROL_USER_ACTIVE && $pue->enrolstatus == ENROL_INSTANCE_ENABLED) {
                 $parentstatus = ENROL_USER_ACTIVE;
-                break;
+                if ($parenttimeend === null || $pue->timeend == 0 || ($parenttimeend && $parenttimeend < $pue->timeend)) {
+                    $parenttimeend = $pue->timeend;
+                }
+                if ($parenttimestart === null || $parenttimestart > $pue->timestart) {
+                    $parenttimestart = $pue->timestart;
+                }
             }
         }
 
-        // enrol user if not enrolled yet or fix status
+        // Enrol user if not enrolled yet or fix status/timestart/timeend. Use the minimum timestart and maximum timeend found above.
         if ($ue) {
-            if ($parentstatus != $ue->status) {
-                $plugin->update_user_enrol($instance, $userid, $parentstatus);
+            if ($parentstatus != $ue->status ||
+                    ($parentstatus == ENROL_USER_ACTIVE && ($parenttimestart != $ue->timestart || $parenttimeend != $ue->timeend))) {
+                $plugin->update_user_enrol($instance, $userid, $parentstatus, $parenttimestart, $parenttimeend);
                 $ue->status = $parentstatus;
+                $ue->timestart = $parenttimestart;
+                $ue->timeend = $parenttimeend;
             }
         } else {
-            $plugin->enrol_user($instance, $userid, NULL, 0, 0, $parentstatus);
+            $plugin->enrol_user($instance, $userid, NULL, (int)$parenttimestart, (int)$parenttimeend, $parentstatus);
             $ue = new stdClass();
             $ue->userid = $userid;
             $ue->enrolid = $instance->id;
@@ -170,11 +180,13 @@ class enrol_meta_handler {
 
         $unenrolaction = $plugin->get_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES);
 
-        // only active users in enabled instances are supposed to have roles (we can reassign the roles any time later)
-        if ($ue->status != ENROL_USER_ACTIVE or $instance->status != ENROL_INSTANCE_ENABLED) {
+        // Only active users in enabled instances are supposed to have roles (we can reassign the roles any time later).
+        if ($ue->status != ENROL_USER_ACTIVE or $instance->status != ENROL_INSTANCE_ENABLED or
+                ($parenttimeend and $parenttimeend < time()) or ($parenttimestart > time())) {
             if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND) {
                 // Always keep the roles.
             } else if ($roles) {
+                // This will only unassign roles that were assigned in this enrolment method, leaving all manual role assignments intact.
                 role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$instance->id));
             }
             return;
@@ -279,17 +291,27 @@ function enrol_meta_sync($courseid = NULL, $verbose = false) {
     $allroles = get_all_roles();
 
 
-    // iterate through all not enrolled yet users
+    // Iterate through all not enrolled yet users. For each active enrolment of each user find the minimum
+    // enrolment startdate and maximum enrolment enddate.
+    // This SQL relies on the fact that ENROL_USER_ACTIVE < ENROL_USER_SUSPENDED
+    // and ENROL_INSTANCE_ENABLED < ENROL_INSTANCE_DISABLED. Condition "pue.status + pe.status = 0" means
+    // that enrolment is active. When MIN(pue.status + pe.status)=0 it means there exists an active
+    // enrolment.
     $onecourse = $courseid ? "AND e.courseid = :courseid" : "";
     list($enabled, $params) = $DB->get_in_or_equal(explode(',', $CFG->enrol_plugins_enabled), SQL_PARAMS_NAMED, 'e');
     $params['courseid'] = $courseid;
-    $sql = "SELECT pue.userid, e.id AS enrolid, pue.status
+    $sql = "SELECT pue.userid, e.id AS enrolid, MIN(pue.status + pe.status) AS status,
+                      MIN(CASE WHEN (pue.status + pe.status = 0) THEN pue.timestart ELSE 9999999999 END) AS timestart,
+                      MAX(CASE WHEN (pue.status + pe.status = 0) THEN
+                                (CASE WHEN pue.timeend = 0 THEN 9999999999 ELSE pue.timeend END)
+                                ELSE 0 END) AS timeend
               FROM {user_enrolments} pue
               JOIN {enrol} pe ON (pe.id = pue.enrolid AND pe.enrol <> 'meta' AND pe.enrol $enabled)
               JOIN {enrol} e ON (e.customint1 = pe.courseid AND e.enrol = 'meta' $onecourse)
               JOIN {user} u ON (u.id = pue.userid AND u.deleted = 0)
          LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = pue.userid)
-             WHERE ue.id IS NULL";
+             WHERE ue.id IS NULL
+             GROUP BY pue.userid, e.id";
 
     $rs = $DB->get_recordset_sql($sql, $params);
     foreach($rs as $ue) {
@@ -314,7 +336,15 @@ function enrol_meta_sync($courseid = NULL, $verbose = false) {
             }
         }
 
-        $meta->enrol_user($instance, $ue->userid, $ue->status);
+        // So now we have aggregated values that we will use for the meta enrolment status, timeend and timestart.
+        // Again, we use the fact that active=0 and disabled/suspended=1. Only when MIN(pue.status + pe.status)=0 the enrolment is active:
+        $ue->status = ($ue->status == ENROL_USER_ACTIVE + ENROL_INSTANCE_ENABLED) ? ENROL_USER_ACTIVE : ENROL_USER_SUSPENDED;
+        // Timeend 9999999999 was used instead of 0 in the "MAX()" function:
+        $ue->timeend = ($ue->timeend == 9999999999) ? 0 : (int)$ue->timeend;
+        // Timestart 9999999999 is only possible when there are no active enrolments:
+        $ue->timestart = ($ue->timestart == 9999999999) ? 0 : (int)$ue->timestart;
+
+        $meta->enrol_user($instance, $ue->userid, null, $ue->timestart, $ue->timeend, $ue->status);
         if ($instance->customint2) {
             groups_add_member($instance->customint2, $ue->userid, 'enrol_meta', $instance->id);
         }
@@ -371,29 +401,38 @@ function enrol_meta_sync($courseid = NULL, $verbose = false) {
     $rs->close();
 
 
-    // update status - meta enrols + start and end dates are ignored, sorry
-    // note the trick here is that the active enrolment and instance constants have value 0
+    // Update status - meta enrols are ignored to avoid recursion.
+    // Note the trick here is that the active enrolment and instance constants have value 0.
     $onecourse = $courseid ? "AND e.courseid = :courseid" : "";
     list($enabled, $params) = $DB->get_in_or_equal(explode(',', $CFG->enrol_plugins_enabled), SQL_PARAMS_NAMED, 'e');
     $params['courseid'] = $courseid;
-    $sql = "SELECT ue.userid, ue.enrolid, pue.pstatus
+    $sql = "SELECT ue.userid, ue.enrolid, pue.pstatus, pue.ptimestart, pue.ptimeend
               FROM {user_enrolments} ue
               JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'meta' $onecourse)
-              JOIN (SELECT xpue.userid, xpe.courseid, MIN(xpue.status + xpe.status) AS pstatus
+              JOIN (SELECT xpue.userid, xpe.courseid, MIN(xpue.status + xpe.status) AS pstatus,
+                      MIN(CASE WHEN (xpue.status + xpe.status = 0) THEN xpue.timestart ELSE 9999999999 END) AS ptimestart,
+                      MAX(CASE WHEN (xpue.status + xpe.status = 0) THEN
+                                (CASE WHEN xpue.timeend = 0 THEN 9999999999 ELSE xpue.timeend END)
+                                ELSE 0 END) AS ptimeend
                       FROM {user_enrolments} xpue
                       JOIN {enrol} xpe ON (xpe.id = xpue.enrolid AND xpe.enrol <> 'meta' AND xpe.enrol $enabled)
                   GROUP BY xpue.userid, xpe.courseid
                    ) pue ON (pue.courseid = e.customint1 AND pue.userid = ue.userid)
-             WHERE (pue.pstatus = 0 AND ue.status > 0) OR (pue.pstatus > 0 and ue.status = 0)";
+             WHERE (pue.pstatus = 0 AND ue.status > 0) OR (pue.pstatus > 0 and ue.status = 0)
+             OR ((CASE WHEN pue.ptimestart = 9999999999 THEN 0 ELSE pue.ptimestart END) <> ue.timestart)
+             OR ((CASE WHEN pue.ptimeend = 9999999999 THEN 0 ELSE pue.ptimeend END) <> ue.timeend)";
     $rs = $DB->get_recordset_sql($sql, $params);
     foreach($rs as $ue) {
         if (!isset($instances[$ue->enrolid])) {
             $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
         }
         $instance = $instances[$ue->enrolid];
-        $ue->pstatus = ($ue->pstatus == ENROL_USER_ACTIVE) ? ENROL_USER_ACTIVE : ENROL_USER_SUSPENDED;
+        $ue->pstatus = ($ue->pstatus == ENROL_USER_ACTIVE + ENROL_INSTANCE_ENABLED) ? ENROL_USER_ACTIVE : ENROL_USER_SUSPENDED;
+        $ue->ptimeend = ($ue->ptimeend == 9999999999) ? 0 : (int)$ue->ptimeend;
+        $ue->ptimestart = ($ue->ptimestart == 9999999999) ? 0 : (int)$ue->ptimestart;
 
-        if ($ue->pstatus == ENROL_USER_ACTIVE and !$syncall and $unenrolaction != ENROL_EXT_REMOVED_UNENROL) {
+        if ($ue->pstatus == ENROL_USER_ACTIVE and (!$ue->ptimeend || $ue->ptimeend > time())
+                and !$syncall and $unenrolaction != ENROL_EXT_REMOVED_UNENROL) {
             // this may be slow if very many users are ignored in sync
             $parentcontext = context_course::instance($instance->customint1);
             list($ignoreroles, $params) = $DB->get_in_or_equal($skiproles, SQL_PARAMS_NAMED, 'ri', false, -1);
@@ -409,7 +448,7 @@ function enrol_meta_sync($courseid = NULL, $verbose = false) {
             }
         }
 
-        $meta->update_user_enrol($instance, $ue->userid, $ue->pstatus);
+        $meta->update_user_enrol($instance, $ue->userid, $ue->pstatus, $ue->ptimestart, $ue->ptimeend);
         if ($verbose) {
             if ($ue->pstatus == ENROL_USER_ACTIVE) {
                 mtrace("  unsuspending: $ue->userid ==> $instance->courseid");
index 772c8d0..4d3b549 100644 (file)
@@ -743,4 +743,117 @@ class enrol_meta_plugin_testcase extends advanced_testcase {
         // Check that the group name has been changed.
         $this->assertEquals('Physics course (3)', $groupinfo->name);
     }
+
+    /**
+     * Test that enrolment timestart-timeend is respected in meta course.
+     */
+    public function test_timeend() {
+        global $CFG, $DB;
+
+        $this->resetAfterTest(true);
+
+        $timeinfuture = time() + DAYSECS;
+        $timeinpast = time() - DAYSECS;
+
+        $metalplugin = enrol_get_plugin('meta');
+        $manplugin = enrol_get_plugin('manual');
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        $user4 = $this->getDataGenerator()->create_user();
+        $user5 = $this->getDataGenerator()->create_user();
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+        $course3 = $this->getDataGenerator()->create_course();
+        $manual1 = $DB->get_record('enrol', array('courseid' => $course1->id, 'enrol' => 'manual'), '*', MUST_EXIST);
+
+        $student = $DB->get_record('role', array('shortname' => 'student'));
+
+        $this->enable_plugin();
+
+        // Create instance of enrol_meta in course2 when there are no enrolments present.
+        $meta2id = $metalplugin->add_instance($course2, array('customint1' => $course1->id));
+
+        $expectedenrolments = array(
+            $user1->id => array(0, 0, ENROL_USER_ACTIVE),
+            $user2->id => array($timeinpast, 0, ENROL_USER_ACTIVE),
+            $user3->id => array(0, $timeinfuture, ENROL_USER_ACTIVE),
+            $user4->id => array($timeinpast, $timeinfuture, ENROL_USER_ACTIVE),
+            $user5->id => array(0, 0, ENROL_USER_SUSPENDED),
+        );
+        foreach ($expectedenrolments as $userid => $data) {
+            $expectedenrolments[$userid] = (object)(array('userid' => $userid) +
+                    array_combine(array('timestart', 'timeend', 'status'), $data));
+        }
+
+        // Enrol users manually in course 1.
+        foreach ($expectedenrolments as $e) {
+            $manplugin->enrol_user($manual1, $e->userid, $student->id, $e->timestart, $e->timeend, $e->status);
+        }
+
+        $enrolments = $DB->get_records('user_enrolments', array('enrolid' => $manual1->id), 'userid', 'userid, timestart, timeend, status');
+        $this->assertEquals($expectedenrolments, $enrolments);
+
+        // Make sure that the same enrolments are now present in course2 under meta enrolment.
+        $enrolments = $DB->get_records('user_enrolments', array('enrolid' => $meta2id), '', 'userid, timestart, timeend, status');
+        $this->assertEquals($expectedenrolments, $enrolments);
+
+        // Create instance of enrol_meta in course3 and run sync.
+        $meta3id = $metalplugin->add_instance($course3, array('customint1' => $course1->id));
+        enrol_meta_sync($course3->id);
+
+        // Make sure that the same enrolments are now present in course3 under meta enrolment.
+        $enrolments = $DB->get_records('user_enrolments', array('enrolid' => $meta3id), '', 'userid, timestart, timeend, status');
+        $this->assertEquals($expectedenrolments, $enrolments);
+
+        // Update some of the manual enrolments.
+        $expectedenrolments[$user2->id]->timestart = $timeinpast - 60;
+        $expectedenrolments[$user3->id]->timeend = $timeinfuture + 60;
+        $expectedenrolments[$user4->id]->status = ENROL_USER_SUSPENDED;
+        $expectedenrolments[$user5->id]->status = ENROL_USER_ACTIVE;
+        foreach ($expectedenrolments as $e) {
+            $manplugin->update_user_enrol($manual1, $e->userid, $e->status, $e->timestart, $e->timeend);
+        }
+
+        // Make sure meta courses are also updated.
+        $enrolments = $DB->get_records('user_enrolments', array('enrolid' => $meta2id), '', 'userid, timestart, timeend, status');
+        $this->assertEquals($expectedenrolments, $enrolments);
+        $enrolments = $DB->get_records('user_enrolments', array('enrolid' => $meta3id), '', 'userid, timestart, timeend, status');
+        $this->assertEquals($expectedenrolments, $enrolments);
+
+        // Test meta sync. Imagine events are not working.
+        $sink = $this->redirectEvents();
+        $expectedenrolments[$user2->id]->timestart = $timeinpast;
+        $expectedenrolments[$user3->id]->timeend = $timeinfuture;
+        $expectedenrolments[$user4->id]->status = ENROL_USER_ACTIVE;
+        $expectedenrolments[$user5->id]->status = ENROL_USER_SUSPENDED;
+        foreach ($expectedenrolments as $e) {
+            $manplugin->update_user_enrol($manual1, $e->userid, $e->status, $e->timestart, $e->timeend);
+        }
+
+        // Make sure meta courses are updated only for the course that was synced.
+        enrol_meta_sync($course3->id);
+
+        $enrolments = $DB->get_records('user_enrolments', array('enrolid' => $meta2id), '', 'userid, timestart, timeend, status');
+        $this->assertNotEquals($expectedenrolments, $enrolments);
+
+        $enrolments = $DB->get_records('user_enrolments', array('enrolid' => $meta3id), '', 'userid, timestart, timeend, status');
+        $this->assertEquals($expectedenrolments, $enrolments);
+
+        $sink->close();
+
+        // Disable manual enrolment in course1 and make sure all user enrolments in course2 are suspended.
+        $manplugin->update_status($manual1, ENROL_INSTANCE_DISABLED);
+        $allsuspendedenrolemnts = array_combine(array_keys($expectedenrolments), array_fill(0, 5, ENROL_USER_SUSPENDED));
+        enrol_meta_sync($course3->id);
+        $enrolmentstatuses = $DB->get_records_menu('user_enrolments', array('enrolid' => $meta3id), '', 'userid, status');
+        $this->assertEquals($allsuspendedenrolemnts, $enrolmentstatuses);
+
+        $manplugin->update_status($manual1, ENROL_INSTANCE_ENABLED);
+        enrol_meta_sync($course3->id);
+        $enrolments = $DB->get_records('user_enrolments', array('enrolid' => $meta3id), '', 'userid, timestart, timeend, status');
+        $this->assertEquals($expectedenrolments, $enrolments);
+    }
 }
index e73bc93..672888a 100644 (file)
@@ -15,7 +15,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Edit and review page for grade categories and items
+ * The Gradebook setup page.
  *
  * @package   core_grades
  * @copyright 2008 Nicolas Connault
@@ -244,7 +244,7 @@ if ($data = data_submitted() and confirm_sesskey()) {
     }
 }
 
-print_grade_page_head($courseid, 'settings', 'setup', get_string('categoriesanditems', 'grades'));
+print_grade_page_head($courseid, 'settings', 'setup', get_string('gradebooksetup', 'grades'));
 
 // Print Table of categories and items
 echo $OUTPUT->box_start('gradetreebox generalbox');
index 0efde54..b6dc052 100644 (file)
@@ -435,9 +435,9 @@ class grade_edit_tree {
         return $str;
     }
 
-    //Trims trailing zeros
-    //Used on the 'categories and items' page for grade items settings like aggregation co-efficient
-    //Grader report has its own decimal place settings so they are handled elsewhere
+    // Trims trailing zeros.
+    // Used on the 'Gradebook setup' page for grade items settings like aggregation co-efficient.
+    // Grader report has its own decimal place settings so they are handled elsewhere.
     static function format_number($number) {
         $formatted = rtrim(format_float($number, 4),'0');
         if (substr($formatted, -1)==get_string('decsep', 'langconfig')) { //if last char is the decimal point
index fbfe359..2216d57 100644 (file)
@@ -2832,9 +2832,9 @@ abstract class grade_helper {
         $context = context_course::instance($courseid);
         self::$managesetting = array();
         if ($courseid != SITEID && has_capability('moodle/grade:manage', $context)) {
-            self::$managesetting['categoriesanditems'] = new grade_plugin_info('setup',
+            self::$managesetting['gradebooksetup'] = new grade_plugin_info('setup',
                 new moodle_url('/grade/edit/tree/index.php', array('id' => $courseid)),
-                get_string('categoriesanditems', 'grades'));
+                get_string('gradebooksetup', 'grades'));
             self::$managesetting['coursesettings'] = new grade_plugin_info('coursesettings',
                 new moodle_url('/grade/edit/settings/index.php', array('id'=>$courseid)),
                 get_string('coursegradesettings', 'grades'));
index e9cbcbd..727aeb0 100644 (file)
@@ -80,7 +80,8 @@ class behat_grade extends behat_base {
     }
 
     /**
-     * Sets a calculated manual grade item. Needs a table with item name - idnumber relation. The step requires you to be in categories and items page.
+     * Sets a calculated manual grade item. Needs a table with item name - idnumber relation.
+     * The step requires you to be in the 'Gradebook setup' page.
      *
      * @Given /^I set "(?P<calculation_string>(?:[^"]|\\")*)" calculation for grade item "(?P<grade_item_string>(?:[^"]|\\")*)" with idnumbers:$/
      * @param string $calculation The calculation.
@@ -139,7 +140,7 @@ class behat_grade extends behat_base {
 
     /**
      * Sets a calculated manual grade category total. Needs a table with item name - idnumber relation.
-     * The step requires you to be in categories and items page.
+     * The step requires you to be in the 'Gradebook setup' page.
      *
      * @Given /^I set "(?P<calculation_string>(?:[^"]|\\")*)" calculation for grade category "(?P<grade_item_string>(?:[^"]|\\")*)" with idnumbers:$/
      * @param string $calculation The calculation.
index d10d14a..09f1b04 100644 (file)
@@ -235,7 +235,7 @@ Feature: We can use calculated grade totals
       | itemname              | course | outcome | gradetype | scale      |
       | Test outcome item one | C1     | OT1     | Scale     | Test Scale |
     And I expand "Setup" node
-    And I follow "Categories and items"
+    And I follow "Gradebook setup"
     And I set the following settings for grade item "Course 1":
       | Aggregation                     | Natural |
       | Include outcomes in aggregation | 1       |
@@ -258,7 +258,7 @@ Feature: We can use calculated grade totals
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
     And I expand "Setup" node
-    And I follow "Categories and items"
+    And I follow "Gradebook setup"
     And I set the following settings for grade item "Test outcome item one":
      | Extra credit     | 1   |
     And I log out
@@ -272,7 +272,7 @@ Feature: We can use calculated grade totals
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
     And I expand "Setup" node
-    And I follow "Categories and items"
+    And I follow "Gradebook setup"
     And I set the following settings for grade item "Course 1":
       | Aggregation                     | Natural |
       | Include outcomes in aggregation | 0       |
@@ -297,7 +297,7 @@ Feature: We can use calculated grade totals
       | Test outcome item one | C1     | OT1     | Scale     | Test Scale |
     And I navigate to "Grades" node in "Course administration"
     And I expand "Setup" node
-    And I follow "Categories and items"
+    And I follow "Gradebook setup"
     And I set the following settings for grade item "Course 1":
       | Aggregation                     | Natural |
       | Include outcomes in aggregation | 1       |
@@ -387,7 +387,7 @@ Feature: We can use calculated grade totals
     And I set the following settings for grade item "Course 1":
       | Aggregation          | Natural |
       | Exclude empty grades | 0       |
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I press "Add category"
     And I set the following fields to these values:
       | Category name | Sub category 3 |
@@ -441,7 +441,7 @@ Feature: We can use calculated grade totals
     And I press "Save changes"
     And I turn editing mode off
     And I should see "250.00 (25.25 %)" in the ".course" "css_element"
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I press "Add category"
     And I set the following fields to these values:
       | Category name | Sub sub category 1 |
@@ -452,7 +452,7 @@ Feature: We can use calculated grade totals
 
   @javascript
   Scenario: Natural aggregation from the setup screen
-    And I select "Categories and items" from the "Grade report" singleselect
+    And I select "Gradebook setup" from the "Grade report" singleselect
     And I set the following settings for grade item "Course 1":
       | Aggregation          | Natural |
     And I set the following settings for grade item "Sub category 1":
@@ -514,7 +514,7 @@ Feature: We can use calculated grade totals
       | Aggregation          | Natural |
       | Exclude empty grades | 0       |
     And I turn editing mode off
-    And I select "Categories and items" from the "Grade report" singleselect
+    And I select "Gradebook setup" from the "Grade report" singleselect
     And I set the field "Override weight of Test assignment one" to "1"
     And I set the field "Weight of Test assignment one" to "0"
     And I set the field "Override weight of Test assignment six" to "1"
index 7453a73..fd792e0 100644 (file)
@@ -336,7 +336,7 @@ Feature: Changing the aggregation of an item affects its weight and extra credit
   @javascript
   Scenario: Switching grade items between categories
     # Move to same aggregation (Natural).
-    Given I navigate to "Categories and items" node in "Grade administration > Setup"
+    Given I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the field "Select Item a1" to "1"
     And I set the field "Select Item a2" to "1"
     And I set the field "Select Item a3" to "1"
@@ -362,7 +362,7 @@ Feature: Changing the aggregation of an item affects its weight and extra credit
     And the field "Extra credit" matches value "1"
     And I press "Cancel"
     # Move to Mean of grades (with extra credit).
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the field "Select Item a1" to "1"
     And I set the field "Select Item a2" to "1"
     And I set the field "Select Item a3" to "1"
@@ -382,7 +382,7 @@ Feature: Changing the aggregation of an item affects its weight and extra credit
     And the field "Extra credit" matches value "1"
     And I press "Cancel"
     # Move to Simple weight mean of grades.
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the field "Select Item a1" to "1"
     And I set the field "Select Item a2" to "1"
     And I set the field "Select Item a3" to "1"
@@ -402,7 +402,7 @@ Feature: Changing the aggregation of an item affects its weight and extra credit
     And the field "Extra credit" matches value "1"
     And I press "Cancel"
     # Move to Weighted mean of grades.
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the field "Select Item a1" to "1"
     And I set the field "Select Item a2" to "1"
     And I set the field "Select Item a3" to "1"
@@ -426,7 +426,7 @@ Feature: Changing the aggregation of an item affects its weight and extra credit
     And I set the field "Item weight" to "11"
     And I press "Save changes"
     # Move to same (Weighted mean of grades).
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the field "Select Item a1" to "1"
     And I set the field "Select Item a2" to "1"
     And I set the field "Select Item a3" to "1"
@@ -447,7 +447,7 @@ Feature: Changing the aggregation of an item affects its weight and extra credit
     And the field "Item weight" matches value "11"
     And I press "Save changes"
     # Move back to Natural.
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the field "Select Item a1" to "1"
     And I set the field "Select Item a2" to "1"
     And I set the field "Select Item a3" to "1"
index 1899e4d..e52ab6a 100644 (file)
@@ -30,7 +30,7 @@ Feature: Average grades are displayed in the gradebook
       | Show average | Show |
     And I press "Save changes"
     # Add a manual grade item
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I press "Add grade item"
     And I set the following fields to these values:
       | Item name | Manual item 1 |
index e295c02..145a39e 100644 (file)
@@ -2,7 +2,7 @@
 Feature: Calculated grade items can be used in the gradebook
   In order to use calculated grade items in the gradebook
   As a teacher
-  I need setup calculated grade items in the categories and items page.
+  I need setup calculated grade items in the 'Gradebook setup' page.
 
   Background:
     Given the following "courses" exist:
@@ -22,7 +22,7 @@ Feature: Calculated grade items can be used in the gradebook
     And I am on site homepage
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
 
   @javascript
   Scenario: The max grade for a category item, with a calculation using Natural aggregation, can be changed
@@ -77,7 +77,7 @@ Feature: Calculated grade items can be used in the gradebook
       | grade item 1                        | -                 | 75.00  | 0–100 | 75.00 %    | -                            |
       | Calc cat totalInclude empty grades. | 100.00 %          | 37.50  | 0–50  | 75.00 %    | -                            |
       | Course total                        | -                 | 37.50  | 0–50  | 75.00 %    | -                            |
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the following settings for grade item "Calc cat":
       | Maximum grade | 40 |
     And I follow "Grader report"
@@ -143,7 +143,7 @@ Feature: Calculated grade items can be used in the gradebook
       | grade item 1 | 66.67 %           | 75.00  | 0–100 | 75.00 %    | 50.00 %                      |
       | calc item    | 33.33 %           | 37.50  | 0–50  | 75.00 %    | 25.00 %                      |
       | Course total | -                 | 112.50 | 0–150 | 75.00 %    | -                            |
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the following settings for grade item "calc item":
       | Maximum grade | 40 |
     And I follow "Grader report"
index 62d5e3e..cd8d791 100644 (file)
@@ -23,7 +23,7 @@ Feature: Gradebook calculations for calculated grade items before the fix 201506
     And I am on site homepage
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
 
   @javascript
   Scenario: The max grade for a category item, with a calculation using Natural aggregation, can be changed
@@ -78,7 +78,7 @@ Feature: Gradebook calculations for calculated grade items before the fix 201506
       | grade item 1                        | -                 | 75.00  | 0–100 | 75.00 %    | -                            |
       | Calc cat totalInclude empty grades. | 100.00 %          | 37.50  | 0–100 | 37.50 %    | -                            |
       | Course total                        | -                 | 37.50  | 0–100 | 37.50 %    | -                            |
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the following settings for grade item "Calc cat":
       | Maximum grade | 40 |
     And I follow "Grader report"
@@ -144,7 +144,7 @@ Feature: Gradebook calculations for calculated grade items before the fix 201506
       | grade item 1 | 50.00 %           | 75.00  | 0–100 | 75.00 %    | 37.50 %                      |
       | calc item    | 50.00 %           | 37.50  | 0–100 | 37.50 %    | 18.75 %                      |
       | Course total | -                 | 112.50 | 0–200 | 56.25 %    | -                            |
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the following settings for grade item "calc item":
       | Maximum grade | 40 |
     And I follow "Grader report"
index e914de5..e899835 100644 (file)
@@ -43,7 +43,7 @@ Feature: We can understand the gradebook user report
     And I set the field "Show weightings" to "Show"
     And I set the field "Show contribution to course total" to "Show"
     And I press "Save changes"
-    And I set the field "Grade report" to "Categories and items"
+    And I set the field "Grade report" to "Gradebook setup"
     And I press "Add category"
     And I set the field "Category name" to "Sub category"
     And I press "Save changes"
index 896c567..7f60a15 100644 (file)
@@ -22,7 +22,7 @@ Feature: Extra credit contributions are normalised when going out of bounds
     And I am on site homepage
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I press "Add grade item"
     And I set the following fields to these values:
       | Item name | Manual item 1 |
@@ -56,7 +56,7 @@ Feature: Extra credit contributions are normalised when going out of bounds
     And I press "Save changes"
 
   Scenario Outline: The contribution of extra credit items is normalised
-    Given I set the field "Grade report" to "Categories and items"
+    Given I set the field "Grade report" to "Gradebook setup"
     When I set the following settings for grade item "Course 1":
       | Aggregation | <aggregation> |
     And I set the following settings for grade item "Manual item 2":
index 919b745..a886c39 100644 (file)
@@ -28,7 +28,7 @@ Feature: We can use a minimum grade different than zero
     And I am on site homepage
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I press "Add grade item"
     And I set the following fields to these values:
       | Item name | Manual item 1 |
@@ -73,7 +73,7 @@ Feature: We can use a minimum grade different than zero
 
   @javascript
   Scenario: Natural aggregation with negative and positive grade
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the following settings for grade item "Sub category 1":
       | Aggregation          | Natural |
       | Exclude empty grades | 0       |
index b956a5a..cb1aa15 100644 (file)
@@ -27,7 +27,7 @@ Feature: We can choose what min or max grade to use when aggregating grades.
     And I am on site homepage
     And I follow "C1"
     And I navigate to "Grades" node in "Course administration"
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I press "Add grade item"
     And I set the following fields to these values:
       | Item name | MI 1 |
@@ -62,7 +62,7 @@ Feature: We can choose what min or max grade to use when aggregating grades.
     And I set the field "Show weightings" to "Show"
     And I set the field "Show contribution to course total" to "Show"
     And I press "Save changes"
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the following settings for grade item "CAT1":
       | Aggregation          | Natural |
     And I log out
@@ -98,7 +98,7 @@ Feature: We can choose what min or max grade to use when aggregating grades.
       | MI 5         | 33.33 %           | 30.00  | 0–100 | 30.00 %    | 10.00 %                      |
       | CAT1 total   | 33.33 %           | 10.00  | 0–100 | 10.00 %    | -                            |
       | Course total | -                 | 60.00  | 0–300 | 20.00 %    | -                            |
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the following settings for grade item "MI 1":
       | Maximum grade          | 50.00 |
       | Minimum grade          | 5.00 |
@@ -126,7 +126,7 @@ Feature: We can choose what min or max grade to use when aggregating grades.
       | MI 5         | 50.00 %           | 30.00  | 0–100 | 30.00 %    | 15.00 %                      |
       | CAT1 total   | 25.00 %           | 10.00  | 0–50  | 20.00 %    | -                            |
       | Course total | -                 | 60.00  | 0–200 | 30.00 %    | -                            |
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the following settings for grade item "MI 5":
       | Maximum grade          | 200.00 |
     And I follow "User report"
index de2f83a..22c35aa 100644 (file)
@@ -26,7 +26,7 @@ Feature: Weights in natural aggregation are adjusted if the items are excluded f
     And I log in as "teacher1"
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
-    And I set the field "Grade report" to "Categories and items"
+    And I set the field "Grade report" to "Gradebook setup"
     And I set the following settings for grade item "Test assignment four (extra)":
       | Extra credit | 1 |
     And I set the following settings for grade item "Test assignment five (extra)":
index b4b97cf..e3722f3 100644 (file)
@@ -27,7 +27,7 @@ Feature: Gradebook calculations for extra credit items before the fix 20150619
     And I log in as "teacher1"
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
-    And I set the field "Grade report" to "Categories and items"
+    And I set the field "Grade report" to "Gradebook setup"
     And I set the following settings for grade item "Test assignment four (extra)":
       | Extra credit | 1 |
     And I set the following settings for grade item "Test assignment five (extra)":
index bd7ea43..6f8a16b 100644 (file)
@@ -33,7 +33,7 @@ Feature: We can use natural aggregation and weights will be normalised to a tota
     And I log in as "teacher1"
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
-    And I set the field "Grade report" to "Categories and items"
+    And I set the field "Grade report" to "Gradebook setup"
 
   @javascript
   Scenario: Setting all weights in a category to exactly one hundred in total.
index b46f33f..6b488e1 100644 (file)
@@ -34,7 +34,7 @@ Feature: Gradebook calculations for natural weights normalisation before the fix
     And I log in as "teacher1"
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
-    And I set the field "Grade report" to "Categories and items"
+    And I set the field "Grade report" to "Gradebook setup"
 
   @javascript
   Scenario: Grade items weights are normalised when all grade item weights are overridden (sum exactly 100). Extra credit is set to zero (before the fix 20150619).
index 5cd48b0..ed42b97 100644 (file)
@@ -90,7 +90,7 @@ Feature: View gradebook when scales are used
       | Test assignment one | C     | F–A   | 50.00 %    | 60.00 %                      |
       | Sub category 1 total      | 3.00  | 0–5   | 60.00 %    | -                            |
       | Course total        | 3.00  | 0–5   | 60.00 %    | -                            |
-    And I select "Categories and items" from the "Grade report" singleselect
+    And I select "Gradebook setup" from the "Grade report" singleselect
     And the following should exist in the "grade_edit_tree_table" table:
       | Name                | Max grade |
       | Test assignment one | 5.00      |
@@ -136,7 +136,7 @@ Feature: View gradebook when scales are used
       | Test assignment one          | C              | F–A   | 50.00 %       | <contrib3>                   |
       | Sub category (<aggregation>) total<aggregation>. | 3.00           | 1–5   | 50.00 %       | -                            |
       | Course total<aggregation>.   | <coursetotal3> | 0–100 | <courseperc3> | -                            |
-    And I select "Categories and items" from the "Grade report" singleselect
+    And I select "Gradebook setup" from the "Grade report" singleselect
     And the following should exist in the "grade_edit_tree_table" table:
       | Name                | Max grade |
       | Test assignment one | A (5)     |
index b71b035..2fec297 100644 (file)
@@ -90,7 +90,7 @@ Feature: Control the aggregation of the scales
     And I turn editing mode on
     When I set the following settings for grade item "Course 1":
       | Aggregation | Natural |
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the field "Override weight of Grade me" to "1"
     Then the field "Override weight of Grade me" matches value "100.00"
     And I click on "Edit" "link" in the "Scale me" "table_row"
@@ -102,7 +102,7 @@ Feature: Control the aggregation of the scales
       | grade_includescalesinaggregation | 1 |
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
-    And I navigate to "Categories and items" node in "Grade administration > Setup"
+    And I navigate to "Gradebook setup" node in "Grade administration > Setup"
     And I set the field "Override weight of Grade me" to "1"
     And the field "Override weight of Grade me" matches value "95.238"
     And I set the field "Override weight of Scale me" to "1"
index 867c70c..190e1f7 100644 (file)
@@ -79,7 +79,7 @@ Feature: View gradebook when single item scales are used
       | Test assignment one | -     | Ace!–Ace! | -                            |
       | Sub category 1 total| -     | 0–1       | -                            |
       | Course total        | -     | 0–1       | -                            |
-    And I set the field "jump" to "Categories and items"
+    And I set the field "jump" to "Gradebook setup"
     And the following should exist in the "grade_edit_tree_table" table:
       | Name                | Max grade |
       | Test assignment one | 1.00      |
@@ -115,7 +115,7 @@ Feature: View gradebook when single item scales are used
       | Test assignment one                              | Ace!           | Ace!–Ace!   | <contrib1>                   |
       | Sub category (<aggregation>) total<aggregation>. | <cattotal1>    | 0–100       | -                            |
       | Course total<aggregation>.                       | <coursetotal1> | 0–100       | -                            |
-    And I set the field "jump" to "Categories and items"
+    And I set the field "jump" to "Gradebook setup"
     And the following should exist in the "grade_edit_tree_table" table:
       | Name                         | Max grade |
       | Test assignment one          | Ace! (1)  |
index 9aa8684..2632fed 100644 (file)
@@ -80,7 +80,7 @@ Feature: We can enter in grades and view reports from the gradebook
     And "Course 1" row "Grade" column of "overview-grade" table should not contain "90.00"
 
   Scenario: We can add a weighting to a grade item and it is displayed properly in the user report
-    When I select "Categories and items" from the "Grade report" singleselect
+    When I select "Gradebook setup" from the "Grade report" singleselect
     And I set the following settings for grade item "Course 1":
       | Aggregation | Weighted mean of grades |
     And I set the field "Extra credit value for Test assignment name" to "0.72"
index aa51b27..cb96fa0 100644 (file)
@@ -30,7 +30,7 @@ require_once($CFG->dirroot.'/grade/edit/tree/lib.php');
 
 
 /**
- * Tests grade_edit_tree (deals with the data on the categories and items page in the gradebook)
+ * Tests grade_edit_tree (deals with the data on the 'Gradebook setup' page in the gradebook)
  */
 class core_grade_edittreelib_testcase extends advanced_testcase {
     public function test_format_number() {
index 6d114c5..173ab92 100644 (file)
@@ -206,7 +206,6 @@ foreach ($members as $gpgid=>$groupdata) {
         echo $OUTPUT->heading($groupings[$gpgid]->formattedname, 3);
         $description = file_rewrite_pluginfile_urls($groupings[$gpgid]->description, 'pluginfile.php', $context->id, 'grouping', 'description', $gpgid);
         $options = new stdClass;
-        $options->noclean = true;
         $options->overflowdiv = true;
         echo $OUTPUT->box(format_text($description, $groupings[$gpgid]->descriptionformat, $options), 'generalbox boxwidthnarrow boxaligncenter');
     }
diff --git a/install/lang/xct/langconfig.php b/install/lang/xct/langconfig.php
new file mode 100644 (file)
index 0000000..5cc0b59
--- /dev/null
@@ -0,0 +1,33 @@
+<?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['thislanguage'] = 'བོད་ཡིག';
index 97bb9df..99715f5 100644 (file)
@@ -1019,7 +1019,8 @@ $string['taskcachecleanup'] = 'Remove expired cache entries';
 $string['taskcachecron'] = 'Background processing for caches';
 $string['taskcalendarcron'] = 'Send calendar notifications';
 $string['taskcheckforupdates'] = 'Check for updates';
-$string['taskcompletioncron'] = 'Calculate completion data';
+$string['taskcompletionregular'] = 'Calculate regular completion data';
+$string['taskcompletiondaily'] = 'Completion mark as started';
 $string['taskcontextcleanup'] = 'Cleanup contexts';
 $string['taskcreatecontexts'] = 'Create missing contexts';
 $string['taskdeletecachetext'] = 'Delete old text cache records';
index f1ba2de..a37b84a 100644 (file)
@@ -68,6 +68,7 @@ $string['courseblogdisable'] = 'Course blogs are not enabled';
 $string['courseblogs'] = 'Users can only see blogs for people who share a course';
 $string['deleteblogassociations'] = 'Delete blog associations';
 $string['deleteblogassociations_help'] = 'If ticked then blog entries will no longer be associated with this course or any course activities or resources.  The blog entries themselves will not be deleted.';
+$string['deleteentry'] = 'Delete entry';
 $string['deleteexternalblog'] = 'Unregister this external blog';
 $string['deleteotagswarn'] = 'Are you sure you want to remove these tags from all blog posts and remove it from the system?';
 $string['description'] = 'Description';
index f5167f3..ab59bdb 100644 (file)
@@ -18,3 +18,5 @@ tagtype_official,core_tag
 thistaghasnodesc,core_tag
 updated,core_tag
 withselectedtags,core_tag
+tag:create,core_role
+categoriesanditems,core_grades
index 0d58b13..dd5ace3 100644 (file)
@@ -388,6 +388,7 @@ $string['movecatcontentstoroot'] = 'Moving the category content to root is not a
 $string['movecategorynotpossible'] = 'You cannot move category \'{$a}\' into the selected category.';
 $string['movecategoryownparent'] = 'You cannot make category \'{$a}\' a parent of itself.';
 $string['movecategoryparentconflict'] = 'You cannot make category \'{$a}\' a subcategory of one of its own subcategories.';
+$string['mssqlrcsmodemissing'] = 'The database is not using the expected READ_COMMITTED_SNAPSHOT mode which can lead to wrong results, especially under high concurrency scenarios. Please enable it for correct behaviour. You can find more information in the <a href="https://docs.moodle.org/en/Installing_MSSQL_for_PHP#Configuration">Moodle Docs</a>.';
 $string['multiplerecordsfound'] = 'Multiple records found, only one record expected.';
 $string['multiplerestorenotallow'] = 'Multiple restore execution not allowed!';
 $string['mustbeloggedin'] = 'You must be logged in to do this';
index 1840aa1..da10569 100644 (file)
@@ -66,7 +66,7 @@ $string['aggregationhintexcluded'] = '( Excluded )';
 $string['aggregationhintextra'] = '( Extra credit )';
 $string['aggregation_link'] = 'grade/aggregation';
 $string['aggregationcoef'] = 'Aggregation coefficient';
-$string['aggregationcoefextra'] = 'Extra credit'; // for the header of the table at Edit categories and items page
+$string['aggregationcoefextra'] = 'Extra credit'; // For the header of the table on the 'Gradebook setup' page.
 $string['aggregationcoefextra_help'] = 'If the aggregation is \'Natural\' or \'Simple weighted mean\' and the extra credit checkbox is ticked, the grade item\'s maximum grade is not added to the category\'s maximum grade. This will result in the possibility of achieving the maximum grade in the category without having the maximum grade in all the grade items. If the site administrator has enabled grades over the maximum, there might be grades over the maximum.
 
 If the aggregation is \'Mean of grades (with extra credits)\' and the extra credit is set to a value greater than zero, the extra credit is the factor by which the grade is multiplied before adding it to the total after the computation of the mean.';
@@ -112,7 +112,6 @@ $string['calculationsaved'] = 'Calculation saved';
 $string['calculationview'] = 'View calculation';
 $string['cannotaccessgroup'] = 'Can not access grades of selected group, sorry.';
 $string['categories'] = 'Categories';
-$string['categoriesanditems'] = 'Categories and items';
 $string['category'] = 'Category';
 $string['categoryedit'] = 'Edit category';
 $string['categoryname'] = 'Category name';
@@ -244,6 +243,7 @@ $string['gradebookcalculationsfixbutton'] = 'Accept grade changes and fix calcul
 $string['gradebookcalculationswarning'] = 'Note: Some errors have been detected in calculating the grades displayed in the gradebook. It is recommended that the errors are fixed by clicking the button below, though this will result in some grades being changed. For details, see the changes between versions {$a->gradebookversion} and {$a->currentversion} in <a href="{$a->url}">Gradebook calculation changes</a>.';
 $string['gradebookhiddenerror'] = 'The gradebook is currently set to hide everything from students.';
 $string['gradebookhistories'] = 'Grade histories';
+$string['gradebooksetup'] = 'Gradebook setup';
 $string['gradeboundary'] = 'Letter grade boundary';
 $string['gradeboundary_help'] = 'This setting determines the minimum percentage over which grades will be assigned the grade letter.';
 $string['gradecategories'] = 'Grade categories';
@@ -789,3 +789,6 @@ $string['writinggradebookinfo'] = 'Writing gradebook settings';
 $string['xml'] = 'XML';
 $string['yes'] = 'Yes';
 $string['yourgrade'] = 'Your grade';
+
+// Deprecated since 3.0.
+$string['categoriesanditems'] = 'Categories and items';
index 0806544..4b2ee09 100644 (file)
@@ -146,6 +146,7 @@ $string['course:reset'] = 'Reset course';
 $string['course:reviewotherusers'] = 'Review other users';
 $string['course:sectionvisibility'] = 'Control section visibility';
 $string['course:setcurrentsection'] = 'Set current section';
+$string['course:tag'] = 'Change course tags';
 $string['course:update'] = 'Update course settings';
 $string['course:useremail'] = 'Enable/disable email address';
 $string['course:view'] = 'View courses without participation';
@@ -373,7 +374,6 @@ $string['site:viewfullnames'] = 'Always see full names of users';
 $string['site:viewparticipants'] = 'View participants';
 $string['site:viewreports'] = 'View reports';
 $string['site:viewuseridentity'] = 'See full user identity in lists';
-$string['tag:create'] = 'Create new tags';
 $string['tag:edit'] = 'Edit existing tags';
 $string['tag:editblocks'] = 'Edit blocks in tags pages';
 $string['tag:manage'] = 'Manage all tags';
@@ -424,3 +424,6 @@ $string['whydoesusernothavecap'] = 'Why does {$a->fullname} not have capability
 $string['xroleassignments'] = '{$a}\'s role assignments';
 $string['xuserswiththerole'] = 'Users with the role "{$a->role}"';
 
+// Deprecated in 3.0.
+
+$string['tag:create'] = 'Create new tags';
index 5437674..ef4be4b 100644 (file)
@@ -29,6 +29,7 @@ $string['alltagpages'] = 'All tag pages';
 $string['confirmdeletetag'] = 'Are you sure you want to delete this tag?';
 $string['confirmdeletetags'] = 'Are you sure you want to delete selected tags?';
 $string['count'] = 'Count';
+$string['coursetags'] = 'Course tags';
 $string['delete'] = 'Delete';
 $string['deleteselected'] = 'Delete selected';
 $string['deleted'] = 'Tag(s) deleted';
index 36079b6..82d7588 100644 (file)
@@ -117,6 +117,7 @@ $string['iprestriction'] = 'IP restriction';
 $string['iprestriction_help'] = 'The user will need to call the web service from the listed IPs (separated by commas).';
 $string['key'] = 'Key';
 $string['keyshelp'] = 'The keys are used to access your Moodle account from external applications.';
+$string['loginrequired'] = 'Restricted to logged in users';
 $string['manageprotocols'] = 'Manage protocols';
 $string['managetokens'] = 'Manage tokens';
 $string['missingcaps'] = 'Missing capabilities';
similarity index 64%
rename from blocks/tags/settings.php
rename to lib/ajax/service-nologin.php
index 4db5098..7120ade 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Settings for the tags block.
+ * This file is used to call any registered externallib function in Moodle.
  *
- * @package   block_tags
- * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
+ * It will process more than one request and return more than one response if required.
+ * It is recommended to add webservice functions and re-use this script instead of
+ * writing any new custom ajax scripts.
+ *
+ * @since Moodle 2.9
+ * @package core
+ * @copyright 2015 Damyon Wiese
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-defined('MOODLE_INTERNAL') || die;
-
-if ($ADMIN->fulltree) {
-    $settings->add(new admin_setting_configcheckbox('block_tags_showcoursetags', get_string('showcoursetags', 'block_tags'),
-                       get_string('showcoursetagsdef', 'block_tags'), 0));
-}
+define('NO_MOODLE_COOKIES', true);
+require_once('service.php');
index 938ffa8..ce58f01 100644 (file)
@@ -32,9 +32,6 @@ define('AJAX_SCRIPT', true);
 require_once(dirname(__FILE__) . '/../../config.php');
 require_once($CFG->libdir . '/externallib.php');
 
-require_login(null, true, null, true, true);
-require_sesskey();
-
 $rawjson = file_get_contents('php://input');
 
 $requests = json_decode($rawjson, true);
@@ -55,9 +52,25 @@ foreach ($requests as $request) {
         $externalfunctioninfo = external_function_info($methodname);
 
         if (!$externalfunctioninfo->allowed_from_ajax) {
+            error_log('This external function is not available to ajax. Failed to call "' . $methodname . '"');
             throw new moodle_exception('servicenotavailable', 'webservice');
         }
 
+        // Do not allow access to write or delete webservices as a public user.
+        if ($externalfunctioninfo->loginrequired) {
+            if (defined('NO_MOODLE_COOKIES') && NO_MOODLE_COOKIES) {
+                error_log('Set "loginrequired" to false in db/service.php when calling entry point service-nologin.php. ' .
+                          'Failed to call "' . $methodname . '"');
+                throw new moodle_exception('servicenotavailable', 'webservice');
+            }
+            if (!isloggedin()) {
+                error_log('This external function is not available to public users. Failed to call "' . $methodname . '"');
+                throw new moodle_exception('servicenotavailable', 'webservice');
+            } else {
+                require_sesskey();
+            }
+        }
+
         // Validate params, this also sorts the params properly, we need the correct order in the next part.
         $callable = array($externalfunctioninfo->classname, 'validate_parameters');
         $params = call_user_func($callable,
index 2a80dfe..7ed899b 100644 (file)
Binary files a/lib/amd/build/ajax.min.js and b/lib/amd/build/ajax.min.js differ
index ace0e93..33d26c9 100644 (file)
Binary files a/lib/amd/build/str.min.js and b/lib/amd/build/str.min.js differ
index ed4aa32..c75432b 100644 (file)
Binary files a/lib/amd/build/templates.min.js and b/lib/amd/build/templates.min.js differ
index 2966cb4..6b497c3 100644 (file)
@@ -104,9 +104,13 @@ define(['jquery', 'core/config'], function($, config) {
          *                   can be attached to the promises returned by this function.
          * @param {Boolean} async Optional, defaults to true.
          *                  If false - this function will not return until the promises are resolved.
+         * @param {Boolean} loginrequired Optional, defaults to true.
+         *                  If false - this function will call the faster nologin ajax script - but
+         *                  will fail unless all functions have been marked as 'loginrequired' => false
+         *                  in services.php
          * @return {Promise[]} Array of promises that will be resolved when the ajax call returns.
          */
-        call: function(requests, async) {
+        call: function(requests, async, loginrequired) {
             var ajaxRequestData = [],
                 i,
                 promises = [];
@@ -144,15 +148,20 @@ define(['jquery', 'core/config'], function($, config) {
                 async: async
             };
 
+            var script = config.wwwroot + '/lib/ajax/service.php?sesskey=' + config.sesskey;
+            if (!loginrequired) {
+                script = config.wwwroot + '/lib/ajax/service-nologin.php?sesskey=' + config.sesskey;
+            }
+
             // Jquery deprecated done and fail with async=false so we need to do this 2 ways.
             if (async) {
-                $.ajax(config.wwwroot + '/lib/ajax/service.php?sesskey=' + config.sesskey, settings)
+                $.ajax(script, settings)
                     .done(requestSuccess)
                     .fail(requestFail);
             } else {
                 settings.success = requestSuccess;
                 settings.error = requestFail;
-                $.ajax(config.wwwroot + '/lib/ajax/service.php?sesskey=' + config.sesskey, settings);
+                $.ajax(script, settings);
             }
 
             return promises;
index d66c164..76830d3 100644 (file)
@@ -140,7 +140,7 @@ define(['jquery', 'core/ajax', 'core/localstorage'], function($, ajax, storage)
                     });
                 }
 
-                var deferreds = ajax.call(ajaxrequests);
+                var deferreds = ajax.call(ajaxrequests, true, false);
                 $.when.apply(null, deferreds).done(
                     function() {
                         // Turn the list of arguments (unknown length) into a real array.
index 93ed7d1..a152e03 100644 (file)
@@ -310,7 +310,7 @@ define([ 'core/mustache',
                 template: name,
                 themename: currentThemeName
             }
-        }], async);
+        }], async, false);
 
         promises[0].done(
             function (templateSource) {
index 17b1ca7..a76248e 100644 (file)
@@ -55,16 +55,6 @@ class external extends external_api {
             );
     }
 
-    /**
-     * Can this function be called directly from ajax?
-     *
-     * @return boolean
-     * @since Moodle 2.9
-     */
-    public static function load_template_is_allowed_from_ajax() {
-        return true;
-    }
-
     /**
      * Return a mustache template, and all the strings it requires.
      *
similarity index 77%
rename from lib/classes/task/completion_cron_task.php
rename to lib/classes/task/completion_daily_task.php
index b34860a..a6597d8 100644 (file)
 namespace core\task;
 
 /**
- * Simple task to run the completion cron.
+ * Simple task to run the daily completion cron.
+ * @copyright  2013 onwards Martin Dougiamas  http://dougiamas.com.
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
  */
-class completion_cron_task extends scheduled_task {
+class completion_daily_task extends scheduled_task {
 
     /**
      * Get a descriptive name for this task (shown to admins).
@@ -34,7 +36,7 @@ class completion_cron_task extends scheduled_task {
      * @return string
      */
     public function get_name() {
-        return get_string('taskcompletioncron', 'admin');
+        return get_string('taskcompletiondaily', 'admin');
     }
 
     /**
@@ -45,9 +47,9 @@ class completion_cron_task extends scheduled_task {
         global $CFG;
 
         if ($CFG->enablecompletion) {
-            // Completion cron.
+            // Daily Completion cron.
             require_once($CFG->dirroot.'/completion/cron.php');
-            completion_cron();
+            completion_cron_mark_started();
         }
     }
 
diff --git a/lib/classes/task/completion_regular_task.php b/lib/classes/task/completion_regular_task.php
new file mode 100644 (file)
index 0000000..19be43f
--- /dev/null
@@ -0,0 +1,57 @@
+<?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/>.
+
+/**
+ * A scheduled task.
+ *
+ * @package    core
+ * @copyright  2015 Josh Willcock
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core\task;
+
+/**
+ * Simple task to run the regular completion cron.
+ * @copyright  2015 Josh Willcock
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
+ */
+class completion_regular_task extends scheduled_task {
+
+    /**
+     * Get a descriptive name for this task (shown to admins).
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('taskcompletionregular', 'admin');
+    }
+
+    /**
+     * Do the job.
+     * Throw exceptions on errors (the job will be retried).
+     */
+    public function execute() {
+        global $CFG;
+
+        if ($CFG->enablecompletion) {
+            // Regular Completion cron.
+            require_once($CFG->dirroot.'/completion/cron.php');
+            completion_cron_criteria();
+            completion_cron_completions();
+        }
+    }
+
+}
index 0dae9d0..7dac929 100644 (file)
@@ -1090,6 +1090,17 @@ $capabilities = array(
         )
     ),
 
+    'moodle/course:tag' => array(
+        'riskbitmask' => RISK_SPAM,
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_COURSE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW,
+        ),
+        'clonepermissionsfrom' => 'moodle/course:update'
+    ),
+
     'moodle/blog:view' => array(
 
         'captype' => 'read',
@@ -1697,23 +1708,10 @@ $capabilities = array(
         'captype' => 'write',
         'contextlevel' => CONTEXT_SYSTEM,
         'archetypes' => array(
-            'teacher' => CAP_ALLOW,
-            'editingteacher' => CAP_ALLOW,
             'manager' => CAP_ALLOW
         )
     ),
 
-    'moodle/tag:create' => array(
-        'riskbitmask' => RISK_SPAM,
-
-        'captype' => 'write',
-        'contextlevel' => CONTEXT_SYSTEM,
-        'archetypes' => array(
-            'manager' => CAP_ALLOW,
-            'user' => CAP_ALLOW
-        )
-    ),
-
     'moodle/tag:edit' => array(
         'riskbitmask' => RISK_SPAM,
 
@@ -1730,7 +1728,6 @@ $capabilities = array(
         'captype' => 'write',
         'contextlevel' => CONTEXT_SYSTEM,
         'archetypes' => array(
-            'manager' => CAP_ALLOW,
             'user' => CAP_ALLOW
         )
     ),
index aa3850f..837a755 100644 (file)
       </KEYS>
       <INDEXES>
         <INDEX NAME="name" UNIQUE="true" FIELDS="name" COMMENT="tag names are unique"/>
-        <INDEX NAME="idname" UNIQUE="true" FIELDS="id, name" COMMENT="Improves performance of autocomplete in tag block"/>
       </INDEXES>
     </TABLE>
     <TABLE NAME="tag_correlation" COMMENT="The rationale for the 'tag_correlation' table is performance.   It works as a cache for a potentially heavy load query done at the 'tag_instance' table.   So, the 'tag_correlation' table stores redundant information derived from the 'tag_instance' table">
index 70187da..b3b43f6 100644 (file)
@@ -975,6 +975,8 @@ $functions = array(
         'classpath'   => 'lib/external/externallib.php',
         'description' => 'Return a translated string - similar to core get_string() call',
         'type'        => 'read',
+        'loginrequired' => false,
+        'ajax'        => true,
     ),
 
     'core_get_strings' => array(
@@ -983,6 +985,8 @@ $functions = array(
         'classpath'   => 'lib/external/externallib.php',
         'description' => 'Return some translated strings - like several core get_string() calls',
         'type'        => 'read',
+        'loginrequired' => false,
+        'ajax'        => true,
     ),
 
     'core_get_component_strings' => array(
@@ -992,6 +996,8 @@ $functions = array(
         'description' => 'Return all raw strings (with {$a->xxx}) for a specific component
             - similar to core get_component_strings() call',
         'type'        => 'read',
+        'loginrequired' => false,
+        'ajax'        => true,
     ),
 
 
@@ -1029,7 +1035,9 @@ $functions = array(
         'classname'   => 'core\output\external',
         'methodname'  => 'load_template',
         'description' => 'Load a template for a renderable',
-        'type'        => 'read'
+        'type'        => 'read',
+        'loginrequired' => false,
+        'ajax'        => true,
     ),
 
     // Completion related functions.
@@ -1071,6 +1079,7 @@ $functions = array(
         'methodname'  => 'update_tags',
         'description' => 'Updates tags',
         'type'        => 'write',
+        'ajax'        => true
     ),
 
     'core_tag_get_tags' => array(
@@ -1078,6 +1087,7 @@ $functions = array(
         'methodname'  => 'get_tags',
         'description' => 'Gets tags by their ids',
         'type'        => 'read',
+        'ajax'        => true
     ),
 
 );
@@ -1124,6 +1134,8 @@ $services = array(
             'mod_forum_get_forums_by_courses',
             'mod_forum_get_forum_discussions_paginated',
             'mod_forum_get_forum_discussion_posts',
+            'mod_forum_add_discussion_post',
+            'mod_forum_add_discussion',
             'core_files_get_files',
             'core_message_get_messages',
             'core_message_create_contacts',
@@ -1160,6 +1172,18 @@ $services = array(
             'mod_page_view_page',
             'mod_resource_view_resource',
             'mod_folder_view_folder',
+            'mod_chat_login_user',
+            'mod_chat_get_chat_users',
+            'mod_chat_send_chat_message',
+            'mod_chat_get_chat_latest_messages',
+            'mod_chat_view_chat',
+            'mod_chat_get_chats_by_courses',
+            'mod_book_view_book',
+            'mod_choice_get_choice_results',
+            'mod_choice_get_choice_options',
+            'mod_choice_submit_choice_response',
+            'mod_choice_view_choice',
+            'mod_imscp_view_imscp',
             ),
         'enabled' => 0,
         'restrictedusers' => 0,
index 6c4d7d2..9723d33 100644 (file)
@@ -159,7 +159,7 @@ $tasks = array(
         'month' => '*'
     ),
     array(
-        'classname' => 'core\task\completion_cron_task',
+        'classname' => 'core\task\completion_regular_task',
         'blocking' => 0,
         'minute' => '*',
         'hour' => '*',
@@ -167,6 +167,15 @@ $tasks = array(
         'dayofweek' => '*',
         'month' => '*'
     ),
+    array(
+        'classname' => 'core\task\completion_daily_task',
+        'blocking' => 0,
+        'minute' => 'R',
+        'hour' => 'R',
+        'day' => '*',
+        'dayofweek' => '*',
+        'month' => '*'
+    ),
     array(
         'classname' => 'core\task\portfolio_cron_task',
         'blocking' => 0,
index 75b5b2b..221943a 100644 (file)
@@ -4530,5 +4530,33 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2015090200.00);
     }
 
+    if ($oldversion < 2015090801.00) {
+        // This upgrade script merges all tag instances pointing to the same course tag.
+        // User id is no longer used for those tag instances.
+        upgrade_course_tags();
+
+        // If configuration variable "Show course tags" is set, disable the block
+        // 'tags' because it can not be used for tagging courses any more.
+        if (!empty($CFG->block_tags_showcoursetags)) {
+            if ($record = $DB->get_record('block', array('name' => 'tags'), 'id, visible')) {
+                if ($record->visible) {
+                    $DB->update_record('block', array('id' => $record->id, 'visible' => 0));
+                }
+            }
+        }
+
+        // Define index idname (unique) to be dropped form tag (it's really weird).
+        $table = new xmldb_table('tag');
+        $index = new xmldb_index('idname', XMLDB_INDEX_UNIQUE, array('id', 'name'));
+
+        // Conditionally launch drop index idname.
+        if ($dbman->index_exists($table, $index)) {
+            $dbman->drop_index($table, $index);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2015090801.00);
+    }
+
     return true;
 }
index 2c901e6..5b0c838 100644 (file)
@@ -667,4 +667,24 @@ function upgrade_calculated_grade_items($courseid = null) {
             }
         }
     }
-}
\ No newline at end of file
+}
+
+/**
+ * This upgrade script merges all tag instances pointing to the same course tag
+ *
+ * User id is no longer used for those tag instances
+ */
+function upgrade_course_tags() {
+    global $DB;
+    $sql = "SELECT min(ti.id)
+        FROM {tag_instance} ti
+        LEFT JOIN {tag_instance} tii on tii.itemtype = ? and tii.itemid = ti.itemid and tii.tiuserid = 0 and tii.tagid = ti.tagid
+        where ti.itemtype = ? and ti.tiuserid <> 0 AND tii.id is null
+        group by ti.tagid, ti.itemid";
+    $ids = $DB->get_fieldset_sql($sql, array('course', 'course'));
+    if ($ids) {
+        list($idsql, $idparams) = $DB->get_in_or_equal($ids);
+        $DB->execute('UPDATE {tag_instance} SET tiuserid = 0 WHERE id ' . $idsql, $idparams);
+    }
+    $DB->execute("DELETE FROM {tag_instance} WHERE itemtype = ? AND tiuserid <> 0", array('course'));
+}
index 82eab5a..54b71d3 100644 (file)
@@ -2398,3 +2398,328 @@ function is_web_crawler() {
     debugging("is_web_crawler() has been deprecated, please use \\core_useragent\\is_web_crawler() instead.", DEBUG_DEVELOPER);
     return core_useragent::is_crawler();
 }
+
+/**
+ * Update user's course completion statuses
+ *
+ * First update all criteria completions, then aggregate all criteria completions
+ * and update overall course completions.
+ *
+ * @deprecated since Moodle 3.0 MDL-50287 - please do not use this function any more.
+ * @todo Remove this function in Moodle 3.2 MDL-51226.
+ */
+function completion_cron() {
+    global $CFG;
+    require_once($CFG->dirroot.'/completion/cron.php');
+
+    debugging('completion_cron() is deprecated. Functionality has been moved to scheduled tasks.', DEBUG_DEVELOPER);
+    completion_cron_mark_started();
+
+    completion_cron_criteria();
+
+    completion_cron_completions();
+}
+
+/**
+ * Returns an ordered array of tags associated with visible courses
+ * (boosted replacement of get_all_tags() allowing association with user and tagtype).
+ *
+ * @deprecated since 3.0
+ * @package  core_tag
+ * @category tag
+ * @param    int      $courseid A course id. Passing 0 will return all distinct tags for all visible courses
+ * @param    int      $userid   (optional) the user id, a default of 0 will return all users tags for the course
+ * @param    string   $tagtype  (optional) The type of tag, empty string returns all types. Currently (Moodle 2.2) there are two
+ *                              types of tags which are used within Moodle, they are 'official' and 'default'.
+ * @param    int      $numtags  (optional) number of tags to display, default of 80 is set in the block, 0 returns all
+ * @param    string   $unused   (optional) was selected sorting, moved to tag_print_cloud()
+ * @return   array
+ */
+function coursetag_get_tags($courseid, $userid=0, $tagtype='', $numtags=0, $unused = '') {
+    debugging('Function coursetag_get_tags() is deprecated. Userid is no longer used for tagging courses.', DEBUG_DEVELOPER);
+
+    global $CFG, $DB;
+
+    // get visible course ids
+    $courselist = array();
+    if ($courseid === 0) {
+        if ($courses = $DB->get_records_select('course', 'visible=1 AND category>0', null, '', 'id')) {
+            foreach ($courses as $key => $value) {
+                $courselist[] = $key;
+            }
+        }
+    }
+
+    // get tags from the db ordered by highest count first
+    $params = array();
+    $sql = "SELECT id as tkey, name, id, tagtype, rawname, f.timemodified, flag, count
+              FROM {tag} t,
+                 (SELECT tagid, MAX(timemodified) as timemodified, COUNT(id) as count
+                    FROM {tag_instance}
+                   WHERE itemtype = 'course' ";
+
+    if ($courseid > 0) {
+        $sql .= "    AND itemid = :courseid ";
+        $params['courseid'] = $courseid;
+    } else {
+        if (!empty($courselist)) {
+            list($usql, $uparams) = $DB->get_in_or_equal($courselist, SQL_PARAMS_NAMED);
+            $sql .= "AND itemid $usql ";
+            $params = $params + $uparams;
+        }
+    }
+
+    if ($userid > 0) {
+        $sql .= "    AND tiuserid = :userid ";
+        $params['userid'] = $userid;
+    }
+
+    $sql .= "   GROUP BY tagid) f
+             WHERE t.id = f.tagid ";
+    if ($tagtype != '') {
+        $sql .= "AND tagtype = :tagtype ";
+        $params['tagtype'] = $tagtype;
+    }
+    $sql .= "ORDER BY count DESC, name ASC";
+
+    // limit the number of tags for output
+    if ($numtags == 0) {
+        $tags = $DB->get_records_sql($sql, $params);
+    } else {
+        $tags = $DB->get_records_sql($sql, $params, 0, $numtags);
+    }
+
+    // prepare the return
+    $return = array();
+    if ($tags) {
+        // avoid print_tag_cloud()'s ksort upsetting ordering by setting the key here
+        foreach ($tags as $value) {
+            $return[] = $value;
+        }
+    }
+
+    return $return;
+
+}
+
+/**
+ * Returns an ordered array of tags
+ * (replaces popular_tags_count() allowing sorting).
+ *
+ * @deprecated since 3.0
+ * @package  core_tag
+ * @category tag
+ * @param    string $unused (optional) was selected sorting - moved to tag_print_cloud()
+ * @param    int    $numtags (optional) number of tags to display, default of 20 is set in the block, 0 returns all
+ * @return   array
+ */
+function coursetag_get_all_tags($unused='', $numtags=0) {
+    debugging('Function coursetag_get_all_tag() is deprecated. Userid is no longer used for tagging courses.', DEBUG_DEVELOPER);
+
+    global $CFG, $DB;
+
+    // note that this selects all tags except for courses that are not visible
+    $sql = "SELECT id, name, tagtype, rawname, f.timemodified, flag, count
+        FROM {tag} t,
+        (SELECT tagid, MAX(timemodified) as timemodified, COUNT(id) as count
+            FROM {tag_instance} WHERE tagid NOT IN
+                (SELECT tagid FROM {tag_instance} ti, {course} c
+                WHERE c.visible = 0
+                AND ti.itemtype = 'course'
+                AND ti.itemid = c.id)
+        GROUP BY tagid) f
+        WHERE t.id = f.tagid
+        ORDER BY count DESC, name ASC";
+    if ($numtags == 0) {
+        $tags = $DB->get_records_sql($sql);
+    } else {
+        $tags = $DB->get_records_sql($sql, null, 0, $numtags);
+    }
+
+    $return = array();
+    if ($tags) {
+        foreach ($tags as $value) {
+            $return[] = $value;
+        }
+    }
+
+    return $return;
+}
+
+/**
+ * Returns javascript for use in tags block and supporting pages
+ *
+ * @deprecated since 3.0
+ * @package  core_tag
+ * @category tag
+ * @return   null
+ */
+function coursetag_get_jscript() {
+    debugging('Function coursetag_get_jscript() is deprecated and obsolete.', DEBUG_DEVELOPER);
+    return '';
+}
+
+/**
+ * Returns javascript to create the links in the tag block footer.
+ *
+ * @deprecated since 3.0
+ * @package  core_tag
+ * @category tag
+ * @param    string   $elementid       the element to attach the footer to
+ * @param    array    $coursetagslinks links arrays each consisting of 'title', 'onclick' and 'text' elements
+ * @return   string   always returns a blank string
+ */
+function coursetag_get_jscript_links($elementid, $coursetagslinks) {
+    debugging('Function coursetag_get_jscript_links() is deprecated and obsolete.', DEBUG_DEVELOPER);
+    return '';
+}
+
+/**
+ * Returns all tags created by a user for a course
+ *
+ * @deprecated since 3.0
+ * @package  core_tag
+ * @category tag
+ * @param    int      $courseid tags are returned for the course that has this courseid
+ * @param    int      $userid   return tags which were created by this user
+ */
+function coursetag_get_records($courseid, $userid) {
+    debugging('Function coursetag_get_records() is deprecated. Userid is no longer used for tagging courses.', DEBUG_DEVELOPER);
+
+    global $CFG, $DB;
+
+    $sql = "SELECT t.id, name, rawname
+              FROM {tag} t, {tag_instance} ti
+             WHERE t.id = ti.tagid
+                 AND ti.tiuserid = :userid
+                 AND ti.itemid = :courseid
+          ORDER BY name ASC";
+
+    return $DB->get_records_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid));
+}
+
+/**
+ * Stores a tag for a course for a user
+ *
+ * @deprecated since 3.0
+ * @package  core_tag
+ * @category tag
+ * @param    array  $tags     simple array of keywords to be stored
+ * @param    int    $courseid the id of the course we wish to store a tag for
+ * @param    int    $userid   the id of the user we wish to store a tag for
+ * @param    string $tagtype  official or default only
+ * @param    string $myurl    (optional) for logging creation of course tags
+ */
+function coursetag_store_keywords($tags, $courseid, $userid=0, $tagtype='official', $myurl='') {
+    debugging('Function coursetag_store_keywords() is deprecated. Userid is no longer used for tagging courses.', DEBUG_DEVELOPER);
+
+    global $CFG;
+    require_once $CFG->dirroot.'/tag/lib.php';
+
+    if (is_array($tags) and !empty($tags)) {
+        foreach ($tags as $tag) {
+            $tag = trim($tag);
+            if (strlen($tag) > 0) {
+                //tag_set_add('course', $courseid, $tag, $userid); //deletes official tags
+
+                //add tag if does not exist
+                if (!$tagid = tag_get_id($tag)) {
+                    $tag_id_array = tag_add(array($tag), $tagtype);
+                    $tagid = $tag_id_array[core_text::strtolower($tag)];
+                }
+                //ordering
+                $ordering = 0;
+                if ($current_ids = tag_get_tags_ids('course', $courseid)) {
+                    end($current_ids);
+                    $ordering = key($current_ids) + 1;
+                }
+                //set type
+                tag_type_set($tagid, $tagtype);
+
+                //tag_instance entry
+                tag_assign('course', $courseid, $tagid, $ordering, $userid, 'core', context_course::instance($courseid)->id);
+            }
+        }
+    }
+
+}
+
+/**
+ * Deletes a personal tag for a user for a course.
+ *
+ * @deprecated since 3.0
+ * @package  core_tag
+ * @category tag
+ * @param    int      $tagid    the tag we wish to delete
+ * @param    int      $userid   the user that the tag is associated with
+ * @param    int      $courseid the course that the tag is associated with
+ */
+function coursetag_delete_keyword($tagid, $userid, $courseid) {
+    debugging('Function coursetag_delete_keyword() is deprecated. Userid is no longer used for tagging courses.', DEBUG_DEVELOPER);
+
+    tag_delete_instance('course', $courseid, $tagid, $userid);
+}
+
+/**
+ * Get courses tagged with a tag
+ *
+ * @deprecated since 3.0
+ * @package  core_tag
+ * @category tag
+ * @param int $tagid
+ * @return array of course objects
+ */
+function coursetag_get_tagged_courses($tagid) {
+    debugging('Function coursetag_get_tagged_courses() is deprecated. Userid is no longer used for tagging courses.', DEBUG_DEVELOPER);
+
+    global $DB;
+
+    $courses = array();
+
+    $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
+
+    $sql = "SELECT c.*, $ctxselect
+            FROM {course} c
+            JOIN {tag_instance} t ON t.itemid = c.id
+            JOIN {context} ctx ON ctx.instanceid = c.id
+            WHERE t.tagid = :tagid AND
+            t.itemtype = 'course' AND
+            ctx.contextlevel = :contextlevel
+            ORDER BY c.sortorder ASC";
+    $params = array('tagid' => $tagid, 'contextlevel' => CONTEXT_COURSE);
+    $rs = $DB->get_recordset_sql($sql, $params);
+    foreach ($rs as $course) {
+        context_helper::preload_from_record($course);
+        if ($course->visible == 1 || has_capability('moodle/course:viewhiddencourses', context_course::instance($course->id))) {
+            $courses[$course->id] = $course;
+        }
+    }
+    return $courses;
+}
+
+/**
+ * Course tagging function used only during the deletion of a course (called by lib/moodlelib.php) to clean up associated tags
+ *
+ * @package core_tag
+ * @deprecated since 3.0
+ * @param   int      $courseid     the course we wish to delete tag instances from
+ * @param   bool     $showfeedback if we should output a notification of the delete to the end user
+ */
+function coursetag_delete_course_tags($courseid, $showfeedback=false) {
+    debugging('Function coursetag_delete_course_tags() is deprecated. Userid is no longer used for tagging courses.', DEBUG_DEVELOPER);
+
+    global $DB, $OUTPUT;
+
+    if ($taginstances = $DB->get_recordset_select('tag_instance', "itemtype = 'course' AND itemid = :courseid",
+        array('courseid' => $courseid), '', 'tagid, tiuserid')) {
+
+        foreach ($taginstances as $record) {
+            tag_delete_instance('course', $courseid, $record->tagid, $record->tiuserid);
+        }
+        $taginstances->close();
+    }
+
+    if ($showfeedback) {
+        echo $OUTPUT->notification(get_string('deletedcoursetags', 'tag'), 'notifysuccess');
+    }
+}
index c6a6b9c..96df899 100644 (file)
@@ -98,6 +98,37 @@ class mssql_native_moodle_database extends moodle_database {
         return get_string('nativemssqlhelp', 'install');
     }
 
+    /**
+     * Diagnose database and tables, this function is used
+     * to verify database and driver settings, db engine types, etc.
+     *
+     * @return string null means everything ok, string means problem found.
+     */
+    public function diagnose() {
+        // Verify the database is running with READ_COMMITTED_SNAPSHOT enabled.
+        // (that's required to get snapshots/row versioning on READ_COMMITED mode).
+        $correctrcsmode = false;
+        $sql = "SELECT is_read_committed_snapshot_on
+                  FROM sys.databases
+                 WHERE name = '{$this->dbname}'";
+        $this->query_start($sql, null, SQL_QUERY_AUX);
+        $result = mssql_query($sql, $this->mssql);
+        $this->query_end($result);
+        if ($result) {
+            if ($row = mssql_fetch_assoc($result)) {
+                $correctrcsmode = (bool)reset($row);
+            }
+        }
+        $this->free_result($result);
+
+        if (!$correctrcsmode) {
+            return get_string('mssqlrcsmodemissing', 'error');
+        }
+
+        // Arrived here, all right.
+        return null;
+    }
+
     /**
      * Connect to db
      * Must be called before other methods.
index 36d7893..07d110b 100644 (file)
@@ -117,6 +117,37 @@ class sqlsrv_native_moodle_database extends moodle_database {
         return get_string('nativesqlsrvhelp', 'install');
     }
 
+    /**
+     * Diagnose database and tables, this function is used
+     * to verify database and driver settings, db engine types, etc.
+     *
+     * @return string null means everything ok, string means problem found.
+     */
+    public function diagnose() {
+        // Verify the database is running with READ_COMMITTED_SNAPSHOT enabled.
+        // (that's required to get snapshots/row versioning on READ_COMMITED mode).
+        $correctrcsmode = false;
+        $sql = "SELECT is_read_committed_snapshot_on
+                  FROM sys.databases
+                 WHERE name = '{$this->dbname}'";
+        $this->query_start($sql, null, SQL_QUERY_AUX);
+        $result = sqlsrv_query($this->sqlsrv, $sql);
+        $this->query_end($result);
+        if ($result) {
+            if ($row = sqlsrv_fetch_array($result)) {
+                $correctrcsmode = (bool)reset($row);
+            }
+        }
+        $this->free_result($result);
+
+        if (!$correctrcsmode) {
+            return get_string('mssqlrcsmodemissing', 'error');
+        }
+
+        // Arrived here, all right.
+        return null;
+    }
+
     /**
      * Connect to db
      * Must be called before most other methods. (you can call methods that return connection configuration parameters)
index a450917..f09653d 100644 (file)
@@ -103,6 +103,11 @@ class atto_texteditor extends texteditor {
                     continue;
                 }
 
+                // Remove manage files if requested.
+                if ($plugin == 'managefiles' && isset($options['enable_filemanagement']) && !$options['enable_filemanagement']) {
+                    continue;
+                }
+
                 $jsplugin = array();
                 $jsplugin['name'] = $plugin;
                 $jsplugin['params'] = array();
index 6aaab94..e827092 100644 (file)
@@ -108,6 +108,13 @@ class tinymce_texteditor extends texteditor {
             $config->disabledsubplugins = '';
         }
 
+        // Remove the manage files button if requested.
+        if (isset($options['enable_filemanagement']) && !$options['enable_filemanagement']) {
+            if (!strpos($config->disabledsubplugins, 'managefiles')) {
+                $config->disabledsubplugins .= ',managefiles';
+            }
+        }
+
         $fontselectlist = empty($config->fontselectlist) ? '' : $config->fontselectlist;
 
         $langrev = -1;
index 1909c2f..a0526c5 100644 (file)
@@ -77,16 +77,6 @@ class core_external extends external_api {
         return $strparams;
     }
 
-    /**
-     * Can this function be called directly from ajax?
-     *
-     * @return boolean
-     * @since Moodle 2.9
-     */
-    public static function get_string_is_allowed_from_ajax() {
-        return true;
-    }
-
     /**
      * Returns description of get_string parameters
      *
@@ -163,17 +153,6 @@ class core_external extends external_api {
         );
     }
 
-    /**
-     * Can this function be called directly from ajax?
-     *
-     * @return boolean
-     * @since Moodle 2.9
-     */
-    public static function get_strings_is_allowed_from_ajax() {
-        return true;
-    }
-
-
     /**
      * Return multiple call to core get_string()
      *
@@ -237,16 +216,6 @@ class core_external extends external_api {
         );
     }
 
-    /**
-     * Can this function be called directly from ajax?
-     *
-     * @return boolean
-     * @since Moodle 2.9
-     */
-    public static function get_component_strings_is_allowed_from_ajax() {
-        return true;
-    }
-
     /**
      * Return all lang strings of a component - call to core get_component_strings().
      *
index 035ba63..4746206 100644 (file)
@@ -77,11 +77,6 @@ function external_function_info($function, $strictness=MUST_EXIST) {
         }
     }
     $function->allowed_from_ajax = false;
-    if (method_exists($function->classname, $function->ajax_method)) {
-        if (call_user_func(array($function->classname, $function->ajax_method)) === true) {
-            $function->allowed_from_ajax = true;
-        }
-    }
 
     // fetch the parameters description
     $function->parameters_desc = call_user_func(array($function->classname, $function->parameters_method));
@@ -112,6 +107,23 @@ function external_function_info($function, $strictness=MUST_EXIST) {
         if (isset($functions[$function->name]['testclientpath'])) {
             $function->testclientpath = $functions[$function->name]['testclientpath'];
         }
+        if (isset($functions[$function->name]['type'])) {
+            $function->type = $functions[$function->name]['type'];
+        }
+        if (isset($functions[$function->name]['ajax'])) {
+            $function->allowed_from_ajax = $functions[$function->name]['ajax'];
+        } else if (method_exists($function->classname, $function->ajax_method)) {
+            if (call_user_func(array($function->classname, $function->ajax_method)) === true) {
+                debugging('External function ' . $function->ajax_method . '() function is deprecated.' .
+                          'Set ajax=>true in db/service.php instead.', DEBUG_DEVELOPER);
+                $function->allowed_from_ajax = true;
+            }
+        }
+        if (isset($functions[$function->name]['loginrequired'])) {
+            $function->loginrequired = $functions[$function->name]['loginrequired'];
+        } else {
+            $function->loginrequired = true;
+        }
     }
 
     return $function;
@@ -346,7 +358,7 @@ class external_api {
      * @param stdClass $context
      * @since Moodle 2.0
      */
-    protected static function validate_context($context) {
+    public static function validate_context($context) {
         global $CFG;
 
         if (empty($context)) {
@@ -876,3 +888,46 @@ class external_settings {
         return $this->file;
     }
 }
+
+/**
+ * Utility functions for the external API.
+ *
+ * @package    core_webservice
+ * @copyright  2015 Juan Leyva
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 3.0
+ */
+class external_util {
+
+    /**
+     * Validate a list of courses, returning the complete course objects for valid courses.
+     *
+     * @param  array $courseids A list of course ids
+     * @return array            An array of courses and the validation warnings
+     */
+    public static function validate_courses($courseids) {
+        // Delete duplicates.
+        $courseids = array_unique($courseids);
+        $courses = array();
+        $warnings = array();
+
+        foreach ($courseids as $cid) {
+            // Check the user can function in this context.
+            try {
+                $context = context_course::instance($cid);
+                external_api::validate_context($context);
+                $courses[$cid] = get_course($cid);
+            } catch (Exception $e) {
+                $warnings[] = array(
+                    'item' => 'course',
+                    'itemid' => $cid,
+                    'warningcode' => '1',
+                    'message' => 'No access rights in course context'
+                );
+            }
+        }
+
+        return array($courses, $warnings);
+    }
+
+}
index 76e534b..0c18b21 100644 (file)
@@ -53,7 +53,7 @@ class MoodleQuickForm_editor extends HTML_QuickForm_element {
     /** @var array options provided to initalize filepicker */
     protected $_options = array('subdirs' => 0, 'maxbytes' => 0, 'maxfiles' => 0, 'changeformat' => 0,
             'areamaxbytes' => FILE_AREA_MAX_BYTES_UNLIMITED, 'context' => null, 'noclean' => 0, 'trusttext' => 0,
-            'return_types' => 7);
+            'return_types' => 7, 'enable_filemanagement' => true);
     // $_options['return_types'] = FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE
 
     /** @var array values for editor */
index f4930bd..8fc5a7e 100644 (file)
@@ -2233,9 +2233,11 @@ function validate_' . $this->_formName . '_' . $escapedElementName . '(element,
   ret = validate_' . $this->_formName . '_' . $escapedElementName.'(frm.elements[\''.$elementName.'\'], \''.$escapedElementName.'\') && ret;
   if (!ret && !first_focus) {
     first_focus = true;
-    Y.Global.fire(M.core.globalEvents.FORM_ERROR, {formid: \''. $this->_attributes['id'] .'\',
-                                                   elementid: \'id_error_'.$escapedElementName.'\'});
-    document.getElementById(\'id_error_'.$escapedElementName.'\').focus();
+    Y.use(\'moodle-core-event\', function() {
+        Y.Global.fire(M.core.globalEvents.FORM_ERROR, {formid: \'' . $this->_attributes['id'] . '\',
+                                                       elementid: \'id_error_' . $escapedElementName . '\'});
+        document.getElementById(\'id_error_' . $escapedElementName . '\').focus();
+    });
   }
 ';
 
index f024bcb..dfc2005 100644 (file)
@@ -1905,12 +1905,6 @@ class grade_category extends grade_object {
      * @return array An array containing 'object', 'type', 'depth' and optionally 'children'
      */
     static private function _fetch_course_tree_recursion($category_array, &$sortorder) {
-        // update the sortorder in db if needed
-        //NOTE: This leads to us resetting sort orders every time the categories and items page is viewed :(
-        //if ($category_array['object']->sortorder != $sortorder) {
-            //$category_array['object']->set_sortorder($sortorder);
-        //}
-
         if (isset($category_array['object']->gradetype) && $category_array['object']->gradetype==GRADE_TYPE_NONE) {
             return null;
         }
index a91cab4..7d89756 100644 (file)
@@ -4736,7 +4736,7 @@ function remove_course_contents($courseid, $showfeedback = true, array $options
     require_once($CFG->libdir.'/questionlib.php');
     require_once($CFG->libdir.'/gradelib.php');
     require_once($CFG->dirroot.'/group/lib.php');
-    require_once($CFG->dirroot.'/tag/coursetagslib.php');
+    require_once($CFG->dirroot.'/tag/lib.php');
     require_once($CFG->dirroot.'/comment/lib.php');
     require_once($CFG->dirroot.'/rating/lib.php');
     require_once($CFG->dirroot.'/notes/lib.php');
@@ -4911,7 +4911,7 @@ function remove_course_contents($courseid, $showfeedback = true, array $options
     $rm->delete_ratings($delopt);
 
     // Delete course tags.
-    coursetag_delete_course_tags($course->id, $showfeedback);
+    tag_set('course', $course->id, array(), 'core', $coursecontext->id);
 
     // Delete calendar events.
     $DB->delete_records('event', array('courseid' => $course->id));
@@ -7558,14 +7558,16 @@ function count_letters($string) {
  * @param int $length The length of the string to be created.
  * @return string
  */
-function random_string ($length=15) {
+function random_string($length=15) {
+    $randombytes = random_bytes_emulate($length);
     $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
     $pool .= 'abcdefghijklmnopqrstuvwxyz';
     $pool .= '0123456789';
     $poollen = strlen($pool);
     $string = '';
     for ($i = 0; $i < $length; $i++) {
-        $string .= substr($pool, (mt_rand()%($poollen)), 1);
+        $rand = ord($randombytes[$i]);
+        $string .= substr($pool, ($rand%($poollen)), 1);
     }
     return $string;
 }
@@ -7586,13 +7588,56 @@ function complex_random_string($length=null) {
     if ($length===null) {
         $length = floor(rand(24, 32));
     }
+    $randombytes = random_bytes_emulate($length);
     $string = '';
     for ($i = 0; $i < $length; $i++) {
-        $string .= $pool[(mt_rand()%$poollen)];
+        $rand = ord($randombytes[$i]);
+        $string .= $pool[($rand%$poollen)];
     }
     return $string;
 }
 
+/**
+ * Try to generates cryptographically secure pseudo-random bytes.
+ *
+ * Note this is achieved by fallbacking between:
+ *  - PHP 7 random_bytes().
+ *  - OpenSSL openssl_random_pseudo_bytes().
+ *  - In house random generator getting its entropy from various, hard to guess, pseudo-random sources.
+ *
+ * @param int $length requested length in bytes
+ * @return string binary data
+ */
+function random_bytes_emulate($length) {
+    global $CFG;
+    if ($length <= 0) {
+        debugging('Invalid random bytes length', DEBUG_DEVELOPER);
+        return '';
+    }
+    if (function_exists('random_bytes')) {
+        // Use PHP 7 goodness.
+        $hash = @random_bytes($length);
+        if ($hash !== false) {
+            return $hash;
+        }
+    }
+    if (function_exists('openssl_random_pseudo_bytes')) {
+        // For PHP 5.3 and later with openssl extension.
+        $hash = openssl_random_pseudo_bytes($length);
+        if ($hash !== false) {
+            return $hash;
+        }
+    }
+
+    // Bad luck, there is no reliable random generator, let's just hash some unique stuff that is hard to guess.
+    $hash = sha1(serialize($CFG) . serialize($_SERVER) . microtime(true) . uniqid('', true), true);
+    // NOTE: the last param in sha1() is true, this means we are getting 20 bytes, not 40 chars as usual.
+    if ($length <= 20) {
+        return substr($hash, 0, $length);
+    }
+    return $hash . random_bytes_emulate($length - 20);
+}
+
 /**
  * Given some text (which may contain HTML) and an ideal length,
  * this function truncates the text neatly on a word boundary if possible
index f3da8f6..19284f7 100644 (file)
@@ -3763,6 +3763,9 @@ class settings_navigation extends navigation_node {
                 $url = new moodle_url('/course/completion.php', array('id'=>$course->id));
                 $coursenode->add(get_string('coursecompletion', 'completion'), $url, self::TYPE_SETTING, null, null, new pix_icon('i/settings', ''));
             }
+        } else if (has_capability('moodle/course:tag', $coursecontext)) {
+            $url = new moodle_url('/course/tags.php', array('id' => $course->id));
+            $coursenode->add(get_string('coursetags', 'tag'), $url, self::TYPE_SETTING, null, 'coursetags', new pix_icon('i/settings', ''));
         }
 
         // add enrol nodes
@@ -3818,6 +3821,13 @@ class settings_navigation extends navigation_node {
             $gradenode = $coursenode->add(get_string('grades'), $url, self::TYPE_SETTING, null, 'grades', new pix_icon('i/grades', ''));
         }
 
+        // Check if we can view the gradebook's setup page.
+        if (has_capability('moodle/grade:manage', $coursecontext)) {
+            $url = new moodle_url('/grade/edit/tree/index.php', array('id' => $course->id));
+            $coursenode->add(get_string('gradebooksetup', 'grades'), $url, self::TYPE_SETTING,
+                null, 'gradebooksetup', new pix_icon('i/settings', ''));
+        }
+
         //  Add outcome if permitted
         if (!empty($CFG->enableoutcomes) && has_capability('moodle/course:update', $coursecontext)) {
             $url = new moodle_url('/grade/edit/outcome/course.php', array('id'=>$course->id));
index d87ce6a..435dec2 100644 (file)
@@ -3946,7 +3946,7 @@ EOD;
      * @return string HTML for the header bar.
      */
     public function context_header($headerinfo = null, $headinglevel = 1) {
-        global $DB, $USER;
+        global $DB, $USER, $CFG;
         $context = $this->page->context;
         // Make sure to use the heading if it has been set.
         if (isset($headerinfo['heading'])) {
@@ -3976,7 +3976,7 @@ EOD;
 
             $imagedata = $this->user_picture($user, array('size' => 100));
             // Check to see if we should be displaying a message button.
-            if ($USER->id != $user->id && has_capability('moodle/site:sendmessage', $context)) {
+            if (!empty($CFG->messaging) && $USER->id != $user->id && has_capability('moodle/site:sendmessage', $context)) {
                 $userbuttons = array(
                     'messages' => array(
                         'buttontype' => 'message',
diff --git a/lib/templates/columns-2to1.mustache b/lib/templates/columns-2to1.mustache
new file mode 100644 (file)
index 0000000..1978fae
--- /dev/null
@@ -0,0 +1,44 @@
+{{!
+    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/>.
+}}
+{{!
+    @template core/columns-2to1
+
+    Moodle columns-2to1 template.
+
+    The purpose of this template is to render 2 columns where the first column has twice the width of the second one.
+    On mobile the second column collapses underneath the first.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * col1content Column 1 contents.
+    * col2content Column 2 contents.
+
+    Example context (json):
+    {
+        "col1content": "<div class='alert alert-info'>Lorem ipsum dolor sit amet, consectetur adipiscing elit. In porttitor vulputate turpis, quis tempor arcu.</div>",
+        "col2content": "<div class='alert alert-success'>Vivamus ac orci in velit fringilla aliquam a a nisl. Cras luctus quam laoreet magna pulvinar aliquet.</div>"
+    }
+}}
+<div class="row-fluid rtl-compatible">
+    <div class="span8">{{$ column1 }}{{{ col1content }}}{{/ column1 }}</div>
+    <div class="span4">{{$ column2 }}{{{ col2content }}}{{/ column2 }}</div>
+</div>
index 7781fb6..04398d7 100644 (file)
@@ -1413,4 +1413,26 @@ class behat_general extends behat_base {
             fwrite(STDOUT, "\033[2A\033[u\033[2B");
         }
     }
+
+    /**
+     * Presses a given button in the browser.
+     * NOTE: Phantomjs and goutte driver reloads page while navigating back and forward.
+     *
+     * @Then /^I press the "(back|forward|reload)" button in the browser$/
+     * @param string $button the button to press.
+     * @throws ExpectationException
+     */
+    public function i_press_in_the_browser($button) {
+        $session = $this->getSession();
+
+        if ($button == 'back') {
+            $session->back();
+        } else if ($button == 'forward') {
+            $session->forward();
+        } else if ($button == 'reload') {
+            $session->reload();
+        } else {
+            throw new ExpectationException('Unknown browser button.', $session);
+        }
+    }
 }
index 8ee6dd2..78b2a2d 100644 (file)
@@ -2906,4 +2906,100 @@ class core_moodlelib_testcase extends advanced_testcase {
         $_SERVER['HTTP_X_FORWARDED_FOR'] = $xforwardedfor;
 
     }
+
+    /*
+     * Test emulation of random_bytes() function.
+     */
+    public function test_random_bytes_emulate() {
+        $result = random_bytes_emulate(10);
+        $this->assertSame(10, strlen($result));
+        $this->assertnotSame($result, random_bytes_emulate(10));
+
+        $result = random_bytes_emulate(21);
+        $this->assertSame(21, strlen($result));
+        $this->assertnotSame($result, random_bytes_emulate(21));
+
+        $result = random_bytes_emulate(666);
+        $this->assertSame(666, strlen($result));
+
+        $this->assertDebuggingNotCalled();
+
+        $result = random_bytes_emulate(0);
+        $this->assertSame('', $result);
+        $this->assertDebuggingCalled();
+
+        $result = random_bytes_emulate(-1);
+        $this->assertSame('', $result);
+        $this->assertDebuggingCalled();
+    }
+
+    /**
+     * Test function for creation of random strings.
+     */
+    public function test_random_string() {
+        $pool = 'a-zA-Z0-9';
+
+        $result = random_string(10);
+        $this->assertSame(10, strlen($result));
+        $this->assertRegExp('/^[' . $pool . ']+$/', $result);
+        $this->assertNotSame($result, random_string(10));
+
+        $result = random_string(21);
+        $this->assertSame(21, strlen($result));
+        $this->assertRegExp('/^[' . $pool . ']+$/', $result);
+        $this->assertNotSame($result, random_string(21));
+
+        $result = random_string(666);
+        $this->assertSame(666, strlen($result));
+        $this->assertRegExp('/^[' . $pool . ']+$/', $result);
+
+        $result = random_string();
+        $this->assertSame(15, strlen($result));
+        $this->assertRegExp('/^[' . $pool . ']+$/', $result);
+
+        $this->assertDebuggingNotCalled();
+
+        $result = random_string(0);
+        $this->assertSame('', $result);
+        $this->assertDebuggingCalled();
+
+        $result = random_string(-1);
+        $this->assertSame('', $result);
+        $this->assertDebuggingCalled();
+    }
+
+    /**
+     * Test function for creation of complex random strings.
+     */
+    public function test_complex_random_string() {
+        $pool = preg_quote('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789`~!@#%^&*()_+-=[];,./<>?:{} ', '/');
+
+        $result = complex_random_string(10);
+        $this->assertSame(10, strlen($result));
+        $this->assertRegExp('/^[' . $pool . ']+$/', $result);
+        $this->assertNotSame($result, complex_random_string(10));
+
+        $result = complex_random_string(21);
+        $this->assertSame(21, strlen($result));
+        $this->assertRegExp('/^[' . $pool . ']+$/', $result);
+        $this->assertNotSame($result, complex_random_string(21));
+
+        $result = complex_random_string(666);
+        $this->assertSame(666, strlen($result));
+        $this->assertRegExp('/^[' . $pool . ']+$/', $result);
+
+        $result = complex_random_string();
+        $this->assertEquals(28, strlen($result), '', 4); // Expected length is 24 - 32.
+        $this->assertRegExp('/^[' . $pool . ']+$/', $result);
+
+        $this->assertDebuggingNotCalled();
+
+        $result = complex_random_string(0);
+        $this->assertSame('', $result);
+        $this->assertDebuggingCalled();
+
+        $result = complex_random_string(-1);
+        $this->assertSame('', $result);
+        $this->assertDebuggingCalled();
+    }
 }
index ee93191..9bb9452 100644 (file)
@@ -26,7 +26,7 @@ Course setup
 3) Create a forum 'f2'
 4) Create a quiz 'q1' with one question (e.g. true/false question)
 5) Create a quiz 'q2' with one question (can use same question)
-6) In gradebook/reports/categories and items, edit q2's grade (show advanced)
+6) In gradebook/reports/gradebook setup, edit q2's grade (show advanced)
    to assign a 'grade to pass' of 5.0.
    [Note: Due to a bug in gradebook at time of writing, you may have to go into
    admin screens and make sure that 'Student' is included on the list of graded
index fc06095..ca40f86 100644 (file)
@@ -741,4 +741,62 @@ class core_upgradelib_testcase extends advanced_testcase {
         $this->assertEquals($gradecategoryitem->grademax, $grade->rawgrademax);
         $this->assertEquals($gradecategoryitem->grademin, $grade->rawgrademin);
     }
+
+    public function test_upgrade_course_tags() {
+        global $DB;
+        $this->resetAfterTest();
+
+        // Running upgrade script when there are no tags.
+        upgrade_course_tags();
+        $this->assertFalse($DB->record_exists('tag_instance', array()));
+
+        // No course entries.
+        $DB->insert_record('tag_instance', array('itemid' => 123, 'tagid' => 101, 'tiuserid' => 0,
+            'itemtype' => 'post', 'component' => 'core', 'contextid' => 1));
+        $DB->insert_record('tag_instance', array('itemid' => 333, 'tagid' => 103, 'tiuserid' => 1002,
+            'itemtype' => 'post', 'component' => 'core', 'contextid' => 1));
+
+        upgrade_course_tags();
+        $records = array_values($DB->get_records('tag_instance', array(), 'id', '*'));
+        $this->assertEquals(2, count($records));
+        $this->assertEquals(123, $records[0]->itemid);
+        $this->assertEquals(333, $records[1]->itemid);
+
+        // Imagine we have tags 101, 102, 103, ... and courses 1, 2, 3, ... and users 1001, 1002, ... .
+        $keys = array('itemid', 'tagid', 'tiuserid');
+        $valuesets = array(
+            array(1, 101, 0),
+            array(1, 102, 0),
+
+            array(2, 102, 0),
+            array(2, 103, 1001),
+
+            array(3, 103, 0),
+            array(3, 103, 1001),
+
+            array(3, 104, 1006),
+            array(3, 104, 1001),
+            array(3, 104, 1002),
+        );
+
+        foreach ($valuesets as $values) {
+            $DB->insert_record('tag_instance', array_combine($keys, $values) +
+                    array('itemtype' => 'course', 'component' => 'core', 'contextid' => 1));
+        }
+
+        upgrade_course_tags();
+        // There are 8 records in 'tag_instance' table and 7 of them do not have tiuserid (except for one 'post').
+        $records = array_values($DB->get_records('tag_instance', array(), 'id', '*'));
+        $this->assertEquals(8, count($records));
+        $this->assertEquals(7, $DB->count_records('tag_instance', array('tiuserid' => 0)));
+        // Course 1 is mapped to tags 101 and 102.
+        $this->assertEquals(array(101, 102), array_values($DB->get_fieldset_select('tag_instance', 'tagid',
+                'itemtype = ? AND itemid = ? ORDER BY tagid', array('course', 1))));
+        // Course 2 is mapped to tags 102 and 103.
+        $this->assertEquals(array(102, 103), array_values($DB->get_fieldset_select('tag_instance', 'tagid',
+                'itemtype = ? AND itemid = ? ORDER BY tagid', array('course', 2))));
+        // Course 1 is mapped to tags 101 and 102.
+        $this->assertEquals(array(103, 104), array_values($DB->get_fieldset_select('tag_instance', 'tagid',
+                'itemtype = ? AND itemid = ? ORDER BY tagid', array('course', 3))));
+    }
 }
index d11e49a..1d60362 100644 (file)
@@ -3,6 +3,8 @@ information provided here is intended especially for developers.
 
 === 3.0 ===
 
+* External functions x_is_allowed_from_ajax() methods have been deprecated. Define 'ajax' => true in db/services.php instead.
+* External functions can be called without a session if they define 'loginrequired' => true in db/services.php.
 * All plugins are required to declare their frankenstyle component name via
   the $plugin->component property in their version.php file. See
   https://docs.moodle.org/dev/version.php for details (MDL-48494).
@@ -132,6 +134,8 @@ information provided here is intended especially for developers.
 * The never unused webdav_locks table was dropped.
 * The actionmenu hideMenu() function now expects an EventFacade object to be passed to it,
   i.e. a call to M.core.actionmenu.instance.hideMenu() should be change to M.core.actionmenu.instance.hideMenu(e)
+* In the html_editors (tinyMCE, Atto), the manage files button can be hidden by changing the 'enable_filemanagement' option to false.
+* external_api::validate_context now is public, it can be called from other classes.
 
 === 2.9.1 ===
 
index f7dc699..ff138b2 100644 (file)
@@ -373,6 +373,7 @@ function upgrade_stale_php_files_present() {
     $someexamplesofremovedfiles = array(
         // Removed in 3.0.
         '/mod/lti/grade.php',
+        '/tag/coursetagslib.php',
         // Removed in 2.9.
         '/lib/timezone.txt',
         // Removed in 2.8.
index 60b65d3..f6a6a91 100644 (file)
@@ -2590,7 +2590,7 @@ function redirect($url, $message='', $delay=-1) {
     // Technically, HTTP/1.1 requires Location: header to contain the absolute path.
     // (In practice browsers accept relative paths - but still, might as well do it properly.)
     // This code turns relative into absolute.
-    if (!preg_match('|^[a-z]+:|', $url)) {
+    if (!preg_match('|^[a-z]+:|i', $url)) {
         // Get host name http://www.wherever.com.
         $hostpart = preg_replace('|^(.*?[^:/])/.*$|', '$1', $CFG->wwwroot);
         if (preg_match('|^/|', $url)) {
index cbf3e39..ca46f73 100644 (file)
Binary files a/lib/yui/build/moodle-core-event/moodle-core-event-debug.js and b/lib/yui/build/moodle-core-event/moodle-core-event-debug.js differ
index 426af2f..ced026e 100644 (file)
Binary files a/lib/yui/build/moodle-core-event/moodle-core-event-min.js and b/lib/yui/build/moodle-core-event/moodle-core-event-min.js differ
index 2a9e776..6a18fe6 100644 (file)
Binary files a/lib/yui/build/moodle-core-event/moodle-core-event.js and b/lib/yui/build/moodle-core-event/moodle-core-event.js differ
index 6f6a28a..5a51c2a 100644 (file)
@@ -28,7 +28,7 @@ var LOGNAME = 'moodle-core-event';
  */
 M.core = M.core || {};
 
-M.core.event = {
+M.core.event = M.core.event || {
     /**
      * This event is triggered when a page has added dynamic nodes to a page
      * that should be processed by the filter system. An example is loading
@@ -42,7 +42,7 @@ M.core.event = {
     FILTER_CONTENT_UPDATED: "filter-content-updated"
 };
 
-M.core.globalEvents = {
+M.core.globalEvents = M.core.globalEvents || {
     /**
      * This event is triggered when form has an error
      *
@@ -72,14 +72,16 @@ var eventDefaultConfig = {
 // Publish all the events with a standard config.
 var key;
 for (key in M.core.event) {
-    if (M.core.event.hasOwnProperty(key)) {
+    if (M.core.event.hasOwnProperty(key) && Y.getEvent(M.core.event[key]) === null) {
         Y.publish(M.core.event[key], eventDefaultConfig);
     }
 }
 
-//Publish all the global events.
+// Publish global events.
 for (key in M.core.globalEvents) {
-    if (M.core.globalEvents.hasOwnProperty(key)) {
-        Y.Global.publish(M.core.globalEvents[key], Y.merge(eventDefaultConfig, {broadcast: 2}));
+    // Make sure the key exists and that the event has not yet been published. Otherwise, skip publishing.
+    if (M.core.globalEvents.hasOwnProperty(key) && Y.Global.getEvent(M.core.globalEvents[key]) === null) {
+        Y.Global.publish(M.core.globalEvents[key], Y.merge(eventDefaultConfig, {broadcast: true}));
+        Y.log('Global event published: ' + key, 'debug', LOGNAME);
     }
 }
index 281c76d..2d43f70 100644 (file)
@@ -192,7 +192,8 @@ class login_signup_form extends moodleform {
      */
     function signup_captcha_enabled() {
         global $CFG;
-        return !empty($CFG->recaptchapublickey) && !empty($CFG->recaptchaprivatekey) && get_config('auth/email', 'recaptcha');
+        $authplugin = get_auth_plugin($CFG->registerauth);
+        return !empty($CFG->recaptchapublickey) && !empty($CFG->recaptchaprivatekey) && $authplugin->is_captcha_enabled();
     }
 
 }
index 82fe49f..2c51e85 100644 (file)
@@ -537,21 +537,26 @@ function xmldb_assign_upgrade($oldversion) {
                                 FROM {assign_submission}
                             GROUP BY assignment, groupid, userid';
 
-            // Note: souterouter looks redundant below, but it forces
-            // MySQL to use an in memory table to store the results of the
-            // inner query. Without this MySQL would complain that the UPDATE
-            // is operating on the same table as the FROM (which is true).
-            $maxattemptidssql = 'SELECT souterouter.id FROM (
-                                    SELECT souter.id
-                                       FROM {assign_submission} souter
-                                       JOIN (' . $maxattemptsql . ') sinner
-                                         ON souter.assignment = sinner.assignment
-                                        AND souter.userid = sinner.userid
-                                        AND souter.groupid = sinner.groupid
-                                        AND souter.attemptnumber = sinner.maxattempt
-                                ) souterouter';
-            $select = 'id IN(' . $maxattemptidssql . ')';
-            $DB->set_field_select('assign_submission', 'latest', 1, $select);
+            $maxattemptidssql = 'SELECT souter.id
+                                   FROM {assign_submission} souter
+                                   JOIN (' . $maxattemptsql . ') sinner
+                                     ON souter.assignment = sinner.assignment
+                                    AND souter.userid = sinner.userid
+                                    AND souter.groupid = sinner.groupid
+                                    AND souter.attemptnumber = sinner.maxattempt';
+
+            // We need to avoid using "WHERE ... IN(SELECT ...)" clause with MySQL for performance reason.
+            // TODO MDL-29589 Remove this dbfamily exception when implemented.
+            if ($DB->get_dbfamily() === 'mysql') {
+                $params = array('latest' => 1);
+                $sql = 'UPDATE {assign_submission}
+                    INNER JOIN (' . $maxattemptidssql . ') souterouter ON souterouter.id = {assign_submission}.id
+                           SET latest = :latest';
+                $DB->execute($sql, $params);
+            } else {
+                $select = 'id IN(' . $maxattemptidssql . ')';
+                $DB->set_field_select('assign_submission', 'latest', 1, $select);
+            }
 
             // Look for grade records with no submission record.
             // This is when a teacher has marked a student before they submitted anything.
index e0e09c2..d6b5674 100644 (file)
@@ -79,7 +79,7 @@ $string['attemptnumber'] = 'Attempt number';
 $string['attempthistory'] = 'Previous attempts';
 $string['attemptsettings'] = 'Attempt settings';
 $string['attemptreopenmethod'] = 'Attempts reopened';
-$string['attemptreopenmethod_help'] = 'Determines how student submission attempts are reopened. The available options are: <ul><li>Never - The student submission cannot be reopened.</li><li>Manually - The student submission can be reopened by a teacher.</li><li>Automatically until pass - The student submission is automatically reopened until the student achieves the grade to pass value set in the Gradebook (Categories and items section) for this assignment.</li></ul>';
+$string['attemptreopenmethod_help'] = 'Determines how student submission attempts are reopened. The available options are: <ul><li>Never - The student submission cannot be reopened.</li><li>Manually - The student submission can be reopened by a teacher.</li><li>Automatically until pass - The student submission is automatically reopened until the student achieves the grade to pass value set in the Gradebook (Gradebook setup section) for this assignment.</li></ul>';
 $string['attemptreopenmethod_manual'] = 'Manually';
 $string['attemptreopenmethod_none'] = 'Never';
 $string['attemptreopenmethod_untilpass'] = 'Automatically until pass';
index 6cfa6dd..61d1e98 100644 (file)
@@ -680,7 +680,14 @@ function assign_print_recent_activity($course, $viewfullnames, $timestart) {
 
     foreach ($show as $submission) {
         $cm = $modinfo->get_cm($submission->cmid);
+        $context = context_module::instance($submission->cmid);
+        $assign = new assign($context, $cm, $cm->course);
         $link = $CFG->wwwroot.'/mod/assign/view.php?id='.$cm->id;
+        // Obscure first and last name if blind marking enabled.
+        if ($assign->is_blind_marking()) {
+            $submission->firstname = get_string('participant', 'mod_assign');
+            $submission->lastname = $assign->get_uniqueid_for_user($submission->userid);
+        }
         print_recent_activity_note($submission->timemodified,
                                    $submission,
                                    $cm->name,
index da657d3..73336ae 100644 (file)
@@ -225,6 +225,30 @@ class mod_assign_lib_testcase extends mod_assign_base_testcase {
         $sink->close();
     }
 
+    /** Make sure blind marking shows participant \d+ not fullname when assign_print_recent_activity is triggered. */
+    public function test_print_recent_activity_fullname_blind_marking() {
+        // Submitting an assignment generates a notification in blind marking.
+        $this->preventResetByRollback();
+        $sink = $this->redirectMessages();
+
+        $this->setUser($this->editingteachers[0]);
+        $assign = $this->create_instance(array('blindmarking' => 1));
+
+        $data = new stdClass();
+        $data->userid = $this->students[0]->id;
+        $notices = array();
+        $this->setUser($this->students[0]);
+        $assign->submit_for_grading($data, $notices);
+
+        $this->setUser($this->editingteachers[0]);
+        $uniqueid = $assign->get_uniqueid_for_user($data->userid);
+        $expectedstr = preg_quote(get_string('participant', 'mod_assign'), '/') . '.*' . $uniqueid;
+        $this->expectOutputRegex("/{$expectedstr}/");
+        assign_print_recent_activity($this->course, false, time() - 3600);
+
+        $sink->close();
+    }
+
     public function test_assign_get_recent_mod_activity() {
         // Submitting an assignment generates a notification.
         $this->preventResetByRollback();
diff --git a/mod/book/classes/external.php b/mod/book/classes/external.php
new file mode 100644 (file)
index 0000000..0ffa8cb
--- /dev/null
@@ -0,0 +1,152 @@
+<?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/>.
+
+/**
+ * Book external API
+ *
+ * @package    mod_book
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.0
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once("$CFG->libdir/externallib.php");
+
+/**
+ * Book external functions
+ *
+ * @package    mod_book
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.0
+ */
+class mod_book_external extends external_api {
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.0
+     */
+    public static function view_book_parameters() {
+        return new external_function_parameters(
+            array(
+                'bookid' => new external_value(PARAM_INT, 'book instance id'),
+                'chapterid' => new external_value(PARAM_INT, 'chapter id', VALUE_DEFAULT, 0)
+            )
+        );
+    }
+
+    /**
+     * Simulate the book/view.php web interface page: trigger events, completion, etc...
+     *
+     * @param int $bookid the book instance id
+     * @param int $chapterid the book chapter id
+     * @return array of warnings and status result
+     * @since Moodle 3.0
+     * @throws moodle_exception
+     */
+    public static function view_book($bookid, $chapterid = 0) {
+        global $DB, $CFG;
+        require_once($CFG->dirroot . "/mod/book/lib.php");
+        require_once($CFG->dirroot . "/mod/book/locallib.php");
+
+        $params = self::validate_parameters(self::view_book_parameters(),
+                                            array(
+                                                'bookid' => $bookid,
+                                                'chapterid' => $chapterid
+                                            ));
+        $bookid = $params['bookid'];
+        $chapterid = $params['chapterid'];
+
+        $warnings = array();
+
+        // Request and permission validation.
+        $book = $DB->get_record('book', array('id' => $bookid), '*', MUST_EXIST);
+        list($course, $cm) = get_course_and_cm_from_instance($book, 'book');
+
+        $context = context_module::instance($cm->id);
+        self::validate_context($context);
+
+        require_capability('mod/book:read', $context);
+
+        $chapters = book_preload_chapters($book);
+        $firstchapterid = 0;
+        $lastchapterid = 0;
+
+        foreach ($chapters as $ch) {
+            if ($ch->hidden) {
+                continue;
+            }
+            if (!$firstchapterid) {
+                $firstchapterid = $ch->id;
+            }
+            $lastchapterid = $ch->id;
+        }
+
+        if (!$chapterid) {
+            // Trigger the module viewed events since we are displaying the book.
+            book_view($book, null, false, $course, $cm, $context);
+            $chapterid = $firstchapterid;
+        }
+
+        // Check if book is empty (warning).
+        if (!$chapterid) {
+            $warnings[] = array(
+                'item' => 'book',
+                'itemid' => $book->id,
+                'warningcode' => '1',
+                'message' => get_string('nocontent', 'mod_book')
+            );
+        } else {
+            $chapter = $DB->get_record('book_chapters', array('id' => $chapterid, 'bookid' => $book->id));
+            $viewhidden = has_capability('mod/book:viewhiddenchapters', $context);
+
+            if (!$chapter or ($chapter->hidden and !$viewhidden)) {
+                throw new moodle_exception('errorchapter', 'mod_book');
+            }
+
+            // Trigger the chapter viewed event.
+            $islastchapter = ($chapter->id == $lastchapterid) ? true : false;
+            book_view($book, $chapter, $islastchapter, $course, $cm, $context);
+        }
+
+        $result = array();
+        $result['status'] = true;
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.0
+     */
+    public static function view_book_returns() {
+        return new external_single_structure(
+            array(
+                'status' => new external_value(PARAM_BOOL, 'status: true if success'),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
+}
diff --git a/mod/book/db/services.php b/mod/book/db/services.php
new file mode 100644 (file)
index 0000000..be081d6
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Book external functions and service definitions.
+ *
+ * @package    mod_book
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.0
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+$functions = array(
+
+    'mod_book_view_book' => array(
+        'classname'     => 'mod_book_external',
+        'methodname'    => 'view_book',
+        'description'   => 'Simulate the view.php web interface book: trigger events, completion, etc...',
+        'type'          => 'write',
+        'capabilities'  => 'mod/book:read'
+    ),
+
+);
index 8c3f1f7..41dddfe 100644 (file)
@@ -597,4 +597,32 @@ function book_export_contents($cm, $baseurl) {
     array_unshift($contents, $structurefile);
 
     return $contents;
-}
\ No newline at end of file
+}
+
+/**
+ * Mark the activity completed (if required) and trigger the course_module_viewed event.
+ *
+ * @param  stdClass $book       book object
+ * @param  stdClass $chapter    chapter object
+ * @param  bool $islaschapter   is the las chapter of the book?
+ * @param  stdClass $course     course object
+ * @param  stdClass $cm         course module object
+ * @param  stdClass $context    context object
+ * @since Moodle 3.0
+ */
+function book_view($book, $chapter, $islastchapter, $course, $cm, $context) {
+
+    // First case, we are just opening the book.
+    if (empty($chapter)) {
+        \mod_book\event\course_module_viewed::create_from_book($book, $context)->trigger();
+
+    } else {
+        \mod_book\event\chapter_viewed::create_from_chapter($book, $context, $chapter)->trigger();
+
+        if ($islastchapter) {
+            // We cheat a bit here in assuming that viewing the last page means the user viewed the whole book.
+            $completion = new completion_info($course);
+            $completion->set_module_viewed($cm);
+        }
+    }
+}
diff --git a/mod/book/tests/externallib_test.php b/mod/book/tests/externallib_test.php
new file mode 100644 (file)
index 0000000..ec7a607
--- /dev/null
@@ -0,0 +1,135 @@
+<?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/>.
+
+/**
+ * External mod_book functions unit tests
+ *
+ * @package    mod_book
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.0
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+
+/**
+ * External mod_book functions unit tests
+ *
+ * @package    mod_book
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.0
+ */
+class mod_book_external_testcase extends externallib_advanced_testcase {
+
+    /**
+     * Test view_book
+     */
+    public function test_view_book() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $this->setAdminUser();
+        // Setup test data.
+        $course = $this->getDataGenerator()->create_course();
+        $book = $this->getDataGenerator()->create_module('book', array('course' => $course->id));
+        $bookgenerator = $this->getDataGenerator()->get_plugin_generator('mod_book');
+        $chapter = $bookgenerator->create_chapter(array('bookid' => $book->id));
+        $chapterhidden = $bookgenerator->create_chapter(array('bookid' => $book->id, 'hidden' => 1));
+
+        $context = context_module::instance($book->cmid);
+        $cm = get_coursemodule_from_instance('book', $book->id);
+
+        // Test invalid instance id.
+        try {
+            mod_book_external::view_book(0);
+            $this->fail('Exception expected due to invalid mod_book instance id.');
+        } catch (moodle_exception $e) {
+            $this->assertEquals('invalidrecord', $e->errorcode);
+        }
+
+        // Test not-enrolled user.
+        $user = self::getDataGenerator()->create_user();
+        $this->setUser($user);
+        try {
+            mod_book_external::view_book($book->id, 0);
+            $this->fail('Exception expected due to not enrolled user.');
+        } catch (moodle_exception $e) {
+            $this->assertEquals('requireloginerror', $e->errorcode);
+        }
+
+        // Test user with full capabilities.
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+
+        $result = mod_book_external::view_book($book->id, 0);
+        $result = external_api::clean_returnvalue(mod_book_external::view_book_returns(), $result);
+
+        $events = $sink->get_events();
+        $this->assertCount(2, $events);
+        $event = array_shift($events);
+
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\mod_book\event\course_module_viewed', $event);
+        $this->assertEquals($context, $event->get_context());
+        $moodleurl = new \moodle_url('/mod/book/view.php', array('id' => $cm->id));
+        $this->assertEquals($moodleurl, $event->get_url());
+        $this->assertEventContextNotUsed($event);
+        $this->assertNotEmpty($event->get_name());
+
+        $event = array_shift($events);
+        $this->assertInstanceOf('\mod_book\event\chapter_viewed', $event);
+        $this->assertEquals($chapter->id, $event->objectid);
+
+        $result = mod_book_external::view_book($book->id, $chapter->id);
+        $result = external_api::clean_returnvalue(mod_book_external::view_book_returns(), $result);
+
+        $events = $sink->get_events();
+        // We expect a total of 3 events.
+        $this->assertCount(3, $events);
+
+        // Try to view a hidden chapter.
+        try {
+            mod_book_external::view_book($book->id, $chapterhidden->id);
+            $this->fail('Exception expected due to missing capability.');
+        } catch (moodle_exception $e) {
+            $this->assertEquals('errorchapter', $e->errorcode);
+        }
+
+        // Test user with no capabilities.
+        // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles.
+        assign_capability('mod/book:read', CAP_PROHIBIT, $studentrole->id, $context->id);
+        accesslib_clear_all_caches_for_unit_testing();
+
+        try {
+            mod_book_external::view_book($book->id, 0);
+            $this->fail('Exception expected due to missing capability.');
+        } catch (moodle_exception $e) {
+            $this->assertEquals('nopermissions', $e->errorcode);
+        }
+
+    }
+}
index 351bf20..53a358c 100644 (file)
@@ -83,4 +83,57 @@ class mod_book_lib_testcase extends advanced_testcase {
         $this->assertEquals(json_encode(array()), $contents[0]['content']);
 
     }
+
+    /**
+     * Test book_view
+     * @return void
+     */
+    public function test_book_view() {
+        global $CFG, $DB;
+
+        $CFG->enablecompletion = 1;
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+        // Setup test data.
+        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
+        $book = $this->getDataGenerator()->create_module('book', array('course' => $course->id),
+                                                            array('completion' => 2, 'completionview' => 1));
+        $bookgenerator = $this->getDataGenerator()->get_plugin_generator('mod_book');
+        $chapter = $bookgenerator->create_chapter(array('bookid' => $book->id));
+
+        $context = context_module::instance($book->cmid);
+        $cm = get_coursemodule_from_instance('book', $book->id);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+
+        // Check just opening the book.
+        book_view($book, 0, false, $course, $cm, $context);
+
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = array_shift($events);
+
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\mod_book\event\course_module_viewed', $event);
+        $this->assertEquals($context, $event->get_context());
+        $moodleurl = new \moodle_url('/mod/book/view.php', array('id' => $cm->id));
+        $this->assertEquals($moodleurl, $event->get_url());
+        $this->assertEventContextNotUsed($event);
+        $this->assertNotEmpty($event->get_name());
+
+        // Check viewing one book chapter (the only one so it will be the first and last).
+        book_view($book, $chapter, true, $course, $cm, $context);
+
+        $events = $sink->get_events();
+        // We expect a total of 4 events. One for module viewed, one for chapter viewed and two belonging to completion.
+        $this->assertCount(4, $events);
+
+        // Check completion status.
+        $completion = new completion_info($course);
+        $completiondata = $completion->get_data($cm);
+        $this->assertEquals(1, $completiondata->completionstate);
+
+    }
 }
index 0d13c50..424459a 100644 (file)
@@ -25,6 +25,6 @@
 defined('MOODLE_INTERNAL') || die;
 
 $plugin->component = 'mod_book'; // Full name of the plugin (used for diagnostics)
-$plugin->version   = 2015051100; // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2015051101; // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015050500; // Requires this Moodle version
 $plugin->cron      = 0;          // Period for cron to check this module (secs)
index f2a6398..0c57b2e 100644 (file)
@@ -23,6 +23,7 @@
  */
 
 require(dirname(__FILE__).'/../../config.php');
+require_once(dirname(__FILE__).'/lib.php');
 require_once(dirname(__FILE__).'/locallib.php');
 require_once($CFG->libdir.'/completionlib.php');
 
@@ -75,7 +76,8 @@ if ($allowedit and !$chapters) {
 }
 // Check chapterid and read chapter data
 if ($chapterid == '0') { // Go to first chapter if no given.
-    \mod_book\event\course_module_viewed::create_from_book($book, $context)->trigger();
+    // Trigger course module viewed event.
+    book_view($book, null, false, $course, $cm, $context);
 
     foreach ($chapters as $ch) {
         if ($edit) {
@@ -109,10 +111,6 @@ unset($id);
 unset($bid);
 unset($chapterid);
 
-// Security checks END.
-
-\mod_book\event\chapter_viewed::create_from_chapter($book, $context, $chapter)->trigger();
-
 // Read standard strings.
 $strbooks = get_string('modulenameplural', 'mod_book');
 $strbook  = get_string('modulename', 'mod_book');
@@ -147,7 +145,7 @@ foreach ($chapters as $ch) {
     $last = $ch->id;
 }
 
-
+$islastchapter = false;
 if ($book->navstyle) {
     $navprevicon = right_to_left() ? 'nav_next' : 'nav_prev';
     $navnexticon = right_to_left() ? 'nav_prev' : 'nav_next';
@@ -195,18 +193,18 @@ if ($book->navstyle) {
                 '<span class="chaptername">' . $navexit . '&nbsp;' . $OUTPUT->uarrow() . '</span></a>';
         }
 
-        // We cheat a bit here in assuming that viewing the last page means the user viewed the whole book.
-        $completion = new completion_info($course);
-        $completion->set_module_viewed($cm);
+        $islastchapter = true;
     }
 }
 
+book_view($book, $chapter, $islastchapter, $course, $cm, $context);
+
 // =====================================================
 // Book display HTML code
 // =====================================================
 
 echo $OUTPUT->header();
-echo $OUTPUT->heading($book->name);
+echo $OUTPUT->heading(format_string($book->name));
 
 $navclasses = book_get_nav_classes();
 
index 076371a..02bd5d4 100644 (file)
@@ -109,13 +109,7 @@ switch ($action) {
             $chatlasttime = time() - $CFG->chat_old_ping;
         }
 
-        $params = array('groupid' => $chatuser->groupid, 'chatid' => $chatuser->chatid, 'lasttime' => $chatlasttime);
-
-        $groupselect = $chatuser->groupid ? " AND (groupid=".$chatuser->groupid." OR groupid=0) " : "";
-
-        $messages = $DB->get_records_select('chat_messages_current',
-            'chatid = :chatid AND timestamp > :lasttime '.$groupselect, $params,
-            'timestamp ASC');
+        $messages = chat_get_latest_messages($chatuser, $chatlasttime);
 
         if (!empty($messages)) {
             $num = count($messages);
diff --git a/mod/chat/classes/external.php b/mod/chat/classes/external.php
new file mode 100644 (file)
index 0000000..56d70c6
--- /dev/null
@@ -0,0 +1,613 @@
+<?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/>.
+
+/**
+ * Chat external API
+ *
+ * @package    mod_chat
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.0
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->libdir . '/externallib.php');
+require_once($CFG->dirroot . '/mod/chat/lib.php');
+
+/**
+ * Chat external functions
+ *
+ * @package    mod_chat
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.0
+ */
+class mod_chat_external extends external_api {
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.0
+     */
+    public static function login_user_parameters() {
+        return new external_function_parameters(
+            array(
+                'chatid' => new external_value(PARAM_INT, 'chat instance id'),
+                'groupid' => new external_value(PARAM_INT, 'group id, 0 means that the function will determine the user group',
+                                                VALUE_DEFAULT, 0),
+            )
+        );
+    }
+
+    /**
+     * Log the current user into a chat room in the given chat.
+     *
+     * @param int $chatid the chat instance id
+     * @param int $groupid the user group id
+     * @return array of warnings and the chat unique session id
+     * @since Moodle 3.0
+     * @throws moodle_exception
+     */
+    public static function login_user($chatid, $groupid = 0) {
+        global $DB;
+
+        $params = self::validate_parameters(self::login_user_parameters(),
+                                            array(
+                                                'chatid' => $chatid,
+                                                'groupid' => $groupid
+                                            ));
+        $warnings = array();
+
+        // Request and permission validation.
+        $chat = $DB->get_record('chat', array('id' => $params['chatid']), '*', MUST_EXIST);
+        list($course, $cm) = get_course_and_cm_from_instance($chat, 'chat');
+
+        $context = context_module::instance($cm->id);
+        self::validate_context($context);
+
+        require_capability('mod/chat:chat', $context);
+
+        if (!empty($params['groupid'])) {
+            $groupid = $params['groupid'];
+            // Determine is the group is visible to user.
+            if (!groups_group_visible($groupid, $course, $cm)) {
+                throw new moodle_exception('notingroup');
+            }
+        } else {
+            // Check to see if groups are being used here.
+            if ($groupmode = groups_get_activity_groupmode($cm)) {
+                $groupid = groups_get_activity_group($cm);
+                // Determine is the group is visible to user (this is particullary for the group 0).
+                if (!groups_group_visible($groupid, $course, $cm)) {
+                    throw new moodle_exception('notingroup');
+                }
+            } else {
+                $groupid = 0;
+            }
+        }
+
+        // Get the unique chat session id.
+        // Since we are going to use the chat via Web Service requests we set the ajax version (since it's the most similar).
+        if (!$chatsid = chat_login_user($chat->id, 'ajax', $groupid, $course)) {
+            throw moodle_exception('cantlogin', 'chat');
+        }
+
+        $result = array();
+        $result['chatsid'] = $chatsid;
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.0
+     */
+    public static function login_user_returns() {
+        return new external_single_structure(
+            array(
+                'chatsid' => new external_value(PARAM_ALPHANUM, 'unique chat session id'),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.0
+     */
+    public static function get_chat_users_parameters() {
+        return new external_function_parameters(
+            array(
+                'chatsid' => new external_value(PARAM_ALPHANUM, 'chat session id (obtained via mod_chat_login_user)')
+            )
+        );
+    }
+
+    /**
+     * Get the list of users in the given chat session.
+     *
+     * @param int $chatsid the chat session id
+     * @return array of warnings and the user lists
+     * @since Moodle 3.0
+     * @throws moodle_exception
+     */
+    public static function get_chat_users($chatsid) {
+        global $DB;
+
+        $params = self::validate_parameters(self::get_chat_users_parameters(),
+                                            array(
+                                                'chatsid' => $chatsid
+                                            ));
+        $warnings = array();
+
+        // Request and permission validation.
+        if (!$chatuser = $DB->get_record('chat_users', array('sid' => $params['chatsid']))) {
+            throw new moodle_exception('notlogged', 'chat');
+        }
+        $chat = $DB->get_record('chat', array('id' => $chatuser->chatid), '*', MUST_EXIST);
+        list($course, $cm) = get_course_and_cm_from_instance($chat, 'chat');
+
+        $context = context_module::instance($cm->id);
+        self::validate_context($context);
+
+        require_capability('mod/chat:chat', $context);
+
+        // First, delete old users from the chats.
+        chat_delete_old_users();
+
+        $users = chat_get_users($chatuser->chatid, $chatuser->groupid, $cm->groupingid);
+        $returnedusers = array();
+
+        foreach ($users as $user) {
+            $usercontext = context_user::instance($user->id, IGNORE_MISSING);
+            $profileimageurl = '';
+
+            if ($usercontext) {
+                $profileimageurl = moodle_url::make_webservice_pluginfile_url(
+                                    $usercontext->id, 'user', 'icon', null, '/', 'f1')->out(false);
+            }
+
+            $returnedusers[] = array(
+                'id' => $user->id,
+                'fullname' => fullname($user),
+                'profileimageurl' => $profileimageurl
+            );
+        }
+
+        $result = array();
+        $result['users'] = $returnedusers;
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.0
+     */
+    public static function get_chat_users_returns() {
+        return new external_single_structure(
+            array(
+                'users' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'id' => new external_value(PARAM_INT, 'user id'),
+                            'fullname' => new external_value(PARAM_NOTAGS, 'user full name'),
+                            'profileimageurl' => new external_value(PARAM_URL, 'user picture URL'),
+                        )
+                    ),
+                    'list of users'
+                ),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.0
+     */
+    public static function send_chat_message_parameters() {
+        return new external_function_parameters(
+            array(
+                'chatsid' => new external_value(PARAM_ALPHANUM, 'chat session id (obtained via mod_chat_login_user)'),
+                'messagetext' => new external_value(PARAM_RAW, 'the message text'),
+                'beepid' => new external_value(PARAM_RAW, 'the beep id', VALUE_DEFAULT, ''),
+
+            )
+        );
+    }
+
+    /**
+     * Send a message on the given chat session.
+     *
+     * @param int $chatsid the chat session id
+     * @param string $messagetext the message text
+     * @param string $beepid the beep message id
+     * @return array of warnings and the new message id (0 if the message was empty)
+     * @since Moodle 3.0
+     * @throws moodle_exception
+     */
+    public static function send_chat_message($chatsid, $messagetext, $beepid = '') {
+        global $DB;
+
+        $params = self::validate_parameters(self::send_chat_message_parameters(),
+                                            array(
+                                                'chatsid' => $chatsid,
+                                                'messagetext' => $messagetext,
+                                                'beepid' => $beepid
+                                            ));
+        $warnings = array();
+
+        // Request and permission validation.
+        if (!$chatuser = $DB->get_record('chat_users', array('sid' => $params['chatsid']))) {
+            throw new moodle_exception('notlogged', 'chat');
+        }
+        $chat = $DB->get_record('chat', array('id' => $chatuser->chatid), '*', MUST_EXIST);
+        list($course, $cm) = get_course_and_cm_from_instance($chat, 'chat');
+
+        $context = context_module::instance($cm->id);
+        self::validate_context($context);
+
+        require_capability('mod/chat:chat', $context);
+
+        $chatmessage = clean_text($params['messagetext'], FORMAT_MOODLE);
+
+        if (!empty($params['beepid'])) {
+            $chatmessage = 'beep ' . $params['beepid'];
+        }
+
+        if (!empty($chatmessage)) {
+            // Send the message.
+            $messageid = chat_send_chatmessage($chatuser, $chatmessage, 0, $cm);
+            // Update ping time.
+            $chatuser->lastmessageping = time() - 2;
+            $DB->update_record('chat_users', $chatuser);
+        } else {
+            $messageid = 0;
+        }
+
+        $result = array();
+        $result['messageid'] = $messageid;
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.0
+     */
+    public static function send_chat_message_returns() {
+        return new external_single_structure(
+            array(
+                'messageid' => new external_value(PARAM_INT, 'message sent id'),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.0
+     */
+    public static function get_chat_latest_messages_parameters() {
+        return new external_function_parameters(
+            array(
+                'chatsid' => new external_value(PARAM_ALPHANUM, 'chat session id (obtained via mod_chat_login_user)'),
+                'chatlasttime' => new external_value(PARAM_INT, 'last time messages were retrieved (epoch time)', VALUE_DEFAULT, 0)
+            )
+        );
+    }
+
+    /**
+     * Get the latest messages from the given chat session.
+     *
+     * @param int $chatsid the chat session id
+     * @param int $chatlasttime last time messages were retrieved (epoch time)
+     * @return array of warnings and the new message id (0 if the message was empty)
+     * @since Moodle 3.0
+     * @throws moodle_exception
+     */
+    public static function get_chat_latest_messages($chatsid, $chatlasttime = 0) {
+        global $DB, $CFG;
+
+        $params = self::validate_parameters(self::get_chat_latest_messages_parameters(),
+                                            array(
+                                                'chatsid' => $chatsid,
+                                                'chatlasttime' => $chatlasttime
+                                            ));
+        $warnings = array();
+
+        // Request and permission validation.
+        if (!$chatuser = $DB->get_record('chat_users', array('sid' => $params['chatsid']))) {
+            throw new moodle_exception('notlogged', 'chat');
+        }
+        $chat = $DB->get_record('chat', array('id' => $chatuser->chatid), '*', MUST_EXIST);
+        list($course, $cm) = get_course_and_cm_from_instance($chat, 'chat');
+
+        $context = context_module::instance($cm->id);
+        self::validate_context($context);
+
+        require_capability('mod/chat:chat', $context);
+
+        $chatlasttime = $params['chatlasttime'];
+        if ((time() - $chatlasttime) > $CFG->chat_old_ping) {
+            chat_delete_old_users();
+        }
+
+        // Set default chat last time (to not retrieve all the conversations).
+        if ($chatlasttime == 0) {
+            $chatlasttime = time() - $CFG->chat_old_ping;
+        }
+
+        if ($latestmessage = chat_get_latest_message($chatuser->chatid, $chatuser->groupid)) {
+            $chatnewlasttime = $latestmessage->timestamp;
+        } else {
+            $chatnewlasttime = 0;
+        }
+
+        $messages = chat_get_latest_messages($chatuser, $chatlasttime);
+        $returnedmessages = array();
+
+        foreach ($messages as $message) {
+
+            // FORMAT_MOODLE is mandatory in the chat plugin.
+            list($messageformatted, $format) = external_format_text($message->message, FORMAT_MOODLE, $context->id, 'mod_chat',
+                                                                    '', 0);
+
+            $returnedmessages[] = array(
+                'id' => $message->id,
+                'userid' => $message->userid,
+                'system' => (bool) $message->system,
+                'message' => $messageformatted,
+                'timestamp' => $message->timestamp,
+            );
+        }
+
+        // Update our status since we are active in the chat.
+        $DB->set_field('chat_users', 'lastping', time(), array('id' => $chatuser->id));
+
+        $result = array();
+        $result['messages'] = $returnedmessages;
+        $result['chatnewlasttime'] = $chatnewlasttime;
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.0
+     */
+    public static function get_chat_latest_messages_returns() {
+        return new external_single_structure(
+            array(
+                'messages' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'id' => new external_value(PARAM_INT, 'message id'),
+                            'userid' => new external_value(PARAM_INT, 'user id'),
+                            'system' => new external_value(PARAM_BOOL, 'true if is a system message (like user joined)'),
+                            'message' => new external_value(PARAM_RAW, 'message text'),
+                            'timestamp' => new external_value(PARAM_INT, 'timestamp for the message'),
+                        )
+                    ),
+                    'list of users'
+                ),
+                'chatnewlasttime' => new external_value(PARAM_INT, 'new last time'),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.0
+     */
+    public static function view_chat_parameters() {
+        return new external_function_parameters(
+            array(
+                'chatid' => new external_value(PARAM_INT, 'chat instance id')
+            )
+        );
+    }
+
+    /**
+     * Trigger the course module viewed event and update the module completion status.
+     *
+     * @param int $chatid the chat instance id
+     * @return array of warnings and status result
+     * @since Moodle 3.0
+     * @throws moodle_exception
+     */
+    public static function view_chat($chatid) {
+        global $DB, $CFG;
+
+        $params = self::validate_parameters(self::view_chat_parameters(),
+                                            array(
+                                                'chatid' => $chatid
+                                            ));
+        $warnings = array();
+
+        // Request and permission validation.
+        $chat = $DB->get_record('chat', array('id' => $params['chatid']), '*', MUST_EXIST);
+        list($course, $cm) = get_course_and_cm_from_instance($chat, 'chat');
+
+        $context = context_module::instance($cm->id);
+        self::validate_context($context);
+
+        require_capability('mod/chat:chat', $context);
+
+        // Call the url/lib API.
+        chat_view($chat, $course, $cm, $context);
+
+        $result = array();
+        $result['status'] = true;
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.0
+     */
+    public static function view_chat_returns() {
+        return new external_single_structure(
+            array(
+                'status' => new external_value(PARAM_BOOL, 'status: true if success'),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
+
+    /**
+     * Describes the parameters for get_chats_by_courses.
+     *
+     * @return external_external_function_parameters
+     * @since Moodle 3.0
+     */
+    public static function get_chats_by_courses_parameters() {
+        return new external_function_parameters (
+            array(
+                'courseids' => new external_multiple_structure(
+                    new external_value(PARAM_INT, 'course id'), 'Array of course ids', VALUE_DEFAULT, array()
+                ),
+            )
+        );
+    }
+
+    /**
+     * Returns a list of chats in a provided list of courses,
+     * if no list is provided all chats that the user can view will be returned.
+     *
+     * @param array $courseids the course ids
+     * @return array of chats details
+     * @since Moodle 3.0
+     */
+    public static function get_chats_by_courses($courseids = array()) {
+        global $CFG;
+
+        $returnedchats = array();
+        $warnings = array();
+
+        $params = self::validate_parameters(self::get_chats_by_courses_parameters(), array('courseids' => $courseids));
+
+        if (empty($params['courseids'])) {
+            $params['courseids'] = array_keys(enrol_get_my_courses());
+        }
+
+        // Ensure there are courseids to loop through.
+        if (!empty($params['courseids'])) {
+
+            list($courses, $warnings) = external_util::validate_courses($params['courseids']);
+
+            // Get the chats in this course, this function checks users visibility permissions.
+            // We can avoid then additional validate_context calls.
+            $chats = get_all_instances_in_courses("chat", $courses);
+            foreach ($chats as $chat) {
+                $chatcontext = context_module::instance($chat->coursemodule);
+                // Entry to return.
+                $chatdetails = array();
+                // First, we return information that any user can see in the web interface.
+                $chatdetails['id'] = $chat->id;
+                $chatdetails['coursemodule']      = $chat->coursemodule;
+                $chatdetails['course']            = $chat->course;
+                $chatdetails['name']              = $chat->name;
+                // Format intro.
+                list($chatdetails['intro'], $chatdetails['introformat']) =
+                    external_format_text($chat->intro, $chat->introformat, $chatcontext->id, 'mod_chat', 'intro', null);
+
+                if (has_capability('mod/chat:chat', $chatcontext)) {
+                    $chatdetails['chatmethod']    = $CFG->chat_method;
+                    $chatdetails['keepdays']      = $chat->keepdays;
+                    $chatdetails['studentlogs']   = $chat->studentlogs;
+                    $chatdetails['chattime']      = $chat->chattime;
+                    $chatdetails['schedule']      = $chat->schedule;
+                }
+
+                if (has_capability('moodle/course:manageactivities', $chatcontext)) {
+                    $chatdetails['timemodified']  = $chat->timemodified;
+                    $chatdetails['section']       = $chat->section;
+                    $chatdetails['visible']       = $chat->visible;
+                    $chatdetails['groupmode']     = $chat->groupmode;
+                    $chatdetails['groupingid']    = $chat->groupingid;
+                }
+                $returnedchats[] = $chatdetails;
+            }
+        }
+        $result = array();
+        $result['chats'] = $returnedchats;
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Describes the get_chats_by_courses return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.0
+     */
+    public static function get_chats_by_courses_returns() {
+        return new external_single_structure(
+            array(
+                'chats' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'id' => new external_value(PARAM_INT, 'Chat id'),
+                            'coursemodule' => new external_value(PARAM_INT, 'Course module id'),
+                            'course' => new external_value(PARAM_TEXT, 'Course id'),
+                            'name' => new external_value(PARAM_TEXT, 'Chat name'),
+                            'intro' => new external_value(PARAM_RAW, 'The Chat intro'),
+                            'introformat' => new external_format_value('intro'),
+                            'chatmethod' => new external_value(PARAM_ALPHA, 'chat method (sockets, daemon)', VALUE_OPTIONAL),
+                            'keepdays' => new external_value(PARAM_INT, 'keep days', VALUE_OPTIONAL),
+                            'studentlogs' => new external_value(PARAM_INT, 'student logs visible to everyone', VALUE_OPTIONAL),
+                            'chattime' => new external_value(PARAM_RAW, 'chat time', VALUE_OPTIONAL),
+                            'schedule' => new external_value(PARAM_INT, 'schedule type', VALUE_OPTIONAL),
+                            'timemodified' => new external_value(PARAM_RAW, 'time of last modification', VALUE_OPTIONAL),
+                            'section' => new external_value(PARAM_INT, 'course section id', VALUE_OPTIONAL),
+                            'visible' => new external_value(PARAM_BOOL, 'visible', VALUE_OPTIONAL),
+                            'groupmode' => new external_value(PARAM_INT, 'group mode', VALUE_OPTIONAL),
+                            'groupingid' => new external_value(PARAM_INT, 'group id', VALUE_OPTIONAL),
+                        ), 'Chats'
+                    )
+                ),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
+}
diff --git a/mod/chat/db/services.php b/mod/chat/db/services.php
new file mode 100644 (file)
index 0000000..2361aac
--- /dev/null
@@ -0,0 +1,79 @@
+<?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/>.
+
+/**
+ * Chat external functions and service definitions.
+ *
+ * @package    mod_chat
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.0
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+$functions = array(
+
+    'mod_chat_login_user' => array(
+        'classname'     => 'mod_chat_external',
+        'methodname'    => 'login_user',
+        'description'   => 'Log a user into a chat room in the given chat.',
+        'type'          => 'write',
+        'capabilities'  => 'mod/chat:chat'
+    ),
+
+    'mod_chat_get_chat_users' => array(
+        'classname'     => 'mod_chat_external',
+        'methodname'    => 'get_chat_users',
+        'description'   => 'Get the list of users in the given chat session.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/chat:chat'
+    ),
+
+    'mod_chat_send_chat_message' => array(
+        'classname'     => 'mod_chat_external',
+        'methodname'    => 'send_chat_message',
+        'description'   => 'Send a message on the given chat session.',
+        'type'          => 'write',
+        'capabilities'  => 'mod/chat:chat'
+    ),
+
+    'mod_chat_get_chat_latest_messages' => array(
+        'classname'     => 'mod_chat_external',
+        'methodname'    => 'get_chat_latest_messages',
+        'description'   => 'Get the latest messages from the given chat session.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/chat:chat'
+    ),
+
+    'mod_chat_view_chat' => array(
+        'classname'     => 'mod_chat_external',
+        'methodname'    => 'view_chat',
+        'description'   => 'Trigger the course module viewed event and update the module completion status.',
+        'type'          => 'write',
+        'capabilities'  => 'mod/chat:chat'
+    ),
+
+    'mod_chat_get_chats_by_courses' => array(
+        'classname'     => 'mod_chat_external',
+        'methodname'    => 'get_chats_by_courses',
+        'description'   => 'Returns a list of chat instances in a provided set of courses,
+                            if no courses are provided then all the chat instances the user has access to will be returned.',
+        'type'          => 'read',
+        'capabilities'  => ''
+    )
+);
index c575fa8..848fd47 100644 (file)
@@ -1304,3 +1304,50 @@ function chat_page_type_list($pagetype, $parentcontext, $currentcontext) {
     $modulepagetype = array('mod-chat-*' => get_string('page-mod-chat-x', 'chat'));
     return $modulepagetype;
 }
+
+/**
+ * Return a list of the latest messages in the given chat session.
+ *
+ * @param  stdClass $chatuser     chat user session data
+ * @param  int      $chatlasttime last time messages were retrieved
+ * @return array    list of messages
+ * @since  Moodle 3.0
+ */
+function chat_get_latest_messages($chatuser, $chatlasttime) {
+    global $DB;
+
+    $params = array('groupid' => $chatuser->groupid, 'chatid' => $chatuser->chatid, 'lasttime' => $chatlasttime);
+
+    $groupselect = $chatuser->groupid ? " AND (groupid=" . $chatuser->groupid . " OR groupid=0) " : "";
+
+    return $DB->get_records_select('chat_messages_current', 'chatid = :chatid AND timestamp > :lasttime ' . $groupselect,
+                                    $params, 'timestamp ASC');
+}
+
+/**
+ * Mark the activity completed (if required) and trigger the course_module_viewed event.
+ *
+ * @param  stdClass $chat       chat object
+ * @param  stdClass $course     course object
+ * @param  stdClass $cm         course module object
+ * @param  stdClass $context    context object
+ * @since Moodle 3.0
+ */
+function chat_view($chat, $course, $cm, $context) {
+
+    // Trigger course_module_viewed event.
+    $params = array(
+        'context' => $context,
+        'objectid' => $chat->id
+    );
+
+    $event = \mod_chat\event\course_module_viewed::create($params);
+    $event->add_record_snapshot('course_modules', $cm);
+    $event->add_record_snapshot('course', $course);
+    $event->add_record_snapshot('chat', $chat);
+    $event->trigger();
+
+    // Completion.
+    $completion = new completion_info($course);
+    $completion->set_module_viewed($cm);
+}
diff --git a/mod/chat/tests/externallib_test.php b/mod/chat/tests/externallib_test.php
new file mode 100644 (file)
index 0000000..cbd335f
--- /dev/null
@@ -0,0 +1,288 @@
+<?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/>.
+
+/**
+ * External mod_chat functions unit tests
+ *
+ * @package    mod_chat
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.0
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+
+/**
+ * External mod_chat functions unit tests
+ *
+ * @package    mod_chat
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.0
+ */
+class mod_chat_external_testcase extends externallib_advanced_testcase {
+
+    /**
+     * Test login user
+     */
+    public function test_login_user() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        // Setup test data.
+        $this->setAdminUser();
+        $course = $this->getDataGenerator()->create_course();
+        $chat = $this->getDataGenerator()->create_module('chat', array('course' => $course->id));
+
+        $user = self::getDataGenerator()->create_user();
+        $this->setUser($user);
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id);
+
+        $result = mod_chat_external::login_user($chat->id);
+        $result = external_api::clean_returnvalue(mod_chat_external::login_user_returns(), $result);
+
+        // Test session started.
+        $sid = $DB->get_field('chat_users', 'sid', array('userid' => $user->id, 'chatid' => $chat->id));
+        $this->assertEquals($result['chatsid'], $sid);
+
+    }
+
+    /**
+     * Test get chat users
+     */
+    public function test_get_chat_users() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        // Setup test data.
+        $this->setAdminUser();
+        $course = $this->getDataGenerator()->create_course();
+        $chat = $this->getDataGenerator()->create_module('chat', array('course' => $course->id));
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, $studentrole->id);
+
+        $result = mod_chat_external::login_user($chat->id);
+        $result = external_api::clean_returnvalue(mod_chat_external::login_user_returns(), $result);
+
+        $this->setUser($user2);
+        $result = mod_chat_external::login_user($chat->id);
+        $result = external_api::clean_returnvalue(mod_chat_external::login_user_returns(), $result);
+
+        // Get users.
+        $result = mod_chat_external::get_chat_users($result['chatsid']);
+        $result = external_api::clean_returnvalue(mod_chat_external::get_chat_users_returns(), $result);
+
+        // Check correct users.
+        $this->assertCount(2, $result['users']);
+        $found = 0;
+        foreach ($result['users'] as $user) {
+            if ($user['id'] == $user1->id or $user['id'] == $user2->id) {
+                $found++;
+            }
+        }
+        $this->assertEquals(2, $found);
+
+    }
+
+    /**
+     * Test send and get chat messages
+     */
+    public function test_send_get_chat_message() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        // Setup test data.
+        $this->setAdminUser();
+        $course = $this->getDataGenerator()->create_course();
+        $chat = $this->getDataGenerator()->create_module('chat', array('course' => $course->id));
+
+        $user = self::getDataGenerator()->create_user();
+        $this->setUser($user);
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id);
+
+        $result = mod_chat_external::login_user($chat->id);
+        $result = external_api::clean_returnvalue(mod_chat_external::login_user_returns(), $result);
+        $chatsid = $result['chatsid'];
+
+        $result = mod_chat_external::send_chat_message($chatsid, 'hello!');
+        $result = external_api::clean_returnvalue(mod_chat_external::send_chat_message_returns(), $result);
+
+        // Test messages received.
+
+        $result = mod_chat_external::get_chat_latest_messages($chatsid, 0);
+        $result = external_api::clean_returnvalue(mod_chat_external::get_chat_latest_messages_returns(), $result);
+
+        foreach ($result['messages'] as $message) {
+            // Ommit system messages, like user just joined in.
+            if ($message['system']) {
+                continue;
+            }
+            $this->assertEquals('hello!', $message['message']);
+        }
+    }
+
+    /**
+     * Test view_chat
+     */
+    public function test_view_chat() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        // Setup test data.
+        $this->setAdminUser();
+        $course = $this->getDataGenerator()->create_course();
+        $chat = $this->getDataGenerator()->create_module('chat', array('course' => $course->id));
+        $context = context_module::instance($chat->cmid);
+        $cm = get_coursemodule_from_instance('chat', $chat->id);
+
+        // Test invalid instance id.
+        try {
+            mod_chat_external::view_chat(0);
+            $this->fail('Exception expected due to invalid mod_chat instance id.');
+        } catch (moodle_exception $e) {
+            $this->assertEquals('invalidrecord', $e->errorcode);
+        }
+
+        // Test not-enrolled user.
+        $user = self::getDataGenerator()->create_user();
+        $this->setUser($user);
+        try {
+            mod_chat_external::view_chat($chat->id);
+            $this->fail('Exception expected due to not enrolled user.');
+        } catch (moodle_exception $e) {
+            $this->assertEquals('requireloginerror', $e->errorcode);
+        }
+
+        // Test user with full capabilities.
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+
+        $result = mod_chat_external::view_chat($chat->id);
+        $result = external_api::clean_returnvalue(mod_chat_external::view_chat_returns(), $result);
+
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = array_shift($events);
+
+        // Checking that the event contains the expected values.
+        $this->assertInstanceOf('\mod_chat\event\course_module_viewed', $event);
+        $this->assertEquals($context, $event->get_context());
+        $moodlechat = new \moodle_url('/mod/chat/view.php', array('id' => $cm->id));
+        $this->assertEquals($moodlechat, $event->get_url());
+        $this->assertEventContextNotUsed($event);
+        $this->assertNotEmpty($event->get_name());
+
+        // Test user with no capabilities.
+        // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles.
+        assign_capability('mod/chat:chat', CAP_PROHIBIT, $studentrole->id, $context->id);
+        accesslib_clear_all_caches_for_unit_testing();
+
+        try {
+            mod_chat_external::view_chat($chat->id);
+            $this->fail('Exception expected due to missing capability.');
+        } catch (moodle_exception $e) {
+            $this->assertEquals('nopermissions', $e->errorcode);
+        }
+    }
+
+    /**
+     * Test get_chats_by_courses
+     */
+    public function test_get_chats_by_courses() {
+        global $DB, $USER;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $course1 = self::getDataGenerator()->create_course();
+        $chatoptions1 = array(
+                              'course' => $course1->id,
+                              'name' => 'First Chat'
+                             );
+        $chat1 = self::getDataGenerator()->create_module('chat', $chatoptions1);
+        $course2 = self::getDataGenerator()->create_course();
+        $chatoptions2 = array(
+                              'course' => $course2->id,
+                              'name' => 'Second Chat'
+                             );
+        $chat2 = self::getDataGenerator()->create_module('chat', $chatoptions2);
+        $student1 = $this->getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+
+        // Enroll Student1 in Course1.
+        self::getDataGenerator()->enrol_user($student1->id,  $course1->id, $studentrole->id);
+        $this->setUser($student1);
+
+        $chats = mod_chat_external::get_chats_by_courses();
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $chats = external_api::clean_returnvalue(mod_chat_external::get_chats_by_courses_returns(), $chats);
+        $this->assertCount(1, $chats['chats']);
+        $this->assertEquals('First Chat', $chats['chats'][0]['name']);
+        // We see 11 fields.
+        $this->assertCount(11, $chats['chats'][0]);
+
+        // As Student you cannot see some chat properties like 'section'.
+        $this->assertFalse(isset($chats['chats'][0]['section']));
+
+        // Student1 is not enrolled in course2. The webservice will return a warning!
+        $chats = mod_chat_external::get_chats_by_courses(array($course2->id));
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $chats = external_api::clean_returnvalue(mod_chat_external::get_chats_by_courses_returns(), $chats);
+        $this->assertCount(0, $chats['chats']);
+        $this->assertEquals(1, $chats['warnings'][0]['warningcode']);
+
+        // Now as admin.
+        $this->setAdminUser();
+        // As Admin we can see this chat.
+        $chats = mod_chat_external::get_chats_by_courses(array($course2->id));
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $chats = external_api::clean_returnvalue(mod_chat_external::get_chats_by_courses_returns(), $chats);
+
+        $this->assertCount(1, $chats['chats']);
+        $this->assertEquals('Second Chat', $chats['chats'][0]['name']);
+        // We see 16 fields.
+        $this->assertCount(16, $chats['chats'][0]);
+        // As an Admin you can see some chat properties like 'section'.
+        $this->assertEquals(0, $chats['chats'][0]['section']);
+
+        // Enrol student in the second course.
+        self::getDataGenerator()->enrol_user($student1->id,  $course2->id, $studentrole->id);
+        $this->setUser($student1);
+        $chats = mod_chat_external::get_chats_by_courses();
+        $chats = external_api::clean_returnvalue(mod_chat_external::get_chats_by_courses_returns(), $chats);
+        $this->assertCount(2, $chats['chats']);
+
+    }
+}
index 0d659b3..13c3965 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015051100; // The current module version (Date: YYYYMMDDXX).
+$plugin->version   = 2015051101; // The current module version (Date: YYYYMMDDXX).
 $plugin->requires  = 2015050500; // Requires this Moodle version.
 $plugin->component = 'mod_chat'; // Full name of the plugin (used for diagnostics).
 $plugin->cron      = 300;
index 8740654..55478d7 100644 (file)
@@ -69,15 +69,8 @@ if (isguestuser()) {
     exit;
 }
 
-// Log this request - the problem here is that the view page
-// does not display the chat content which is actually in a new window.
-$params = array(
-    'objectid' => $chat->id,
-    'context' => $context
-);
-$event = \mod_chat\event\course_module_viewed::create($params);
-$event->add_record_snapshot('chat', $chat);
-$event->trigger();
+// Completion and trigger events.
+chat_view($chat, $course, $cm, $context);
 
 $strenterchat    = get_string('enterchat', 'chat');
 $stridle         = get_string('idle', 'chat');
@@ -87,10 +80,6 @@ $strnextsession  = get_string('nextsession', 'chat');
 $courseshortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
 $title = $courseshortname . ': ' . format_string($chat->name);
 
-// Mark viewed by user (if required).
-$completion = new completion_info($course);
-$completion->set_module_viewed($cm);
-
 // Initialize $PAGE.
 $PAGE->set_url('/mod/chat/view.php', array('id' => $cm->id));
 $PAGE->set_title($title);
diff --git a/mod/choice/classes/external.php b/mod/choice/classes/external.php
new file mode 100644 (file)
index 0000000..7a835a1
--- /dev/null
@@ -0,0 +1,454 @@
+<?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/>.
+
+/**
+ * Choice&nb