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) {
protected function add_permission_cells($capability) {
// One cell for each possible permission.
+ $content = '';
foreach ($this->displaypermissions as $perm => $permname) {
$strperm = $this->strperms[$permname];
$extraclass = '';
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;
}
}
$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;
}
}
}
// One cell for each possible permission.
+ $content = '';
foreach ($this->displaypermissions as $perm => $permname) {
$strperm = $this->strperms[$permname];
$extraclass = '';
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];
}
$strperm .= ' (' . $inherited . ')';
}
- echo '<span class="note">' . $strperm . '</span>';
- echo '</label></td>';
+ $content .= '<span class="note">' . $strperm . '</span>';
+ $content .= '</label></td>';
}
+ return $content;
}
}
} else {
$default = " ";
}
- echo '<td class="' . $permname . '">' . $this->strperms[$permname] . '<span class="note">' .
+ return '<td class="' . $permname . '">' . $this->strperms[$permname] . '<span class="note">' .
$default . '</span></td>';
}
And I follow "Course 1"
And I navigate to "Grades" node in "Course administration"
And I expand "Setup" node
- And I follow "Categories and items"
+ And I follow "Gradebook setup"
Then I should see "Test Grade Item 1"
And I follow "Edit Test Grade Item 1"
And I expand all fieldsets
And I follow "Course 1"
And I navigate to "Grades" node in "Course administration"
And I expand "Setup" node
- And I follow "Categories and items"
+ And I follow "Gradebook setup"
Then I should see "Test Outcome Grade Item 1"
And I follow "Edit Test Outcome Grade Item 1"
And the field "Outcome" matches value "Grade outcome 1"
component: component,
template: name
}
- }]);
+ }], true, false);
// When returns a new promise that is resolved when all the passed in promises are resolved.
// The arguments to the done become the values of each resolved promise.
args: { component: componentStr, search: searchStr },
done: reloadListTemplate,
fail: notification.exception }
- ]);
+ ], true, false);
};
var throttle = null;
return new external_function_parameters($params);
}
- /**
- * Expose to AJAX
- * @return boolean
- */
- public static function list_templates_is_allowed_from_ajax() {
- return true;
- }
-
/**
* Loads the list of templates.
* @param string $component Limit the search to a component.
);
}
- /**
- * Can this function be called directly from ajax?
- *
- * @return boolean
- * @since Moodle 2.9
- */
- public static function load_canonical_template_is_allowed_from_ajax() {
- return true;
- }
-
/**
* Return a mustache template.
* Note - this function differs from the function core_output_load_template
'description' => 'List/search templates by component.',
'type' => 'read',
'capabilities'=> '',
+ 'ajax' => true,
+ 'loginrequired' => false,
),
'tool_templatelibrary_load_canonical_template' => array(
'classname' => 'tool_templatelibrary\external',
'methodname' => 'load_canonical_template',
'description' => 'Load a canonical template by name (not the theme overidden one).',
- 'type' => 'read'
+ 'type' => 'read',
+ 'ajax' => true,
+ 'loginrequired' => false,
),
);
*/
function is_captcha_enabled() {
global $CFG;
- return isset($CFG->recaptchapublickey) && isset($CFG->recaptchaprivatekey) && get_config("auth/{$this->authtype}", 'recaptcha');
+ return $this->config->recaptcha;
}
}
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;
}
+++ /dev/null
-/**
- * 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";
-}
* @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.';
$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';
--- /dev/null
+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
+++ /dev/null
-.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
+++ /dev/null
-@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
$PAGE->set_heading($SITE->fullname);
echo $OUTPUT->header();
+ // Output edit mode title.
+ echo $OUTPUT->heading($strblogs . ': ' . get_string('deleteentry', 'blog'), 2);
+
// Output the entry.
$entry->prepare_render();
echo $output->render($entry);
die;
}
} else if ($action == 'add') {
- $PAGE->set_title("$SITE->shortname: $strblogs: " . get_string('addnewentry', 'blog'));
+ $editmodetitle = $strblogs . ': ' . get_string('addnewentry', 'blog');
+ $PAGE->set_title("$SITE->shortname: $editmodetitle");
$PAGE->set_heading(fullname($USER));
} else if ($action == 'edit') {
- $PAGE->set_title("$SITE->shortname: $strblogs: " . get_string('editentry', 'blog'));
+ $editmodetitle = $strblogs . ': ' . get_string('editentry', 'blog');
+ $PAGE->set_title("$SITE->shortname: $editmodetitle");
$PAGE->set_heading(fullname($USER));
}
$entry->courseid = $courseid;
echo $OUTPUT->header();
+// Output title for editing mode.
+if (isset($editmodetitle)) {
+ echo $OUTPUT->heading($editmodetitle, 2);
+}
$blogeditform->display();
echo $OUTPUT->footer();
'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'
), ''
// 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
*
$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;
}
}
+ 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) {
* @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);
// 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,
* @return void
*/
function update_course($data, $editoroptions = NULL) {
- global $DB;
+ global $DB, $CFG;
+ require_once($CFG->dirroot.'/tag/lib.php');
$data->timemodified = time();
// 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,
--- /dev/null
+<?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();
--- /dev/null
+<?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();
+
+ }
+}
--- /dev/null
+@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
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";
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;
$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;
$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) {
}
}
- $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);
}
$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);
}
}
- $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");
// 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);
+ }
}
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
- * Edit and review page for grade categories and items
+ * The Gradebook setup page.
*
* @package core_grades
* @copyright 2008 Nicolas Connault
}
}
-print_grade_page_head($courseid, 'settings', 'setup', get_string('categoriesanditems', 'grades'));
+print_grade_page_head($courseid, 'settings', 'setup', get_string('gradebooksetup', 'grades'));
// Print Table of categories and items
echo $OUTPUT->box_start('gradetreebox generalbox');
return $str;
}
- //Trims trailing zeros
- //Used on the 'categories and items' page for grade items settings like aggregation co-efficient
- //Grader report has its own decimal place settings so they are handled elsewhere
+ // Trims trailing zeros.
+ // Used on the 'Gradebook setup' page for grade items settings like aggregation co-efficient.
+ // Grader report has its own decimal place settings so they are handled elsewhere.
static function format_number($number) {
$formatted = rtrim(format_float($number, 4),'0');
if (substr($formatted, -1)==get_string('decsep', 'langconfig')) { //if last char is the decimal point
$context = context_course::instance($courseid);
self::$managesetting = array();
if ($courseid != SITEID && has_capability('moodle/grade:manage', $context)) {
- self::$managesetting['categoriesanditems'] = new grade_plugin_info('setup',
+ self::$managesetting['gradebooksetup'] = new grade_plugin_info('setup',
new moodle_url('/grade/edit/tree/index.php', array('id' => $courseid)),
- get_string('categoriesanditems', 'grades'));
+ get_string('gradebooksetup', 'grades'));
self::$managesetting['coursesettings'] = new grade_plugin_info('coursesettings',
new moodle_url('/grade/edit/settings/index.php', array('id'=>$courseid)),
get_string('coursegradesettings', 'grades'));
}
/**
- * Sets a calculated manual grade item. Needs a table with item name - idnumber relation. The step requires you to be in categories and items page.
+ * Sets a calculated manual grade item. Needs a table with item name - idnumber relation.
+ * The step requires you to be in the 'Gradebook setup' page.
*
* @Given /^I set "(?P<calculation_string>(?:[^"]|\\")*)" calculation for grade item "(?P<grade_item_string>(?:[^"]|\\")*)" with idnumbers:$/
* @param string $calculation The calculation.
/**
* Sets a calculated manual grade category total. Needs a table with item name - idnumber relation.
- * The step requires you to be in categories and items page.
+ * The step requires you to be in the 'Gradebook setup' page.
*
* @Given /^I set "(?P<calculation_string>(?:[^"]|\\")*)" calculation for grade category "(?P<grade_item_string>(?:[^"]|\\")*)" with idnumbers:$/
* @param string $calculation The calculation.
| itemname | course | outcome | gradetype | scale |
| Test outcome item one | C1 | OT1 | Scale | Test Scale |
And I expand "Setup" node
- And I follow "Categories and items"
+ And I follow "Gradebook setup"
And I set the following settings for grade item "Course 1":
| Aggregation | Natural |
| Include outcomes in aggregation | 1 |
And I follow "Course 1"
And I navigate to "Grades" node in "Course administration"
And I expand "Setup" node
- And I follow "Categories and items"
+ And I follow "Gradebook setup"
And I set the following settings for grade item "Test outcome item one":
| Extra credit | 1 |
And I log out
And I follow "Course 1"
And I navigate to "Grades" node in "Course administration"
And I expand "Setup" node
- And I follow "Categories and items"
+ And I follow "Gradebook setup"
And I set the following settings for grade item "Course 1":
| Aggregation | Natural |
| Include outcomes in aggregation | 0 |
| Test outcome item one | C1 | OT1 | Scale | Test Scale |
And I navigate to "Grades" node in "Course administration"
And I expand "Setup" node
- And I follow "Categories and items"
+ And I follow "Gradebook setup"
And I set the following settings for grade item "Course 1":
| Aggregation | Natural |
| Include outcomes in aggregation | 1 |
And I set the following settings for grade item "Course 1":
| Aggregation | Natural |
| Exclude empty grades | 0 |
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I press "Add category"
And I set the following fields to these values:
| Category name | Sub category 3 |
And I press "Save changes"
And I turn editing mode off
And I should see "250.00 (25.25 %)" in the ".course" "css_element"
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I press "Add category"
And I set the following fields to these values:
| Category name | Sub sub category 1 |
@javascript
Scenario: Natural aggregation from the setup screen
- And I select "Categories and items" from the "Grade report" singleselect
+ And I select "Gradebook setup" from the "Grade report" singleselect
And I set the following settings for grade item "Course 1":
| Aggregation | Natural |
And I set the following settings for grade item "Sub category 1":
| Aggregation | Natural |
| Exclude empty grades | 0 |
And I turn editing mode off
- And I select "Categories and items" from the "Grade report" singleselect
+ And I select "Gradebook setup" from the "Grade report" singleselect
And I set the field "Override weight of Test assignment one" to "1"
And I set the field "Weight of Test assignment one" to "0"
And I set the field "Override weight of Test assignment six" to "1"
@javascript
Scenario: Switching grade items between categories
# Move to same aggregation (Natural).
- Given I navigate to "Categories and items" node in "Grade administration > Setup"
+ Given I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I set the field "Select Item a1" to "1"
And I set the field "Select Item a2" to "1"
And I set the field "Select Item a3" to "1"
And the field "Extra credit" matches value "1"
And I press "Cancel"
# Move to Mean of grades (with extra credit).
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I set the field "Select Item a1" to "1"
And I set the field "Select Item a2" to "1"
And I set the field "Select Item a3" to "1"
And the field "Extra credit" matches value "1"
And I press "Cancel"
# Move to Simple weight mean of grades.
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I set the field "Select Item a1" to "1"
And I set the field "Select Item a2" to "1"
And I set the field "Select Item a3" to "1"
And the field "Extra credit" matches value "1"
And I press "Cancel"
# Move to Weighted mean of grades.
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I set the field "Select Item a1" to "1"
And I set the field "Select Item a2" to "1"
And I set the field "Select Item a3" to "1"
And I set the field "Item weight" to "11"
And I press "Save changes"
# Move to same (Weighted mean of grades).
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I set the field "Select Item a1" to "1"
And I set the field "Select Item a2" to "1"
And I set the field "Select Item a3" to "1"
And the field "Item weight" matches value "11"
And I press "Save changes"
# Move back to Natural.
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I set the field "Select Item a1" to "1"
And I set the field "Select Item a2" to "1"
And I set the field "Select Item a3" to "1"
| Show average | Show |
And I press "Save changes"
# Add a manual grade item
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I press "Add grade item"
And I set the following fields to these values:
| Item name | Manual item 1 |
Feature: Calculated grade items can be used in the gradebook
In order to use calculated grade items in the gradebook
As a teacher
- I need setup calculated grade items in the categories and items page.
+ I need setup calculated grade items in the 'Gradebook setup' page.
Background:
Given the following "courses" exist:
And I am on site homepage
And I follow "Course 1"
And I navigate to "Grades" node in "Course administration"
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
@javascript
Scenario: The max grade for a category item, with a calculation using Natural aggregation, can be changed
| grade item 1 | - | 75.00 | 0–100 | 75.00 % | - |
| Calc cat totalInclude empty grades. | 100.00 % | 37.50 | 0–50 | 75.00 % | - |
| Course total | - | 37.50 | 0–50 | 75.00 % | - |
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I set the following settings for grade item "Calc cat":
| Maximum grade | 40 |
And I follow "Grader report"
| grade item 1 | 66.67 % | 75.00 | 0–100 | 75.00 % | 50.00 % |
| calc item | 33.33 % | 37.50 | 0–50 | 75.00 % | 25.00 % |
| Course total | - | 112.50 | 0–150 | 75.00 % | - |
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I set the following settings for grade item "calc item":
| Maximum grade | 40 |
And I follow "Grader report"
And I am on site homepage
And I follow "Course 1"
And I navigate to "Grades" node in "Course administration"
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
@javascript
Scenario: The max grade for a category item, with a calculation using Natural aggregation, can be changed
| grade item 1 | - | 75.00 | 0–100 | 75.00 % | - |
| Calc cat totalInclude empty grades. | 100.00 % | 37.50 | 0–100 | 37.50 % | - |
| Course total | - | 37.50 | 0–100 | 37.50 % | - |
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I set the following settings for grade item "Calc cat":
| Maximum grade | 40 |
And I follow "Grader report"
| grade item 1 | 50.00 % | 75.00 | 0–100 | 75.00 % | 37.50 % |
| calc item | 50.00 % | 37.50 | 0–100 | 37.50 % | 18.75 % |
| Course total | - | 112.50 | 0–200 | 56.25 % | - |
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I set the following settings for grade item "calc item":
| Maximum grade | 40 |
And I follow "Grader report"
And I set the field "Show weightings" to "Show"
And I set the field "Show contribution to course total" to "Show"
And I press "Save changes"
- And I set the field "Grade report" to "Categories and items"
+ And I set the field "Grade report" to "Gradebook setup"
And I press "Add category"
And I set the field "Category name" to "Sub category"
And I press "Save changes"
And I am on site homepage
And I follow "Course 1"
And I navigate to "Grades" node in "Course administration"
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I press "Add grade item"
And I set the following fields to these values:
| Item name | Manual item 1 |
And I press "Save changes"
Scenario Outline: The contribution of extra credit items is normalised
- Given I set the field "Grade report" to "Categories and items"
+ Given I set the field "Grade report" to "Gradebook setup"
When I set the following settings for grade item "Course 1":
| Aggregation | <aggregation> |
And I set the following settings for grade item "Manual item 2":
And I am on site homepage
And I follow "Course 1"
And I navigate to "Grades" node in "Course administration"
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I press "Add grade item"
And I set the following fields to these values:
| Item name | Manual item 1 |
@javascript
Scenario: Natural aggregation with negative and positive grade
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I set the following settings for grade item "Sub category 1":
| Aggregation | Natural |
| Exclude empty grades | 0 |
And I am on site homepage
And I follow "C1"
And I navigate to "Grades" node in "Course administration"
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I press "Add grade item"
And I set the following fields to these values:
| Item name | MI 1 |
And I set the field "Show weightings" to "Show"
And I set the field "Show contribution to course total" to "Show"
And I press "Save changes"
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I set the following settings for grade item "CAT1":
| Aggregation | Natural |
And I log out
| MI 5 | 33.33 % | 30.00 | 0–100 | 30.00 % | 10.00 % |
| CAT1 total | 33.33 % | 10.00 | 0–100 | 10.00 % | - |
| Course total | - | 60.00 | 0–300 | 20.00 % | - |
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I set the following settings for grade item "MI 1":
| Maximum grade | 50.00 |
| Minimum grade | 5.00 |
| MI 5 | 50.00 % | 30.00 | 0–100 | 30.00 % | 15.00 % |
| CAT1 total | 25.00 % | 10.00 | 0–50 | 20.00 % | - |
| Course total | - | 60.00 | 0–200 | 30.00 % | - |
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I set the following settings for grade item "MI 5":
| Maximum grade | 200.00 |
And I follow "User report"
And I log in as "teacher1"
And I follow "Course 1"
And I navigate to "Grades" node in "Course administration"
- And I set the field "Grade report" to "Categories and items"
+ And I set the field "Grade report" to "Gradebook setup"
And I set the following settings for grade item "Test assignment four (extra)":
| Extra credit | 1 |
And I set the following settings for grade item "Test assignment five (extra)":
And I log in as "teacher1"
And I follow "Course 1"
And I navigate to "Grades" node in "Course administration"
- And I set the field "Grade report" to "Categories and items"
+ And I set the field "Grade report" to "Gradebook setup"
And I set the following settings for grade item "Test assignment four (extra)":
| Extra credit | 1 |
And I set the following settings for grade item "Test assignment five (extra)":
And I log in as "teacher1"
And I follow "Course 1"
And I navigate to "Grades" node in "Course administration"
- And I set the field "Grade report" to "Categories and items"
+ And I set the field "Grade report" to "Gradebook setup"
@javascript
Scenario: Setting all weights in a category to exactly one hundred in total.
And I log in as "teacher1"
And I follow "Course 1"
And I navigate to "Grades" node in "Course administration"
- And I set the field "Grade report" to "Categories and items"
+ And I set the field "Grade report" to "Gradebook setup"
@javascript
Scenario: Grade items weights are normalised when all grade item weights are overridden (sum exactly 100). Extra credit is set to zero (before the fix 20150619).
| Test assignment one | C | F–A | 50.00 % | 60.00 % |
| Sub category 1 total | 3.00 | 0–5 | 60.00 % | - |
| Course total | 3.00 | 0–5 | 60.00 % | - |
- And I select "Categories and items" from the "Grade report" singleselect
+ And I select "Gradebook setup" from the "Grade report" singleselect
And the following should exist in the "grade_edit_tree_table" table:
| Name | Max grade |
| Test assignment one | 5.00 |
| Test assignment one | C | F–A | 50.00 % | <contrib3> |
| Sub category (<aggregation>) total<aggregation>. | 3.00 | 1–5 | 50.00 % | - |
| Course total<aggregation>. | <coursetotal3> | 0–100 | <courseperc3> | - |
- And I select "Categories and items" from the "Grade report" singleselect
+ And I select "Gradebook setup" from the "Grade report" singleselect
And the following should exist in the "grade_edit_tree_table" table:
| Name | Max grade |
| Test assignment one | A (5) |
And I turn editing mode on
When I set the following settings for grade item "Course 1":
| Aggregation | Natural |
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I set the field "Override weight of Grade me" to "1"
Then the field "Override weight of Grade me" matches value "100.00"
And I click on "Edit" "link" in the "Scale me" "table_row"
| grade_includescalesinaggregation | 1 |
And I follow "Course 1"
And I navigate to "Grades" node in "Course administration"
- And I navigate to "Categories and items" node in "Grade administration > Setup"
+ And I navigate to "Gradebook setup" node in "Grade administration > Setup"
And I set the field "Override weight of Grade me" to "1"
And the field "Override weight of Grade me" matches value "95.238"
And I set the field "Override weight of Scale me" to "1"
| Test assignment one | - | Ace!–Ace! | - |
| Sub category 1 total| - | 0–1 | - |
| Course total | - | 0–1 | - |
- And I set the field "jump" to "Categories and items"
+ And I set the field "jump" to "Gradebook setup"
And the following should exist in the "grade_edit_tree_table" table:
| Name | Max grade |
| Test assignment one | 1.00 |
| Test assignment one | Ace! | Ace!–Ace! | <contrib1> |
| Sub category (<aggregation>) total<aggregation>. | <cattotal1> | 0–100 | - |
| Course total<aggregation>. | <coursetotal1> | 0–100 | - |
- And I set the field "jump" to "Categories and items"
+ And I set the field "jump" to "Gradebook setup"
And the following should exist in the "grade_edit_tree_table" table:
| Name | Max grade |
| Test assignment one | Ace! (1) |
And "Course 1" row "Grade" column of "overview-grade" table should not contain "90.00"
Scenario: We can add a weighting to a grade item and it is displayed properly in the user report
- When I select "Categories and items" from the "Grade report" singleselect
+ When I select "Gradebook setup" from the "Grade report" singleselect
And I set the following settings for grade item "Course 1":
| Aggregation | Weighted mean of grades |
And I set the field "Extra credit value for Test assignment name" to "0.72"
/**
- * Tests grade_edit_tree (deals with the data on the categories and items page in the gradebook)
+ * Tests grade_edit_tree (deals with the data on the 'Gradebook setup' page in the gradebook)
*/
class core_grade_edittreelib_testcase extends advanced_testcase {
public function test_format_number() {
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');
}
--- /dev/null
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package installer
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['thislanguage'] = 'བོད་ཡིག';
$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';
$string['courseblogs'] = 'Users can only see blogs for people who share a course';
$string['deleteblogassociations'] = 'Delete blog associations';
$string['deleteblogassociations_help'] = 'If ticked then blog entries will no longer be associated with this course or any course activities or resources. The blog entries themselves will not be deleted.';
+$string['deleteentry'] = 'Delete entry';
$string['deleteexternalblog'] = 'Unregister this external blog';
$string['deleteotagswarn'] = 'Are you sure you want to remove these tags from all blog posts and remove it from the system?';
$string['description'] = 'Description';
thistaghasnodesc,core_tag
updated,core_tag
withselectedtags,core_tag
+tag:create,core_role
+categoriesanditems,core_grades
$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';
$string['aggregationhintextra'] = '( Extra credit )';
$string['aggregation_link'] = 'grade/aggregation';
$string['aggregationcoef'] = 'Aggregation coefficient';
-$string['aggregationcoefextra'] = 'Extra credit'; // for the header of the table at Edit categories and items page
+$string['aggregationcoefextra'] = 'Extra credit'; // For the header of the table on the 'Gradebook setup' page.
$string['aggregationcoefextra_help'] = 'If the aggregation is \'Natural\' or \'Simple weighted mean\' and the extra credit checkbox is ticked, the grade item\'s maximum grade is not added to the category\'s maximum grade. This will result in the possibility of achieving the maximum grade in the category without having the maximum grade in all the grade items. If the site administrator has enabled grades over the maximum, there might be grades over the maximum.
If the aggregation is \'Mean of grades (with extra credits)\' and the extra credit is set to a value greater than zero, the extra credit is the factor by which the grade is multiplied before adding it to the total after the computation of the mean.';
$string['calculationview'] = 'View calculation';
$string['cannotaccessgroup'] = 'Can not access grades of selected group, sorry.';
$string['categories'] = 'Categories';
-$string['categoriesanditems'] = 'Categories and items';
$string['category'] = 'Category';
$string['categoryedit'] = 'Edit category';
$string['categoryname'] = 'Category name';
$string['gradebookcalculationswarning'] = 'Note: Some errors have been detected in calculating the grades displayed in the gradebook. It is recommended that the errors are fixed by clicking the button below, though this will result in some grades being changed. For details, see the changes between versions {$a->gradebookversion} and {$a->currentversion} in <a href="{$a->url}">Gradebook calculation changes</a>.';
$string['gradebookhiddenerror'] = 'The gradebook is currently set to hide everything from students.';
$string['gradebookhistories'] = 'Grade histories';
+$string['gradebooksetup'] = 'Gradebook setup';
$string['gradeboundary'] = 'Letter grade boundary';
$string['gradeboundary_help'] = 'This setting determines the minimum percentage over which grades will be assigned the grade letter.';
$string['gradecategories'] = 'Grade categories';
$string['xml'] = 'XML';
$string['yes'] = 'Yes';
$string['yourgrade'] = 'Your grade';
+
+// Deprecated since 3.0.
+$string['categoriesanditems'] = 'Categories and items';
$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';
$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';
$string['xroleassignments'] = '{$a}\'s role assignments';
$string['xuserswiththerole'] = 'Users with the role "{$a->role}"';
+// Deprecated in 3.0.
+
+$string['tag:create'] = 'Create new tags';
$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';
$string['iprestriction_help'] = 'The user will need to call the web service from the listed IPs (separated by commas).';
$string['key'] = 'Key';
$string['keyshelp'] = 'The keys are used to access your Moodle account from external applications.';
+$string['loginrequired'] = 'Restricted to logged in users';
$string['manageprotocols'] = 'Manage protocols';
$string['managetokens'] = 'Manage tokens';
$string['missingcaps'] = 'Missing capabilities';
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
- * Settings for the tags block.
+ * This file is used to call any registered externallib function in Moodle.
*
- * @package block_tags
- * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
+ * It will process more than one request and return more than one response if required.
+ * It is recommended to add webservice functions and re-use this script instead of
+ * writing any new custom ajax scripts.
+ *
+ * @since Moodle 2.9
+ * @package core
+ * @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-defined('MOODLE_INTERNAL') || die;
-
-if ($ADMIN->fulltree) {
- $settings->add(new admin_setting_configcheckbox('block_tags_showcoursetags', get_string('showcoursetags', 'block_tags'),
- get_string('showcoursetagsdef', 'block_tags'), 0));
-}
+define('NO_MOODLE_COOKIES', true);
+require_once('service.php');
require_once(dirname(__FILE__) . '/../../config.php');
require_once($CFG->libdir . '/externallib.php');
-require_login(null, true, null, true, true);
-require_sesskey();
-
$rawjson = file_get_contents('php://input');
$requests = json_decode($rawjson, true);
$externalfunctioninfo = external_function_info($methodname);
if (!$externalfunctioninfo->allowed_from_ajax) {
+ error_log('This external function is not available to ajax. Failed to call "' . $methodname . '"');
throw new moodle_exception('servicenotavailable', 'webservice');
}
+ // Do not allow access to write or delete webservices as a public user.
+ if ($externalfunctioninfo->loginrequired) {
+ if (defined('NO_MOODLE_COOKIES') && NO_MOODLE_COOKIES) {
+ error_log('Set "loginrequired" to false in db/service.php when calling entry point service-nologin.php. ' .
+ 'Failed to call "' . $methodname . '"');
+ throw new moodle_exception('servicenotavailable', 'webservice');
+ }
+ if (!isloggedin()) {
+ error_log('This external function is not available to public users. Failed to call "' . $methodname . '"');
+ throw new moodle_exception('servicenotavailable', 'webservice');
+ } else {
+ require_sesskey();
+ }
+ }
+
// Validate params, this also sorts the params properly, we need the correct order in the next part.
$callable = array($externalfunctioninfo->classname, 'validate_parameters');
$params = call_user_func($callable,
* can be attached to the promises returned by this function.
* @param {Boolean} async Optional, defaults to true.
* If false - this function will not return until the promises are resolved.
+ * @param {Boolean} loginrequired Optional, defaults to true.
+ * If false - this function will call the faster nologin ajax script - but
+ * will fail unless all functions have been marked as 'loginrequired' => false
+ * in services.php
* @return {Promise[]} Array of promises that will be resolved when the ajax call returns.
*/
- call: function(requests, async) {
+ call: function(requests, async, loginrequired) {
var ajaxRequestData = [],
i,
promises = [];
async: async
};
+ var script = config.wwwroot + '/lib/ajax/service.php?sesskey=' + config.sesskey;
+ if (!loginrequired) {
+ script = config.wwwroot + '/lib/ajax/service-nologin.php?sesskey=' + config.sesskey;
+ }
+
// Jquery deprecated done and fail with async=false so we need to do this 2 ways.
if (async) {
- $.ajax(config.wwwroot + '/lib/ajax/service.php?sesskey=' + config.sesskey, settings)
+ $.ajax(script, settings)
.done(requestSuccess)
.fail(requestFail);
} else {
settings.success = requestSuccess;
settings.error = requestFail;
- $.ajax(config.wwwroot + '/lib/ajax/service.php?sesskey=' + config.sesskey, settings);
+ $.ajax(script, settings);
}
return promises;
});
}
- var deferreds = ajax.call(ajaxrequests);
+ var deferreds = ajax.call(ajaxrequests, true, false);
$.when.apply(null, deferreds).done(
function() {
// Turn the list of arguments (unknown length) into a real array.
template: name,
themename: currentThemeName
}
- }], async);
+ }], async, false);
promises[0].done(
function (templateSource) {
);
}
- /**
- * Can this function be called directly from ajax?
- *
- * @return boolean
- * @since Moodle 2.9
- */
- public static function load_template_is_allowed_from_ajax() {
- return true;
- }
-
/**
* Return a mustache template, and all the strings it requires.
*
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).
* @return string
*/
public function get_name() {
- return get_string('taskcompletioncron', 'admin');
+ return get_string('taskcompletiondaily', 'admin');
}
/**
global $CFG;
if ($CFG->enablecompletion) {
- // Completion cron.
+ // Daily Completion cron.
require_once($CFG->dirroot.'/completion/cron.php');
- completion_cron();
+ completion_cron_mark_started();
}
}
--- /dev/null
+<?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();
+ }
+ }
+
+}
)
),
+ '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',
'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,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => array(
- 'manager' => CAP_ALLOW,
'user' => CAP_ALLOW
)
),
</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">
'classpath' => 'lib/external/externallib.php',
'description' => 'Return a translated string - similar to core get_string() call',
'type' => 'read',
+ 'loginrequired' => false,
+ 'ajax' => true,
),
'core_get_strings' => array(
'classpath' => 'lib/external/externallib.php',
'description' => 'Return some translated strings - like several core get_string() calls',
'type' => 'read',
+ 'loginrequired' => false,
+ 'ajax' => true,
),
'core_get_component_strings' => array(
'description' => 'Return all raw strings (with {$a->xxx}) for a specific component
- similar to core get_component_strings() call',
'type' => 'read',
+ 'loginrequired' => false,
+ 'ajax' => true,
),
'classname' => 'core\output\external',
'methodname' => 'load_template',
'description' => 'Load a template for a renderable',
- 'type' => 'read'
+ 'type' => 'read',
+ 'loginrequired' => false,
+ 'ajax' => true,
),
// Completion related functions.
'methodname' => 'update_tags',
'description' => 'Updates tags',
'type' => 'write',
+ 'ajax' => true
),
'core_tag_get_tags' => array(
'methodname' => 'get_tags',
'description' => 'Gets tags by their ids',
'type' => 'read',
+ 'ajax' => true
),
);
'mod_forum_get_forums_by_courses',
'mod_forum_get_forum_discussions_paginated',
'mod_forum_get_forum_discussion_posts',
+ 'mod_forum_add_discussion_post',
+ 'mod_forum_add_discussion',
'core_files_get_files',
'core_message_get_messages',
'core_message_create_contacts',
'mod_page_view_page',
'mod_resource_view_resource',
'mod_folder_view_folder',
+ 'mod_chat_login_user',
+ 'mod_chat_get_chat_users',
+ 'mod_chat_send_chat_message',
+ 'mod_chat_get_chat_latest_messages',
+ 'mod_chat_view_chat',
+ 'mod_chat_get_chats_by_courses',
+ 'mod_book_view_book',
+ 'mod_choice_get_choice_results',
+ 'mod_choice_get_choice_options',
+ 'mod_choice_submit_choice_response',
+ 'mod_choice_view_choice',
+ 'mod_imscp_view_imscp',
),
'enabled' => 0,
'restrictedusers' => 0,
'month' => '*'
),
array(
- 'classname' => 'core\task\completion_cron_task',
+ 'classname' => 'core\task\completion_regular_task',
'blocking' => 0,
'minute' => '*',
'hour' => '*',
'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,
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;
}
}
}
}
-}
\ 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'));
+}
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');
+ }
+}
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.
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)
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();
$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;
return $strparams;
}
- /**
- * Can this function be called directly from ajax?
- *
- * @return boolean
- * @since Moodle 2.9
- */
- public static function get_string_is_allowed_from_ajax() {
- return true;
- }
-
/**
* Returns description of get_string parameters
*
);
}
- /**
- * Can this function be called directly from ajax?
- *
- * @return boolean
- * @since Moodle 2.9
- */
- public static function get_strings_is_allowed_from_ajax() {
- return true;
- }
-
-
/**
* Return multiple call to core get_string()
*
);
}
- /**
- * Can this function be called directly from ajax?
- *
- * @return boolean
- * @since Moodle 2.9
- */
- public static function get_component_strings_is_allowed_from_ajax() {
- return true;
- }
-
/**
* Return all lang strings of a component - call to core get_component_strings().
*
}
}
$function->allowed_from_ajax = false;
- if (method_exists($function->classname, $function->ajax_method)) {
- if (call_user_func(array($function->classname, $function->ajax_method)) === true) {
- $function->allowed_from_ajax = true;
- }
- }
// fetch the parameters description
$function->parameters_desc = call_user_func(array($function->classname, $function->parameters_method));
if (isset($functions[$function->name]['testclientpath'])) {
$function->testclientpath = $functions[$function->name]['testclientpath'];
}
+ if (isset($functions[$function->name]['type'])) {
+ $function->type = $functions[$function->name]['type'];
+ }
+ if (isset($functions[$function->name]['ajax'])) {
+ $function->allowed_from_ajax = $functions[$function->name]['ajax'];
+ } else if (method_exists($function->classname, $function->ajax_method)) {
+ if (call_user_func(array($function->classname, $function->ajax_method)) === true) {
+ debugging('External function ' . $function->ajax_method . '() function is deprecated.' .
+ 'Set ajax=>true in db/service.php instead.', DEBUG_DEVELOPER);
+ $function->allowed_from_ajax = true;
+ }
+ }
+ if (isset($functions[$function->name]['loginrequired'])) {
+ $function->loginrequired = $functions[$function->name]['loginrequired'];
+ } else {
+ $function->loginrequired = true;
+ }
}
return $function;
* @param stdClass $context
* @since Moodle 2.0
*/
- protected static function validate_context($context) {
+ public static function validate_context($context) {
global $CFG;
if (empty($context)) {
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);
+ }
+
+}
/** @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 */
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();
+ });
}
';
* @return array An array containing 'object', 'type', 'depth' and optionally 'children'
*/
static private function _fetch_course_tree_recursion($category_array, &$sortorder) {
- // update the sortorder in db if needed
- //NOTE: This leads to us resetting sort orders every time the categories and items page is viewed :(
- //if ($category_array['object']->sortorder != $sortorder) {
- //$category_array['object']->set_sortorder($sortorder);
- //}
-
if (isset($category_array['object']->gradetype) && $category_array['object']->gradetype==GRADE_TYPE_NONE) {
return null;
}
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');
$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));
* @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;
}
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
$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
$gradenode = $coursenode->add(get_string('grades'), $url, self::TYPE_SETTING, null, 'grades', new pix_icon('i/grades', ''));
}
+ // Check if we can view the gradebook's setup page.
+ if (has_capability('moodle/grade:manage', $coursecontext)) {
+ $url = new moodle_url('/grade/edit/tree/index.php', array('id' => $course->id));
+ $coursenode->add(get_string('gradebooksetup', 'grades'), $url, self::TYPE_SETTING,
+ null, 'gradebooksetup', new pix_icon('i/settings', ''));
+ }
+
// Add outcome if permitted
if (!empty($CFG->enableoutcomes) && has_capability('moodle/course:update', $coursecontext)) {
$url = new moodle_url('/grade/edit/outcome/course.php', array('id'=>$course->id));
* @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'])) {
$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',
--- /dev/null
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+ @template core/columns-2to1
+
+ Moodle columns-2to1 template.
+
+ The purpose of this template is to render 2 columns where the first column has twice the width of the second one.
+ On mobile the second column collapses underneath the first.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Context variables required for this template:
+ * col1content Column 1 contents.
+ * col2content Column 2 contents.
+
+ Example context (json):
+ {
+ "col1content": "<div class='alert alert-info'>Lorem ipsum dolor sit amet, consectetur adipiscing elit. In porttitor vulputate turpis, quis tempor arcu.</div>",
+ "col2content": "<div class='alert alert-success'>Vivamus ac orci in velit fringilla aliquam a a nisl. Cras luctus quam laoreet magna pulvinar aliquet.</div>"
+ }
+}}
+<div class="row-fluid rtl-compatible">
+ <div class="span8">{{$ column1 }}{{{ col1content }}}{{/ column1 }}</div>
+ <div class="span4">{{$ column2 }}{{{ col2content }}}{{/ column2 }}</div>
+</div>
fwrite(STDOUT, "\033[2A\033[u\033[2B");
}
}
+
+ /**
+ * Presses a given button in the browser.
+ * NOTE: Phantomjs and goutte driver reloads page while navigating back and forward.
+ *
+ * @Then /^I press the "(back|forward|reload)" button in the browser$/
+ * @param string $button the button to press.
+ * @throws ExpectationException
+ */
+ public function i_press_in_the_browser($button) {
+ $session = $this->getSession();
+
+ if ($button == 'back') {
+ $session->back();
+ } else if ($button == 'forward') {
+ $session->forward();
+ } else if ($button == 'reload') {
+ $session->reload();
+ } else {
+ throw new ExpectationException('Unknown browser button.', $session);
+ }
+ }
}
$_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();
+ }
}
3) Create a forum 'f2'
4) Create a quiz 'q1' with one question (e.g. true/false question)
5) Create a quiz 'q2' with one question (can use same question)
-6) In gradebook/reports/categories and items, edit q2's grade (show advanced)
+6) In gradebook/reports/gradebook setup, edit q2's grade (show advanced)
to assign a 'grade to pass' of 5.0.
[Note: Due to a bug in gradebook at time of writing, you may have to go into
admin screens and make sure that 'Student' is included on the list of graded
$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))));
+ }
}
=== 3.0 ===
+* External functions x_is_allowed_from_ajax() methods have been deprecated. Define 'ajax' => true in db/services.php instead.
+* External functions can be called without a session if they define 'loginrequired' => true in db/services.php.
* All plugins are required to declare their frankenstyle component name via
the $plugin->component property in their version.php file. See
https://docs.moodle.org/dev/version.php for details (MDL-48494).
* 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 ===
$someexamplesofremovedfiles = array(
// Removed in 3.0.
'/mod/lti/grade.php',
+ '/tag/coursetagslib.php',
// Removed in 2.9.
'/lib/timezone.txt',
// Removed in 2.8.
// Technically, HTTP/1.1 requires Location: header to contain the absolute path.
// (In practice browsers accept relative paths - but still, might as well do it properly.)
// This code turns relative into absolute.
- if (!preg_match('|^[a-z]+:|', $url)) {
+ if (!preg_match('|^[a-z]+:|i', $url)) {
// Get host name http://www.wherever.com.
$hostpart = preg_replace('|^(.*?[^:/])/.*$|', '$1', $CFG->wwwroot);
if (preg_match('|^/|', $url)) {
*/
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
FILTER_CONTENT_UPDATED: "filter-content-updated"
};
-M.core.globalEvents = {
+M.core.globalEvents = M.core.globalEvents || {
/**
* This event is triggered when form has an error
*
// 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);
}
}
*/
function signup_captcha_enabled() {
global $CFG;
- return !empty($CFG->recaptchapublickey) && !empty($CFG->recaptchaprivatekey) && get_config('auth/email', 'recaptcha');
+ $authplugin = get_auth_plugin($CFG->registerauth);
+ return !empty($CFG->recaptchapublickey) && !empty($CFG->recaptchaprivatekey) && $authplugin->is_captcha_enabled();
}
}
FROM {assign_submission}
GROUP BY assignment, groupid, userid';
- // Note: souterouter looks redundant below, but it forces
- // MySQL to use an in memory table to store the results of the
- // inner query. Without this MySQL would complain that the UPDATE
- // is operating on the same table as the FROM (which is true).
- $maxattemptidssql = 'SELECT souterouter.id FROM (
- SELECT souter.id
- FROM {assign_submission} souter
- JOIN (' . $maxattemptsql . ') sinner
- ON souter.assignment = sinner.assignment
- AND souter.userid = sinner.userid
- AND souter.groupid = sinner.groupid
- AND souter.attemptnumber = sinner.maxattempt
- ) souterouter';
- $select = 'id IN(' . $maxattemptidssql . ')';
- $DB->set_field_select('assign_submission', 'latest', 1, $select);
+ $maxattemptidssql = 'SELECT souter.id
+ FROM {assign_submission} souter
+ JOIN (' . $maxattemptsql . ') sinner
+ ON souter.assignment = sinner.assignment
+ AND souter.userid = sinner.userid
+ AND souter.groupid = sinner.groupid
+ AND souter.attemptnumber = sinner.maxattempt';
+
+ // We need to avoid using "WHERE ... IN(SELECT ...)" clause with MySQL for performance reason.
+ // TODO MDL-29589 Remove this dbfamily exception when implemented.
+ if ($DB->get_dbfamily() === 'mysql') {
+ $params = array('latest' => 1);
+ $sql = 'UPDATE {assign_submission}
+ INNER JOIN (' . $maxattemptidssql . ') souterouter ON souterouter.id = {assign_submission}.id
+ SET latest = :latest';
+ $DB->execute($sql, $params);
+ } else {
+ $select = 'id IN(' . $maxattemptidssql . ')';
+ $DB->set_field_select('assign_submission', 'latest', 1, $select);
+ }
// Look for grade records with no submission record.
// This is when a teacher has marked a student before they submitted anything.
$string['attempthistory'] = 'Previous attempts';
$string['attemptsettings'] = 'Attempt settings';
$string['attemptreopenmethod'] = 'Attempts reopened';
-$string['attemptreopenmethod_help'] = 'Determines how student submission attempts are reopened. The available options are: <ul><li>Never - The student submission cannot be reopened.</li><li>Manually - The student submission can be reopened by a teacher.</li><li>Automatically until pass - The student submission is automatically reopened until the student achieves the grade to pass value set in the Gradebook (Categories and items section) for this assignment.</li></ul>';
+$string['attemptreopenmethod_help'] = 'Determines how student submission attempts are reopened. The available options are: <ul><li>Never - The student submission cannot be reopened.</li><li>Manually - The student submission can be reopened by a teacher.</li><li>Automatically until pass - The student submission is automatically reopened until the student achieves the grade to pass value set in the Gradebook (Gradebook setup section) for this assignment.</li></ul>';
$string['attemptreopenmethod_manual'] = 'Manually';
$string['attemptreopenmethod_none'] = 'Never';
$string['attemptreopenmethod_untilpass'] = 'Automatically until pass';
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,
$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();
--- /dev/null
+<?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()
+ )
+ );
+ }
+
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Book external functions and service definitions.
+ *
+ * @package mod_book
+ * @category external
+ * @copyright 2015 Juan Leyva <juan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 3.0
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+$functions = array(
+
+ 'mod_book_view_book' => array(
+ 'classname' => 'mod_book_external',
+ 'methodname' => 'view_book',
+ 'description' => 'Simulate the view.php web interface book: trigger events, completion, etc...',
+ 'type' => 'write',
+ 'capabilities' => 'mod/book:read'
+ ),
+
+);
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);
+ }
+ }
+}
--- /dev/null
+<?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);
+ }
+
+ }
+}
$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);
+
+ }
}
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)
*/
require(dirname(__FILE__).'/../../config.php');
+require_once(dirname(__FILE__).'/lib.php');
require_once(dirname(__FILE__).'/locallib.php');
require_once($CFG->libdir.'/completionlib.php');
}
// 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) {
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');
$last = $ch->id;
}
-
+$islastchapter = false;
if ($book->navstyle) {
$navprevicon = right_to_left() ? 'nav_next' : 'nav_prev';
$navnexticon = right_to_left() ? 'nav_prev' : 'nav_next';
'<span class="chaptername">' . $navexit . ' ' . $OUTPUT->uarrow() . '</span></a>';
}
- // We cheat a bit here in assuming that viewing the last page means the user viewed the whole book.
- $completion = new completion_info($course);
- $completion->set_module_viewed($cm);
+ $islastchapter = true;
}
}
+book_view($book, $chapter, $islastchapter, $course, $cm, $context);
+
// =====================================================
// Book display HTML code
// =====================================================
echo $OUTPUT->header();
-echo $OUTPUT->heading($book->name);
+echo $OUTPUT->heading(format_string($book->name));
$navclasses = book_get_nav_classes();
$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);
--- /dev/null
+<?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(),
+ )
+ );
+ }
+}
--- /dev/null
+<?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' => ''
+ )
+);
$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);
+}
--- /dev/null
+<?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']);
+
+ }
+}
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;
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');
$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);
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Choice&nb