Merge branch 'MDL-50015-master' of git://github.com/jleyva/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 9 Sep 2015 23:42:06 +0000 (01:42 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 9 Sep 2015 23:42:06 +0000 (01:42 +0200)
114 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
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]
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
group/overview.php
lang/en/admin.php
lang/en/deprecated.txt
lang/en/error.php
lang/en/role.php
lang/en/tag.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/externallib.php
lib/form/editor.php
lib/formslib.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputrenderers.php
lib/tests/behat/behat_general.php
lib/tests/moodlelib_test.php
lib/tests/upgradelib_test.php
lib/upgrade.txt
lib/upgradelib.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
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 [moved from blocks/tags/settings.php with 55% similarity]
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/lib.php
mod/forum/tests/externallib_test.php
mod/forum/tests/lib_test.php
mod/forum/version.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
rating/classes/external.php
rating/index.php
rating/tests/externallib_test.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/style/core.css
theme/base/style/course.css
theme/bootstrapbase/less/moodle.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/style/moodle.css
version.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 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 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 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');
     }
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 f5167f3..f1de91d 100644 (file)
@@ -18,3 +18,4 @@ tagtype_official,core_tag
 thistaghasnodesc,core_tag
 updated,core_tag
 withselectedtags,core_tag
+tag:create,core_role
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 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';
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 5335abf..ba37842 100644 (file)
@@ -1124,6 +1124,7 @@ $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',
             'core_files_get_files',
             'core_message_get_messages',
             'core_message_create_contacts',
@@ -1160,6 +1161,17 @@ $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,
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 035ba63..fc87c50 100644 (file)
@@ -346,7 +346,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 +876,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 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..efcde10 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
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',
index 7781fb6..78f7004 100644 (file)
@@ -1413,4 +1413,25 @@ class behat_general extends behat_base {
             fwrite(STDOUT, "\033[2A\033[u\033[2B");
         }
     }
+
+    /**
+     * Presses a given button in the browser.
+     *
+     * @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 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..874ea3d 100644 (file)
@@ -132,6 +132,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 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 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()
+            )
+        );
+    }
+
+}
similarity index 55%
rename from blocks/tags/settings.php
rename to mod/book/db/services.php
index 4db5098..be081d6 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Settings for the tags block.
+ * Book external functions and service definitions.
  *
- * @package   block_tags
- * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @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;
 
-if ($ADMIN->fulltree) {
-    $settings->add(new admin_setting_configcheckbox('block_tags_showcoursetags', get_string('showcoursetags', 'block_tags'),
-                       get_string('showcoursetagsdef', 'block_tags'), 0));
-}
+$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..0340fd6 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,12 +193,12 @@ 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
 // =====================================================
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 module external API
+ *
+ * @package    mod_choice
+ * @category   external
+ * @copyright  2015 Costantino Cito <ccito@cvaconsulting.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/choice/lib.php');
+
+/**
+ * Choice module external functions
+ *
+ * @package    mod_choice
+ * @category   external
+ * @copyright  2015 Costantino Cito <ccito@cvaconsulting.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.0
+ */
+class mod_choice_external extends external_api {
+
+    /**
+     * Describes the parameters for get_choices_by_courses.
+     *
+     * @return external_external_function_parameters
+     * @since Moodle 3.0
+     */
+    public static function get_choice_results_parameters() {
+        return new external_function_parameters (array('choiceid' => new external_value(PARAM_INT, 'choice instance id')));
+    }
+    /**
+     * Returns user's results for a specific choice
+     * and a list of those users that did not answered yet.
+     *
+     * @param int $choiceid the choice instance id
+     * @return array of responses details
+     * @since Moodle 3.0
+     */
+    public static function get_choice_results($choiceid) {
+        global $USER;
+
+        $params = self::validate_parameters(self::get_choice_results_parameters(), array('choiceid' => $choiceid));
+
+        if (!$choice = choice_get_choice($params['choiceid'])) {
+            throw new moodle_exception("invalidcoursemodule", "error");
+        }
+        list($course, $cm) = get_course_and_cm_from_instance($choice, 'choice');
+
+        $context = context_module::instance($cm->id);
+        self::validate_context($context);
+
+        $groupmode = groups_get_activity_groupmode($cm);
+        // Check if we have to include responses from inactive users.
+        $onlyactive = $choice->includeinactive ? false : true;
+        $users = choice_get_response_data($choice, $cm, $groupmode, $onlyactive);
+        // Show those who haven't answered the question.
+        if (!empty($choice->showunanswered)) {
+            $choice->option[0] = get_string('notanswered', 'choice');
+            $choice->maxanswers[0] = 0;
+        }
+        $results = prepare_choice_show_results($choice, $course, $cm, $users);
+
+        $options = array();
+        $fullnamecap = has_capability('moodle/site:viewfullnames', $context);
+        foreach ($results->options as $optionid => $option) {
+
+            $userresponses = array();
+            $numberofuser = 0;
+            $percentageamount = 0;
+            if (property_exists($option, 'user') and
+                (has_capability('mod/choice:readresponses', $context) or choice_can_view_results($choice))) {
+                $numberofuser = count($option->user);
+                $percentageamount = ((float)$numberofuser / (float)$results->numberofuser) * 100.0;
+                if ($choice->publish) {
+                    foreach ($option->user as $userresponse) {
+                        $response = array();
+                        $response['userid'] = $userresponse->id;
+                        $response['fullname'] = fullname($userresponse, $fullnamecap);
+                        $usercontext = context_user::instance($userresponse->id, IGNORE_MISSING);
+                        if ($usercontext) {
+                            $profileimageurl = moodle_url::make_webservice_pluginfile_url($usercontext->id, 'user', 'icon', null,
+                                                                                         '/', 'f1')->out(false);
+                        } else {
+                            $profileimageurl = '';
+                        }
+                        $response['profileimageurl'] = $profileimageurl;
+                        // Add optional properties.
+                        foreach (array('answerid', 'timemodified') as $field) {
+                            if (property_exists($userresponse, 'answerid')) {
+                                $response[$field] = $userresponse->$field;
+                            }
+                        }
+                        $userresponses[] = $response;
+                    }
+                }
+            }
+
+            $options[] = array('id'               => $optionid,
+                               'text'             => format_string($option->text, true, array('context' => $context)),
+                               'maxanswer'        => $option->maxanswer,
+                               'userresponses'    => $userresponses,
+                               'numberofuser'     => $numberofuser,
+                               'percentageamount' => $percentageamount
+                              );
+        }
+
+        $warnings = array();
+        return array(
+            'options' => $options,
+            'warnings' => $warnings
+        );
+    }
+
+    /**
+     * Describes the get_choice_results return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.0
+     */
+    public static function get_choice_results_returns() {
+        return new external_single_structure(
+            array(
+                'options' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'id' => new external_value(PARAM_INT, 'choice instance id'),
+                            'text' => new external_value(PARAM_RAW, 'text of the choice'),
+                            'maxanswer' => new external_value(PARAM_INT, 'maximum number of answers'),
+                            'userresponses' => new external_multiple_structure(
+                                 new external_single_structure(
+                                     array(
+                                        'userid' => new external_value(PARAM_INT, 'user id'),
+                                        'fullname' => new external_value(PARAM_NOTAGS, 'user full name'),
+                                        'profileimageurl' => new external_value(PARAM_URL, 'profile user image url'),
+                                        'answerid' => new external_value(PARAM_INT, 'answer id', VALUE_OPTIONAL),
+                                        'timemodified' => new external_value(PARAM_INT, 'time of modification', VALUE_OPTIONAL),
+                                     ), 'User responses'
+                                 )
+                            ),
+                            'numberofuser' => new external_value(PARAM_INT, 'number of users answers'),
+                            'percentageamount' => new external_value(PARAM_FLOAT, 'percentage of users answers')
+                        ), 'Options'
+                    )
+                ),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
+
+    /**
+     * Describes the parameters for mod_choice_get_choice_options.
+     *
+     * @return external_external_function_parameters
+     * @since Moodle 3.0
+     */
+    public static function get_choice_options_parameters() {
+        return new external_function_parameters (array('choiceid' => new external_value(PARAM_INT, 'choice instance id')));
+    }
+
+    /**
+     * Returns options for a specific choice
+     *
+     * @param int $choiceid the choice instance id
+     * @return array of options details
+     * @since Moodle 3.0
+     */
+    public static function get_choice_options($choiceid) {
+        global $USER;
+        $warnings = array();
+        $params = self::validate_parameters(self::get_choice_options_parameters(), array('choiceid' => $choiceid));
+
+        if (!$choice = choice_get_choice($params['choiceid'])) {
+            throw new moodle_exception("invalidcoursemodule", "error");
+        }
+        list($course, $cm) = get_course_and_cm_from_instance($choice, 'choice');
+
+        $context = context_module::instance($cm->id);
+        self::validate_context($context);
+
+        require_capability('mod/choice:choose', $context);
+
+        $groupmode = groups_get_activity_groupmode($cm);
+        $onlyactive = $choice->includeinactive ? false : true;
+        $allresponses = choice_get_response_data($choice, $cm, $groupmode, $onlyactive);
+
+        $timenow = time();
+        $choiceopen = true;
+        $showpreview = false;
+
+        if ($choice->timeclose != 0) {
+            if ($choice->timeopen > $timenow) {
+                $choiceopen = false;
+                $warnings[1] = get_string("notopenyet", "choice", userdate($choice->timeopen));
+                if ($choice->showpreview) {
+                    $warnings[2] = get_string('previewonly', 'choice', userdate($choice->timeopen));
+                    $showpreview = true;
+                }
+            }
+            if ($timenow > $choice->timeclose) {
+                $choiceopen = false;
+                $warnings[3] = get_string("expired", "choice", userdate($choice->timeclose));
+            }
+        }
+        $optionsarray = array();
+
+        if ($choiceopen or $showpreview) {
+
+            $options = choice_prepare_options($choice, $USER, $cm, $allresponses);
+
+            foreach ($options['options'] as $option) {
+                $optionarr = array();
+                $optionarr['id']            = $option->attributes->value;
+                $optionarr['text']          = format_string($option->text, true, array('context' => $context));
+                $optionarr['maxanswers']    = $option->maxanswers;
+                $optionarr['displaylayout'] = $option->displaylayout;
+                $optionarr['countanswers']  = $option->countanswers;
+                foreach (array('checked', 'disabled') as $field) {
+                    if (property_exists($option->attributes, $field) and $option->attributes->$field == 1) {
+                        $optionarr[$field] = 1;
+                    } else {
+                        $optionarr[$field] = 0;
+                    }
+                }
+                // When showpreview is active, we show options as disabled.
+                if ($showpreview or ($optionarr['checked'] == 1 and !$choice->allowupdate)) {
+                    $optionarr['disabled'] = 1;
+                }
+                $optionsarray[] = $optionarr;
+            }
+        }
+        foreach ($warnings as $key => $message) {
+            $warnings[$key] = array(
+                'item' => 'choice',
+                'itemid' => $cm->id,
+                'warningcode' => $key,
+                'message' => $message
+            );
+        }
+        return array(
+            'options' => $optionsarray,
+            'warnings' => $warnings
+        );
+    }
+
+    /**
+     * Describes the get_choice_results return value.
+     *
+     * @return external_multiple_structure
+     * @since Moodle 3.0
+     */
+    public static function get_choice_options_returns() {
+        return new external_single_structure(
+            array(
+                'options' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'id' => new external_value(PARAM_INT, 'option id'),
+                            'text' => new external_value(PARAM_RAW, 'text of the choice'),
+                            'maxanswers' => new external_value(PARAM_INT, 'maximum number of answers'),
+                            'displaylayout' => new external_value(PARAM_BOOL, 'true for orizontal, otherwise vertical'),
+                            'countanswers' => new external_value(PARAM_INT, 'number of answers'),
+                            'checked' => new external_value(PARAM_BOOL, 'we already answered'),
+                            'disabled' => new external_value(PARAM_BOOL, 'option disabled'),
+                            )
+                    ), 'Options'
+                ),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
+
+    /**
+     * Describes the parameters for submit_choice_response.
+     *
+     * @return external_external_function_parameters
+     * @since Moodle 3.0
+     */
+    public static function submit_choice_response_parameters() {
+        return new external_function_parameters (
+            array(
+                'choiceid' => new external_value(PARAM_INT, 'choice instance id'),
+                'responses' => new external_multiple_structure(
+                    new external_value(PARAM_INT, 'answer id'),
+                    'Array of response ids'
+                ),
+            )
+        );
+    }
+
+    /**
+     * Submit choice responses
+     *
+     * @param int $choiceid the choice instance id
+     * @param array $responses the response ids
+     * @return array ansers informatinon and warnings
+     * @since Moodle 3.0
+     */
+    public static function submit_choice_response($choiceid, $responses) {
+        global $USER;
+
+        $warnings = array();
+        $params = self::validate_parameters(self::submit_choice_response_parameters(),
+                                            array(
+                                                'choiceid' => $choiceid,
+                                                'responses' => $responses
+                                            ));
+
+        if (!$choice = choice_get_choice($params['choiceid'])) {
+            throw new moodle_exception("invalidcoursemodule", "error");
+        }
+        list($course, $cm) = get_course_and_cm_from_instance($choice, 'choice');
+
+        $context = context_module::instance($cm->id);
+        self::validate_context($context);
+
+        require_capability('mod/choice:choose', $context);
+
+        $timenow = time();
+        if ($choice->timeclose != 0) {
+            if ($choice->timeopen > $timenow) {
+                throw new moodle_exception("notopenyet", "choice", '', userdate($choice->timeopen));
+            } else if ($timenow > $choice->timeclose) {
+                throw new moodle_exception("expired", "choice", '', userdate($choice->timeclose));
+            }
+        }
+        if (!choice_get_my_response($choice) or $choice->allowupdate) {
+            // When a single response is given, we convert the array to a simple variable
+            // in order to avoid choice_user_submit_response to check with allowmultiple even
+            // for a single response.
+            if (count($params['responses']) == 1) {
+                $params['responses'] = reset($params['responses']);
+            }
+            choice_user_submit_response($params['responses'], $choice, $USER->id, $course, $cm);
+        } else {
+            throw new moodle_exception('missingrequiredcapability', 'webservice', '', 'allowupdate');
+        }
+        $answers = choice_get_my_response($choice);
+
+        return array(
+            'answers' => $answers,
+            'warnings' => $warnings
+        );
+    }
+
+    /**
+     * Describes the submit_choice_response return value.
+     *
+     * @return external_multiple_structure
+     * @since Moodle 3.0
+     */
+    public static function submit_choice_response_returns() {
+        return new external_single_structure(
+            array(
+                'answers' => new external_multiple_structure(
+                     new external_single_structure(
+                         array(
+                             'id'           => new external_value(PARAM_INT, 'answer id'),
+                             'choiceid'     => new external_value(PARAM_INT, 'choiceid'),
+                             'userid'       => new external_value(PARAM_INT, 'user id'),
+                             'optionid'     => new external_value(PARAM_INT, 'optionid'),
+                             'timemodified' => new external_value(PARAM_INT, 'time of last modification')
+                         ), 'Answers'
+                     )
+                ),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.0
+     */
+    public static function view_choice_parameters() {
+        return new external_function_parameters(
+            array(
+                'choiceid' => new external_value(PARAM_INT, 'choice instance id')
+            )
+        );
+    }
+
+    /**
+     * Trigger the course module viewed event and update the module completion status.
+     *
+     * @param int $choiceid the choice instance id
+     * @return array of warnings and status result
+     * @since Moodle 3.0
+     * @throws moodle_exception
+     */
+    public static function view_choice($choiceid) {
+        global $CFG;
+
+        $params = self::validate_parameters(self::view_choice_parameters(),
+                                            array(
+                                                'choiceid' => $choiceid
+                                            ));
+        $warnings = array();
+
+        // Request and permission validation.
+        if (!$choice = choice_get_choice($params['choiceid'])) {
+            throw new moodle_exception("invalidcoursemodule", "error");
+        }
+        list($course, $cm) = get_course_and_cm_from_instance($choice, 'choice');
+
+        $context = context_module::instance($cm->id);
+        self::validate_context($context);
+
+        // Trigger course_module_viewed event and completion.
+        choice_view($choice, $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_choice_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/choice/db/services.php b/mod/choice/db/services.php
new file mode 100644 (file)
index 0000000..bbe3181
--- /dev/null
@@ -0,0 +1,62 @@
+<?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 external functions and service definitions.
+ *
+ * @package    mod_choice
+ * @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_choice_get_choice_results' => array(
+        'classname'     => 'mod_choice_external',
+        'methodname'    => 'get_choice_results',
+        'description'   => 'Retrieve users results for a given choice.',
+        'type'          => 'read',
+        'capabilities'  => ''
+    ),
+
+    'mod_choice_get_choice_options' => array(
+        'classname'     => 'mod_choice_external',
+        'methodname'    => 'get_choice_options',
+        'description'   => 'Retrieve options for a specific choice.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/choice:choose'
+    ),
+
+    'mod_choice_submit_choice_response' => array(
+        'classname'     => 'mod_choice_external',
+        'methodname'    => 'submit_choice_response',
+        'description'   => 'Submit responses to a specific choice item.',
+        'type'          => 'write',
+        'capabilities'  => 'mod/choice:choose'
+    ),
+
+    'mod_choice_view_choice' => array(
+        'classname'     => 'mod_choice_external',
+        'methodname'    => 'view_choice',
+        'description'   => 'Trigger the course module viewed event and update the module completion status.',
+        'type'          => 'write',
+        'capabilities'  => ''
+    ),
+);
index 8d62076..b04ce60 100644 (file)
@@ -921,3 +921,75 @@ function choice_print_overview($courses, &$htmlarray) {
     }
     return;
 }
+
+
+/**
+ * Get my responses on a given choice.
+ *
+ * @param stdClass $choice Choice record
+ * @return array of choice answers records
+ * @since  Moodle 3.0
+ */
+function choice_get_my_response($choice) {
+    global $DB, $USER;
+    return $DB->get_records('choice_answers', array('choiceid' => $choice->id, 'userid' => $USER->id));
+}
+
+/**
+ * Return true if we are allowd to view the choice results.
+ *
+ * @param stdClass $choice Choice record
+ * @param rows|null $current my choice responses
+ * @param bool|null $choiceopen if the choice is open
+ * @return bool true if we can view the results, false otherwise.
+ * @since  Moodle 3.0
+ */
+function choice_can_view_results($choice, $current = null, $choiceopen = null) {
+
+    if (is_null($choiceopen)) {
+        $timenow = time();
+        if ($choice->timeclose != 0 && $timenow > $choice->timeclose) {
+            $choiceopen = false;
+        } else {
+            $choiceopen = true;
+        }
+    }
+    if (empty($current)) {
+        $current = choice_get_my_response($choice);
+    }
+
+    if ($choice->showresults == CHOICE_SHOWRESULTS_ALWAYS or
+       ($choice->showresults == CHOICE_SHOWRESULTS_AFTER_ANSWER and !empty($current)) or
+       ($choice->showresults == CHOICE_SHOWRESULTS_AFTER_CLOSE and !$choiceopen)) {
+        return true;
+    }
+    return false;
+}
+
+/**
+ * Mark the activity completed (if required) and trigger the course_module_viewed event.
+ *
+ * @param  stdClass $choice     choice object
+ * @param  stdClass $course     course object
+ * @param  stdClass $cm         course module object
+ * @param  stdClass $context    context object
+ * @since Moodle 3.0
+ */
+function choice_view($choice, $course, $cm, $context) {
+
+    // Trigger course_module_viewed event.
+    $params = array(
+        'context' => $context,
+        'objectid' => $choice->id
+    );
+
+    $event = \mod_choice\event\course_module_viewed::create($params);
+    $event->add_record_snapshot('course_modules', $cm);
+    $event->add_record_snapshot('course', $course);
+    $event->add_record_snapshot('choice', $choice);
+    $event->trigger();
+
+    // Completion.
+    $completion = new completion_info($course);
+    $completion->set_module_viewed($cm);
+}
diff --git a/mod/choice/tests/externallib_test.php b/mod/choice/tests/externallib_test.php
new file mode 100644 (file)
index 0000000..f227c32
--- /dev/null
@@ -0,0 +1,353 @@
+<?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 choice functions unit tests
+ *
+ * @package    mod_choice
+ * @category   external
+ * @copyright  2015 Costantino Cito <ccito@cvaconsulting.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+require_once($CFG->dirroot . '/mod/choice/lib.php');
+
+/**
+ * External choice functions unit tests
+ *
+ * @package    mod_choice
+ * @category   external
+ * @copyright  2015 Costantino Cito <ccito@cvaconsulting.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_choice_externallib_testcase extends externallib_advanced_testcase {
+
+    /**
+     * Test get_choice_results
+     */
+    public function test_get_choice_results() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $course = self::getDataGenerator()->create_course();
+        $params = new stdClass();
+        $params->course = $course->id;
+        $params->option = array('fried rice', 'spring rolls', 'sweet and sour pork', 'satay beef', 'gyouza');
+        $params->name = 'First Choice Activity';
+        $params->showresults = CHOICE_SHOWRESULTS_AFTER_ANSWER;
+        $params->publish = 1;
+        $params->allowmultiple = 1;
+        $params->showunanswered = 1;
+        $choice = self::getDataGenerator()->create_module('choice', $params);
+
+        $cm = get_coursemodule_from_id('choice', $choice->cmid);
+        $choiceinstance = choice_get_choice($cm->instance);
+        $options = array_keys($choiceinstance->option);
+        $student1 = $this->getDataGenerator()->create_user();
+        $student2 = $this->getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+
+        // Enroll Students in Course1.
+        self::getDataGenerator()->enrol_user($student1->id,  $course->id, $studentrole->id);
+        self::getDataGenerator()->enrol_user($student2->id,  $course->id, $studentrole->id);
+
+        $this->setUser($student1);
+        $myanswer = $options[2];
+        choice_user_submit_response($myanswer, $choice, $student1->id, $course, $cm);
+        $results = mod_choice_external::get_choice_results($choice->id);
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $results = external_api::clean_returnvalue(mod_choice_external::get_choice_results_returns(), $results);
+
+        // Create an array with optionID as Key.
+        $resultsarr = array();
+        foreach ($results['options'] as $option) {
+            $resultsarr[$option['id']] = $option['userresponses'];
+        }
+        // The stundent1 is the userid who choosed the myanswer(option 3).
+        $this->assertEquals($resultsarr[$myanswer][0]['userid'], $student1->id);
+        // The stundent2 is the userid who didn't answered yet.
+        $this->assertEquals($resultsarr[0][0]['userid'], $student2->id);
+
+        // As Stundent2 we cannot see results (until we answered).
+        $this->setUser($student2);
+        $results = mod_choice_external::get_choice_results($choice->id);
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $results = external_api::clean_returnvalue(mod_choice_external::get_choice_results_returns(), $results);
+        // We do not retrieve any response!
+        foreach ($results['options'] as $option) {
+            $this->assertCount(0, $option['userresponses']);
+        }
+
+        $timenow = time();
+        // We can see results only after activity close (even if we didn't answered).
+        $choice->showresults = CHOICE_SHOWRESULTS_AFTER_CLOSE;
+        // Set timeopen and timeclose in the past.
+        $choice->timeopen = $timenow - (60 * 60 * 24 * 3);
+        $choice->timeclose = $timenow + (60 * 60 * 24 * 2);
+        $DB->update_record('choice', $choice);
+
+        $results = mod_choice_external::get_choice_results($choice->id);
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $results = external_api::clean_returnvalue(mod_choice_external::get_choice_results_returns(), $results);
+        // We do not retrieve any response (activity is still open).
+        foreach ($results['options'] as $option) {
+            $this->assertCount(0, $option['userresponses']);
+        }
+
+        // We close the activity (setting timeclose in the past).
+        $choice->timeclose = $timenow - (60 * 60 * 24 * 2);
+        $DB->update_record('choice', $choice);
+        // Now as Stundent2 we will see results!
+        $results = mod_choice_external::get_choice_results($choice->id);
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $results = external_api::clean_returnvalue(mod_choice_external::get_choice_results_returns(), $results);
+        // Create an array with optionID as Key.
+        $resultsarr = array();
+        foreach ($results['options'] as $option) {
+            $resultsarr[$option['id']] = $option['userresponses'];
+        }
+        // The stundent1 is the userid who choosed the myanswer(option 3).
+        $this->assertEquals($resultsarr[$myanswer][0]['userid'], $student1->id);
+        // The stundent2 is the userid who didn't answered yet.
+        $this->assertEquals($resultsarr[0][0]['userid'], $student2->id);
+
+        // Do not publish user names!
+        $choice->publish = 0;
+        $DB->update_record('choice', $choice);
+        $results = mod_choice_external::get_choice_results($choice->id);
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $results = external_api::clean_returnvalue(mod_choice_external::get_choice_results_returns(), $results);
+        // Create an array with optionID as Key.
+        $resultsarr = array();
+        // Does not show any user response!
+        foreach ($results['options'] as $option) {
+            $this->assertCount(0, $option['userresponses']);
+            $resultsarr[$option['id']] = $option;
+        }
+        // But we can see totals and percentages.
+        $this->assertEquals(1, $resultsarr[$myanswer]['numberofuser']);
+    }
+
+    /**
+     * Test get_choice_options
+     */
+    public function test_get_choice_options() {
+        global $DB;
+
+        // Warningcodes.
+        $notopenyet = 1;
+        $previewonly = 2;
+        $expired = 3;
+
+        $this->resetAfterTest(true);
+        $timenow = time();
+        $timeopen = $timenow + (60 * 60 * 24 * 2);
+        $timeclose = $timenow + (60 * 60 * 24 * 7);
+        $course = self::getDataGenerator()->create_course();
+        $possibleoptions = array('fried rice', 'spring rolls', 'sweet and sour pork', 'satay beef', 'gyouza');
+        $params = array();
+        $params['course'] = $course->id;
+        $params['option'] = $possibleoptions;
+        $params['name'] = 'First Choice Activity';
+        $params['showpreview'] = 0;
+
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
+        $choice = $generator->create_instance($params);
+
+        $student1 = $this->getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        // Enroll Students in Course.
+        self::getDataGenerator()->enrol_user($student1->id,  $course->id, $studentrole->id);
+        $this->setUser($student1);
+
+        $results = mod_choice_external::get_choice_options($choice->id);
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $results = external_api::clean_returnvalue(mod_choice_external::get_choice_options_returns(), $results);
+        // We should retrieve all options.
+        $this->assertCount(count($possibleoptions), $results['options']);
+
+        // Here we force timeopen/close in the future.
+        $choice->timeopen = $timeopen;
+        $choice->timeclose = $timeclose;
+        $DB->update_record('choice', $choice);
+
+        $results = mod_choice_external::get_choice_options($choice->id);
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $results = external_api::clean_returnvalue(mod_choice_external::get_choice_options_returns(), $results);
+        // We should retrieve no options.
+        $this->assertCount(0, $results['options']);
+        $this->assertEquals($notopenyet, $results['warnings'][0]['warningcode']);
+
+        // Here we see the options because of preview!
+        $choice->showpreview = 1;
+        $DB->update_record('choice', $choice);
+        $results = mod_choice_external::get_choice_options($choice->id);
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $results = external_api::clean_returnvalue(mod_choice_external::get_choice_options_returns(), $results);
+        // We should retrieve all options.
+        $this->assertCount(count($possibleoptions), $results['options']);
+
+        foreach ($results['options'] as $option) {
+            // Each option is disabled as this is only the preview!
+            $this->assertEquals(1, $option['disabled']);
+        }
+        $warnings = array();
+        foreach ($results['warnings'] as $warning) {
+            $warnings[$warning['warningcode']] = $warning['message'];
+        }
+        $this->assertTrue(isset($warnings[$previewonly]));
+        $this->assertTrue(isset($warnings[$notopenyet]));
+
+        // Simulate activity as opened!
+        $choice->timeopen = $timenow - (60 * 60 * 24 * 3);
+        $choice->timeclose = $timenow + (60 * 60 * 24 * 2);
+        $DB->update_record('choice', $choice);
+        $cm = get_coursemodule_from_id('choice', $choice->cmid);
+        $choiceinstance = choice_get_choice($cm->instance);
+        $optionsids = array_keys($choiceinstance->option);
+        $myanswerid = $optionsids[2];
+        choice_user_submit_response($myanswerid, $choice, $student1->id, $course, $cm);
+
+        $results = mod_choice_external::get_choice_options($choice->id);
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $results = external_api::clean_returnvalue(mod_choice_external::get_choice_options_returns(), $results);
+        // We should retrieve all options.
+        $this->assertCount(count($possibleoptions), $results['options']);
+        foreach ($results['options'] as $option) {
+            // When we answered and we cannot update our choice.
+            if ($option['id'] == $myanswerid and !$choice->allowupdate) {
+                $this->assertEquals(1, $option['disabled']);
+                $this->assertEquals(1, $option['checked']);
+            } else {
+                $this->assertEquals(0, $option['disabled']);
+            }
+        }
+
+        // Set timeopen and timeclose as older than today!
+        // We simulate what happens when the activity is closed.
+        $choice->timeopen = $timenow - (60 * 60 * 24 * 3);
+        $choice->timeclose = $timenow - (60 * 60 * 24 * 2);
+        $DB->update_record('choice', $choice);
+        $results = mod_choice_external::get_choice_options($choice->id);
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $results = external_api::clean_returnvalue(mod_choice_external::get_choice_options_returns(), $results);
+        // We should retrieve no options.
+        $this->assertCount(0, $results['options']);
+        $this->assertEquals($expired, $results['warnings'][0]['warningcode']);
+
+    }
+
+    /**
+     * Test submit_choice_response
+     */
+    public function test_submit_choice_response() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $course = self::getDataGenerator()->create_course();
+        $params = new stdClass();
+        $params->course = $course->id;
+        $params->option = array('fried rice', 'spring rolls', 'sweet and sour pork', 'satay beef', 'gyouza');
+        $params->name = 'First Choice Activity';
+        $params->showresults = CHOICE_SHOWRESULTS_ALWAYS;
+        $params->allowmultiple = 1;
+        $params->showunanswered = 1;
+        $choice = self::getDataGenerator()->create_module('choice', $params);
+        $cm = get_coursemodule_from_id('choice', $choice->cmid);
+        $choiceinstance = choice_get_choice($cm->instance);
+        $options = array_keys($choiceinstance->option);
+        $student1 = $this->getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+
+        // Enroll Students in Course1.
+        self::getDataGenerator()->enrol_user($student1->id,  $course->id, $studentrole->id);
+
+        $this->setUser($student1);
+        $myresponse = $options[2];
+        $results = mod_choice_external::submit_choice_response($choice->id, array($myresponse));
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $results = external_api::clean_returnvalue(mod_choice_external::submit_choice_response_returns(), $results);
+        $myanswers = $DB->get_records('choice_answers', array('choiceid' => $choice->id, 'userid' => $student1->id));
+        $myanswer = reset($myanswers);
+        $this->assertEquals($results['answers'][0]['id'], $myanswer->id);
+        $this->assertEquals($results['answers'][0]['choiceid'], $myanswer->choiceid);
+        $this->assertEquals($results['answers'][0]['userid'], $myanswer->userid);
+        $this->assertEquals($results['answers'][0]['timemodified'], $myanswer->timemodified);
+    }
+
+    /**
+     * Test view_choice
+     */
+    public function test_view_choice() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        // Setup test data.
+        $course = $this->getDataGenerator()->create_course();
+        $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id));
+        $context = context_module::instance($choice->cmid);
+        $cm = get_coursemodule_from_instance('choice', $choice->id);
+
+        // Test invalid instance id.
+        try {
+            mod_choice_external::view_choice(0);
+            $this->fail('Exception expected due to invalid mod_choice instance id.');
+        } catch (moodle_exception $e) {
+            $this->assertEquals('invalidcoursemodule', $e->errorcode);
+        }
+
+        // Test not-enrolled user.
+        $user = self::getDataGenerator()->create_user();
+        $this->setUser($user);
+        try {
+            mod_choice_external::view_choice($choice->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_choice_external::view_choice($choice->id);
+        $result = external_api::clean_returnvalue(mod_choice_external::view_choice_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_choice\event\course_module_viewed', $event);
+        $this->assertEquals($context, $event->get_context());
+        $moodlechoice = new \moodle_url('/mod/choice/view.php', array('id' => $cm->id));
+        $this->assertEquals($moodlechoice, $event->get_url());
+        $this->assertEventContextNotUsed($event);
+        $this->assertNotEmpty($event->get_name());
+
+    }
+}
diff --git a/mod/choice/tests/lib_test.php b/mod/choice/tests/lib_test.php
new file mode 100644 (file)
index 0000000..0f58117
--- /dev/null
@@ -0,0 +1,175 @@
+<?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 module library functions tests
+ *
+ * @package    mod_choice
+ * @category   test
+ * @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');
+require_once($CFG->dirroot . '/mod/choice/lib.php');
+
+/**
+ * Choice module library functions tests
+ *
+ * @package    mod_choice
+ * @category   test
+ * @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_choice_lib_testcase extends externallib_advanced_testcase {
+
+    /**
+     * Test choice_view
+     * @return void
+     */
+    public function test_choice_view() {
+        global $CFG;
+
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+        // Setup test data.
+        $course = $this->getDataGenerator()->create_course();
+        $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id));
+        $context = context_module::instance($choice->cmid);
+        $cm = get_coursemodule_from_instance('choice', $choice->id);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+
+        choice_view($choice, $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_choice\event\course_module_viewed', $event);
+        $this->assertEquals($context, $event->get_context());
+        $url = new \moodle_url('/mod/choice/view.php', array('id' => $cm->id));
+        $this->assertEquals($url, $event->get_url());
+        $this->assertEventContextNotUsed($event);
+        $this->assertNotEmpty($event->get_name());
+    }
+
+    /**
+     * Test choice_can_view_results
+     * @return void
+     */
+    public function test_choice_can_view_results() {
+        global $DB, $USER;
+
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+        // Setup test data.
+        $course = $this->getDataGenerator()->create_course();
+        $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id));
+        $context = context_module::instance($choice->cmid);
+        $cm = get_coursemodule_from_instance('choice', $choice->id);
+
+        // Default values are false, user cannot view results.
+        $canview = choice_can_view_results($choice);
+        $this->assertFalse($canview);
+
+        // Show results forced.
+        $choice->showresults = CHOICE_SHOWRESULTS_ALWAYS;
+        $DB->update_record('choice', $choice);
+        $canview = choice_can_view_results($choice);
+        $this->assertTrue($canview);
+
+        // Show results after closing.
+        $choice->showresults = CHOICE_SHOWRESULTS_AFTER_CLOSE;
+        $DB->update_record('choice', $choice);
+        $canview = choice_can_view_results($choice);
+        $this->assertFalse($canview);
+
+        $choice->timeclose = time() - HOURSECS;
+        $DB->update_record('choice', $choice);
+        $canview = choice_can_view_results($choice);
+        $this->assertTrue($canview);
+
+        // Show results after answering.
+        $choice->timeclose = 0;
+        $choice->showresults = CHOICE_SHOWRESULTS_AFTER_ANSWER;
+        $DB->update_record('choice', $choice);
+        $canview = choice_can_view_results($choice);
+        $this->assertFalse($canview);
+
+        // Get the first option.
+        $choicewithoptions = choice_get_choice($choice->id);
+        $optionids = array_keys($choicewithoptions->option);
+
+        choice_user_submit_response($optionids[0], $choice, $USER->id, $course, $cm);
+
+        $canview = choice_can_view_results($choice);
+        $this->assertTrue($canview);
+
+    }
+
+    /**
+     * Test choice_get_my_response
+     * @return void
+     */
+    public function test_choice_get_my_response() {
+        global $USER;
+
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+        // Setup test data.
+        $course = $this->getDataGenerator()->create_course();
+        $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id));
+        $context = context_module::instance($choice->cmid);
+        $cm = get_coursemodule_from_instance('choice', $choice->id);
+
+        $choicewithoptions = choice_get_choice($choice->id);
+        $optionids = array_keys($choicewithoptions->option);
+
+        choice_user_submit_response($optionids[0], $choice, $USER->id, $course, $cm);
+        $responses = choice_get_my_response($choice, $course, $cm, $context);
+        $this->assertCount(1, $responses);
+        $response = array_shift($responses);
+        $this->assertEquals($optionids[0], $response->optionid);
+
+        // Multiple responses.
+        $choice = $this->getDataGenerator()->create_module('choice', array('course' => $course->id, 'allowmultiple' => 1));
+        $context = context_module::instance($choice->cmid);
+        $cm = get_coursemodule_from_instance('choice', $choice->id);
+
+        $choicewithoptions = choice_get_choice($choice->id);
+        $optionids = array_keys($choicewithoptions->option);
+
+        choice_user_submit_response($optionids, $choice, $USER->id, $course, $cm);
+        $responses = choice_get_my_response($choice, $course, $cm, $context);
+        $this->assertCount(count($optionids), $responses);
+        foreach ($responses as $resp) {
+            $this->assertContains($resp->optionid, $optionids);
+        }
+    }
+
+}
index bb646e3..0263b8d 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_choice';     // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index 500db55..d40beba 100644 (file)
@@ -51,10 +51,6 @@ if ($action == 'delchoice' and confirm_sesskey() and is_enrolled($context, NULL,
 $PAGE->set_title($choice->name);
 $PAGE->set_heading($course->fullname);
 
-// Mark viewed by user (if required)
-$completion = new completion_info($course);
-$completion->set_module_viewed($cm);
-
 /// Submit any new data if there is any
 if (data_submitted() && is_enrolled($context, NULL, 'mod/choice:choose') && confirm_sesskey()) {
     $timenow = time();
@@ -83,6 +79,9 @@ if (data_submitted() && is_enrolled($context, NULL, 'mod/choice:choose') && conf
     }
 }
 
+// Completion and trigger events.
+choice_view($choice, $course, $cm, $context);
+
 echo $OUTPUT->header();
 echo $OUTPUT->heading(format_string($choice->name), 2, null);
 
@@ -99,11 +98,6 @@ $eventdata = array();
 $eventdata['objectid'] = $choice->id;
 $eventdata['context'] = $context;
 
-$event = \mod_choice\event\course_module_viewed::create($eventdata);
-$event->add_record_snapshot('course_modules', $cm);
-$event->add_record_snapshot('course', $course);
-$event->trigger();
-
 /// Check to see if groups are being used in this choice
 $groupmode = groups_get_activity_groupmode($cm);
 
@@ -129,7 +123,7 @@ if ($choice->intro) {
 }
 
 $timenow = time();
-$current = $DB->get_records('choice_answers', array('choiceid' => $choice->id, 'userid' => $USER->id));
+$current = choice_get_my_response($choice);
 //if user has already made a selection, and they are not allowed to update it or if choice is not open, show their selected answer.
 if (isloggedin() && (!empty($current)) &&
     (empty($choice->allowupdate) || ($timenow > $choice->timeclose)) ) {
@@ -194,9 +188,7 @@ if (!$choiceformshown) {
 }
 
 // print the results at the bottom of the screen
-if ( $choice->showresults == CHOICE_SHOWRESULTS_ALWAYS or
-    ($choice->showresults == CHOICE_SHOWRESULTS_AFTER_ANSWER and $current) or
-    ($choice->showresults == CHOICE_SHOWRESULTS_AFTER_CLOSE and !$choiceopen)) {
+if (choice_can_view_results($choice, $current, $choiceopen)) {
 
     if (!empty($choice->showunanswered)) {
         $choice->option[0] = get_string('notanswered', 'choice');
index e7b6ccd..d664107 100644 (file)
@@ -1524,6 +1524,53 @@ function data_rating_validate($params) {
     return true;
 }
 
+/**
+ * Can the current user see ratings for a given itemid?
+ *
+ * @param array $params submitted data
+ *            contextid => int contextid [required]
+ *            component => The component for this module - should always be mod_data [required]
+ *            ratingarea => object the context in which the rated items exists [required]
+ *            itemid => int the ID of the object being rated [required]
+ *            scaleid => int scale id [optional]
+ * @return bool
+ * @throws coding_exception
+ * @throws rating_exception
+ */
+function mod_data_rating_can_see_item_ratings($params) {
+    global $DB;
+
+    // Check the component is mod_data.
+    if (!isset($params['component']) || $params['component'] != 'mod_data') {
+        throw new rating_exception('invalidcomponent');
+    }
+
+    // Check the ratingarea is entry (the only rating area in data).
+    if (!isset($params['ratingarea']) || $params['ratingarea'] != 'entry') {
+        throw new rating_exception('invalidratingarea');
+    }
+
+    if (!isset($params['itemid'])) {
+        throw new rating_exception('invaliditemid');
+    }
+
+    $datasql = "SELECT d.id as dataid, d.course, r.groupid
+                  FROM {data_records} r
+                  JOIN {data} d ON r.dataid = d.id
+                 WHERE r.id = :itemid";
+    $dataparams = array('itemid' => $params['itemid']);
+    if (!$info = $DB->get_record_sql($datasql, $dataparams)) {
+        // Item doesn't exist.
+        throw new rating_exception('invaliditemid');
+    }
+
+    $course = $DB->get_record('course', array('id' => $info->course), '*', MUST_EXIST);
+    $cm = get_coursemodule_from_instance('data', $info->dataid, $course->id, false, MUST_EXIST);
+
+    // Make sure groups allow this user to see the item they're rating.
+    return groups_group_visible($info->groupid, $course, $cm);
+}
+
 
 /**
  * function that takes in the current data, number of items per page,
index 78ab7ed..fbd744c 100644 (file)
@@ -35,9 +35,9 @@ require_once($CFG->dirroot . '/mod/data/lib.php');
  * @copyright  2013 Adrian Greeve
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class data_lib_testcase extends advanced_testcase {
+class mod_data_lib_testcase extends advanced_testcase {
 
-    function test_data_delete_record() {
+    public function test_data_delete_record() {
         global $DB;
 
         $this->resetAfterTest();
@@ -231,4 +231,103 @@ class data_lib_testcase extends advanced_testcase {
         $this->assertEquals($url, $event->get_url());
         $this->assertEventContextNotUsed($event);
     }
+
+    /**
+     * Tests for mod_data_rating_can_see_item_ratings().
+     *
+     * @throws coding_exception
+     * @throws rating_exception
+     */
+    public function test_mod_data_rating_can_see_item_ratings() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Setup test data.
+        $course = new stdClass();
+        $course->groupmode = SEPARATEGROUPS;
+        $course->groupmodeforce = true;
+        $course = $this->getDataGenerator()->create_course($course);
+        $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id));
+        $cm = get_coursemodule_from_instance('data', $data->id);
+        $context = context_module::instance($cm->id);
+
+        // Create users.
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        $user4 = $this->getDataGenerator()->create_user();
+
+        // Groups and stuff.
+        $role = $DB->get_record('role', array('shortname' => 'teacher'), '*', MUST_EXIST);
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id, $role->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, $role->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id, $role->id);
+        $this->getDataGenerator()->enrol_user($user4->id, $course->id, $role->id);
+
+        $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        groups_add_member($group1, $user1);
+        groups_add_member($group1, $user2);
+        groups_add_member($group2, $user3);
+        groups_add_member($group2, $user4);
+
+        // Add data.
+        $field = data_get_field_new('text', $data);
+
+        $fielddetail = new stdClass();
+        $fielddetail->name = 'Name';
+        $fielddetail->description = 'Some name';
+
+        $field->define_field($fielddetail);
+        $field->insert_field();
+        $recordid = data_add_record($data, $group1->id);
+
+        $datacontent = array();
+        $datacontent['fieldid'] = $field->field->id;
+        $datacontent['recordid'] = $recordid;
+        $datacontent['content'] = 'Asterix';
+        $DB->insert_record('data_content', $datacontent);
+
+        // Now try to access it as various users.
+        unassign_capability('moodle/site:accessallgroups', $role->id);
+        $params = array('contextid' => 2,
+                        'component' => 'mod_data',
+                        'ratingarea' => 'entry',
+                        'itemid' => $recordid,
+                        'scaleid' => 2);
+        $this->setUser($user1);
+        $this->assertTrue(mod_data_rating_can_see_item_ratings($params));
+        $this->setUser($user2);
+        $this->assertTrue(mod_data_rating_can_see_item_ratings($params));
+        $this->setUser($user3);
+        $this->assertFalse(mod_data_rating_can_see_item_ratings($params));
+        $this->setUser($user4);
+        $this->assertFalse(mod_data_rating_can_see_item_ratings($params));
+
+        // Now try with accessallgroups cap and make sure everything is visible.
+        assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $role->id, $context->id);
+        $this->setUser($user1);
+        $this->assertTrue(mod_data_rating_can_see_item_ratings($params));
+        $this->setUser($user2);
+        $this->assertTrue(mod_data_rating_can_see_item_ratings($params));
+        $this->setUser($user3);
+        $this->assertTrue(mod_data_rating_can_see_item_ratings($params));
+        $this->setUser($user4);
+        $this->assertTrue(mod_data_rating_can_see_item_ratings($params));
+
+        // Change group mode and verify visibility.
+        $course->groupmode = VISIBLEGROUPS;
+        $DB->update_record('course', $course);
+        unassign_capability('moodle/site:accessallgroups', $role->id);
+        $this->setUser($user1);
+        $this->assertTrue(mod_data_rating_can_see_item_ratings($params));
+        $this->setUser($user2);
+        $this->assertTrue(mod_data_rating_can_see_item_ratings($params));
+        $this->setUser($user3);
+        $this->assertTrue(mod_data_rating_can_see_item_ratings($params));
+        $this->setUser($user4);
+        $this->assertTrue(mod_data_rating_can_see_item_ratings($params));
+
+    }
 }
index d80ddd0..7754e8d 100644 (file)
@@ -81,4 +81,13 @@ $functions = array(
         'type' => 'write',
         'capabilities' => 'mod/forum:viewdiscussion'
     ),
+
+    'mod_forum_add_discussion_post' => array(
+        'classname' => 'mod_forum_external',
+        'methodname' => 'add_discussion_post',
+        'classpath' => 'mod/forum/externallib.php',
+        'description' => 'Create new posts into an existing discussion.',
+        'type' => 'write',
+        'capabilities' => 'mod/forum:replypost'
+    ),
 );
index 340c88c..b3365c6 100644 (file)
@@ -965,4 +965,159 @@ class mod_forum_external extends external_api {
         );
     }
 
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.0
+     */
+    public static function add_discussion_post_parameters() {
+        return new external_function_parameters(
+            array(
+                'postid' => new external_value(PARAM_INT, 'the post id we are going to reply to
+                                                (can be the initial discussion post'),
+                'subject' => new external_value(PARAM_TEXT, 'new post subject'),
+                'message' => new external_value(PARAM_RAW, 'new post message (only html format allowed)'),
+                'options' => new external_multiple_structure (
+                    new external_single_structure(
+                        array(
+                            'name' => new external_value(PARAM_ALPHANUM,
+                                        'The allowed keys (value format) are:
+                                        discussionsubscribe (bool); subscribe to the discussion?, default to true
+                            '),
+                            'value' => new external_value(PARAM_RAW, 'the value of the option,
+                                                            this param is validated in the external function.'
+                        )
+                    )
+                ), 'Options', VALUE_DEFAULT, array())
+            )
+        );
+    }
+
+    /**
+     * Create new posts into an existing discussion.
+     *
+     * @param int $postid the post id we are going to reply to
+     * @param string $subject new post subject
+     * @param string $message new post message (only html format allowed)
+     * @param array $options optional settings
+     * @return array of warnings and the new post id
+     * @since Moodle 3.0
+     * @throws moodle_exception
+     */
+    public static function add_discussion_post($postid, $subject, $message, $options = array()) {
+        global $DB, $CFG, $USER;
+        require_once($CFG->dirroot . "/mod/forum/lib.php");
+
+        $params = self::validate_parameters(self::add_discussion_post_parameters(),
+                                            array(
+                                                'postid' => $postid,
+                                                'subject' => $subject,
+                                                'message' => $message,
+                                                'options' => $options
+                                            ));
+        // Validate options.
+        $options = array(
+            'discussionsubscribe' => true
+        );
+        foreach ($params['options'] as $option) {
+            $name = trim($option['name']);
+            switch ($name) {
+                case 'discussionsubscribe':
+                    $value = clean_param($option['value'], PARAM_BOOL);
+                    break;
+                default:
+                    throw new moodle_exception('errorinvalidparam', 'webservice', '', $name);
+            }
+            $options[$name] = $value;
+        }
+
+        $warnings = array();
+
+        if (!$parent = forum_get_post_full($params['postid'])) {
+            throw new moodle_exception('invalidparentpostid', 'forum');
+        }
+
+        if (!$discussion = $DB->get_record("forum_discussions", array("id" => $parent->discussion))) {
+            throw new moodle_exception('notpartofdiscussion', 'forum');
+        }
+
+        // Request and permission validation.
+        $forum = $DB->get_record('forum', array('id' => $discussion->forum), '*', MUST_EXIST);
+        list($course, $cm) = get_course_and_cm_from_instance($forum, 'forum');
+
+        $context = context_module::instance($cm->id);
+        self::validate_context($context);
+
+        if (!forum_user_can_post($forum, $discussion, $USER, $cm, $course, $context)) {
+            throw new moodle_exception('nopostforum', 'forum');
+        }
+
+        $thresholdwarning = forum_check_throttling($forum, $cm);
+        forum_check_blocking_threshold($thresholdwarning);
+
+        // Create the post.
+        $post = new stdClass();
+        $post->discussion = $discussion->id;
+        $post->parent = $parent->id;
+        $post->subject = $params['subject'];
+        $post->message = $params['message'];
+        $post->messageformat = FORMAT_HTML;   // Force formatting for now.
+        $post->messagetrust = trusttext_trusted($context);
+        $post->itemid = 0;
+
+        if ($postid = forum_add_new_post($post, null)) {
+
+            $post->id = $postid;
+
+            // Trigger events and completion.
+            $params = array(
+                'context' => $context,
+                'objectid' => $post->id,
+                'other' => array(
+                    'discussionid' => $discussion->id,
+                    'forumid' => $forum->id,
+                    'forumtype' => $forum->type,
+                )
+            );
+            $event = \mod_forum\event\post_created::create($params);
+            $event->add_record_snapshot('forum_posts', $post);
+            $event->add_record_snapshot('forum_discussions', $discussion);
+            $event->trigger();
+
+            // Update completion state.
+            $completion = new completion_info($course);
+            if ($completion->is_enabled($cm) &&
+                    ($forum->completionreplies || $forum->completionposts)) {
+                $completion->update_state($cm, COMPLETION_COMPLETE);
+            }
+
+            $settings = new stdClass();
+            $settings->discussionsubscribe = $options['discussionsubscribe'];
+            forum_post_subscription($settings, $forum, $discussion);
+        } else {
+            throw new moodle_exception('couldnotadd', 'forum');
+        }
+
+        $result = array();
+        $result['postid'] = $postid;
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.0
+     */
+    public static function add_discussion_post_returns() {
+        return new external_single_structure(
+            array(
+                'postid' => new external_value(PARAM_INT, 'new post id'),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
 }
index 5f4d874..7259b43 100644 (file)
@@ -3664,6 +3664,48 @@ function forum_rating_validate($params) {
     return true;
 }
 
+/**
+ * Can the current user see ratings for a given itemid?
+ *
+ * @param array $params submitted data
+ *            contextid => int contextid [required]
+ *            component => The component for this module - should always be mod_forum [required]
+ *            ratingarea => object the context in which the rated items exists [required]
+ *            itemid => int the ID of the object being rated [required]
+ *            scaleid => int scale id [optional]
+ * @return bool
+ * @throws coding_exception
+ * @throws rating_exception
+ */
+function mod_forum_rating_can_see_item_ratings($params) {
+    global $DB, $USER;
+
+    // Check the component is mod_forum.
+    if (!isset($params['component']) || $params['component'] != 'mod_forum') {
+        throw new rating_exception('invalidcomponent');
+    }
+
+    // Check the ratingarea is post (the only rating area in forum).
+    if (!isset($params['ratingarea']) || $params['ratingarea'] != 'post') {
+        throw new rating_exception('invalidratingarea');
+    }
+
+    if (!isset($params['itemid'])) {
+        throw new rating_exception('invaliditemid');
+    }
+
+    $post = $DB->get_record('forum_posts', array('id' => $params['itemid']), '*', MUST_EXIST);
+    $discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion), '*', MUST_EXIST);
+    $forum = $DB->get_record('forum', array('id' => $discussion->forum), '*', MUST_EXIST);
+    $course = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST);
+    $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id , false, MUST_EXIST);
+
+    // Perform some final capability checks.
+    if (!forum_user_can_see_post($forum, $discussion, $post, $USER, $cm)) {
+        return false;
+    }
+    return true;
+}
 
 /**
  * This function prints the overview of a discussion in the forum listing.
index 2627ffa..26ef466 100644 (file)
@@ -762,4 +762,85 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
         $this->assertCount(0, $discussions['warnings']);
 
     }
+
+    /**
+     * Test add_discussion_post
+     */
+    public function test_add_discussion_post() {
+        global $CFG;
+
+        $this->resetAfterTest(true);
+
+        $user = self::getDataGenerator()->create_user();
+        $otheruser = self::getDataGenerator()->create_user();
+
+        self::setAdminUser();
+
+        // Create course to add the module.
+        $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0));
+
+        // Forum with tracking off.
+        $record = new stdClass();
+        $record->course = $course->id;
+        $forum = self::getDataGenerator()->create_module('forum', $record);
+        $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST);
+        $forumcontext = context_module::instance($forum->cmid);
+
+        // Add discussions to the forums.
+        $record = new stdClass();
+        $record->course = $course->id;
+        $record->userid = $user->id;
+        $record->forum = $forum->id;
+        $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        // Try to post (user not enrolled).
+        self::setUser($user);
+        try {
+            mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
+            $this->fail('Exception expected due to being unenrolled from the course.');
+        } catch (moodle_exception $e) {
+            $this->assertEquals('requireloginerror', $e->errorcode);
+        }
+
+        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+        $this->getDataGenerator()->enrol_user($otheruser->id, $course->id);
+
+        $post = mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
+        $post = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $post);
+
+        $posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
+        $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
+        // We receive the discussion and the post.
+        $this->assertEquals(2, count($posts['posts']));
+
+        $tested = false;
+        foreach ($posts['posts'] as $postel) {
+            if ($post['postid'] == $postel['id']) {
+                $this->assertEquals('some subject', $postel['subject']);
+                $this->assertEquals('some text here...', $postel['message']);
+                $tested = true;
+            }
+        }
+        $this->assertTrue($tested);
+
+        // Check not posting in groups the user is not member of.
+        $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        groups_add_member($group->id, $otheruser->id);
+
+        $forum = self::getDataGenerator()->create_module('forum', $record, array('groupmode' => SEPARATEGROUPS));
+        $record->forum = $forum->id;
+        $record->userid = $otheruser->id;
+        $record->groupid = $group->id;
+        $discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        try {
+            mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...');
+            $this->fail('Exception expected due to invalid permissions for posting.');
+        } catch (moodle_exception $e) {
+            // Expect debugging since we are switching context, and this is something WS_SERVER mode don't like.
+            $this->assertDebuggingCalled();
+            $this->assertEquals('nopostforum', $e->errorcode);
+        }
+
+    }
 }
index 19419b7..bc38397 100644 (file)
@@ -26,6 +26,7 @@ defined('MOODLE_INTERNAL') || die();
 
 global $CFG;
 require_once($CFG->dirroot . '/mod/forum/lib.php');
+require_once($CFG->dirroot . '/rating/lib.php');
 
 class mod_forum_lib_testcase extends advanced_testcase {
 
@@ -1394,4 +1395,107 @@ class mod_forum_lib_testcase extends advanced_testcase {
         return $discussion;
     }
 
+    /**
+     * Tests for mod_forum_rating_can_see_item_ratings().
+     *
+     * @throws coding_exception
+     * @throws rating_exception
+     */
+    public function test_mod_forum_rating_can_see_item_ratings() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Setup test data.
+        $course = new stdClass();
+        $course->groupmode = SEPARATEGROUPS;
+        $course->groupmodeforce = true;
+        $course = $this->getDataGenerator()->create_course($course);
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $generator = self::getDataGenerator()->get_plugin_generator('mod_forum');
+        $cm = get_coursemodule_from_instance('forum', $forum->id);
+        $context = context_module::instance($cm->id);
+
+        // Create users.
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        $user4 = $this->getDataGenerator()->create_user();
+
+        // Groups and stuff.
+        $role = $DB->get_record('role', array('shortname' => 'teacher'), '*', MUST_EXIST);
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id, $role->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, $role->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id, $role->id);
+        $this->getDataGenerator()->enrol_user($user4->id, $course->id, $role->id);
+
+        $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        groups_add_member($group1, $user1);
+        groups_add_member($group1, $user2);
+        groups_add_member($group2, $user3);
+        groups_add_member($group2, $user4);
+
+        $record = new stdClass();
+        $record->course = $forum->course;
+        $record->forum = $forum->id;
+        $record->userid = $user1->id;
+        $record->groupid = $group1->id;
+        $discussion = $generator->create_discussion($record);
+
+        // Retrieve the first post.
+        $post = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
+
+        $ratingoptions = new stdClass;
+        $ratingoptions->context = $context;
+        $ratingoptions->ratingarea = 'post';
+        $ratingoptions->component = 'mod_forum';
+        $ratingoptions->itemid  = $post->id;
+        $ratingoptions->scaleid = 2;
+        $ratingoptions->userid  = $user2->id;
+        $rating = new rating($ratingoptions);
+        $rating->update_rating(2);
+
+        // Now try to access it as various users.
+        unassign_capability('moodle/site:accessallgroups', $role->id);
+        $params = array('contextid' => 2,
+                        'component' => 'mod_forum',
+                        'ratingarea' => 'post',
+                        'itemid' => $post->id,
+                        'scaleid' => 2);
+        $this->setUser($user1);
+        $this->assertTrue(mod_forum_rating_can_see_item_ratings($params));
+        $this->setUser($user2);
+        $this->assertTrue(mod_forum_rating_can_see_item_ratings($params));
+        $this->setUser($user3);
+        $this->assertFalse(mod_forum_rating_can_see_item_ratings($params));
+        $this->setUser($user4);
+        $this->assertFalse(mod_forum_rating_can_see_item_ratings($params));
+
+        // Now try with accessallgroups cap and make sure everything is visible.
+        assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $role->id, $context->id);
+        $this->setUser($user1);
+        $this->assertTrue(mod_forum_rating_can_see_item_ratings($params));
+        $this->setUser($user2);
+        $this->assertTrue(mod_forum_rating_can_see_item_ratings($params));
+        $this->setUser($user3);
+        $this->assertTrue(mod_forum_rating_can_see_item_ratings($params));
+        $this->setUser($user4);
+        $this->assertTrue(mod_forum_rating_can_see_item_ratings($params));
+
+        // Change group mode and verify visibility.
+        $course->groupmode = VISIBLEGROUPS;
+        $DB->update_record('course', $course);
+        unassign_capability('moodle/site:accessallgroups', $role->id);
+        $this->setUser($user1);
+        $this->assertTrue(mod_forum_rating_can_see_item_ratings($params));
+        $this->setUser($user2);
+        $this->assertTrue(mod_forum_rating_can_see_item_ratings($params));
+        $this->setUser($user3);
+        $this->assertTrue(mod_forum_rating_can_see_item_ratings($params));
+        $this->setUser($user4);
+        $this->assertTrue(mod_forum_rating_can_see_item_ratings($params));
+
+    }
+
 }
index 2caf094..b6c95cf 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015051101;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2015051102;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015050500;       // Requires this Moodle version
 $plugin->component = 'mod_forum';      // Full name of the plugin (used for diagnostics)
index e2d683a..ba5e769 100644 (file)
@@ -168,10 +168,6 @@ if ($canmanage) {
 if ($result->attemptsremaining != 0 && $lesson->review && !$reviewmode) {
     $lesson->add_message(get_string('attemptsremaining', 'lesson', $result->attemptsremaining));
 }
-// Report if max attempts reached
-if ($result->maxattemptsreached != 0 && $lesson->review && !$reviewmode) {
-    $lesson->add_message('('.get_string("maximumnumberofattemptsreached", "lesson").')');
-}
 
 $PAGE->set_url('/mod/lesson/view.php', array('id' => $cm->id, 'pageid' => $page->id));
 $PAGE->set_subpage($page->id);
@@ -187,7 +183,7 @@ if ($lesson->displayleft) {
 if ($lesson->ongoing && !$reviewmode) {
     echo $lessonoutput->ongoing_score($lesson);
 }
-if (!$result->maxattemptsreached && !$reviewmode) {
+if (!$reviewmode) {
     echo $result->feedback;
 }
 
index 884f497..d6bb73a 100644 (file)
@@ -283,9 +283,7 @@ $string['mediaheight'] = 'Popup window height';
 $string['mediawidth'] = 'Popup window width';
 $string['messageprovider:graded_essay'] = 'Lesson essay graded notification';
 $string['minimumnumberofquestions'] = 'Minimum number of questions';
-$string['minimumnumberofquestions_help'] = 'This setting specifies the minimum number of questions that will be used to calculate a grade for the activity. If the lesson contains one or more content pages, the minimum number of questions should be set to zero.
-
-If set to say 20, it is suggested that the following text is added to the opening page of the lesson: "In this lesson you are expected to attempt at least 20 questions. You can attempt more if you wish. However, if you attempt less than 20 questions, your grade will be calculated as though you attempted 20."';
+$string['minimumnumberofquestions_help'] = 'This setting specifies the minimum number of questions that will be used to calculate a grade for the activity.';
 $string['missingname'] = 'Please enter a nickname';
 $string['modattempts'] = 'Allow student review';
 $string['modattempts_help'] = 'If enabled, students can navigate through the lesson again from the start.';
@@ -351,7 +349,7 @@ $string['numberofpagestoshow'] = 'Number of pages to show';
 $string['numberofpagestoshow_help'] = 'This setting specifies the number of pages shown in a lesson. It is only applicable for lessons with pages shown in a random order (when "Action after correct answer" is set to "Show an unseen page" or "Show an unanswered page"). If set to zero, then all pages are shown.';
 $string['numberofpagesviewed'] = 'Number of questions answered: {$a}';
 $string['numberofpagesviewedheader'] = 'Number of questions answered';
-$string['numberofpagesviewednotice'] = 'Number of questions answered: {$a->nquestions}; (You should answer at least: {$a->minquestions})';
+$string['numberofpagesviewednotice'] = 'Number of questions answered: {$a->nquestions} (You should answer at least {$a->minquestions})';
 $string['numerical'] = 'Numerical';
 $string['ongoing'] = 'Display ongoing score';
 $string['ongoing_help'] = 'If enabled, each page will display the student\'s current points earned out of the total possible thus far.';
index 75b97c8..6b84bf4 100644 (file)
@@ -2426,6 +2426,19 @@ abstract class lesson_page extends lesson_base {
         } else {
             if (!has_capability('mod/lesson:manage', $context)) {
                 $nretakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->lesson->id, "userid"=>$USER->id));
+
+                // Get the number of attempts that have been made on this question for this student and retake,
+                $nattempts = $DB->count_records('lesson_attempts', array('lessonid' => $this->lesson->id,
+                    'userid' => $USER->id, 'pageid' => $this->properties->id, 'retry' => $nretakes));
+
+                // Check if they have reached (or exceeded) the maximum number of attempts allowed.
+                if ($nattempts >= $this->lesson->maxattempts) {
+                    $result->maxattemptsreached = true;
+                    $result->feedback = get_string('maximumnumberofattemptsreached', 'lesson');
+                    $result->newpageid = $this->lesson->get_next_page($this->properties->nextpageid);
+                    return $result;
+                }
+
                 // record student's attempt
                 $attempt = new stdClass;
                 $attempt->lessonid = $this->lesson->id;
@@ -2462,14 +2475,13 @@ abstract class lesson_page extends lesson_base {
                         $event->add_record_snapshot('lesson_attempts', $attempt);
                         $event->trigger();
 
+                        // Increase the number of attempts made.
+                        $nattempts++;
                     }
                 }
                 // "number of attempts remaining" message if $this->lesson->maxattempts > 1
                 // displaying of message(s) is at the end of page for more ergonomic display
                 if (!$result->correctanswer && ($result->newpageid == 0)) {
-                    // wrong answer and student is stuck on this page - check how many attempts
-                    // the student has had at this page/question
-                    $nattempts = $DB->count_records("lesson_attempts", array("pageid"=>$this->properties->id, "userid"=>$USER->id, "retry" => $attempt->retry));
                     // retreive the number of attempts left counter for displaying at bottom of feedback page
                     if ($nattempts >= $this->lesson->maxattempts) {
                         if ($this->lesson->maxattempts > 1) { // don't bother with message if only one attempt
index 218e0b8..7ba9166 100644 (file)
@@ -84,8 +84,7 @@ Feature: In a lesson activity, students can navigate through a series of pages i
     And I set the following fields to these values:
       | Your answer | 2 |
     And I press "Submit"
-    And I should see "Correct answer"
-    And I should not see "Incorrect answer"
+    And I should see "Maximum number of attempts reached - Moving to next page"
     And I press "Continue"
     And I should see "Congratulations - end of lesson reached"
     And I should see "Your score is 0 (out of 1)."
@@ -128,6 +127,6 @@ Feature: In a lesson activity, students can navigate through a series of pages i
     And I set the following fields to these values:
       | wrong | 1 |
     And I press "Submit"
-    And I should see "(Maximum number of attempts reached - Moving to next page)"
+    And I should not see "Yes, I'd like to try again"
     And I press "Continue"
     And I should see "Congratulations - end of lesson reached"
diff --git a/mod/lesson/tests/behat/lesson_question_attempts.feature b/mod/lesson/tests/behat/lesson_question_attempts.feature
new file mode 100644 (file)
index 0000000..e02050b
--- /dev/null
@@ -0,0 +1,207 @@
+@mod @mod_lesson
+Feature: In a lesson activity, students can not re-attempt a question more than the allowed amount
+  In order to check a lesson question can not be attempted more than the allowed amount
+  As a student I need to check I cannot reattempt a question more than I should be allowed
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Lesson" to section "1"
+    And I set the following fields to these values:
+      | Name | Test lesson name |
+      | Description | Test lesson description |
+      | Re-takes allowed | Yes |
+      | Minimum number of questions | 3 |
+    And I press "Save and return to course"
+    And I follow "Test lesson name"
+    And I follow "Add a content page"
+    And I set the following fields to these values:
+      | Page title | First page name |
+      | Page contents | First page contents |
+      | id_answer_editor_0 | Next page |
+      | id_jumpto_0 | Next page |
+    And I press "Save page"
+    And I select "Add a content page" from the "qtype" singleselect
+    And I set the following fields to these values:
+      | Page title | Third page name |
+      | Page contents | Third page contents |
+      | id_answer_editor_0 | Previous page |
+      | id_jumpto_0 | Previous page |
+      | id_answer_editor_1 | Next page |
+      | id_jumpto_1 | Next page |
+    And I press "Save page"
+    And I select "Question" from the "qtype" singleselect
+    And I set the field "Select a question type" to "True/false"
+    And I press "Add a question page"
+    And I set the following fields to these values:
+      | Page title | True/false question 3 |
+      | Page contents | Paper is made from trees. |
+      | id_answer_editor_0 | True |
+      | id_response_editor_0 | Correct |
+      | id_jumpto_0 | Next page |
+      | id_answer_editor_1 | False |
+      | id_response_editor_1 | Wrong |
+      | id_jumpto_1 | This page |
+    And I press "Save page"
+    And I select "Add a content page" from the "qtype" singleselect
+    And I set the following fields to these values:
+      | Page title | Second page name |
+      | Page contents | Second page contents |
+      | id_answer_editor_0 | Previous page |
+      | id_jumpto_0 | Previous page |
+      | id_answer_editor_1 | Next page |
+      | id_jumpto_1 | Next page |
+    And I press "Save page"
+    And I select "Question" from the "qtype" singleselect
+    And I set the field "Select a question type" to "True/false"
+    And I press "Add a question page"
+    And I set the following fields to these values:
+      | Page title | True/false question 2 |
+      | Page contents | Kermit is a frog |
+      | id_answer_editor_0 | True |
+      | id_response_editor_0 | Correct |
+      | id_jumpto_0 | Next page |
+      | id_answer_editor_1 | False |
+      | id_response_editor_1 | Wrong |
+      | id_jumpto_1 | This page |
+    And I press "Save page"
+    And I select "Question" from the "qtype" singleselect
+    And I set the field "Select a question type" to "True/false"
+    And I press "Add a question page"
+    And I set the following fields to these values:
+      | Page title | True/false question 1 |
+      | Page contents | The earth is round. |
+      | id_answer_editor_0 | True |
+      | id_response_editor_0 | Correct |
+      | id_jumpto_0 | Next page |
+      | id_answer_editor_1 | False |
+      | id_response_editor_1 | Wrong |
+      | id_jumpto_1 | This page |
+    And I press "Save page"
+    And I log out
+
+  Scenario: Check that we can leave a quiz and when we re-enter we can not re-attempt the question again
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I should see "First page contents"
+    And I press "Next page"
+    And I should see "The earth is round"
+    And I set the following fields to these values:
+      | False| 1 |
+    And I press "Submit"
+    And I should see "Wrong"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I should see "Do you want to start at the last page you saw?"
+    And I follow "No"
+    And I should see "First page contents"
+    And I press "Next page"
+    And I should see "The earth is round"
+    And I set the following fields to these values:
+      | False| 1 |
+    When I press "Submit"
+    Then I should see "Maximum number of attempts reached - Moving to next page"
+
+  @javascript
+  Scenario: Check that we can not click back on the browser at the last quiz result page and re-attempt the last question to get full marks
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I should see "First page contents"
+    And I press "Next page"
+    And I should see "The earth is round"
+    And I set the following fields to these values:
+      | True| 1 |
+    And I press "Submit"
+    And I should see "Correct"
+    And I press "Continue"
+    And I should see "Kermit is a frog"
+    And I set the following fields to these values:
+      | True| 1 |
+    And I press "Submit"
+    And I should see "Correct"
+    And I press "Continue"
+    And I should see "Second page contents"
+    And I press "Next page"
+    And I should see "Paper is made from trees"
+    And I set the following fields to these values:
+      | False | 1 |
+    And I press "Submit"
+    And I should see "Wrong"
+    And I press "Continue"
+    And I should see "Third page contents"
+    And I press "Next page"
+    And I should see "Congratulations - end of lesson reached"
+    And I should see "Your score is 2 (out of 3)"
+    And I press the "back" button in the browser
+    And I press the "back" button in the browser
+    And I press the "back" button in the browser
+    And I should see "Paper is made from trees"
+    And I set the following fields to these values:
+      | True | 1 |
+    And I press "Submit"
+    And I should see "Correct"
+    And I press "Continue"
+    And I should see "Third page contents"
+    When I press "Next page"
+    Then I should see "Number of questions answered: 1 (You should answer at least 3)"
+
+  @javascript
+  Scenario: Check that we can not click back on the browser and re-attempt a question
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test lesson name"
+    And I should see "First page contents"
+    And I press "Next page"
+    And I should see "The earth is round"
+    And I set the following fields to these values:
+      | False | 1 |
+    And I press "Submit"
+    And I should see "Wrong"
+    And I press the "back" button in the browser
+    And I set the following fields to these values:
+      | True | 1 |
+    When I press "Submit"
+    Then I should see "Maximum number of attempts reached - Moving to next page"
+    And I press "Continue"
+    And I should see "Kermit is a frog"
+    And I set the following fields to these values:
+      | False | 1 |
+    And I press "Submit"
+    And I should see "Wrong"
+    And I press the "back" button in the browser
+    And I set the following fields to these values:
+      | True | 1 |
+    And I press "Submit"
+    And I should see "Maximum number of attempts reached - Moving to next page"
+    And I press "Continue"
+    And I should see "Second page contents"
+    And I press "Next page"
+    And I should see "Paper is made from trees"
+    And I set the following fields to these values:
+      | True | 1 |
+    And I press "Submit"
+    And I should see "Correct"
+    And I press the "back" button in the browser
+    And I set the following fields to these values:
+      | False | 1 |
+    And I press "Submit"
+    And I should see "Maximum number of attempts reached - Moving to next page"
+    And I press "Continue"
+    And I should see "Third page contents"
+    And I press "Next page"
+    And I should see "Congratulations - end of lesson reached"
+    And I should see "Your score is 1 (out of 3)"
index 1f384f0..e7ef772 100644 (file)
@@ -297,12 +297,6 @@ $timer = null;
 if ($pageid != LESSON_EOL) {
     /// This is the code updates the lessontime for a timed test
     $startlastseen = optional_param('startlastseen', '', PARAM_ALPHA);
-    if ($startlastseen == 'no') {
-        // this deletes old records  not totally sure if this is necessary anymore
-        $retries = $DB->count_records('lesson_grades', array('lessonid'=>$lesson->id, 'userid'=>$USER->id));
-        $DB->delete_records('lesson_attempts', array('userid' => $USER->id, 'lessonid' => $lesson->id, 'retry' => $retries));
-        $DB->delete_records('lesson_branch', array('userid' => $USER->id, 'lessonid' => $lesson->id, 'retry' => $retries));
-    }
 
     $page = $lesson->load_page($pageid);
     // Check if the page is of a special type and if so take any nessecary action
@@ -448,111 +442,132 @@ if ($pageid != LESSON_EOL) {
     // Used to check to see if the student ran out of time
     $outoftime = optional_param('outoftime', '', PARAM_ALPHA);
 
-    // We are using level 3 header because the page title is a sub-heading of lesson title (MDL-30911).
-    $lessoncontent .= $OUTPUT->heading(get_string("congratulations", "lesson"), 3);
-    $lessoncontent .= $OUTPUT->box_start('generalbox boxaligncenter');
     $ntries = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$USER->id));
     if (isset($USER->modattempts[$lesson->id])) {
         $ntries--;  // need to look at the old attempts :)
     }
-    if (!$canmanage) {
-        // Store this now before any modifications to pages viewed.
-        $progressbar = $lessonoutput->progress_bar($lesson);
-        // Update the clock / get time information for this user.
-        $lesson->stop_timer();
-        $gradeinfo = lesson_grade($lesson, $ntries);
-
-        // Update completion state.
-        $completion = new completion_info($course);
-        if ($completion->is_enabled($cm) && $lesson->completionendreached) {
-            $completion->update_state($cm, COMPLETION_COMPLETE);
+    $gradelesson = true;
+    $gradeinfo = lesson_grade($lesson, $ntries);
+    if ($lesson->custom && !$canmanage) {
+        // Before we calculate the custom score make sure they answered the minimum
+        // number of questions. We only need to do this for custom scoring as we can
+        // not get the miniumum score the user should achieve. If we are not using
+        // custom scoring (so all questions are valued as 1) then we simply check if
+        // they answered more than the minimum questions, if not, we mark it out of the
+        // number specified in the minimum questions setting - which is done in lesson_grade().
+        // Get the number of answers given.
+        if ($gradeinfo->nquestions < $lesson->minquestions) {
+            $gradelesson = false;
+            $a = new stdClass;
+            $a->nquestions = $gradeinfo->nquestions;
+            $a->minquestions = $lesson->minquestions;
+            $lessoncontent .= $OUTPUT->box_start('generalbox boxaligncenter');
+            $lesson->add_message(get_string('numberofpagesviewednotice', 'lesson', $a));
         }
-
-        if ($lesson->completiontimespent > 0) {
-            $duration = $DB->get_field_sql(
-                            "SELECT SUM(lessontime - starttime)
-                               FROM {lesson_timer}
-                              WHERE lessonid = :lessonid
-                                AND userid = :userid",
-                            array('userid' => $USER->id, 'lessonid' => $lesson->id));
-            if (!$duration) {
-                $duration = 0;
+    }
+    if ($gradelesson) {
+        // We are using level 3 header because the page title is a sub-heading of lesson title (MDL-30911).
+        $lessoncontent .= $OUTPUT->heading(get_string("congratulations", "lesson"), 3);
+        $lessoncontent .= $OUTPUT->box_start('generalbox boxaligncenter');
+    }
+    if (!$canmanage) {
+        if ($gradelesson) {
+            // Store this now before any modifications to pages viewed.
+            $progressbar = $lessonoutput->progress_bar($lesson);
+            // Update the clock / get time information for this user.
+            $lesson->stop_timer();
+
+            // Update completion state.
+            $completion = new completion_info($course);
+            if ($completion->is_enabled($cm) && $lesson->completionendreached) {
+                $completion->update_state($cm, COMPLETION_COMPLETE);
             }
 
-            // If student has not spend enough time in the lesson, display a message.
-            if ($duration < $lesson->completiontimespent) {
-                $a = new stdClass;
-                $a->timespent = format_time($duration);
-                $a->timerequired = format_time($lesson->completiontimespent);
-                $lessoncontent .= $lessonoutput->paragraph(get_string("notenoughtimespent", "lesson", $a), 'center');
+            if ($lesson->completiontimespent > 0) {
+                $duration = $DB->get_field_sql(
+                    "SELECT SUM(lessontime - starttime)
+                                   FROM {lesson_timer}
+                                  WHERE lessonid = :lessonid
+                                    AND userid = :userid",
+                    array('userid' => $USER->id, 'lessonid' => $lesson->id));
+                if (!$duration) {
+                    $duration = 0;
+                }
+
+                // If student has not spend enough time in the lesson, display a message.
+                if ($duration < $lesson->completiontimespent) {
+                    $a = new stdClass;
+                    $a->timespent = format_time($duration);
+                    $a->timerequired = format_time($lesson->completiontimespent);
+                    $lessoncontent .= $lessonoutput->paragraph(get_string("notenoughtimespent", "lesson", $a), 'center');
+                }
             }
-        }
 
 
-        if ($gradeinfo->attempts) {
-            if (!$lesson->custom) {
-                $lessoncontent .= $lessonoutput->paragraph(get_string("numberofpagesviewed", "lesson", $gradeinfo->nquestions), 'center');
-                if ($lesson->minquestions) {
-                    if ($gradeinfo->nquestions < $lesson->minquestions) {
-                        // print a warning and set nviewed to minquestions
-                        $lessoncontent .= $lessonoutput->paragraph(get_string("youshouldview", "lesson", $lesson->minquestions), 'center');
+            if ($gradeinfo->attempts) {
+                if (!$lesson->custom) {
+                    $lessoncontent .= $lessonoutput->paragraph(get_string("numberofpagesviewed", "lesson", $gradeinfo->nquestions), 'center');
+                    if ($lesson->minquestions) {
+                        if ($gradeinfo->nquestions < $lesson->minquestions) {
+                            // print a warning and set nviewed to minquestions
+                            $lessoncontent .= $lessonoutput->paragraph(get_string("youshouldview", "lesson", $lesson->minquestions), 'center');
+                        }
                     }
+                    $lessoncontent .= $lessonoutput->paragraph(get_string("numberofcorrectanswers", "lesson", $gradeinfo->earned), 'center');
                 }
-                $lessoncontent .= $lessonoutput->paragraph(get_string("numberofcorrectanswers", "lesson", $gradeinfo->earned), 'center');
-            }
-            $a = new stdClass;
-            $a->score = $gradeinfo->earned;
-            $a->grade = $gradeinfo->total;
-            if ($gradeinfo->nmanual) {
-                $a->tempmaxgrade = $gradeinfo->total - $gradeinfo->manualpoints;
-                $a->essayquestions = $gradeinfo->nmanual;
-                $lessoncontent .= $OUTPUT->box(get_string("displayscorewithessays", "lesson", $a), 'center');
-            } else {
-                $lessoncontent .= $OUTPUT->box(get_string("displayscorewithoutessays", "lesson", $a), 'center');
-            }
-            if ($lesson->grade != GRADE_TYPE_NONE) {
                 $a = new stdClass;
-                $a->grade = number_format($gradeinfo->grade * $lesson->grade / 100, 1);
-                $a->total = $lesson->grade;
-                $lessoncontent .= $lessonoutput->paragraph(get_string("yourcurrentgradeisoutof", "lesson", $a), 'center');
-            }
+                $a->score = $gradeinfo->earned;
+                $a->grade = $gradeinfo->total;
+                if ($gradeinfo->nmanual) {
+                    $a->tempmaxgrade = $gradeinfo->total - $gradeinfo->manualpoints;
+                    $a->essayquestions = $gradeinfo->nmanual;
+                    $lessoncontent .= $OUTPUT->box(get_string("displayscorewithessays", "lesson", $a), 'center');
+                } else {
+                    $lessoncontent .= $OUTPUT->box(get_string("displayscorewithoutessays", "lesson", $a), 'center');
+                }
+                if ($lesson->grade != GRADE_TYPE_NONE) {
+                    $a = new stdClass;
+                    $a->grade = number_format($gradeinfo->grade * $lesson->grade / 100, 1);
+                    $a->total = $lesson->grade;
+                    $lessoncontent .= $lessonoutput->paragraph(get_string("yourcurrentgradeisoutof", "lesson", $a), 'center');
+                }
 
-            $grade = new stdClass();
-            $grade->lessonid = $lesson->id;
-            $grade->userid = $USER->id;
-            $grade->grade = $gradeinfo->grade;
-            $grade->completed = time();
-            if (isset($USER->modattempts[$lesson->id])) { // If reviewing, make sure update old grade record.
-                if (!$grades = $DB->get_records("lesson_grades",
+                $grade = new stdClass();
+                $grade->lessonid = $lesson->id;
+                $grade->userid = $USER->id;
+                $grade->grade = $gradeinfo->grade;
+                $grade->completed = time();
+                if (isset($USER->modattempts[$lesson->id])) { // If reviewing, make sure update old grade record.
+                    if (!$grades = $DB->get_records("lesson_grades",
                         array("lessonid" => $lesson->id, "userid" => $USER->id), "completed DESC", '*', 0, 1)) {
-                    print_error('cannotfindgrade', 'lesson');
-                }
-                $oldgrade = array_shift($grades);
-                $grade->id = $oldgrade->id;
-                $DB->update_record("lesson_grades", $grade);
-            } else {
-                $newgradeid = $DB->insert_record("lesson_grades", $grade);
-            }
-        } else {
-            if ($lesson->timelimit) {
-                if ($outoftime == 'normal') {
-                    $grade = new stdClass();
-                    $grade->lessonid = $lesson->id;
-                    $grade->userid = $USER->id;
-                    $grade->grade = 0;
-                    $grade->completed = time();
+                        print_error('cannotfindgrade', 'lesson');
+                    }
+                    $oldgrade = array_shift($grades);
+                    $grade->id = $oldgrade->id;
+                    $DB->update_record("lesson_grades", $grade);
+                } else {
                     $newgradeid = $DB->insert_record("lesson_grades", $grade);
-                    $lessoncontent .= $lessonoutput->paragraph(get_string("eolstudentoutoftimenoanswers", "lesson"));
                 }
             } else {
-                $lessoncontent .= $lessonoutput->paragraph(get_string("welldone", "lesson"));
+                if ($lesson->timelimit) {
+                    if ($outoftime == 'normal') {
+                        $grade = new stdClass();
+                        $grade->lessonid = $lesson->id;
+                        $grade->userid = $USER->id;
+                        $grade->grade = 0;
+                        $grade->completed = time();
+                        $newgradeid = $DB->insert_record("lesson_grades", $grade);
+                        $lessoncontent .= $lessonoutput->paragraph(get_string("eolstudentoutoftimenoanswers", "lesson"));
+                    }
+                } else {
+                    $lessoncontent .= $lessonoutput->paragraph(get_string("welldone", "lesson"));
+                }
             }
-        }
-
-        // update central gradebook
-        lesson_update_grades($lesson, $USER->id);
-        $lessoncontent .= $progressbar;
 
+            // update central gradebook
+            lesson_update_grades($lesson, $USER->id);
+            $lessoncontent .= $progressbar;
+        }
     } else {
         // display for teacher
         if ($lesson->grade != GRADE_TYPE_NONE) {