"plusplus": false,
"predef": [
"M",
- "define"
+ "define",
+ "require"
],
"proto": false,
"regexdash": false,
module.exports = function(grunt) {
var path = require('path'),
+ fs = require('fs'),
tasks = {},
cwd = process.env.PWD || process.cwd();
args.push('--lint-stderr');
}
+ var execShifter = function() {
+
+ shifter = exec("node", args, {
+ cwd: cwd,
+ stdio: 'inherit',
+ env: process.env
+ });
+
+ // Tidy up after exec.
+ shifter.on('exit', function (code) {
+ if (code) {
+ grunt.fail.fatal('Shifter failed with code: ' + code);
+ } else {
+ grunt.log.ok('Shifter build complete.');
+ done();
+ }
+ });
+ };
+
// Actually run shifter.
- shifter = exec("node", args, {
- cwd: cwd,
- stdio: 'inherit',
- env: process.env
- });
-
- // Tidy up after exec.
- shifter.on('exit', function (code) {
- if (code) {
- grunt.fail.fatal('Shifter failed with code: ' + code);
- } else {
- grunt.log.ok('Shifter build complete.');
- done();
- }
- });
+ if (!options.recursive) {
+ execShifter();
+ } else {
+ // Check that there are yui modules otherwise shifter ends with exit code 1.
+ var found = false;
+ var hasYuiModules = function(directory, callback) {
+ fs.readdir(directory, function(err, files) {
+ if (err) {
+ return callback(err, null);
+ }
+
+ // If we already found a match there is no need to continue scanning.
+ if (found === true) {
+ return;
+ }
+
+ // We need to track the number of files to know when we return a result.
+ var pending = files.length;
+
+ // We first check files, so if there is a match we don't need further
+ // async calls and we just return a true.
+ for (var i = 0; i < files.length; i++) {
+ if (files[i] === 'yui') {
+ return callback(null, true);
+ }
+ }
+
+ // Iterate through subdirs if there were no matches.
+ files.forEach(function (file) {
+
+ var p = path.join(directory, file);
+ stat = fs.statSync(p);
+ if (!stat.isDirectory()) {
+ pending--;
+ } else {
+
+ // We defer the pending-1 until we scan the whole dir and subdirs.
+ hasYuiModules(p, function(err, result) {
+ if (err) {
+ return callback(err);
+ }
+
+ if (result === true) {
+ // Once we get a true we notify the caller.
+ found = true;
+ return callback(null, true);
+ }
+
+ pending--;
+ if (pending === 0) {
+ // Notify the caller that the whole dir has been scaned and there are no matches.
+ return callback(null, false);
+ }
+ });
+ }
+
+ // No subdirs here, otherwise the return would be deferred until all subdirs are scanned.
+ if (pending === 0) {
+ return callback(null, false);
+ }
+ });
+ });
+ };
+
+ hasYuiModules(cwd, function(err, result) {
+ if (err) {
+ grunt.fail.fatal(err.message);
+ }
+
+ if (result === true) {
+ execShifter();
+ } else {
+ grunt.log.ok('No YUI modules to build.');
+ done();
+ }
+ });
+ }
};
tasks.startup = function() {
// set up language
$lang = clean_param($options['lang'], PARAM_SAFEDIR);
-if (file_exists($CFG->dirroot.'/install/lang/'.$lang)) {
+$languages = get_string_manager()->get_list_of_translations();
+if (array_key_exists($lang, $languages)) {
$CFG->lang = $lang;
}
//Fist select language
if ($interactive) {
cli_separator();
- $languages = get_string_manager()->get_list_of_translations();
// Do not put the langs into columns because it is not compatible with RTL.
- $langlist = implode("\n", $languages);
$default = $CFG->lang;
- cli_heading(get_string('availablelangs', 'install'));
- echo $langlist."\n";
+ cli_heading(get_string('chooselanguagehead', 'install'));
+ if (array_key_exists($default, $languages)) {
+ echo $default.' - '.$languages[$default]."\n";
+ }
+ if ($default !== 'en') {
+ echo 'en - English (en)'."\n";
+ }
+ echo '? - '.get_string('availablelangs', 'install')."\n";
$prompt = get_string('clitypevaluedefault', 'admin', $CFG->lang);
$error = '';
do {
echo $error;
$input = cli_input($prompt, $default);
- $input = clean_param($input, PARAM_SAFEDIR);
- if (!file_exists($CFG->dirroot.'/install/lang/'.$input)) {
- $error = get_string('cliincorrectvalueretry', 'admin')."\n";
+ if ($input === '?') {
+ echo implode("\n", $languages)."\n";
+ $error = "\n";
+
} else {
- $error = '';
+ $input = clean_param($input, PARAM_SAFEDIR);
+
+ if (!array_key_exists($input, $languages)) {
+ $error = get_string('cliincorrectvalueretry', 'admin')."\n";
+ } else {
+ $error = '';
+ }
}
} while ($error !== '');
$CFG->lang = $input;
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>';
}
+++ /dev/null
-@tool @tool_behat
-Feature: Page contents assertions
- In order to write good tests
- As a tests writer
- I need to check the page contents
-
- @javascript
- Scenario: Basic contents assertions
- Given I log in as "admin"
- And I am on site homepage
- And I expand "Users" node
- And I follow "Groups"
- And I press "Create group"
- And I set the following fields to these values:
- | Group name | I'm the name |
- | Group description | I'm the description |
- And I press "Save changes"
- When I follow "Overview"
- And I wait until the page is ready
- And I wait "2" seconds
- And I hover "#region-main .generaltable td span" "css_element"
- Then I should see "I'm the description"
- And "Grouping" "select" in the "region-main" "region" should be visible
- And "Group" "select" should be visible
- And "Activity report" "link" in the "Administration" "block" should not be visible
- And "Event monitoring rules" "link" should not be visible
- And I should see "Filter groups by"
- And I should not see "Filter groupssss by"
- And I should see "Group members" in the "#region-main table th.c1" "css_element"
- And I should not see "Group membersssss" in the "#region-main table th.c1" "css_element"
- And I follow "Groups"
- And the "#groupeditform #showcreateorphangroupform" "css_element" should be enabled
- And the "#groupeditform #showeditgroupsettingsform" "css_element" should be disabled
-
- @javascript
- Scenario: Locators inside specific DOM nodes using CSS selectors
- Given the following "courses" exist:
- | fullname | shortname | category |
- | Course 1 | C1 | 0 |
- And I log in as "admin"
- And I am on site homepage
- And I follow "Course 1"
- When I dock "Administration" block
- Then I should not see "Question bank" in the ".block-region" "css_element"
- And I click on "//div[@id='dock']/descendant::h2[normalize-space(.)='Administration']" "xpath_element"
-
- @javascript
- Scenario: Locators inside specific DOM nodes using XPath
- Given the following "courses" exist:
- | fullname | shortname | category |
- | Course 1 | C1 | 0 |
- And I log in as "admin"
- When I dock "Administration" block
- Then I should not see "Turn editing on" in the ".block-region" "css_element"
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"
$predbqueries = $DB->perf_get_queries();
$pretime = microtime(true);
- mtrace("Scheduled task: " . $task->get_name());
+ $fullname = $task->get_name() . ' (' . get_class($task) . ')';
+ mtrace('Execute scheduled task: ' . $fullname);
// NOTE: it would be tricky to move this code to \core\task\manager class,
// because we want to do detailed error reporting.
$cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries");
mtrace("... used " . (microtime(1) - $pretime) . " seconds");
}
- mtrace("Task completed.");
+ mtrace('Scheduled task complete: ' . $fullname);
\core\task\manager::scheduled_task_complete($task);
get_mailer('close');
exit(0);
}
mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries");
mtrace("... used " . (microtime(true) - $pretime) . " seconds");
- mtrace("Task failed: " . $e->getMessage());
+ mtrace('Scheduled task failed: ' . $fullname . ',' . $e->getMessage());
if ($CFG->debugdeveloper) {
if (!empty($e->debuginfo)) {
mtrace("Debug info:");
}
if (context) {
templates.render(templateName, context).done(function(html, js) {
- $('[data-region="displaytemplateexample"]').empty();
- $('[data-region="displaytemplateexample"]').append(html);
- templates.runTemplateJS(js);
+ templates.replaceNodeContents($('[data-region="displaytemplateexample"]'), html, js);
}).fail(notification.exception);
} else {
str.get_string('templatehasnoexample', 'tool_templatelibrary').done(function(s) {
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.
*/
var reloadListTemplate = function(templateList) {
templates.render('tool_templatelibrary/search_results', { templates: templateList })
- .done(function (result) {
- $('[data-region="searchresults"]').replaceWith(result);
+ .done(function (result, js) {
+ templates.replaceNode($('[data-region="searchresults"]'), result, js);
}).fail(notification.exception);
};
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,
),
);
echo $OUTPUT->header();
$fullname = fullname($user, true);
echo $OUTPUT->heading(get_string('deleteuser', 'admin'));
+
$optionsyes = array('delete'=>$delete, 'confirm'=>md5($delete), 'sesskey'=>sesskey());
- echo $OUTPUT->confirm(get_string('deletecheckfull', '', "'$fullname'"), new moodle_url($returnurl, $optionsyes), $returnurl);
+ $deleteurl = new moodle_url($returnurl, $optionsyes);
+ $deletebutton = new single_button($deleteurl, get_string('delete'), 'post');
+
+ echo $OUTPUT->confirm(get_string('deletecheckfull', '', "'$fullname'"), $deletebutton, $returnurl);
echo $OUTPUT->footer();
die;
} else if (data_submitted() and !$user->deleted) {
}
/**
- * Returns whether or not the captcha element is enabled, and the admin settings fulfil its requirements.
+ * Returns whether or not the captcha element is enabled.
* @return bool
*/
function is_captcha_enabled() {
- global $CFG;
- return isset($CFG->recaptchapublickey) && isset($CFG->recaptchaprivatekey) && get_config("auth/{$this->authtype}", 'recaptcha');
+ return get_config("auth/{$this->authtype}", 'recaptcha');
}
}
}
// Overwrite redirect in order to send user to Shibboleth logout page and let him return back
- $redirect = $this->config->logout_handler.'?return='.urlencode($temp_redirect);
+ $redirecturl = new moodle_url($this->config->logout_handler, array('return' => $temp_redirect));
+ $redirect = $redirecturl->out();
}
}
This files describes API changes in /auth/* - plugins,
information provided here is intended especially for developers.
+=== 3.0 ===
+
+* login_signup_form::signup_captcha_enabled() now calls is_captcha_enabled() from the current auth plugin instead of from auth_email
+
=== 2.9 ===
* Do not update user->firstaccess from any auth plugin, the complete_user_login() does it automatically.
public function __construct($quiz, $questions, $manifest, $section, $question_node, $rootpath, $contextid, $outdir) {
parent::__construct($quiz, $questions, $manifest, $section, $question_node, $rootpath, $contextid, $outdir);
$this->qtype = cc_qti_profiletype::true_false;
- $this->correct_answer_node_id = $this->questions->nodeValue(
- 'plugin_qtype_truefalse_question/truefalse/trueanswer', $this->question_node);
+
+ // Determine the correct answer by finding out which answer has the non zero fraction...
+ // This is because a true / false question type can have 'false' as the correct answer.
+ $answers = $this->questions->nodeList('plugin_qtype_truefalse_question/answers/answer', $this->question_node);
+ foreach ($answers as $answer) {
+ $fraction = $this->questions->nodeValue('fraction', $answer);
+
+ if ($fraction != 0) {
+ $this->correct_answer_node_id = (int)$this->questions->nodeValue('@id', $answer);
+ }
+ }
+
$maximum_quiz_grade = (int)$this->quiz->nodeValue('/activity/quiz/grade');
$this->total_grade_value = ($maximum_quiz_grade + 1).'.0000000';
}
$sheet_question_categories_question = cc2moodle::loadsheet(SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION_TRUE_FALSE);
- $max_score = 0;
- $true_answer_id = 0;
- $false_answer_id = 0;
+ $trueanswer = null;
+ $falseanswer = null;
if (!empty($question['answers'])) {
+ // Identify the true and false answers.
foreach ($question['answers'] as $answer) {
- if ($answer['score'] > $max_score) {
- $max_score = $answer['score'];
- $true_answer_id = $answer['id'];
+ if ($answer['identifier'] == 'true') {
+ $trueanswer = $answer;
+ } else if ($answer['identifier'] == 'false') {
+ $falseanswer = $answer;
+ } else {
+ // Should not happen, but just in case.
+ throw new coding_exception("Unknown answer identifier detected" .
+ " in true/false quiz question with id {$question['id']}.");
}
$node_course_question_categories_question_answer .= $this->create_node_course_question_categories_question_category_question_answer($answer);
}
- foreach ($question['answers'] as $answer) {
-
- if ($answer['id'] != $true_answer_id) {
- $max_score = $answer['score'];
- $false_answer_id = $answer['id'];
- }
+ // Make sure the true and false answer was found.
+ if (is_null($trueanswer) || is_null($falseanswer)) {
+ throw new coding_exception("Unable to correctly identify the " .
+ "true and false answers in the question with id {$question['id']}.");
}
}
'[#false_answer_id#]');
$replace_values = array($node_course_question_categories_question_answer,
- $true_answer_id,
- $false_answer_id);
+ $trueanswer['id'],
+ $falseanswer['id']);
$node_question_categories_question = str_replace($find_tags, $replace_values, $sheet_question_categories_question);
$sheet_question_categories_question = cc112moodle::loadsheet(SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION_TRUE_FALSE);
- $max_score = 0;
- $true_answer_id = 0;
- $false_answer_id = 0;
+ $trueanswer = null;
+ $falseanswer = null;
if (!empty($question['answers'])) {
+ // Identify the true and false answers.
foreach ($question['answers'] as $answer) {
- if ($answer['score'] > $max_score) {
- $max_score = $answer['score'];
- $true_answer_id = $answer['id'];
+ if ($answer['identifier'] == 'true') {
+ $trueanswer = $answer;
+ } else if ($answer['identifier'] == 'false') {
+ $falseanswer = $answer;
+ } else {
+ // Should not happen, but just in case.
+ throw new coding_exception("Unknown answer identifier detected " .
+ "in true/false quiz question with id {$question['id']}.");
}
$node_course_question_categories_question_answer .= $this->create_node_course_question_categories_question_category_question_answer($answer);
}
- foreach ($question['answers'] as $answer) {
-
- if ($answer['id'] != $true_answer_id) {
- $max_score = $answer['score'];
- $false_answer_id = $answer['id'];
- }
+ // Make sure the true and false answer was found.
+ if (is_null($trueanswer) || is_null($falseanswer)) {
+ throw new coding_exception("Unable to correctly identify the " .
+ "true and false answers in the question with id {$question['id']}.");
}
}
'[#false_answer_id#]');
$replace_values = array($node_course_question_categories_question_answer,
- $true_answer_id,
- $false_answer_id);
+ $trueanswer['id'],
+ $falseanswer['id']);
$node_question_categories_question = str_replace($find_tags, $replace_values, $sheet_question_categories_question);
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();
$ev->add_property('class', 'PUBLIC'); // PUBLIC / PRIVATE / CONFIDENTIAL
$ev->add_property('last-modified', Bennu::timestamp_to_datetime($event->timemodified));
$ev->add_property('dtstamp', Bennu::timestamp_to_datetime()); // now
- $ev->add_property('dtstart', Bennu::timestamp_to_datetime($event->timestart)); // when event starts
if ($event->timeduration > 0) {
//dtend is better than duration, because it works in Microsoft Outlook and works better in Korganizer
+ $ev->add_property('dtstart', Bennu::timestamp_to_datetime($event->timestart)); // when event starts.
$ev->add_property('dtend', Bennu::timestamp_to_datetime($event->timestart + $event->timeduration));
+ } else {
+ // When no duration is present, ie an all day event, VALUE should be date instead of time and dtend = dtstart + 1 day.
+ $ev->add_property('dtstart', Bennu::timestamp_to_date($event->timestart), array('value' => 'DATE')); // All day event.
+ $ev->add_property('dtend', Bennu::timestamp_to_date($event->timestart + DAYSECS), array('value' => 'DATE')); // All day event.
}
if ($event->courseid != 0) {
$coursecontext = context_course::instance($event->courseid);
'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'
), ''
);
}
+ /**
+ * Describes the parameters for mark_course_self_completed.
+ *
+ * @return external_external_function_parameters
+ * @since Moodle 3.0
+ */
+ public static function mark_course_self_completed_parameters() {
+ return new external_function_parameters (
+ array(
+ 'courseid' => new external_value(PARAM_INT, 'Course ID')
+ )
+ );
+ }
+
+ /**
+ * Update the course completion status for the current user (if course self-completion is enabled).
+ *
+ * @param int $courseid Course id
+ * @return array Result and possible warnings
+ * @since Moodle 3.0
+ * @throws moodle_exception
+ */
+ public static function mark_course_self_completed($courseid) {
+ global $USER;
+
+ $warnings = array();
+ $params = self::validate_parameters(self::mark_course_self_completed_parameters(),
+ array('courseid' => $courseid));
+
+ $course = get_course($params['courseid']);
+ $context = context_course::instance($course->id);
+ self::validate_context($context);
+
+ // Set up completion object and check it is enabled.
+ $completion = new completion_info($course);
+ if (!$completion->is_enabled()) {
+ throw new moodle_exception('completionnotenabled', 'completion');
+ }
+
+ if (!$completion->is_tracked_user($USER->id)) {
+ throw new moodle_exception('nottracked', 'completion');
+ }
+
+ $completion = $completion->get_completion($USER->id, COMPLETION_CRITERIA_TYPE_SELF);
+
+ // Self completion criteria not enabled.
+ if (!$completion) {
+ throw new moodle_exception('noselfcompletioncriteria', 'completion');
+ }
+
+ // Check if the user has already marked himself as complete.
+ if ($completion->is_complete()) {
+ throw new moodle_exception('useralreadymarkedcomplete', 'completion');
+ }
+
+ // Mark the course complete.
+ $completion->mark_complete();
+
+ $result = array();
+ $result['status'] = true;
+ $result['warnings'] = $warnings;
+ return $result;
+ }
+
+ /**
+ * Describes the mark_course_self_completed return value.
+ *
+ * @return external_single_structure
+ * @since Moodle 3.0
+ */
+ public static function mark_course_self_completed_returns() {
+
+ return new external_single_structure(
+ array(
+ 'status' => new external_value(PARAM_BOOL, 'status, true if success'),
+ 'warnings' => new external_warnings(),
+ )
+ );
+ }
+
}
// 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
*
new Given('I press "'.get_string('savechangesanddisplay').'"')
);
}
+
+ /**
+ * Checks if the activity with specified name is maked as complete.
+ *
+ * @Given /^the "(?P<activityname_string>(?:[^"]|\\")*)" "(?P<activitytype_string>(?:[^"]|\\")*)" activity with "(manual|auto)" completion should be marked as complete$/
+ * @return array
+ */
+ public function activity_marked_as_complete($activityname, $activitytype, $completiontype) {
+ if ($completiontype == "manual") {
+ $imgalttext = get_string("completion-alt-manual-y", 'core_completion', $activityname);
+ } else {
+ $imgalttext = get_string("completion-alt-auto-y", 'core_completion', $activityname);
+ }
+ $csselementforactivitytype = "li.modtype_".strtolower($activitytype);
+
+ return new Given('"//img[contains(@alt, \''.$imgalttext.'\')]" "xpath_element" ' .
+ 'should exist in the "'.$csselementforactivitytype.'" "css_element"');
+ }
+
+ /**
+ * Checks if the activity with specified name is maked as complete.
+ *
+ * @Given /^the "(?P<activityname_string>(?:[^"]|\\")*)" "(?P<activitytype_string>(?:[^"]|\\")*)" activity with "(manual|auto)" completion should be marked as not complete$/
+ * @return array
+ */
+ public function activity_marked_as_not_complete($activityname, $activitytype, $completiontype) {
+ if ($completiontype == "manual") {
+ $imgalttext = get_string("completion-alt-manual-n", 'core_completion', $activityname);
+ } else {
+ $imgalttext = get_string("completion-alt-auto-n", 'core_completion', $activityname);
+ }
+ $csselementforactivitytype = "li.modtype_".strtolower($activitytype);
+
+ return new Given('"//img[contains(@alt, \''.$imgalttext.'\')]" "xpath_element" ' .
+ 'should exist in the "'.$csselementforactivitytype.'" "css_element"');
+ }
}
}
+ /**
+ * Test mark_course_self_completed
+ */
+ public function test_mark_course_self_completed() {
+ global $DB, $CFG;
+ require_once($CFG->dirroot.'/completion/criteria/completion_criteria_self.php');
+
+ $this->resetAfterTest(true);
+
+ $CFG->enablecompletion = true;
+ $student = $this->getDataGenerator()->create_user();
+ $teacher = $this->getDataGenerator()->create_user();
+
+ $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
+
+ $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+ $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
+
+ // Set completion rules.
+ $completion = new completion_info($course);
+
+ $criteriadata = new stdClass();
+ $criteriadata->id = $course->id;
+ $criteriadata->criteria_activity = array();
+
+ // Self completion.
+ $criteriadata->criteria_self = COMPLETION_CRITERIA_TYPE_SELF;
+ $class = 'completion_criteria_self';
+ $criterion = new $class();
+ $criterion->update_config($criteriadata);
+
+ // Handle overall aggregation.
+ $aggdata = array(
+ 'course' => $course->id,
+ 'criteriatype' => null
+ );
+ $aggregation = new completion_aggregation($aggdata);
+ $aggregation->setMethod(COMPLETION_AGGREGATION_ALL);
+ $aggregation->save();
+
+ $this->setUser($student);
+
+ $result = core_completion_external::mark_course_self_completed($course->id);
+ // We need to execute the return values cleaning process to simulate the web service server.
+ $result = external_api::clean_returnvalue(
+ core_completion_external::mark_course_self_completed_returns(), $result);
+
+ // We expect a valid result.
+ $this->assertEquals(true, $result['status']);
+
+ $result = core_completion_external::get_course_completion_status($course->id, $student->id);
+ // We need to execute the return values cleaning process to simulate the web service server.
+ $result = external_api::clean_returnvalue(
+ core_completion_external::get_course_completion_status_returns(), $result);
+
+ // Course must be completed.
+ $this->assertEquals(COMPLETION_COMPLETE, $result['completionstatus']['completions'][0]['complete']);
+
+ try {
+ $result = core_completion_external::mark_course_self_completed($course->id);
+ $this->fail('Exception expected due course already self completed.');
+ } catch (moodle_exception $e) {
+ $this->assertEquals('useralreadymarkedcomplete', $e->errorcode);
+ }
+
+ }
+
}
$message = "{$strdeletecoursecheck}<br /><br />{$coursefullname} ({$courseshortname})";
$continueurl = new moodle_url('/course/delete.php', array('id' => $course->id, 'delete' => md5($course->timemodified)));
+$continuebutton = new single_button($continueurl, get_string('delete'), 'post');
$PAGE->navbar->add($strdeletecheck);
$PAGE->set_title("$SITE->shortname: $strdeletecheck");
$PAGE->set_heading($SITE->fullname);
echo $OUTPUT->header();
-echo $OUTPUT->confirm($message, $continueurl, $categoryurl);
+echo $OUTPUT->confirm($message, $continuebutton, $categoryurl);
echo $OUTPUT->footer();
-exit;
\ No newline at end of file
+exit;
$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) {
echo $OUTPUT->box_start('noticebox');
$optionsyes = array('id' => $id, 'confirm' => 1, 'delete' => 1, 'sesskey' => sesskey());
$deleteurl = new moodle_url('/course/editsection.php', $optionsyes);
- $formcontinue = new single_button($deleteurl, get_string('continue'));
+ $formcontinue = new single_button($deleteurl, get_string('delete'));
$formcancel = new single_button($cancelurl, get_string('cancel'), 'get');
echo $OUTPUT->confirm(get_string('confirmdeletesection', '',
get_section_name($course, $sectioninfo)), $formcontinue, $formcancel);
);
}
+ /**
+ * Returns description of method parameters
+ *
+ * @return external_function_parameters
+ * @since Moodle 3.0
+ */
+ public static function search_courses_parameters() {
+ return new external_function_parameters(
+ array(
+ 'criterianame' => new external_value(PARAM_ALPHA, 'criteria name
+ (search, modulelist (only admins), blocklist (only admins), tagid)'),
+ 'criteriavalue' => new external_value(PARAM_RAW, 'criteria value'),
+ 'page' => new external_value(PARAM_INT, 'page number (0 based)', VALUE_DEFAULT, 0),
+ 'perpage' => new external_value(PARAM_INT, 'items per page', VALUE_DEFAULT, 0)
+ )
+ );
+ }
+
+ /**
+ * Search courses following the specified criteria.
+ *
+ * @param string $criterianame Criteria name (search, modulelist (only admins), blocklist (only admins), tagid)
+ * @param string $criteriavalue Criteria value
+ * @param int $page Page number (for pagination)
+ * @param int $perpage Items per page
+ * @return array of course objects and warnings
+ * @since Moodle 3.0
+ * @throws moodle_exception
+ */
+ public static function search_courses($criterianame, $criteriavalue, $page=0, $perpage=0) {
+ global $CFG;
+ require_once($CFG->libdir . '/coursecatlib.php');
+
+ $warnings = array();
+
+ $parameters = array(
+ 'criterianame' => $criterianame,
+ 'criteriavalue' => $criteriavalue,
+ 'page' => $page,
+ 'perpage' => $perpage
+ );
+ $params = self::validate_parameters(self::search_courses_parameters(), $parameters);
+
+ $allowedcriterianames = array('search', 'modulelist', 'blocklist', 'tagid');
+ if (!in_array($params['criterianame'], $allowedcriterianames)) {
+ throw new invalid_parameter_exception('Invalid value for criterianame parameter (value: '.$params['criterianame'].'),' .
+ 'allowed values are: '.implode(',', $allowedcriterianames));
+ }
+
+ if ($params['criterianame'] == 'modulelist' or $params['criterianame'] == 'blocklist') {
+ require_capability('moodle/site:config', context_system::instance());
+ }
+
+ $paramtype = array(
+ 'search' => PARAM_RAW,
+ 'modulelist' => PARAM_PLUGIN,
+ 'blocklist' => PARAM_INT,
+ 'tagid' => PARAM_INT
+ );
+ $params['criteriavalue'] = clean_param($params['criteriavalue'], $paramtype[$params['criterianame']]);
+
+ // Prepare the search API options.
+ $searchcriteria = array();
+ $searchcriteria[$params['criterianame']] = $params['criteriavalue'];
+
+ $options = array();
+ if ($params['perpage'] != 0) {
+ $offset = $params['page'] * $params['perpage'];
+ $options = array('offset' => $offset, 'limit' => $params['perpage']);
+ }
+
+ // Search the courses.
+ $courses = coursecat::search_courses($searchcriteria, $options);
+ $totalcount = coursecat::search_courses_count($searchcriteria);
+
+ $finalcourses = array();
+ $categoriescache = array();
+
+ foreach ($courses as $course) {
+
+ $coursecontext = context_course::instance($course->id);
+
+ // Category information.
+ if (!isset($categoriescache[$course->category])) {
+ $categoriescache[$course->category] = coursecat::get($course->category);
+ }
+ $category = $categoriescache[$course->category];
+
+ // Retrieve course overfiew used files.
+ $files = array();
+ foreach ($course->get_course_overviewfiles() as $file) {
+ $fileurl = moodle_url::make_webservice_pluginfile_url($file->get_contextid(), $file->get_component(),
+ $file->get_filearea(), null, $file->get_filepath(),
+ $file->get_filename())->out(false);
+ $files[] = array(
+ 'filename' => $file->get_filename(),
+ 'fileurl' => $fileurl,
+ 'filesize' => $file->get_filesize()
+ );
+ }
+
+ // Retrieve the course contacts,
+ // we need here the users fullname since if we are not enrolled can be difficult to obtain them via other Web Services.
+ $coursecontacts = array();
+ foreach ($course->get_course_contacts() as $contact) {
+ $coursecontacts[] = array(
+ 'id' => $contact['user']->id,
+ 'fullname' => $contact['username']
+ );
+ }
+
+ // Allowed enrolment methods (maybe we can self-enrol).
+ $enroltypes = array();
+ $instances = enrol_get_instances($course->id, true);
+ foreach ($instances as $instance) {
+ $enroltypes[] = $instance->enrol;
+ }
+
+ // Format summary.
+ list($summary, $summaryformat) =
+ external_format_text($course->summary, $course->summaryformat, $coursecontext->id, 'course', 'summary', null);
+
+ $coursereturns = array();
+ $coursereturns['id'] = $course->id;
+ $coursereturns['fullname'] = $course->get_formatted_fullname();
+ $coursereturns['shortname'] = $course->get_formatted_shortname();
+ $coursereturns['categoryid'] = $course->category;
+ $coursereturns['categoryname'] = $category->name;
+ $coursereturns['summary'] = $summary;
+ $coursereturns['summaryformat'] = $summaryformat;
+ $coursereturns['overviewfiles'] = $files;
+ $coursereturns['contacts'] = $coursecontacts;
+ $coursereturns['enrollmentmethods'] = $enroltypes;
+ $finalcourses[] = $coursereturns;
+ }
+
+ return array(
+ 'total' => $totalcount,
+ 'courses' => $finalcourses,
+ 'warnings' => $warnings
+ );
+ }
+
+ /**
+ * Returns description of method result value
+ *
+ * @return external_description
+ * @since Moodle 3.0
+ */
+ public static function search_courses_returns() {
+
+ return new external_single_structure(
+ array(
+ 'total' => new external_value(PARAM_INT, 'total course count'),
+ 'courses' => new external_multiple_structure(
+ new external_single_structure(
+ array(
+ 'id' => new external_value(PARAM_INT, 'course id'),
+ 'fullname' => new external_value(PARAM_TEXT, 'course full name'),
+ 'shortname' => new external_value(PARAM_TEXT, 'course short name'),
+ 'categoryid' => new external_value(PARAM_INT, 'category id'),
+ 'categoryname' => new external_value(PARAM_TEXT, 'category name'),
+ 'summary' => new external_value(PARAM_RAW, 'summary'),
+ 'summaryformat' => new external_format_value('summary'),
+ 'overviewfiles' => new external_multiple_structure(
+ new external_single_structure(
+ array(
+ 'filename' => new external_value(PARAM_FILE, 'overview file name'),
+ 'fileurl' => new external_value(PARAM_URL, 'overview file url'),
+ 'filesize' => new external_value(PARAM_INT, 'overview file size'),
+ )
+ ),
+ 'additional overview files attached to this course'
+ ),
+ 'contacts' => new external_multiple_structure(
+ new external_single_structure(
+ array(
+ 'id' => new external_value(PARAM_INT, 'contact user id'),
+ 'fullname' => new external_value(PARAM_NOTAGS, 'contact user fullname'),
+ )
+ ),
+ 'contact users'
+ ),
+ 'enrollmentmethods' => new external_multiple_structure(
+ new external_value(PARAM_PLUGIN, 'enrollment method'),
+ 'enrollment methods list'
+ ),
+ )
+ ), 'course'
+ ),
+ 'warnings' => new external_warnings()
+ )
+ );
+ }
+
+ /**
+ * Returns description of method parameters
+ *
+ * @return external_function_parameters
+ * @since Moodle 3.0
+ */
+ public static function get_course_module_parameters() {
+ return new external_function_parameters(
+ array(
+ 'cmid' => new external_value(PARAM_INT, 'The course module id')
+ )
+ );
+ }
+
+ /**
+ * Return information about a course module.
+ *
+ * @param int $cmid the course module id
+ * @return array of warnings and the course module
+ * @since Moodle 3.0
+ * @throws moodle_exception
+ */
+ public static function get_course_module($cmid) {
+
+ $params = self::validate_parameters(self::get_course_module_parameters(),
+ array(
+ 'cmid' => $cmid,
+ ));
+
+ $warnings = array();
+
+ $cm = get_coursemodule_from_id(null, $params['cmid'], 0, true, MUST_EXIST);
+ $context = context_module::instance($cm->id);
+ self::validate_context($context);
+
+ // If the user has permissions to manage the activity, return all the information.
+ if (has_capability('moodle/course:manageactivities', $context)) {
+ $info = $cm;
+ } else {
+ // Return information is safe to show to any user.
+ $info = new stdClass();
+ $info->id = $cm->id;
+ $info->course = $cm->course;
+ $info->module = $cm->module;
+ $info->modname = $cm->modname;
+ $info->instance = $cm->instance;
+ $info->section = $cm->section;
+ $info->sectionnum = $cm->sectionnum;
+ $info->groupmode = $cm->groupmode;
+ $info->groupingid = $cm->groupingid;
+ $info->completion = $cm->completion;
+ }
+ // Format name.
+ $info->name = format_string($cm->name, true, array('context' => $context));
+
+ $result = array();
+ $result['cm'] = $info;
+ $result['warnings'] = $warnings;
+ return $result;
+ }
+
+ /**
+ * Returns description of method result value
+ *
+ * @return external_description
+ * @since Moodle 3.0
+ */
+ public static function get_course_module_returns() {
+ return new external_single_structure(
+ array(
+ 'cm' => new external_single_structure(
+ array(
+ 'id' => new external_value(PARAM_INT, 'The course module id'),
+ 'course' => new external_value(PARAM_INT, 'The course id'),
+ 'module' => new external_value(PARAM_INT, 'The module type id'),
+ 'name' => new external_value(PARAM_TEXT, 'The activity name'),
+ 'modname' => new external_value(PARAM_COMPONENT, 'The module component name (forum, assign, etc..)'),
+ 'instance' => new external_value(PARAM_INT, 'The activity instance id'),
+ 'section' => new external_value(PARAM_INT, 'The module section id'),
+ 'sectionnum' => new external_value(PARAM_INT, 'The module section number'),
+ 'groupmode' => new external_value(PARAM_INT, 'Group mode'),
+ 'groupingid' => new external_value(PARAM_INT, 'Grouping id'),
+ 'completion' => new external_value(PARAM_INT, 'If completion is enabled'),
+ 'idnumber' => new external_value(PARAM_RAW, 'Module id number', VALUE_OPTIONAL),
+ 'added' => new external_value(PARAM_INT, 'Time added', VALUE_OPTIONAL),
+ 'score' => new external_value(PARAM_INT, 'Score', VALUE_OPTIONAL),
+ 'indent' => new external_value(PARAM_INT, 'Indentation', VALUE_OPTIONAL),
+ 'visible' => new external_value(PARAM_INT, 'If visible', VALUE_OPTIONAL),
+ 'visibleold' => new external_value(PARAM_INT, 'Visible old', VALUE_OPTIONAL),
+ 'completiongradeitemnumber' => new external_value(PARAM_INT, 'Completion grade item', VALUE_OPTIONAL),
+ 'completionview' => new external_value(PARAM_INT, 'Completion view setting', VALUE_OPTIONAL),
+ 'completionexpected' => new external_value(PARAM_INT, 'Completion time expected', VALUE_OPTIONAL),
+ 'showdescription' => new external_value(PARAM_INT, 'If the description is showed', VALUE_OPTIONAL),
+ 'availability' => new external_value(PARAM_RAW, 'Availability settings', VALUE_OPTIONAL),
+ )
+ ),
+ 'warnings' => new external_warnings()
+ )
+ );
+ }
+
}
/**
Scenario: Deleting the last section in topics format
When I click on "Delete topic" "link" in the "li#section-5" "css_element"
Then I should see "Are you absolutely sure you want to completely delete \"Topic 5\" and all the activities it contains?"
- And I press "Continue"
+ And I press "Delete"
And I should not see "Topic 5"
And I navigate to "Edit settings" node in "Course administration"
And I expand all fieldsets
Scenario: Deleting the middle section in topics format
When I click on "Delete topic" "link" in the "li#section-4" "css_element"
- And I press "Continue"
+ And I press "Delete"
Then I should not see "Topic 5"
And I should not see "Test chat name"
And I should see "Test choice name" in the "li#section-4" "css_element"
When I follow "Reduce the number of sections"
Then I should see "Orphaned activities (section 5)" in the "li#section-5" "css_element"
And I click on "Delete topic" "link" in the "li#section-5" "css_element"
- And I press "Continue"
+ And I press "Delete"
And I should not see "Topic 5"
And I should not see "Orphaned activities"
And "li#section-5" "css_element" should not exist
And "li#section-5.orphaned" "css_element" should exist
And "li#section-4.orphaned" "css_element" should not exist
And I click on "Delete topic" "link" in the "li#section-1" "css_element"
- And I press "Continue"
+ And I press "Delete"
And I should not see "Test book name"
And I should see "Orphaned activities (section 4)" in the "li#section-4" "css_element"
And "li#section-5" "css_element" should not exist
Given I should see "29 May - 4 June" in the "li#section-5" "css_element"
When I click on "Delete week" "link" in the "li#section-5" "css_element"
Then I should see "Are you absolutely sure you want to completely delete \"29 May - 4 June\" and all the activities it contains?"
- And I press "Continue"
+ And I press "Delete"
And I should not see "29 May - 4 June"
And I navigate to "Edit settings" node in "Course administration"
And I expand all fieldsets
Scenario: Deleting the middle section in weeks format
Given I should see "29 May - 4 June" in the "li#section-5" "css_element"
When I click on "Delete week" "link" in the "li#section-4" "css_element"
- And I press "Continue"
+ And I press "Delete"
Then I should not see "29 May - 4 June"
And I should not see "Test chat name"
And I should see "Test choice name" in the "li#section-4" "css_element"
When I follow "Reduce the number of sections"
Then I should see "Orphaned activities (section 5)" in the "li#section-5" "css_element"
And I click on "Delete week" "link" in the "li#section-5" "css_element"
- And I press "Continue"
+ And I press "Delete"
And I should not see "29 May - 4 June"
And I should not see "Orphaned activities"
And "li#section-5" "css_element" should not exist
And "li#section-5.orphaned" "css_element" should exist
And "li#section-4.orphaned" "css_element" should not exist
And I click on "Delete week" "link" in the "li#section-1" "css_element"
- And I press "Continue"
+ And I press "Delete"
And I should not see "Test book name"
And I should see "Orphaned activities (section 4)" in the "li#section-4" "css_element"
And "li#section-5" "css_element" should not exist
* @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,
echo $OUTPUT->spacer(array('height'=>30, 'br'=>true)); // should be done with CSS instead
}
echo $OUTPUT->box_start();
- if (!empty($activity->name)) {
+ if (strval($activity->name) !== '') {
echo html_writer::tag('h2', $activity->name);
}
$inbox = true;
$module->name = clean_param($title, PARAM_CLEANHTML);
}
- if (!empty($module->name)) {
+ if (strval($module->name) !== '') {
$DB->update_record($cm->modname, $module);
$cm->name = $module->name;
\core\event\course_module_updated::create_from_cm($cm, $modcontext)->trigger();
--- /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
# Redirect
And I should see "Delete TCCAC"
And I should see "Test course: create a course (TCCAC)"
- And I press "Continue"
+ And I press "Delete"
# Redirect
And I should see "Deleting TCCAC"
And I should see "TCCAC has been completely deleted"
# Redirect
And I should see "Delete TCCAC"
And I should see "Test course: create a course (TCCAC)"
- And I press "Continue"
+ And I press "Delete"
# Redirect
And I should see "Deleting TCCAC"
And I should see "TCCAC has been completely deleted"
$this->assertEquals($DB->count_records('course'), count($courses));
}
+ /**
+ * Test search_courses
+ */
+ public function test_search_courses () {
+
+ global $DB, $CFG;
+ require_once($CFG->dirroot . '/tag/lib.php');
+
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+ $generatedcourses = array();
+ $coursedata1['fullname'] = 'FIRST COURSE';
+ $course1 = self::getDataGenerator()->create_course($coursedata1);
+ $coursedata2['fullname'] = 'SECOND COURSE';
+ $course2 = self::getDataGenerator()->create_course($coursedata2);
+ // Search by name.
+ $results = core_course_external::search_courses('search', 'FIRST');
+ $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
+ $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']);
+ $this->assertCount(1, $results['courses']);
+
+ // Create the forum.
+ $record = new stdClass();
+ $record->introformat = FORMAT_HTML;
+ $record->course = $course2->id;
+ // Set Aggregate type = Average of ratings.
+ $forum = self::getDataGenerator()->create_module('forum', $record);
+
+ // Search by module.
+ $results = core_course_external::search_courses('modulelist', 'forum');
+ $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
+ $this->assertEquals(1, $results['total']);
+
+ // Enable coursetag option.
+ set_config('block_tags_showcoursetags', true);
+ // Add tag 'TAG-LABEL ON SECOND COURSE' to Course2.
+ tag_set('course', $course2->id, array('TAG-LABEL ON SECOND COURSE'), 'core', context_course::instance($course2->id)->id);
+ $taginstance = $DB->get_record('tag_instance', array('itemtype' => 'course', 'itemid' => $course2->id), '*', MUST_EXIST);
+ // Search by tagid.
+ $results = core_course_external::search_courses('tagid', $taginstance->tagid);
+ $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
+ $this->assertEquals($coursedata2['fullname'], $results['courses'][0]['fullname']);
+
+ // Search by block (use news_items default block).
+ $blockid = $DB->get_field('block', 'id', array('name' => 'news_items'));
+ $results = core_course_external::search_courses('blocklist', $blockid);
+ $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
+ $this->assertEquals(2, $results['total']);
+
+ // Now as a normal user.
+ $user = self::getDataGenerator()->create_user();
+ $this->setUser($user);
+
+ $results = core_course_external::search_courses('search', 'FIRST');
+ $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
+ $this->assertCount(1, $results['courses']);
+ $this->assertEquals(1, $results['total']);
+ $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']);
+
+ // Search by block (use news_items default block). Should fail (only admins allowed).
+ $this->setExpectedException('required_capability_exception');
+ $results = core_course_external::search_courses('blocklist', $blockid);
+
+ }
+
/**
* Create a course with contents
* @return array A list with the course object and course modules objects
$this->assertEmpty($event->other);
}
+
+ /**
+ * Test get_course_module
+ */
+ public function test_get_course_module() {
+ global $DB;
+
+ $this->resetAfterTest(true);
+
+ $this->setAdminUser();
+ $course = self::getDataGenerator()->create_course();
+ $record = array(
+ 'course' => $course->id,
+ 'name' => 'First Chat'
+ );
+ $options = array(
+ 'idnumber' => 'ABC',
+ 'visible' => 0
+ );
+ // Hidden activity.
+ $chat = self::getDataGenerator()->create_module('chat', $record, $options);
+
+ // Test admin user can see the complete hidden activity.
+ $result = core_course_external::get_course_module($chat->cmid);
+ $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result);
+
+ $this->assertCount(0, $result['warnings']);
+ // Test we retrieve all the fields.
+ $this->assertCount(22, $result['cm']);
+ $this->assertEquals($record['name'], $result['cm']['name']);
+ $this->assertEquals($options['idnumber'], $result['cm']['idnumber']);
+
+ $student = $this->getDataGenerator()->create_user();
+ $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+
+ self::getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
+ $this->setUser($student);
+
+ // The user shouldn't be able to see the activity.
+ try {
+ core_course_external::get_course_module($chat->cmid);
+ $this->fail('Exception expected due to invalid permissions.');
+ } catch (moodle_exception $e) {
+ $this->assertEquals('requireloginerror', $e->errorcode);
+ }
+
+ // Make module visible.
+ set_coursemodule_visible($chat->cmid, 1);
+
+ // Test student user.
+ $result = core_course_external::get_course_module($chat->cmid);
+ $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result);
+
+ $this->assertCount(0, $result['warnings']);
+ // Test we retrieve only the few files we can see.
+ $this->assertCount(11, $result['cm']);
+ $this->assertEquals($chat->cmid, $result['cm']['id']);
+ $this->assertEquals($course->id, $result['cm']['course']);
+ $this->assertEquals('chat', $result['cm']['modname']);
+ $this->assertEquals($chat->id, $result['cm']['instance']);
+
+ }
}
--- /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/>.
+
+/**
+ * Scheduled task for processing flatfile enrolments.
+ *
+ * @package enrol_flatfile
+ * @copyright 2014 Troy Williams
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace enrol_flatfile\task;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Simple task to run sync enrolments.
+ *
+ * @copyright 2014 Troy Williams
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class flatfile_sync_task extends \core\task\scheduled_task {
+
+ /**
+ * Get a descriptive name for this task (shown to admins).
+ *
+ * @return string
+ */
+ public function get_name() {
+ return get_string('flatfilesync', 'enrol_flatfile');
+ }
+
+ /**
+ * Do the job.
+ * Throw exceptions on errors (the job will be retried).
+ */
+ public function execute() {
+ global $CFG;
+
+ require_once($CFG->dirroot . '/enrol/flatfile/lib.php');
+
+ if (!enrol_is_enabled('flatfile')) {
+ return;
+ }
+
+ // Instance of enrol_flatfile_plugin.
+ $plugin = enrol_get_plugin('flatfile');
+ $result = $plugin->sync(new \null_progress_trace());
+ return $result;
+
+ }
+
+}
* - you need to change the "www-data" to match the apache user account
* - use "su" if "sudo" not available
*
+ * Update
+ *
+ * This plugin now has a enrolment sync scheduled task. Scheduled tasks were
+ * introduced in Moodle 2.7. It is possible to override the scheduled tasks
+ * configuration and run a single scheduled task immediately using the
+ * admin/tool/task/cli/schedule_task.php script. This is the recommended
+ * method to use for immediate enrollment synchronisation.
+ *
+ * Usage help:
+ * $ php admin/tool/task/cli/schedule_task.php -h
+ *
+ * Execute task:
+ * $ sudo -u www-data /usr/bin/php admin/tool/task/cli/schedule_task.php /
+ * --execute=\\enrol_flatfile\\task\\flatfile_sync_task
+ *
* @package enrol_flatfile
* @copyright 2012 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
--- /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/>.
+
+/**
+ * Definition of flatfile enrolment scheduled tasks.
+ *
+ * @package enrol_flatfile
+ * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$tasks = array(
+ array(
+ 'classname' => '\enrol_flatfile\task\flatfile_sync_task',
+ 'blocking' => 0,
+ 'minute' => '15',
+ 'hour' => '*',
+ 'day' => '*',
+ 'dayofweek' => '*',
+ 'month' => '*'
+ )
+);
$string['filelockedmailsubject'] = 'Important error: Enrolment file';
$string['flatfile:manage'] = 'Manage user enrolments manually';
$string['flatfile:unenrol'] = 'Unenrol users from the course manually';
+$string['flatfilesync'] = 'Flat file enrolment sync';
$string['location'] = 'File location';
$string['location_desc'] = 'Specify full path to the enrolment file. The file is automatically deleted after processing.';
$string['notifyadmin'] = 'Notify administrator';
}
}
- public function cron() {
- $trace = new text_progress_trace();
- $this->sync($trace);
- }
-
/**
* Execute synchronisation.
* @param progress_trace
$notify = false;
if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$user->id))) {
// Update only.
- $this->update_user_enrol($instance, $user->id, ENROL_USER_ACTIVE, $roleid, $timestart, $timeend);
+ $this->update_user_enrol($instance, $user->id, ENROL_USER_ACTIVE, $timestart, $timeend);
if (!$DB->record_exists('role_assignments', array('contextid'=>$context->id, 'roleid'=>$roleid, 'userid'=>$user->id, 'component'=>'enrol_flatfile', 'itemid'=>$instance->id))) {
role_assign($roleid, $user->id, $context->id, 'enrol_flatfile', $instance->id);
}
$this->assertEquals(1, $DB->count_records('role_assignments', array('roleid'=>$teacherrole->id)));
$this->assertEquals(0, $DB->count_records('role_assignments', array('roleid'=>$managerrole->id)));
}
+
+ /**
+ * Flatfile enrolment sync task test.
+ */
+ public function test_flatfile_sync_task() {
+ global $CFG, $DB;
+ $this->resetAfterTest();
+
+ $flatfileplugin = enrol_get_plugin('flatfile');
+
+ $trace = new null_progress_trace();
+ $this->enable_plugin();
+ $file = "$CFG->dataroot/enrol.txt";
+ $flatfileplugin->set_config('location', $file);
+
+ $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+ $this->assertNotEmpty($studentrole);
+
+ $user1 = $this->getDataGenerator()->create_user(array('idnumber' => 'u1'));
+ $course1 = $this->getDataGenerator()->create_course(array('idnumber' => 'c1'));
+ $context1 = context_course::instance($course1->id);
+
+ $data =
+ "add,student,u1,c1";
+ file_put_contents($file, $data);
+
+ $task = new enrol_flatfile\task\flatfile_sync_task;
+ $task->execute();
+
+ $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid' => $studentrole->id)));
+ }
}
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2015051100; // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version = 2015090700; // The current plugin version (Date: YYYYMMDDRR)
$plugin->requires = 2015050500; // Requires this Moodle version
$plugin->component = 'enrol_flatfile'; // Full name of the plugin (used for diagnostics)
-$plugin->cron = 60;
$roleid = null;
}
+ if (empty($startdate)) {
+ if (!$startdate = get_config('enrol_manual', 'enrolstart')) {
+ // Default to now if there is no system setting.
+ $startdate = 4;
+ }
+ }
+
switch($startdate) {
case 2:
$timestart = $course->startdate;
break;
+ case 4:
+ // We mimic get_enrolled_sql round(time(), -2) but always floor as we want users to always access their
+ // courses once they are enrolled.
+ $timestart = intval(substr(time(), 0, 8) . '00') - 1;
+ break;
case 3:
default:
$today = time();
// Moodle v2.9.0 release upgrade line.
// Put any upgrade step following this.
+ if ($oldversion < 2015091500) {
+ // We keep today as default enrolment start time on upgrades.
+ set_config('enrolstart', 3, 'enrol_manual');
+ upgrade_plugin_savepoint(true, 2015091500, 'enrol', 'manual');
+ }
+
return true;
}
$string['browseusers'] = 'Browse users';
$string['browsecohorts'] = 'Browse cohorts';
$string['confirmbulkdeleteenrolment'] = 'Are you sure you want to delete these users enrolments?';
+$string['defaultstart'] = 'Default enrolment start';
$string['defaultperiod'] = 'Default enrolment duration';
$string['defaultperiod_desc'] = 'Default length of time that the enrolment is valid. If set to zero, the enrolment duration will be unlimited by default.';
$string['defaultperiod_help'] = 'Default length of time that the enrolment is valid, starting with the moment the user is enrolled. If disabled, the enrolment duration will be unlimited by default.';
$string['manual:unenrol'] = 'Unenrol users from the course';
$string['manual:unenrolself'] = 'Unenrol self from the course';
$string['messageprovider:expiry_notification'] = 'Manual enrolment expiry notifications';
+$string['now'] = 'Now';
$string['pluginname'] = 'Manual enrolments';
$string['pluginname_desc'] = 'The manual enrolments plugin allows users to be enrolled manually via a link in the course administration settings, by a user with appropriate permissions such as a teacher. The plugin should normally be enabled, since certain other enrolment plugins, such as self enrolment, require it.';
$string['status'] = 'Enable manual enrolments';
$string['wscannotenrol'] = 'Plugin instance cannot manually enrol a user in the course id = {$a->courseid}';
$string['wsnoinstance'] = 'Manual enrolment plugin instance doesn\'t exist or is disabled for the course (id = {$a->courseid})';
$string['wsusercannotassign'] = 'You don\'t have the permission to assign this role ({$a->roleid}) to this user ({$a->userid}) in this course({$a->courseid}).';
-$string['manualpluginnotinstalled'] = 'The "Manual" plugin has not yet been installed';
\ No newline at end of file
+$string['manualpluginnotinstalled'] = 'The "Manual" plugin has not yet been installed';
$button->class .= ' enrol_manual_plugin';
$startdate = $manager->get_course()->startdate;
+ if (!$defaultstart = get_config('enrol_manual', 'enrolstart')) {
+ // Default to now if there is no system setting.
+ $defaultstart = 4;
+ }
$startdateoptions = array();
- $timeformat = get_string('strftimedatefullshort');
+ $dateformat = get_string('strftimedatefullshort');
if ($startdate > 0) {
- $startdateoptions[2] = get_string('coursestart') . ' (' . userdate($startdate, $timeformat) . ')';
+ $startdateoptions[2] = get_string('coursestart') . ' (' . userdate($startdate, $dateformat) . ')';
}
- $today = time();
- $today = make_timestamp(date('Y', $today), date('m', $today), date('d', $today), 0, 0, 0);
- $startdateoptions[3] = get_string('today') . ' (' . userdate($today, $timeformat) . ')' ;
+ $now = time();
+ $today = make_timestamp(date('Y', $now), date('m', $now), date('d', $now), 0, 0, 0);
+ $startdateoptions[3] = get_string('today') . ' (' . userdate($today, $dateformat) . ')';
+ $startdateoptions[4] = get_string('now', 'enrol_manual') . ' (' . userdate($now, get_string('strftimedatetimeshort')) . ')';
$defaultduration = $instance->enrolperiod > 0 ? $instance->enrolperiod / 86400 : '';
$modules = array('moodle-enrol_manual-quickenrolment', 'moodle-enrol_manual-quickenrolment-skin');
'optionsStartDate' => $startdateoptions,
'defaultRole' => $instance->roleid,
'defaultDuration' => $defaultduration,
+ 'defaultStartDate' => (int)$defaultstart,
'disableGradeHistory' => $CFG->disablegradehistory,
'recoverGradesDefault'=> '',
'cohortsAvailable' => cohort_get_available_cohorts($manager->get_context(), COHORT_WITH_NOTENROLLED_MEMBERS_ONLY, 0, 1) ? true : false
$enrolid = required_param('enrolid', PARAM_INT);
$roleid = optional_param('roleid', -1, PARAM_INT);
$extendperiod = optional_param('extendperiod', 0, PARAM_INT);
-$extendbase = optional_param('extendbase', 3, PARAM_INT);
+$extendbase = optional_param('extendbase', 0, PARAM_INT);
$instance = $DB->get_record('enrol', array('id'=>$enrolid, 'enrol'=>'manual'), '*', MUST_EXIST);
$course = $DB->get_record('course', array('id'=>$instance->courseid), '*', MUST_EXIST);
$seconds = $i * 86400;
$periodmenu[$seconds] = get_string('numdays', '', $i);
}
-// Work out the apropriate default setting.
+// Work out the apropriate default settings.
if ($extendperiod) {
$defaultperiod = $extendperiod;
} else {
$defaultperiod = $instance->enrolperiod;
}
+if (empty($extendbase)) {
+ if (!$extendbase = get_config('enrol_manual', 'enrolstart')) {
+ // Default to now if there is no system setting.
+ $extendbase = 4;
+ }
+}
// Build the list of options for the starting from dropdown.
-$timeformat = get_string('strftimedatefullshort');
-$today = time();
-$today = make_timestamp(date('Y', $today), date('m', $today), date('d', $today), 0, 0, 0);
+$now = time();
+$today = make_timestamp(date('Y', $now), date('m', $now), date('d', $now), 0, 0, 0);
+$dateformat = get_string('strftimedatefullshort');
// Enrolment start.
$basemenu = array();
if ($course->startdate > 0) {
- $basemenu[2] = get_string('coursestart') . ' (' . userdate($course->startdate, $timeformat) . ')';
+ $basemenu[2] = get_string('coursestart') . ' (' . userdate($course->startdate, $dateformat) . ')';
}
-$basemenu[3] = get_string('today') . ' (' . userdate($today, $timeformat) . ')' ;
+$basemenu[3] = get_string('today') . ' (' . userdate($today, $dateformat) . ')';
+$basemenu[4] = get_string('now', 'enrol_manual') . ' (' . userdate($now, get_string('strftimedatetimeshort')) . ')';
// Process add and removes.
if ($canenrol && optional_param('add', false, PARAM_BOOL) && confirm_sesskey()) {
case 2:
$timestart = $course->startdate;
break;
+ case 4:
+ // We mimic get_enrolled_sql round(time(), -2) but always floor as we want users to always access their
+ // courses once they are enrolled.
+ $timestart = intval(substr($now, 0, 8) . '00') - 1;
+ break;
case 3:
default:
$timestart = $today;
get_string('defaultrole', 'role'), '', $student->id, $options));
}
+ $options = array(2 => get_string('coursestart'), 3 => get_string('today'), 4 => get_string('now', 'enrol_manual'));
+ $settings->add(
+ new admin_setting_configselect('enrol_manual/enrolstart', get_string('defaultstart', 'enrol_manual'), '', 4, $options)
+ );
+
$settings->add(new admin_setting_configduration('enrol_manual/enrolperiod',
get_string('defaultperiod', 'enrol_manual'), get_string('defaultperiod_desc', 'enrol_manual'), 0));
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2015051100; // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version = 2015091500; // The current plugin version (Date: YYYYMMDDXX)
$plugin->requires = 2015050500; // Requires this Moodle version
$plugin->component = 'enrol_manual'; // Full name of the plugin (used for diagnostics)
$plugin->cron = 600;
var options = this.get(UEP.OPTIONSTARTDATE);
var index = 0, count = 0;
for (var i in options) {
- count++;
var option = create('<option value="'+i+'">'+options[i]+'</option>');
if (i == defaultvalue) {
index = count;
}
select.append(option);
+ count++;
}
select.set('selectedIndex', index);
},
value : 0
},
defaultStartDate : {
- value : 2,
+ value : 4,
validator : Y.Lang.isNumber
},
defaultDuration : {
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);
+ }
}
--- /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/>.
+
+/**
+ * Empty enrol_self form.
+ *
+ * Useful to mimic valid enrol instances UI when the enrolment instance is not available.
+ *
+ * @package enrol_self
+ * @copyright 2015 David Monllaó
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+
+class enrol_self_empty_form extends moodleform {
+
+ /**
+ * Form definition.
+ * @return void
+ */
+ public function definition() {
+ $this->_form->addElement('header', 'selfheader', $this->_customdata->header);
+ $this->_form->addElement('static', 'info', '', $this->_customdata->info);
+ }
+}
$enrolstatus = $this->can_self_enrol($instance);
- // Don't show enrolment instance form, if user can't enrol using it.
if (true === $enrolstatus) {
+ // This user can self enrol using this instance.
$form = new enrol_self_enrol_form(NULL, $instance);
$instanceid = optional_param('instance', 0, PARAM_INT);
if ($instance->id == $instanceid) {
$this->enrol_self($instance, $data);
}
}
-
- ob_start();
- $form->display();
- $output = ob_get_clean();
- return $OUTPUT->box($output);
} else {
- return $OUTPUT->box($enrolstatus);
- }
+ // This user can not self enrol using this instance. Using an empty form to keep
+ // the UI consistent with other enrolment plugins that returns a form.
+ $data = new stdClass();
+ $data->header = $this->get_instance_name($instance);
+ $data->info = $enrolstatus;
+
+ // The can_self_enrol call returns a button to the login page if the user is a
+ // guest, setting the login url to the form if that is the case.
+ $url = isguestuser() ? get_login_url() : null;
+ $form = new enrol_self_empty_form($url, $data);
+ }
+
+ ob_start();
+ $form->display();
+ $output = ob_get_clean();
+ return $OUTPUT->box($output);
}
/**
alertpanels: {},
initializer : function() {
var self = this;
- Y.delegate('click', function(e){
- e.preventDefault();
+ require(['core/event'], function(event) {
+ Y.delegate('click', function(e){
+ e.preventDefault();
- //display a progress indicator
- var title = '',
- content = Y.Node.create('<div id="glossaryfilteroverlayprogress">' +
- '<img src="' + M.cfg.loadingicon + '" class="spinner" />' +
- '</div>'),
- o = new Y.Overlay({
- headerContent : title,
- bodyContent : content
- }),
- fullurl,
- cfg;
- self.overlay = o;
- o.render(Y.one(document.body));
+ //display a progress indicator
+ var title = '',
+ content = Y.Node.create('<div id="glossaryfilteroverlayprogress">' +
+ '<img src="' + M.cfg.loadingicon + '" class="spinner" />' +
+ '</div>'),
+ o = new Y.Overlay({
+ headerContent : title,
+ bodyContent : content
+ }),
+ fullurl,
+ cfg;
+ self.overlay = o;
+ o.render(Y.one(document.body));
- //Switch over to the ajax url and fetch the glossary item
- fullurl = this.getAttribute('href').replace('showentry.php','showentry_ajax.php');
- cfg = {
- method: 'get',
- context : self,
- on: {
- success: function(id, o) {
- this.display_callback(o.responseText);
- },
- failure: function(id, o) {
- var debuginfo = o.statusText;
- if (M.cfg.developerdebug) {
- o.statusText += ' (' + fullurl + ')';
+ //Switch over to the ajax url and fetch the glossary item
+ fullurl = this.getAttribute('href').replace('showentry.php','showentry_ajax.php');
+ cfg = {
+ method: 'get',
+ context : self,
+ on: {
+ success: function(id, o) {
+ this.display_callback(o.responseText, event);
+ },
+ failure: function(id, o) {
+ var debuginfo = o.statusText;
+ if (M.cfg.developerdebug) {
+ o.statusText += ' (' + fullurl + ')';
+ }
+ new M.core.exception({ message: debuginfo });
}
- this.display_callback('bodyContent',debuginfo);
}
- }
- };
- Y.io(fullurl, cfg);
+ };
+ Y.io(fullurl, cfg);
- }, Y.one(document.body), 'a.glossary.autolink.concept');
+ }, Y.one(document.body), 'a.glossary.autolink.concept');
+ });
},
- display_callback : function(content) {
+ /**
+ * @method display_callback
+ * @param {String} content - Content to display
+ * @param {Object} event The amd event module used to fire events for jquery and yui.
+ */
+ display_callback : function(content, event) {
var data,
key,
alertpanel,
definition = data.entries[key].definition + data.entries[key].attachments;
alertpanel = new M.core.alert({title:data.entries[key].concept, draggable: true,
message:definition, modal:false, yesLabel: M.util.get_string('ok', 'moodle')});
- Y.fire(M.core.event.FILTER_CONTENT_UPDATED, {nodes: (new Y.NodeList(alertpanel.get('boundingBox')))});
+ // Notify the filters about the modified nodes.
+ event.notifyFilterContentUpdated(alertpanel.get('boundingBox').getDOMNode());
Y.Node.one('#id_yuialertconfirm-' + alertpanel.get('COUNT')).focus();
// Register alertpanel for stacking.
// 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
foreach ($userdata->grades as $itemid => $grade) {
$grade_item = $this->grade_items[$itemid];
$grade->grade_item =& $grade_item;
- $gradestr = $this->format_grade($grade, $this->displaytype); // no formating for now
// MDL-11669, skip exported grades or bad grades (if setting says so)
if ($export_tracking) {
fwrite($handle, "\t\t<assignment>{$grade_item->idnumber}</assignment>\n");
// this column should be customizable to use either student id, idnumber, uesrname or email.
fwrite($handle, "\t\t<student>{$user->idnumber}</student>\n");
- fwrite($handle, "\t\t<score>$gradestr</score>\n");
+ // Format and display the grade in the selected display type (real, letter, percentage).
+ if (is_array($this->displaytype)) {
+ // Grades display type came from the return of export_bulk_export_data() on grade publishing.
+ foreach ($this->displaytype as $gradedisplayconst) {
+ $gradestr = $this->format_grade($grade, $gradedisplayconst);
+ fwrite($handle, "\t\t<score>$gradestr</score>\n");
+ }
+ } else {
+ // Grade display type submitted directly from the grade export form.
+ $gradestr = $this->format_grade($grade, $this->displaytype);
+ fwrite($handle, "\t\t<score>$gradestr</score>\n");
+ }
+
if ($this->export_feedback) {
$feedbackstr = $this->format_feedback($userdata->feedbacks[$itemid]);
fwrite($handle, "\t\t<feedback>$feedbackstr</feedback>\n");
} else if ($this->get_context()->contextlevel >= CONTEXT_COURSE) {
list($context, $course, $cm) = get_context_info_array($this->get_context()->id);
- if (!empty($cm->name)) {
+ if (strval($cm->name) !== '') {
$title = $cm->name;
} else {
debugging('Gradable areas are currently supported at the course module level only', DEBUG_DEVELOPER);
$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'));
GRADE_REPORT_SHOW_TOTAL_IF_CONTAINS_HIDDEN => get_string('hidetotalshowexhiddenitems', 'grades'),
GRADE_REPORT_SHOW_REAL_TOTAL_IF_CONTAINS_HIDDEN => get_string('hidetotalshowinchiddenitems', 'grades') );
- if (empty($CFG->grade_report_overview_showtotalsifcontainhidden)) {
+ if (!array_key_exists($CFG->grade_report_overview_showtotalsifcontainhidden, $options)) {
$options[-1] = get_string('defaultprev', 'grades', $options[0]);
} else {
- $options[-1] = get_string('defaultprev', 'grades', $options[1]);
+ $options[-1] = get_string('defaultprev', 'grades', $options[$CFG->grade_report_overview_showtotalsifcontainhidden]);
}
$mform->addElement('select', 'report_overview_showtotalsifcontainhidden', get_string('hidetotalifhiddenitems', 'grades'), $options);
}
/**
- * 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 % |