**/yui/build/** -diff
**/amd/build/** -diff
+lib/dml/oci_native_moodle_package.sql text eol=lf
theme/bootstrapbase/style/editor.css -diff
theme/bootstrapbase/style/moodle.css -diff
debugging("The time splitting method '{$modeldata->timesplitting}' should include a '{$identifier}_help'
string to describe its purpose.", DEBUG_DEVELOPER);
}
+ } else {
+ $helpicon = new \help_icon('timesplittingnotdefined', 'tool_analytics');
+ $modeldata->timesplittinghelp = $helpicon->export_for_template($output);
}
// Has this model generated predictions?.
}
// Enable / disable.
- if ($model->is_enabled()) {
- $action = 'disable';
- $text = get_string('disable');
- $icontype = 't/block';
- } else {
- $action = 'enable';
- $text = get_string('enable');
- $icontype = 'i/checked';
+ if ($model->is_enabled() || !empty($modeldata->timesplitting)) {
+ // If there is no timesplitting method set, the model can not be enabled.
+ if ($model->is_enabled()) {
+ $action = 'disable';
+ $text = get_string('disable');
+ $icontype = 't/block';
+ } else {
+ $action = 'enable';
+ $text = get_string('enable');
+ $icontype = 'i/checked';
+ }
+ $urlparams['action'] = $action;
+ $url = new \moodle_url('model.php', $urlparams);
+ $icon = new \action_menu_link_secondary($url, new \pix_icon($icontype, $text), $text);
+ $actionsmenu->add($icon);
}
- $urlparams['action'] = $action;
- $url = new \moodle_url('model.php', $urlparams);
- $icon = new \action_menu_link_secondary($url, new \pix_icon($icontype, $text), $text);
- $actionsmenu->add($icon);
// Export training data.
if (!$model->is_static() && $model->is_trained()) {
$string['samestartdate'] = 'Current start date is good';
$string['sameenddate'] = 'Current end date is good';
$string['target'] = 'Target';
+$string['timesplittingnotdefined'] = 'Time splitting is not defined.';
+$string['timesplittingnotdefined_help'] = 'You need to select a time-splitting method before enabling the model.';
$string['trainandpredictmodel'] = 'Training model and calculating predictions';
$string['trainingprocessfinished'] = 'Training process finished';
$string['trainingresults'] = 'Training results';
{{/timesplitting}}
{{^timesplitting}}
{{#str}}notdefined, tool_analytics{{/str}}
+ {{#timesplittinghelp}}
+ {{>core/help_icon}}
+ {{/timesplittinghelp}}
{{/timesplitting}}
</td>
<td>
// Show process name in first row.
foreach ($processes as $name => $process) {
// If we don't have enough space to show full run name then show runX.
- if ($lengthofprocessline < strlen($name + 2)) {
+ if ($lengthofprocessline < strlen($name) + 2) {
$name = substr($name, -5);
}
// One extra padding as we are adding | separator for rest of the data.
"Test image from another site should be replaced" => [
"content" => '<img src="' . $this->getExternalTestFileUrl('/test.jpg', false) . '">',
"outputregex" => '/UPDATE/',
- "expectedcontent" => '<img src="' . $this->getExternalTestFileUrl('/test.jpg', true) . '">',
+ "expectedcontent" => '<img src="' . $this->get_converted_http_link('/test.jpg') . '">',
],
"Test object from another site should be replaced" => [
"content" => '<object data="' . $this->getExternalTestFileUrl('/test.swf', false) . '">',
"outputregex" => '/UPDATE/',
- "expectedcontent" => '<object data="' . $this->getExternalTestFileUrl('/test.swf', true) . '">',
+ "expectedcontent" => '<object data="' . $this->get_converted_http_link('/test.swf') . '">',
],
"Test image from a site with international name should be replaced" => [
"content" => '<img src="http://中国互联网络信息中心.中国/logosy/201706/W01.png">',
"Search for params should be case insensitive" => [
"content" => '<object DATA="' . $this->getExternalTestFileUrl('/test.swf', false) . '">',
"outputregex" => '/UPDATE/',
- "expectedcontent" => '<object DATA="' . $this->getExternalTestFileUrl('/test.swf', true) . '">',
+ "expectedcontent" => '<object DATA="' . $this->get_converted_http_link('/test.swf') . '">',
],
"URL should be case insensitive" => [
"content" => '<object data="HTTP://some.site/path?query">',
"content" => '<img alt="A picture" src="' . $this->getExternalTestFileUrl('/test.png', false) .
'" width="1”><p style="font-size: \'20px\'"></p>',
"outputregex" => '/UPDATE/',
- "expectedcontent" => '<img alt="A picture" src="' . $this->getExternalTestFileUrl('/test.png', true) .
+ "expectedcontent" => '<img alt="A picture" src="' . $this->get_converted_http_link('/test.png') .
'" width="1”><p style="font-size: \'20px\'"></p>',
],
"Broken URL should not be changed" => [
$this->getExternalTestFileUrl('/test.jpg', false) . '"></a>',
"outputregex" => '/UPDATE/',
"expectedcontent" => '<a href="' . $this->getExternalTestFileUrl('/test.png', false) . '"><img src="' .
- $this->getExternalTestFileUrl('/test.jpg', true) . '"></a>',
+ $this->get_converted_http_link('/test.jpg') . '"></a>',
],
];
}
+ /**
+ * Convert the HTTP external test file URL to use HTTPS.
+ *
+ * Note: We *must not* use getExternalTestFileUrl with the True option
+ * here, becase it is reasonable to have only one of these set due to
+ * issues with SSL certificates.
+ *
+ * @param string $path Path to be rewritten
+ * @return string
+ */
+ protected function get_converted_http_link($path) {
+ return preg_replace('/^http:/', 'https:', $this->getExternalTestFileUrl($path, false));
+ }
+
/**
* Test upgrade_http_links
* @param string $content Example content that we'll attempt to replace.
// Get the http url, since the default test wwwroot is https.
$wwwrootdomain = 'www.example.com';
$wwwroothttp = preg_replace('/^https:/', 'http:', $CFG->wwwroot);
- $testdomain = 'download.moodle.org';
+ $testdomain = $this->get_converted_http_link('');
return [
"Test image from an available site so shouldn't be reported" => [
"content" => '<img src="' . $this->getExternalTestFileUrl('/test.jpg', false) . '">',
<li>
<a href="#">{{#str}}edit{{/str}}</a><b class="caret"></b>
<ul class="dropdown dropdown-menu">
+ {{#canmanage}}
<li class="dropdown-item">
<a href="#" data-action="edit">
{{#pix}}t/edit{{/pix}} {{#str}}edit{{/str}}
{{#pix}}t/down{{/pix}} {{#str}}movedown{{/str}}
</a>
</li>
+ {{/canmanage}}
<li class="dropdown-item">
<a href="#" data-action="linkedcourses">
{{#pix}}t/viewdetails{{/pix}} {{#str}}linkedcourses, tool_lp{{/str}}
</a>
</li>
+ {{#canmanage}}
<li class="dropdown-item">
<a href="#" data-action="relatedcompetencies">
{{#pix}}t/add{{/pix}} {{#str}}addcrossreferencedcompetency, tool_lp{{/str}}
{{#pix}}t/edit{{/pix}} {{#str}}competencyrule, tool_lp{{/str}}
</a>
</li>
+ {{/canmanage}}
</ul>
</li>
</ul>
<p data-region="competencyinfo">
{{#str}}nocompetencyselected, tool_lp{{/str}}
</p>
- {{#canmanage}}
<div data-region="competencyactions">
+ {{#canmanage}}
<button class="btn btn-secondary" data-action="add">{{#pix}}t/add{{/pix}} <span data-region="term"></span></button>
+ {{/canmanage}}
</div>
- {{/canmanage}}
</div>
</div>
</div>
if (!$this->is_static()) {
$this->model->trained = 0;
}
+ } else if (empty($this->model->timesplitting)) {
+ // A valid timesplitting method needs to be supplied before a model can be enabled.
+ throw new \moodle_exception('invalidtimesplitting', 'analytics', '', $this->model->id);
+
}
// Purge pages with insights as this may change things.
$this->model->mark_as_trained();
$this->assertEquals($originaluniqueid, $this->model->get_unique_id());
- $this->model->enable();
+ $this->model->enable('\core\analytics\time_splitting\deciles');
$this->assertEquals($originaluniqueid, $this->model->get_unique_id());
// Wait 1 sec so the timestamp changes.
entry in its database. This module can read user attributes from LDAP and prefill
wanted fields in Moodle. For following logins only the username and
password are checked.';
-$string['auth_ldap_expiration_desc'] = 'Select No to disable expired password checking or LDAP to read passwordexpiration time directly from LDAP';
+$string['auth_ldap_expiration_desc'] = 'Select \'{$a->no}\' to disable expired password checking or \'{$a->ldapserver}\' to read the password expiration time directly from the LDAP server';
$string['auth_ldap_expiration_key'] = 'Expiration';
$string['auth_ldap_expiration_warning_desc'] = 'Number of days before password expiration warning is issued.';
$string['auth_ldap_expiration_warning_key'] = 'Expiration warning';
new lang_string('auth_ldap_passwdexpire_settings', 'auth_ldap'), ''));
// Password Expiration.
+
+ // Create the description lang_string object.
+ $strno = get_string('no');
+ $strldapserver = get_string('pluginname', 'auth_ldap');
+ $langobject = new stdClass();
+ $langobject->no = $strno;
+ $langobject->ldapserver = $strldapserver;
+ $description = new lang_string('auth_ldap_expiration_desc', 'auth_ldap', $langobject);
+
+ // Now create the options.
$expiration = array();
- $expiration['0'] = 'no';
- $expiration['1'] = 'LDAP';
+ $expiration['0'] = $strno;
+ $expiration['1'] = $strldapserver;
+
+ // Add the setting.
$settings->add(new admin_setting_configselect('auth_ldap/expiration',
new lang_string('auth_ldap_expiration_key', 'auth_ldap'),
- new lang_string('auth_ldap_expiration_desc', 'auth_ldap'), 0 , $expiration));
+ $description, 0 , $expiration));
// Password Expiration warning.
$settings->add(new admin_setting_configtext('auth_ldap/expiration_warning',
// with info so that the IDP can maintain mnetservice_enrol_enrolments
$mnetrequest->add_param($remoteuser->username);
$fields = 'id, category, sortorder, fullname, shortname, idnumber, summary, startdate, visible';
- $courses = enrol_get_users_courses($localuser->id, false, $fields, 'visible DESC,sortorder ASC');
+ $courses = enrol_get_users_courses($localuser->id, false, $fields);
if (is_array($courses) && !empty($courses)) {
// Second request to do the JOINs that we'd have done
// inside enrol_get_users_courses() if we had been allowed
$link = 'http://invalidurldetected/';
}
} else {
- $link = $rawlink;
+ $link = htmlspecialchars(trim($rawlink), ENT_COMPAT, 'UTF-8', false);
}
}
}
if (empty($CFG->disablemycourses) and isloggedin() and !isguestuser() and
!(has_capability('moodle/course:update', context_system::instance()) and $adminseesall)) { // Just print My Courses
- // As this is producing navigation sort order should default to $CFG->navsortmycoursessort instead
- // of using the default.
- if (!empty($CFG->navsortmycoursessort)) {
- $sortorder = 'visible DESC, ' . $CFG->navsortmycoursessort . ' ASC';
- } else {
- $sortorder = 'visible DESC, sortorder ASC';
- }
- if ($courses = enrol_get_my_courses(NULL, $sortorder)) {
+ if ($courses = enrol_get_my_courses()) {
foreach ($courses as $course) {
$coursecontext = context_course::instance($course->id);
$linkcss = $course->visible ? "" : " class=\"dimmed\" ";
public function export_for_template(renderer_base $output) {
global $USER;
- $courses = enrol_get_my_courses('*', 'fullname ASC');
+ $courses = enrol_get_my_courses('*');
$coursesprogress = [];
foreach ($courses as $course) {
$params['aftereventid'] = null;
}
- $courses = enrol_get_my_courses('*', 'visible DESC,sortorder ASC', 0, [$courseid]);
+ $courses = enrol_get_my_courses('*', null, 0, [$courseid]);
$courses = array_values($courses);
if (empty($courses)) {
}
$renderer = $PAGE->get_renderer('core_calendar');
- $courses = enrol_get_my_courses('*', 'visible DESC,sortorder ASC', 0, $params['courseids']);
+ $courses = enrol_get_my_courses('*', null, 0, $params['courseids']);
$courses = array_values($courses);
if (empty($courses)) {
}
$this->properties = $data;
-
- if (empty($data->context)) {
- $this->properties->context = $this->calculate_context();
- }
}
/**
return $context;
}
+ /**
+ * Returns the context for this event. The context is calculated
+ * the first time is is requested and then stored in a member
+ * variable to be returned each subsequent time.
+ *
+ * This is a magical getter function that will be called when
+ * ever the context property is accessed, e.g. $event->context.
+ *
+ * @return context
+ */
+ protected function get_context() {
+ if (!isset($this->properties->context)) {
+ $this->properties->context = $this->calculate_context();
+ }
+
+ return $this->properties->context;
+ }
+
/**
* Returns an array of editoroptions for this event.
*
// Check if we have already resolved the context for this event.
if ($this->editorcontext === null) {
// Switch on the event type to decide upon the appropriate context to use for this event.
- $this->editorcontext = $this->properties->context;
+ $this->editorcontext = $this->get_context();
if (!calendar_is_valid_eventtype($this->properties->eventtype)) {
return clean_text($this->properties->description, $this->properties->format);
}
// Prepare event data.
$eventargs = array(
- 'context' => $this->properties->context,
+ 'context' => $this->get_context(),
'objectid' => $this->properties->id,
'other' => array(
'repeatid' => empty($this->properties->repeatid) ? 0 : $this->properties->repeatid,
// were set when calculate_context() was called from the constructor.
if ($usingeditor) {
$this->properties->context = $this->calculate_context();
- $this->editorcontext = $this->properties->context;
+ $this->editorcontext = $this->get_context();
}
$editor = $this->properties->description;
// Log the event entry.
$eventargs['objectid'] = $this->properties->id;
- $eventargs['context'] = $this->properties->context;
+ $eventargs['context'] = $this->get_context();
$event = \core\event\calendar_event_created::create($eventargs);
$event->trigger();
// Trigger an event for the delete action.
$eventargs = array(
- 'context' => $this->properties->context,
+ 'context' => $this->get_context(),
'objectid' => $this->properties->id,
'other' => array(
'repeatid' => empty($this->properties->repeatid) ? 0 : $this->properties->repeatid,
// If the editor context hasn't already been set then set it now.
if ($this->editorcontext === null) {
- $this->editorcontext = $this->properties->context;
+ $this->editorcontext = $this->get_context();
}
// If the context has been set delete all associated files.
if ($properties->eventtype === 'site') {
// Site context.
- $this->editorcontext = $this->properties->context;
+ $this->editorcontext = $this->get_context();
} else if ($properties->eventtype === 'user') {
// User context.
- $this->editorcontext = $this->properties->context;
+ $this->editorcontext = $this->get_context();
} else if ($properties->eventtype === 'group' || $properties->eventtype === 'course') {
// First check the course is valid.
$course = $DB->get_record('course', array('id' => $properties->courseid));
print_error('invalidcourse');
}
// Course context.
- $this->editorcontext = $this->properties->context;
+ $this->editorcontext = $this->get_context();
// We have a course and are within the course context so we had
// better use the courses max bytes value.
$this->editoroptions['maxbytes'] = $course->maxbytes;
// First check the course is valid.
\coursecat::get($properties->categoryid, MUST_EXIST, true);
// Course context.
- $this->editorcontext = $this->properties->context;
+ $this->editorcontext = $this->get_context();
// We have a course and are within the course context so we had
// better use the courses max bytes value.
$this->editoroptions['maxbytes'] = $course->maxbytes;
// Prepare event data.
$eventargs = array(
- 'context' => $this->properties->context,
+ 'context' => $this->get_context(),
'objectid' => $this->properties->id,
'other' => array(
'repeatid' => empty($this->properties->repeatid) ? 0 : $this->properties->repeatid,
if ($this->editorcontext === null) {
// Switch on the event type to decide upon the appropriate context to use for this event.
- $this->editorcontext = $this->properties->context;
+ $this->editorcontext = $this->get_context();
if (!calendar_is_valid_eventtype($this->properties->eventtype)) {
// We don't have a context here, do a normal format_text.
'default' => '',
'null' => NULL_ALLOWED
),
+ 'description' => array(
+ 'type' => PARAM_TEXT,
+ 'default' => '',
+ 'null' => NULL_ALLOWED
+ ),
+ 'descriptionformat' => array(
+ 'type' => PARAM_INT,
+ 'default' => FORMAT_HTML,
+ 'null' => NULL_ALLOWED
+ ),
'visible' => array(
'type' => PARAM_BOOL,
)
require_capability('moodle/competency:competencymanage', $competency->get_context());
// Reset the sortorder, use reorder instead.
- $competency->set('sortorder', null);
+ $competency->set('sortorder', 0);
$competency->create();
\core\event\competency_created::create_from_competency($competency)->trigger();
'default' => FORMAT_HTML
),
'sortorder' => array(
- 'default' => null,
+ 'default' => 0,
'type' => PARAM_INT
),
'parentid' => array(
),
'sortorder' => array(
'type' => PARAM_INT,
- 'default' => null,
+ 'default' => 0,
),
);
}
ORDER BY p.timesproficient ASC, c.id DESC';
$results = $DB->get_records_sql($sql, $params, $skip, $limit);
- $a = $DB->get_records_sql('SELECT * from {' . self::TABLE . '}');
$comps = array();
foreach ($results as $r) {
),
'sortorder' => array(
'type' => PARAM_INT,
- 'default' => null,
+ 'default' => 0,
),
);
}
'idnumber' => 'idnumber' . $number,
'description' => 'description' . $number,
'descriptionformat' => FORMAT_HTML,
- 'competencyframeworkid' => $frameworkid,
- 'sortorder' => 0
+ 'competencyframeworkid' => $frameworkid
);
$result = external::create_competency($competency);
return (object) external_api::clean_returnvalue(external::create_competency_returns(), $result);
'shortname' => 'shortname' . $number,
'idnumber' => 'idnumber' . $number,
'description' => 'description' . $number,
- 'descriptionformat' => FORMAT_HTML,
- 'sortorder' => 0
+ 'descriptionformat' => FORMAT_HTML
);
$result = external::update_competency($competency);
return external_api::clean_returnvalue(external::update_competency_returns(), $result);
$completion = new completion_info($course);
$activities = $completion->get_activities();
- $progresses = $completion->get_progress_all();
+ $progresses = $completion->get_progress_all('u.id = :uid', ['uid' => $params['userid']]);
$userprogress = $progresses[$user->id];
$results = array();
$details['requirement'][] = get_string('markingyourselfcomplete', 'completion');
} elseif ($cm->completion == COMPLETION_TRACKING_AUTOMATIC) {
if ($cm->completionview) {
- $details['requirement'][] = get_string('viewingactivity', 'completion', $this->module);
+ $modulename = core_text::strtolower(get_string('modulename', $this->module));
+ $details['requirement'][] = get_string('viewingactivity', 'completion', $modulename);
}
if (!is_null($cm->completiongradeitemnumber)) {
}
$output = '';
- if (!empty($CFG->navsortmycoursessort)) {
- // sort courses the same as in navigation menu
- $sortorder = 'visible DESC,'. $CFG->navsortmycoursessort.' ASC';
- } else {
- $sortorder = 'visible DESC,sortorder ASC';
- }
- $courses = enrol_get_my_courses('summary, summaryformat', $sortorder);
+ $courses = enrol_get_my_courses('summary, summaryformat');
$rhosts = array();
$rcourses = array();
if (!empty($CFG->mnet_dispatcher_mode) && $CFG->mnet_dispatcher_mode==='strict') {
$this->set_xml_file(array($imsuser));
$this->imsplugin->cron();
- $this->assertEquals(1, $DB->get_field('user', 'deleted', array('id' => $user->id), '*', MUST_EXIST));
+ $this->assertEquals(1, $DB->get_field('user', 'deleted', array('id' => $user->id), MUST_EXIST));
}
/**
$this->set_xml_file(array($imsuser));
$this->imsplugin->cron();
- $this->assertEquals(0, $DB->get_field('user', 'deleted', array('id' => $user->id), '*', MUST_EXIST));
+ $this->assertEquals(0, $DB->get_field('user', 'deleted', array('id' => $user->id), MUST_EXIST));
}
/**
$category1 = $this->getDataGenerator()->create_category(array('visible'=>0));
$category2 = $this->getDataGenerator()->create_category();
- $course1 = $this->getDataGenerator()->create_course(array('category'=>$category1->id));
- $course2 = $this->getDataGenerator()->create_course(array('category'=>$category2->id));
- $course3 = $this->getDataGenerator()->create_course(array('category'=>$category2->id, 'visible'=>0));
- $course4 = $this->getDataGenerator()->create_course(array('category'=>$category2->id));
+
+ $course1 = $this->getDataGenerator()->create_course(array(
+ 'shortname' => 'Z',
+ 'category' => $category1->id,
+ ));
+ $course2 = $this->getDataGenerator()->create_course(array(
+ 'shortname' => 'X',
+ 'category' => $category2->id,
+ ));
+ $course3 = $this->getDataGenerator()->create_course(array(
+ 'shortname' => 'Y',
+ 'category' => $category2->id,
+ 'visible' => 0,
+ ));
+ $course4 = $this->getDataGenerator()->create_course(array(
+ 'shortname' => 'W',
+ 'category' => $category2->id,
+ ));
$maninstance1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'manual'), '*', MUST_EXIST);
$DB->set_field('enrol', 'status', ENROL_INSTANCE_DISABLED, array('id'=>$maninstance1->id));
$courses = enrol_get_all_users_courses($user2->id, false, null, 'id DESC');
$this->assertEquals(array($course3->id, $course2->id, $course1->id), array_keys($courses));
+
+ // Make sure that implicit sorting defined in navsortmycoursessort is respected.
+
+ $CFG->navsortmycoursessort = 'shortname';
+
+ $courses = enrol_get_all_users_courses($user1->id);
+ $this->assertEquals(array($course2->id, $course3->id, $course1->id), array_keys($courses));
+
+ // But still the explicit sorting takes precedence over the implicit one.
+
+ $courses = enrol_get_all_users_courses($user1->id, false, null, 'shortname DESC');
+ $this->assertEquals(array($course1->id, $course3->id, $course2->id), array_keys($courses));
}
public function test_enrol_user_sees_own_courses() {
// Create test user and 4 courses, two of which have guest access enabled.
$user = $this->getDataGenerator()->create_user();
$course1 = $this->getDataGenerator()->create_course(
- (object)array('shortname' => 'Z',
+ (object)array('shortname' => 'X',
'enrol_guest_status_0' => ENROL_INSTANCE_DISABLED,
'enrol_guest_password_0' => ''));
$course2 = $this->getDataGenerator()->create_course(
- (object)array('shortname' => 'Y',
+ (object)array('shortname' => 'Z',
'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
'enrol_guest_password_0' => ''));
$course3 = $this->getDataGenerator()->create_course(
- (object)array('shortname' => 'X',
+ (object)array('shortname' => 'Y',
'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
'enrol_guest_password_0' => 'frog'));
$course4 = $this->getDataGenerator()->create_course(
$this->assertObjectHasAttribute('summary', $courses[$course3->id]);
$this->assertObjectHasAttribute('summaryformat', $courses[$course3->id]);
- // Check sort parameter still works.
- $courses = enrol_get_my_courses(null, 'shortname', 0, [], true);
+ // By default, courses are ordered by sortorder - which by default is most recent first.
+ $courses = enrol_get_my_courses(null, null, 0, [], true);
$this->assertEquals([$course3->id, $course2->id, $course1->id], array_keys($courses));
+ // Make sure that implicit sorting defined in navsortmycoursessort is respected.
+ $CFG->navsortmycoursessort = 'shortname';
+ $courses = enrol_get_my_courses(null, null, 0, [], true);
+ $this->assertEquals([$course1->id, $course3->id, $course2->id], array_keys($courses));
+
+ // But still the explicit sorting takes precedence over the implicit one.
+ $courses = enrol_get_my_courses(null, 'shortname DESC', 0, [], true);
+ $this->assertEquals([$course2->id, $course3->id, $course1->id], array_keys($courses));
+
// Check filter parameter still works.
$courses = enrol_get_my_courses(null, 'id', 0, [$course2->id, $course3->id, $course4->id], true);
$this->assertEquals([$course2->id, $course3->id], array_keys($courses));
This files describes API changes in /enrol/* - plugins,
information provided here is intended especially for developers.
+=== 3.5 ===
+
+* Default sorting in enrol_get_my_courses(), enrol_get_all_users_courses() and enrol_get_users_courses() now respects
+ the site setting "navsortmycoursessort" and should be consistently used when displaying the courses in the UI.
+
=== 3.4 ===
* render_course_enrolment_users_table method has been removed from the renderer. The enrolled users page is now
if ($status === conversion::STATUS_PENDING || $status === conversion::STATUS_FAILED) {
// The current status is either pending or failed.
// Attempt to pick up a new converter and convert the document.
- $from = \core_filetypes::get_file_extension($file->get_mimetype());
+ $from = pathinfo($file->get_filename(), PATHINFO_EXTENSION);
$converters = $this->get_document_converter_classes($from, $format);
$currentconverter = $this->get_next_converter($converters, $conversion->get('converter'));
return false;
}
- $from = \core_filetypes::get_file_extension($file->get_mimetype());
+ $from = pathinfo($file->get_filename(), PATHINFO_EXTENSION);
if (!$from) {
- // No mime type could be found. Unable to determine converter.
+ // No file extension could be found. Unable to determine converter.
return false;
}
}
/**
- * Test the can_convert_storedfile_to function with a file with indistinguished mimetype.
+ * Test the can_convert_storedfile_to function with a file with a known mimetype and extension.
*/
public function test_can_convert_storedfile_to_docx() {
$returnvalue = (object) [];
$types = \core_filetypes::get_types();
- // A file with filename '.' is a directory.
- $file = $this->get_stored_file('example content', 'example', [
+ $file = $this->get_stored_file('example content', 'example.docx', [
'mimetype' => $types['docx']['type'],
]);
/**
* Unit tests for grade/edit/tree/lib.php.
*
- * @pacakge core_grade
+ * @package core_grades
* @category phpunit
* @author Andrew Davis
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
/**
* Unit tests for grade/import/lib.php.
*
- * @package core_grade
+ * @package core_grades
* @category phpunit
* @copyright 2015 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
/**
* Unit tests for grade quering
*
- * @pacakge core_grade
+ * @package core_grades
* @category phpunit
* @copyright 2011 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
/**
* Unit tests for grade/report/user/lib.php.
*
- * @package core_grade
+ * @package core_grades
* @category phpunit
* @copyright 2012 Andrew Davis
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
/**
* Unit tests for grade/report/lib.php.
*
- * @pacakge core_grade
+ * @package core_grades
* @category phpunit
* @author Andrew Davis
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
/**
* Unit tests for grade/report/user/lib.php.
*
- * @package core_grade
+ * @package core_grades
* @category phpunit
* @copyright 2012 Andrew Davis
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
$string['cliyesnoprompt'] = 'idatzi b (bai esateko) edo e (ez esateko)';
$string['environmentrequireinstall'] = 'derrigorrezkoa da instalatuta eta gaituta izatea';
$string['environmentrequireversion'] = '{$a->needed} bertsioa beharrezkoa da eta zu {$a->current} ari zara egikaritzen';
+$string['upgradekeyset'] = 'Eguneraketa-kodea (utzi hutsik kodea erabili nahi ez baduzu)';
--- /dev/null
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package installer
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['thislanguage'] = 'Igbo';
--- /dev/null
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package installer
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['thislanguage'] = 'Pidgin';
$sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
FROM {role_capabilities} rc
JOIN {context} ctx ON rc.contextid = ctx.id
- WHERE rc.roleid $sql
- ORDER BY ctx.path, rc.roleid, rc.capability";
+ WHERE rc.roleid $sql";
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $rd) {
}
$rs->close();
+
+ // Sometimes (e.g. get_user_capability_course_helper::get_capability_info_at_each_context)
+ // we process role definitinons in a way that requires we see parent contexts
+ // before child contexts. This sort ensures that works (and is faster than
+ // sorting in the SQL query).
+ foreach ($rdefs as $roleid => $rdef) {
+ ksort($rdefs[$roleid]);
+ }
+
return $rdefs;
}
// We are mapping to a remote field here.
// Mapping.
$settings->add(new admin_setting_configtext("auth_{$auth}/field_map_{$field}",
- get_string('auth_fieldmapping', 'auth', $fieldname), '', '', PARAM_ALPHANUMEXT, 30));
+ get_string('auth_fieldmapping', 'auth', $fieldname), '', '', PARAM_RAW, 30));
// Update local.
$settings->add(new admin_setting_configselect("auth_{$auth}/field_updatelocal_{$field}",
* an array of capability values at each relevant context for the given user and capability.
*
* This is organised by the effective context path (the one at which the capability takes
- * effect) and then by role id.
+ * effect) and then by role id. Note, however, that the resulting array only has
+ * the information that will be needed later. If there are Prohibits present in some
+ * roles, then they cannot be overridden by other roles or role overrides in lower contexts,
+ * therefore, such information, if any, is absent from the results.
*
* @param int $userid User id
* @param string $capability Capability e.g. 'moodle/course:view'
}
$rdefs = get_role_definitions(array_keys($roleids));
+ // A prohibit in any relevant role prevents the capability
+ // in that context and all subcontexts. We need to track that.
+ // Here, the array keys are the paths where there is a prohibit the values are the role id.
+ $prohibitpaths = [];
+
// Get data for required capability at each context path where the user has a role that can
// affect it.
- $systemcontext = \context_system::instance();
$pathroleperms = [];
- foreach ($accessdata['ra'] as $userpath => $roles) {
+ foreach ($accessdata['ra'] as $rapath => $roles) {
+
foreach ($roles as $roleid) {
// Get role definition for that role.
- foreach ($rdefs[$roleid] as $rolepath => $caps) {
+ foreach ($rdefs[$roleid] as $rdefpath => $caps) {
// Ignore if this override/definition doesn't refer to the relevant cap.
if (!array_key_exists($capability, $caps)) {
continue;
}
- // Check path is /1 or matches a path the user has.
- if ($rolepath === '/' . $systemcontext->id) {
- // Note /1 is listed first in the array so this entry will be overridden
- // if there is an override for the role on this actual level.
- $effectivepath = $userpath;
- } else if (preg_match('~^' . $userpath . '($|/)~', $rolepath)) {
- $effectivepath = $rolepath;
+ // Check a role definition or override above ra.
+ if (self::path_is_above($rdefpath, $rapath)) {
+ // Note that $rdefs is sorted by path, so if a more specific override
+ // exists, it will be processed later and override this one.
+ $effectivepath = $rapath;
+ } else if (self::path_is_above($rapath, $rdefpath)) {
+ $effectivepath = $rdefpath;
} else {
// Not inside an area where the user has the role, so ignore.
continue;
}
+ // Check for already seen prohibits in higher context. Overrides can't change that.
+ if (self::any_path_is_above($prohibitpaths, $effectivepath)) {
+ continue;
+ }
+
+ // This is a releavant role assignment / permission combination. Save it.
if (!array_key_exists($effectivepath, $pathroleperms)) {
$pathroleperms[$effectivepath] = [];
}
$pathroleperms[$effectivepath][$roleid] = $caps[$capability];
+
+ // Update $prohibitpaths if necessary.
+ if ($caps[$capability] == CAP_PROHIBIT) {
+ // First remove any lower-context prohibits that might have come from other roles.
+ foreach ($prohibitpaths as $otherprohibitpath => $notused) {
+ if (self::path_is_above($effectivepath, $otherprohibitpath)) {
+ unset($prohibitpaths[$otherprohibitpath]);
+ }
+ }
+ $prohibitpaths[$effectivepath] = $roleid;
+ }
}
}
}
+ // Finally, if a later role had a higher-level prohibit that an earlier role,
+ // there may be more bits we can prune - but don't prune the prohibits!
+ foreach ($pathroleperms as $effectivepath => $roleperms) {
+ if ($roleid = self::any_path_is_above($prohibitpaths, $effectivepath)) {
+ unset($pathroleperms[$effectivepath]);
+ $pathroleperms[$effectivepath][$roleid] = CAP_PROHIBIT;
+ }
+ }
+
return $pathroleperms;
}
+ /**
+ * Test if a context path $otherpath is the same as, or underneath, $parentpath.
+ *
+ * @param string $parentpath the path of the parent context.
+ * @param string $otherpath the path of another context.
+ * @return bool true if $otherpath is underneath (or equal to) $parentpath.
+ */
+ protected static function path_is_above($parentpath, $otherpath) {
+ return preg_match('~^' . $parentpath . '($|/)~', $otherpath);
+ }
+
+ /**
+ * Test if a context path $otherpath is the same as, or underneath, any of $prohibitpaths.
+ *
+ * @param array $prohibitpaths array keys are context paths.
+ * @param string $otherpath the path of another context.
+ * @return int releavant $roleid if $otherpath is underneath (or equal to)
+ * any of the $prohibitpaths, 0 otherwise (so, can be used as a bool).
+ */
+ protected static function any_path_is_above($prohibitpaths, $otherpath) {
+ foreach ($prohibitpaths as $prohibitpath => $roleid) {
+ if (self::path_is_above($prohibitpath, $otherpath)) {
+ return $roleid;
+ }
+ }
+ return 0;
+ }
+
/**
* Calculates a permission tree based on an array of information about role permissions.
*
registration::require_registration();
}
- if (extension_loaded('xmlrpc')) {
- // Use XMLRPC protocol.
- return self::call_xmlrpc($token, $function, $data);
- } else {
- // Use REST.
- return self::call_rest($token, $function, $data);
- }
- }
-
- /**
- * Performs REST request to moodle.net (using GET method)
- *
- * @param string $token
- * @param string $function
- * @param array $data
- * @return mixed
- * @throws moodle_exception
- */
- protected static function call_xmlrpc($token, $function, array $data) {
- global $CFG;
- require_once($CFG->dirroot . "/webservice/xmlrpc/lib.php");
-
- $serverurl = HUB_MOODLEORGHUBURL . "/local/hub/webservice/webservices.php";
- $xmlrpcclient = new webservice_xmlrpc_client($serverurl, $token);
- try {
- return $xmlrpcclient->call($function, $data);
- } catch (\Exception $e) {
- // Function webservice_xmlrpc_client::call() can throw Exception, wrap it into moodle_exception.
- throw new moodle_exception('errorws', 'hub', '', $e->getMessage());
- }
+ return self::call_rest($token, $function, $data);
}
/**
$cleanhuburl = clean_param(HUB_MOODLEORGHUBURL, PARAM_ALPHANUMEXT);
foreach (self::FORM_FIELDS as $field) {
$siteinfo[$field] = get_config('hub', 'site_'.$field.'_' . $cleanhuburl);
- if ($siteinfo[$field] === false && array_key_exists($field, $defaults)) {
- $siteinfo[$field] = $defaults[$field];
+ if ($siteinfo[$field] === false) {
+ $siteinfo[$field] = array_key_exists($field, $defaults) ? $defaults[$field] : null;
}
}
class gradingform extends base {
public function is_uninstall_allowed() {
- return false;
+ return true;
+ }
+
+ /**
+ * Pre-uninstall hook.
+ * This is intended for disabling of plugin, some DB table purging, etc.
+ */
+ public function uninstall_cleanup() {
+ global $DB;
+
+ // Find all definitions and templates.
+ $definitions = $DB->get_fieldset_select('grading_definitions', 'id', 'method = ?', [$this->name]);
+ if ($definitions) {
+ // Delete instances and definitions. Deleting instance will not delete grades because they were
+ // already pushed to the module and gradebook.
+ list($sqld, $paramsd) = $DB->get_in_or_equal($definitions);
+ $DB->delete_records_select('grading_instances', 'definitionid ' . $sqld, $paramsd);
+ $DB->delete_records_select('grading_definitions', 'id ' . $sqld, $paramsd);
+ }
+ // Delete templates for this grading method.
+ $DB->delete_records_select('grading_areas', 'component = ? AND activemethod = ?', array('core_grading', $this->name));
+ // Update the remaining grading areas to use simple grading method instead of this grading method.
+ $DB->execute('UPDATE {grading_areas} SET activemethod = NULL WHERE activemethod = ?', [$this->name]);
+
+ parent::uninstall_cleanup();
}
}
throw new exception('redissessionhandlerproblem', 'error');
}
- try {
- // One second timeout was chosen as it is long for connection, but short enough for a user to be patient.
- if (!$this->connection->connect($this->host, $this->port, 1)) {
- throw new RedisException('Unable to connect to host.');
- }
+ // MDL-59866: Add retries for connections (up to 5 times) to make sure it goes through.
+ $counter = 1;
+ $maxnumberofretries = 5;
+
+ while ($counter <= $maxnumberofretries) {
+
+ try {
+
+ $delay = rand(100000, 500000);
- if ($this->auth !== '') {
- if (!$this->connection->auth($this->auth)) {
- throw new RedisException('Unable to authenticate.');
+ // One second timeout was chosen as it is long for connection, but short enough for a user to be patient.
+ if (!$this->connection->connect($this->host, $this->port, 1, null, $delay)) {
+ throw new RedisException('Unable to connect to host.');
}
- }
- if (!$this->connection->setOption(\Redis::OPT_SERIALIZER, $this->serializer)) {
- throw new RedisException('Unable to set Redis PHP Serializer option.');
- }
+ if ($this->auth !== '') {
+ if (!$this->connection->auth($this->auth)) {
+ throw new RedisException('Unable to authenticate.');
+ }
+ }
- if ($this->prefix !== '') {
- // Use custom prefix on sessions.
- if (!$this->connection->setOption(\Redis::OPT_PREFIX, $this->prefix)) {
- throw new RedisException('Unable to set Redis Prefix option.');
+ if (!$this->connection->setOption(\Redis::OPT_SERIALIZER, $this->serializer)) {
+ throw new RedisException('Unable to set Redis PHP Serializer option.');
}
- }
- if ($this->database !== 0) {
- if (!$this->connection->select($this->database)) {
- throw new RedisException('Unable to select Redis database '.$this->database.'.');
+
+ if ($this->prefix !== '') {
+ // Use custom prefix on sessions.
+ if (!$this->connection->setOption(\Redis::OPT_PREFIX, $this->prefix)) {
+ throw new RedisException('Unable to set Redis Prefix option.');
+ }
+ }
+ if ($this->database !== 0) {
+ if (!$this->connection->select($this->database)) {
+ throw new RedisException('Unable to select Redis database '.$this->database.'.');
+ }
}
+ $this->connection->ping();
+ return true;
+ } catch (RedisException $e) {
+ $logstring = "Failed to connect (try {$counter} out of {$maxnumberofretries}) to redis ";
+ $logstring .= "at {$this->host}:{$this->port}, error returned was: {$e->getMessage()}";
+
+ // @codingStandardsIgnoreStart
+ error_log($logstring);
+ // @codingStandardsIgnoreEnd
}
- $this->connection->ping();
- return true;
- } catch (RedisException $e) {
- error_log('Failed to connect to redis at '.$this->host.':'.$this->port.', error returned was: '.$e->getMessage());
- return false;
+
+ $counter++;
+
+ // Introduce a random sleep between 100ms and 500ms.
+ usleep(rand(100000, 500000));
}
+
+ // We have exhausted our retries, time to give up.
+ return false;
}
/**
$cuttime = $timenow - ($CFG->deleteunconfirmed * 3600);
$rs = $DB->get_recordset_sql ("SELECT *
FROM {user}
- WHERE confirmed = 0 AND firstaccess > 0
- AND firstaccess < ? AND deleted = 0", array($cuttime));
+ WHERE confirmed = 0 AND timecreated > 0
+ AND timecreated < ? AND deleted = 0", array($cuttime));
foreach ($rs as $user) {
delete_user($user); // We MUST delete user properly first.
$DB->delete_records('user', array('id' => $user->id)); // This is a bloody hack, but it might work.
<?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20171026" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20171205" COMMENT="XMLDB file for core Moodle tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="categoryid" TYPE="foreign" FIELDS="categoryid" REFTABLE="course_categories" REFFIELDS="id"/>
+ <KEY NAME="subscriptionid" TYPE="foreign" FIELDS="subscriptionid" REFTABLE="event_subscriptions" REFFIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="courseid" UNIQUE="false" FIELDS="courseid"/>
<INDEX NAME="userid" UNIQUE="false" FIELDS="userid"/>
<INDEX NAME="timestart" UNIQUE="false" FIELDS="timestart"/>
<INDEX NAME="timeduration" UNIQUE="false" FIELDS="timeduration"/>
+ <INDEX NAME="uuid" UNIQUE="false" FIELDS="uuid"/>
<INDEX NAME="type-timesort" UNIQUE="false" FIELDS="type, timesort"/>
<INDEX NAME="groupid-courseid-categoryid-visible-userid" UNIQUE="false" FIELDS="groupid, courseid, categoryid, visible, userid" COMMENT="used for calendar view"/>
</INDEXES>
</KEYS>
</TABLE>
</TABLES>
-</XMLDB>
+</XMLDB>
\ No newline at end of file
upgrade_main_savepoint(true, 2017111300.02);
}
+ if ($oldversion < 2017121200.00) {
+
+ // Define key subscriptionid (foreign) to be added to event.
+ $table = new xmldb_table('event');
+ $key = new xmldb_key('subscriptionid', XMLDB_KEY_FOREIGN, array('subscriptionid'), 'event_subscriptions', array('id'));
+
+ // Launch add key subscriptionid.
+ $dbman->add_key($table, $key);
+
+ // Define index uuid (not unique) to be added to event.
+ $table = new xmldb_table('event');
+ $index = new xmldb_index('uuid', XMLDB_INDEX_NOTUNIQUE, array('uuid'));
+
+ // Conditionally launch add index uuid.
+ if (!$dbman->index_exists($table, $index)) {
+ $dbman->add_index($table, $index);
+ }
+
+ // Main savepoint reached.
+ upgrade_main_savepoint(true, 2017121200.00);
+ }
+
return true;
}
// Enclose the column name by the proper quotes if it's a reserved word.
$columnname = $this->get_manager()->generator->getEncQuoted($column->name);
+
+ $searchsql = $this->sql_like($columnname, '?');
+ $searchparam = '%'.$this->sql_like_escape($search).'%';
+
$sql = "UPDATE {".$table."}
SET $columnname = REPLACE($columnname, ?, ?)
- WHERE $columnname IS NOT NULL";
+ WHERE $searchsql";
if ($column->meta_type === 'X') {
- $this->execute($sql, array($search, $replace));
+ $this->execute($sql, array($search, $replace, $searchparam));
} else if ($column->meta_type === 'C') {
if (core_text::strlen($search) < core_text::strlen($replace)) {
$colsize = $column->max_length;
$sql = "UPDATE {".$table."}
SET $columnname = " . $this->sql_substr("REPLACE(" . $columnname . ", ?, ?)", 1, $colsize) . "
- WHERE $columnname IS NOT NULL";
+ WHERE $searchsql";
}
- $this->execute($sql, array($search, $replace));
+ $this->execute($sql, array($search, $replace, $searchparam));
}
}
* - bit ops: To provide cross-db bitwise operations to be used by the
* sql_bitXXX() helper functions
* - one space hacks: One space empty string substitute hacks.
+ *
+ * Moodle will not parse this file correctly if it uses Windows line endings.
*/
CREATE OR REPLACE PACKAGE MOODLELIB AS
/**
* Returns list of courses current $USER is enrolled in and can access
*
- * - $fields is an array of field names to ADD
- * so name the fields you really need, which will
- * be added and uniq'd
+ * The $fields param is a list of field names to ADD so name just the fields you really need,
+ * which will be added and uniq'd.
*
* If $allaccessible is true, this will additionally return courses that the current user is not
* enrolled in, but can access because they are open to the user for other reasons (course view
* permission, currently viewing course as a guest, or course allows guest access without
* password).
*
- * @param string|array $fields
- * @param string $sort
+ * @param string|array $fields Extra fields to be returned (array or comma-separated list).
+ * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
* @param int $limit max number of courses
* @param array $courseids the list of course ids to filter by
* @param bool $allaccessible Include courses user is not enrolled in, but can access
* @return array
*/
-function enrol_get_my_courses($fields = null, $sort = 'visible DESC,sortorder ASC',
- $limit = 0, $courseids = [], $allaccessible = false) {
+function enrol_get_my_courses($fields = null, $sort = null, $limit = 0, $courseids = [], $allaccessible = false) {
global $DB, $USER, $CFG;
+ if ($sort === null) {
+ if (empty($CFG->navsortmycoursessort)) {
+ $sort = 'visible DESC, sortorder ASC';
+ } else {
+ $sort = 'visible DESC, '.$CFG->navsortmycoursessort.' ASC';
+ }
+ }
+
// Guest account does not have any enrolled courses.
if (!$allaccessible && (isguestuser() or !isloggedin())) {
return array();
} else if (is_array($fields)) {
$fields = array_unique(array_merge($basefields, $fields));
} else {
- throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
+ throw new coding_exception('Invalid $fields parameter in enrol_get_my_courses()');
}
if (in_array('*', $fields)) {
$fields = array('*');
/**
* Returns list of courses user is enrolled into.
- * (Note: use enrol_get_all_users_courses if you want to use the list wihtout any cap checks )
*
- * - $fields is an array of fieldnames to ADD
- * so name the fields you really need, which will
- * be added and uniq'd
+ * Note: Use {@link enrol_get_all_users_courses()} if you need the list without any capability checks.
*
- * @param int $userid
- * @param bool $onlyactive return only active enrolments in courses user may see
- * @param string|array $fields
- * @param string $sort
+ * The $fields param is a list of field names to ADD so name just the fields you really need,
+ * which will be added and uniq'd.
+ *
+ * @param int $userid User whose courses are returned, defaults to the current user.
+ * @param bool $onlyactive Return only active enrolments in courses user may see.
+ * @param string|array $fields Extra fields to be returned (array or comma-separated list).
+ * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
* @return array
*/
-function enrol_get_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
+function enrol_get_users_courses($userid, $onlyactive = false, $fields = null, $sort = null) {
global $DB;
$courses = enrol_get_all_users_courses($userid, $onlyactive, $fields, $sort);
}
/**
- * Returns list of courses user is enrolled into without any capability checks
- * - $fields is an array of fieldnames to ADD
- * so name the fields you really need, which will
- * be added and uniq'd
+ * Returns list of courses user is enrolled into without performing any capability checks.
*
- * @param int $userid
- * @param bool $onlyactive return only active enrolments in courses user may see
- * @param string|array $fields
- * @param string $sort
+ * The $fields param is a list of field names to ADD so name just the fields you really need,
+ * which will be added and uniq'd.
+ *
+ * @param int $userid User whose courses are returned, defaults to the current user.
+ * @param bool $onlyactive Return only active enrolments in courses user may see.
+ * @param string|array $fields Extra fields to be returned (array or comma-separated list).
+ * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
* @return array
*/
-function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
- global $DB;
+function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = null, $sort = null) {
+ global $CFG, $DB;
+
+ if ($sort === null) {
+ if (empty($CFG->navsortmycoursessort)) {
+ $sort = 'visible DESC, sortorder ASC';
+ } else {
+ $sort = 'visible DESC, '.$CFG->navsortmycoursessort.' ASC';
+ }
+ }
// Guest account does not have any courses
if (isguestuser($userid) or empty($userid)) {
} else if (is_array($fields)) {
$fields = array_unique(array_merge($basefields, $fields));
} else {
- throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
+ throw new coding_exception('Invalid $fields parameter in enrol_get_all_users_courses()');
}
if (in_array('*', $fields)) {
$fields = array('*');
$filerecord->filesize = $newfile->get_filesize();
$filerecord->referencefileid = $newfile->get_referencefileid();
$filerecord->userid = $newfile->get_userid();
+ $oldcontenthash = $this->get_contenthash();
$this->update($filerecord);
+ $this->filesystem->remove_file($oldcontenthash);
}
/**
*/
public function exportValue(&$submitValues, $assoc = false) {
if (!$this->is_tagging_enabled()) {
- return $assoc ? array($this->getName() => array()) : array();
+ return $this->_prepareValue([], $assoc);
+ }
+ if ($this->_findValue($submitValues) === '_qf__force_multiselect_submission') {
+ // Nothing was selected.
+ return $this->_prepareValue([], $assoc);
}
return parent::exportValue($submitValues, $assoc);
$url = new moodle_url('/tag/manage.php', array('tc' => $this->get_tag_collection()));
$context['managestandardtagsurl'] = $url->out(false);
}
+ $context['nameraw'] = $this->getName();
return $context;
}
if (!isset($hiddenfields['mycourses'])) {
$showallcourses = optional_param('showallcourses', 0, PARAM_INT);
- if ($mycourses = enrol_get_all_users_courses($user->id, true, null, 'visible DESC, sortorder ASC')) {
+ if ($mycourses = enrol_get_all_users_courses($user->id, true, null)) {
$shown = 0;
$courselisting = html_writer::start_tag('ul');
foreach ($mycourses as $mycourse) {
$limit = (int) $CFG->navcourselimit;
- $sortorder = 'visible DESC';
- // Prevent undefined $CFG->navsortmycoursessort errors.
- if (empty($CFG->navsortmycoursessort)) {
- $CFG->navsortmycoursessort = 'sortorder';
- }
- // Append the chosen sortorder.
- $sortorder = $sortorder . ',' . $CFG->navsortmycoursessort . ' ASC';
- $courses = enrol_get_my_courses('*', $sortorder);
+ $courses = enrol_get_my_courses('*');
$flatnavcourses = [];
// Go through the courses and see which ones we want to display in the flatnav.
$generator = $this->getDataGenerator();
$cap = 'moodle/course:view';
+ // The structure being created here is this:
+ //
+ // All tests work with the single capability 'moodle/course:view'.
+ //
+ // ROLE DEF/OVERRIDE ROLE ASSIGNS
+ // Role: Allow Prohib Empty Def user u1 u2 u3 u4 u5 u6 u7 u8
+ // System ALLOW PROHIBIT A E A+E
+ // cat1 ALLOW
+ // C1 (ALLOW) P
+ // C2 ALLOW E P
+ // cat2 PREVENT
+ // C3 ALLOW E
+ // C4
+ // Misc. A
+ // C5 PREVENT A
+ // C6 PROHIBIT
+ //
+ // Front-page and guest role stuff from the end of this test not included in the diagram.
+
// Create a role which allows course:view and one that prohibits it, and one neither.
$allowroleid = $generator->create_role();
$prohibitroleid = $generator->create_role();
$cat2 = $generator->create_category(['parent' => $cat1->id]);
// Create six courses - two in cat1, two in cat2, and two in default category.
+ // Shortnames are used for a sorting test. Otherwise they are not significant.
$c1 = $generator->create_course(['category' => $cat1->id, 'shortname' => 'Z']);
$c2 = $generator->create_course(['category' => $cat1->id, 'shortname' => 'Y']);
$c3 = $generator->create_course(['category' => $cat2->id, 'shortname' => 'X']);
context_course::instance($c6->id)->id);
assign_capability($cap, CAP_ALLOW, $emptyroleid,
context_course::instance($c3->id)->id);
+ assign_capability($cap, CAP_ALLOW, $prohibitroleid,
+ context_course::instance($c2->id)->id);
// User 1 has no roles except default user role.
$u1 = $generator->create_user();
$courses = get_user_capability_course($cap, $u6->id, true, '', 'id');
$this->assert_course_ids([$c3->id], $courses);
+ // User 7 has empty role in C2.
+ $u7 = $generator->create_user();
+ role_assign($emptyroleid, $u7->id, context_course::instance($c2->id)->id);
+
+ // Should get C1 by the default user role override, and C2 by the cat1 level override.
+ $courses = get_user_capability_course($cap, $u7->id, true, '', 'id');
+ $this->assert_course_ids([$c1->id, $c2->id], $courses);
+
+ // User 8 has prohibit role as system context, to verify that prohibits can't be overridden.
+ $u8 = $generator->create_user();
+ role_assign($prohibitroleid, $u8->id, context_course::instance($c2->id)->id);
+
+ // Should get C1 by the default user role override, no other courses because the prohibit cannot be overridden.
+ $courses = get_user_capability_course($cap, $u8->id, true, '', 'id');
+ $this->assert_course_ids([$c1->id], $courses);
+
// Admin user gets everything....
$courses = get_user_capability_course($cap, get_admin()->id, true, '', 'id');
$this->assert_course_ids([SITEID, $c1->id, $c2->id, $c3->id, $c4->id, $c5->id, $c6->id],
/**
* Unit tests for /lib/gradelib.php.
*
- * @package core_grade
+ * @package core_grades
* @category phpunit
* @copyright 2012 Andrew Davis
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
$string['commentcontextmenu'] = 'Comment context menu';
$string['couldnotsavepage'] = 'Could not save page {$a}';
$string['currentstamp'] = 'Stamp';
+$string['default'] = 'Enabled by default';
+$string['default_help'] = 'If set, this feedback method will be enabled by default for all new assignments.';
$string['deleteannotation'] = 'Delete annotation';
$string['deletecomment'] = 'Delete comment';
$string['deletefeedback'] = 'Delete feedback PDF';
}
/**
- * Automatically enable or disable editpdf feedback plugin based on
- * whether the ghostscript path is set correctly.
+ * Determine if ghostscript is available and working.
*
* @return bool
*/
- public function is_enabled() {
+ public function is_available() {
if ($this->enabledcache === null) {
$testpath = assignfeedback_editpdf\pdf::test_gs_path(false);
$this->enabledcache = ($testpath->status == assignfeedback_editpdf\pdf::GSPATH_OK);
return $this->enabledcache;
}
/**
- * Automatically hide the setting for the editpdf feedback plugin.
+ * Prevent enabling this plugin if ghostscript is not available.
*
* @return bool false
*/
public function is_configurable() {
- return false;
+ return $this->is_available();
}
/**
defined('MOODLE_INTERNAL') || die();
+// Enabled by default.
+$settings->add(new admin_setting_configcheckbox('assignfeedback_editpdf/default',
+ new lang_string('default', 'assignfeedback_editpdf'),
+ new lang_string('default_help', 'assignfeedback_editpdf'), 1));
+
// Stamp files setting.
$name = 'assignfeedback_editpdf/stamps';
$title = get_string('stamps','assignfeedback_editpdf');
$groupid = 0;
// Get the group name as other fields are not transcribed in the logs and this information is important.
if (empty($submission->userid) && !empty($submission->groupid)) {
- $groupname = $DB->get_field('groups', 'name', array('id' => $submission->groupid), '*', MUST_EXIST);
+ $groupname = $DB->get_field('groups', 'name', array('id' => $submission->groupid), MUST_EXIST);
$groupid = $submission->groupid;
} else {
$params['relateduserid'] = $submission->userid;
$groupid = 0;
// Get the group name as other fields are not transcribed in the logs and this information is important.
if (empty($submission->userid) && !empty($submission->groupid)) {
- $groupname = $DB->get_field('groups', 'name', array('id' => $submission->groupid), '*', MUST_EXIST);
+ $groupname = $DB->get_field('groups', 'name', array('id' => $submission->groupid), MUST_EXIST);
$groupid = $submission->groupid;
} else {
$params['relateduserid'] = $submission->userid;
$string['cannotdeletepreset'] = 'Error deleting a preset!';
$string['cannotoverwritepreset'] = 'Error overwriting preset';
$string['cannotunziptopreset'] = 'Cannot unzip to the preset directory';
+$string['closebeforeopen'] = 'You have specified an end date before the start date.';
$string['columns'] = 'columns';
$string['comment'] = 'Comment';
$string['commentdeleted'] = 'Comment deleted';
$string['numnotapproved'] = 'Pending';
$string['numrecords'] = '{$a} entries';
$string['ods'] = '<acronym title="OpenDocument Spreadsheet">ODS</acronym> (OpenOffice)';
+$string['openafterclose'] = 'You have specified an open date after the close date';
$string['optionaldescription'] = 'Short description (optional)';
$string['optionalfilename'] = 'Filename (optional)';
$string['other'] = 'Other';
}
return $descriptions;
}
+
+/**
+ * This function calculates the minimum and maximum cutoff values for the timestart of
+ * the given event.
+ *
+ * It will return an array with two values, the first being the minimum cutoff value and
+ * the second being the maximum cutoff value. Either or both values can be null, which
+ * indicates there is no minimum or maximum, respectively.
+ *
+ * If a cutoff is required then the function must return an array containing the cutoff
+ * timestamp and error string to display to the user if the cutoff value is violated.
+ *
+ * A minimum and maximum cutoff return value will look like:
+ * [
+ * [1505704373, 'The due date must be after the sbumission start date'],
+ * [1506741172, 'The due date must be before the cutoff date']
+ * ]
+ *
+ * @param calendar_event $event The calendar event to get the time range for
+ * @param stdClass $instance The module instance to get the range from
+ * @return array
+ */
+function mod_data_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $instance) {
+ $mindate = null;
+ $maxdate = null;
+
+ if ($event->eventtype == DATA_EVENT_TYPE_OPEN) {
+ // The start time of the open event can't be equal to or after the
+ // close time of the database activity.
+ if (!empty($instance->timeavailableto)) {
+ $maxdate = [
+ $instance->timeavailableto,
+ get_string('openafterclose', 'data')
+ ];
+ }
+ } else if ($event->eventtype == DATA_EVENT_TYPE_CLOSE) {
+ // The start time of the close event can't be equal to or earlier than the
+ // open time of the database activity.
+ if (!empty($instance->timeavailablefrom)) {
+ $mindate = [
+ $instance->timeavailablefrom,
+ get_string('closebeforeopen', 'data')
+ ];
+ }
+ }
+
+ return [$mindate, $maxdate];
+}
+
+/**
+ * This function will update the data module according to the
+ * event that has been modified.
+ *
+ * It will set the timeopen or timeclose value of the data instance
+ * according to the type of event provided.
+ *
+ * @throws \moodle_exception
+ * @param \calendar_event $event
+ * @param stdClass $data The module instance to get the range from
+ */
+function mod_data_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $data) {
+ global $DB;
+
+ if (empty($event->instance) || $event->modulename != 'data') {
+ return;
+ }
+
+ if ($event->instance != $data->id) {
+ return;
+ }
+
+ if (!in_array($event->eventtype, [DATA_EVENT_TYPE_OPEN, DATA_EVENT_TYPE_CLOSE])) {
+ return;
+ }
+
+ $courseid = $event->courseid;
+ $modulename = $event->modulename;
+ $instanceid = $event->instance;
+ $modified = false;
+
+ $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid];
+ $context = context_module::instance($coursemodule->id);
+
+ // The user does not have the capability to modify this activity.
+ if (!has_capability('moodle/course:manageactivities', $context)) {
+ return;
+ }
+
+ if ($event->eventtype == DATA_EVENT_TYPE_OPEN) {
+ // If the event is for the data activity opening then we should
+ // set the start time of the data activity to be the new start
+ // time of the event.
+ if ($data->timeavailablefrom != $event->timestart) {
+ $data->timeavailablefrom = $event->timestart;
+ $data->timemodified = time();
+ $modified = true;
+ }
+ } else if ($event->eventtype == DATA_EVENT_TYPE_CLOSE) {
+ // If the event is for the data activity closing then we should
+ // set the end time of the data activity to be the new start
+ // time of the event.
+ if ($data->timeavailableto != $event->timestart) {
+ $data->timeavailableto = $event->timestart;
+ $modified = true;
+ }
+ }
+
+ if ($modified) {
+ $data->timemodified = time();
+ $DB->update_record('data', $data);
+ $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context);
+ $event->trigger();
+ }
+}
* @param int $courseid
* @param int $instanceid The data id.
* @param string $eventtype The event type. eg. DATA_EVENT_TYPE_OPEN.
+ * @param int|null $timestart The start timestamp for the event
* @return bool|calendar_event
*/
- private function create_action_event($courseid, $instanceid, $eventtype) {
+ private function create_action_event($courseid, $instanceid, $eventtype, $timestart = null) {
$event = new stdClass();
$event->name = 'Calendar event';
$event->modulename = 'data';
$event->instance = $instanceid;
$event->type = CALENDAR_EVENT_TYPE_ACTION;
$event->eventtype = $eventtype;
- $event->timestart = time();
+ if ($timestart) {
+ $event->timestart = $timestart;
+ } else {
+ $event->timestart = time();
+ }
return calendar_event::create($event);
}
$this->assertEquals(mod_data_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions);
$this->assertEquals(mod_data_get_completion_active_rule_descriptions(new stdClass()), []);
}
+
+ /**
+ * An unknown event type should not change the data instance.
+ */
+ public function test_mod_data_core_calendar_event_timestart_updated_unknown_event() {
+ global $CFG, $DB;
+ require_once($CFG->dirroot . "/calendar/lib.php");
+
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course();
+ $datagenerator = $generator->get_plugin_generator('mod_data');
+ $timeopen = time();
+ $timeclose = $timeopen + DAYSECS;
+ $data = $datagenerator->create_instance(['course' => $course->id]);
+ $data->timeavailablefrom = $timeopen;
+ $data->timeavailableto = $timeclose;
+ $DB->update_record('data', $data);
+
+ // Create a valid event.
+ $event = new \calendar_event([
+ 'name' => 'Test event',
+ 'description' => '',
+ 'format' => 1,
+ 'courseid' => $course->id,
+ 'groupid' => 0,
+ 'userid' => 2,
+ 'modulename' => 'data',
+ 'instance' => $data->id,
+ 'eventtype' => DATA_EVENT_TYPE_OPEN . "SOMETHING ELSE",
+ 'timestart' => 1,
+ 'timeduration' => 86400,
+ 'visible' => 1
+ ]);
+
+ mod_data_core_calendar_event_timestart_updated($event, $data);
+ $data = $DB->get_record('data', ['id' => $data->id]);
+ $this->assertEquals($timeopen, $data->timeavailablefrom);
+ $this->assertEquals($timeclose, $data->timeavailableto);
+ }
+
+ /**
+ * A DATA_EVENT_TYPE_OPEN event should update the timeavailablefrom property of the data activity.
+ */
+ public function test_mod_data_core_calendar_event_timestart_updated_open_event() {
+ global $CFG, $DB;
+ require_once($CFG->dirroot . "/calendar/lib.php");
+
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course();
+ $datagenerator = $generator->get_plugin_generator('mod_data');
+ $timeopen = time();
+ $timeclose = $timeopen + DAYSECS;
+ $timemodified = 1;
+ $newtimeopen = $timeopen - DAYSECS;
+ $data = $datagenerator->create_instance(['course' => $course->id]);
+ $data->timeavailablefrom = $timeopen;
+ $data->timeavailableto = $timeclose;
+ $data->timemodified = $timemodified;
+ $DB->update_record('data', $data);
+
+ // Create a valid event.
+ $event = new \calendar_event([
+ 'name' => 'Test event',
+ 'description' => '',
+ 'format' => 1,
+ 'courseid' => $course->id,
+ 'groupid' => 0,
+ 'userid' => 2,
+ 'modulename' => 'data',
+ 'instance' => $data->id,
+ 'eventtype' => DATA_EVENT_TYPE_OPEN,
+ 'timestart' => $newtimeopen,
+ 'timeduration' => 86400,
+ 'visible' => 1
+ ]);
+
+ // Trigger and capture the event when adding a contact.
+ $sink = $this->redirectEvents();
+ mod_data_core_calendar_event_timestart_updated($event, $data);
+ $triggeredevents = $sink->get_events();
+ $moduleupdatedevents = array_filter($triggeredevents, function($e) {
+ return is_a($e, 'core\event\course_module_updated');
+ });
+ $data = $DB->get_record('data', ['id' => $data->id]);
+
+ // Ensure the timeavailablefrom property matches the event timestart.
+ $this->assertEquals($newtimeopen, $data->timeavailablefrom);
+ // Ensure the timeavailableto isn't changed.
+ $this->assertEquals($timeclose, $data->timeavailableto);
+ // Ensure the timemodified property has been changed.
+ $this->assertNotEquals($timemodified, $data->timemodified);
+ // Confirm that a module updated event is fired when the module is changed.
+ $this->assertNotEmpty($moduleupdatedevents);
+ }
+
+ /**
+ * A DATA_EVENT_TYPE_CLOSE event should update the timeavailableto property of the data activity.
+ */
+ public function test_mod_data_core_calendar_event_timestart_updated_close_event() {
+ global $CFG, $DB;
+ require_once($CFG->dirroot . "/calendar/lib.php");
+
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course();
+ $datagenerator = $generator->get_plugin_generator('mod_data');
+ $timeopen = time();
+ $timeclose = $timeopen + DAYSECS;
+ $timemodified = 1;
+ $newtimeclose = $timeclose + DAYSECS;
+ $data = $datagenerator->create_instance(['course' => $course->id]);
+ $data->timeavailablefrom = $timeopen;
+ $data->timeavailableto = $timeclose;
+ $data->timemodified = $timemodified;
+ $DB->update_record('data', $data);
+
+ // Create a valid event.
+ $event = new \calendar_event([
+ 'name' => 'Test event',
+ 'description' => '',
+ 'format' => 1,
+ 'courseid' => $course->id,
+ 'groupid' => 0,
+ 'userid' => 2,
+ 'modulename' => 'data',
+ 'instance' => $data->id,
+ 'eventtype' => DATA_EVENT_TYPE_CLOSE,
+ 'timestart' => $newtimeclose,
+ 'timeduration' => 86400,
+ 'visible' => 1
+ ]);
+
+ // Trigger and capture the event when adding a contact.
+ $sink = $this->redirectEvents();
+ mod_data_core_calendar_event_timestart_updated($event, $data);
+ $triggeredevents = $sink->get_events();
+ $moduleupdatedevents = array_filter($triggeredevents, function($e) {
+ return is_a($e, 'core\event\course_module_updated');
+ });
+ $data = $DB->get_record('data', ['id' => $data->id]);
+
+ // Ensure the timeavailableto property matches the event timestart.
+ $this->assertEquals($newtimeclose, $data->timeavailableto);
+ // Ensure the timeavailablefrom isn't changed.
+ $this->assertEquals($timeopen, $data->timeavailablefrom);
+ // Ensure the timemodified property has been changed.
+ $this->assertNotEquals($timemodified, $data->timemodified);
+ // Confirm that a module updated event is fired when the module is changed.
+ $this->assertNotEmpty($moduleupdatedevents);
+ }
+
+ /**
+ * An unknown event type should not have any limits.
+ */
+ public function test_mod_data_core_calendar_get_valid_event_timestart_range_unknown_event() {
+ global $CFG;
+ require_once($CFG->dirroot . "/calendar/lib.php");
+
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course();
+ $timeopen = time();
+ $timeclose = $timeopen + DAYSECS;
+ $data = new \stdClass();
+ $data->timeavailablefrom = $timeopen;
+ $data->timeavailableto = $timeclose;
+
+ // Create a valid event.
+ $event = new \calendar_event([
+ 'name' => 'Test event',
+ 'description' => '',
+ 'format' => 1,
+ 'courseid' => $course->id,
+ 'groupid' => 0,
+ 'userid' => 2,
+ 'modulename' => 'data',
+ 'instance' => 1,
+ 'eventtype' => DATA_EVENT_TYPE_OPEN . "SOMETHING ELSE",
+ 'timestart' => 1,
+ 'timeduration' => 86400,
+ 'visible' => 1
+ ]);
+
+ list ($min, $max) = mod_data_core_calendar_get_valid_event_timestart_range($event, $data);
+ $this->assertNull($min);
+ $this->assertNull($max);
+ }
+
+ /**
+ * The open event should be limited by the data's timeclose property, if it's set.
+ */
+ public function test_mod_data_core_calendar_get_valid_event_timestart_range_open_event() {
+ global $CFG;
+ require_once($CFG->dirroot . "/calendar/lib.php");
+
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course();
+ $timeopen = time();
+ $timeclose = $timeopen + DAYSECS;
+ $data = new \stdClass();
+ $data->timeavailablefrom = $timeopen;
+ $data->timeavailableto = $timeclose;
+
+ // Create a valid event.
+ $event = new \calendar_event([
+ 'name' => 'Test event',
+ 'description' => '',
+ 'format' => 1,
+ 'courseid' => $course->id,
+ 'groupid' => 0,
+ 'userid' => 2,
+ 'modulename' => 'data',
+ 'instance' => 1,
+ 'eventtype' => DATA_EVENT_TYPE_OPEN,
+ 'timestart' => 1,
+ 'timeduration' => 86400,
+ 'visible' => 1
+ ]);
+
+ // The max limit should be bounded by the timeclose value.
+ list ($min, $max) = mod_data_core_calendar_get_valid_event_timestart_range($event, $data);
+ $this->assertNull($min);
+ $this->assertEquals($timeclose, $max[0]);
+
+ // No timeclose value should result in no upper limit.
+ $data->timeavailableto = 0;
+ list ($min, $max) = mod_data_core_calendar_get_valid_event_timestart_range($event, $data);
+ $this->assertNull($min);
+ $this->assertNull($max);
+ }
+
+ /**
+ * The close event should be limited by the data's timeavailablefrom property, if it's set.
+ */
+ public function test_mod_data_core_calendar_get_valid_event_timestart_range_close_event() {
+ global $CFG;
+
+ require_once($CFG->dirroot . "/calendar/lib.php");
+
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course();
+ $timeopen = time();
+ $timeclose = $timeopen + DAYSECS;
+ $data = new \stdClass();
+ $data->timeavailablefrom = $timeopen;
+ $data->timeavailableto = $timeclose;
+
+ // Create a valid event.
+ $event = new \calendar_event([
+ 'name' => 'Test event',
+ 'description' => '',
+ 'format' => 1,
+ 'courseid' => $course->id,
+ 'groupid' => 0,
+ 'userid' => 2,
+ 'modulename' => 'data',
+ 'instance' => 1,
+ 'eventtype' => DATA_EVENT_TYPE_CLOSE,
+ 'timestart' => 1,
+ 'timeduration' => 86400,
+ 'visible' => 1
+ ]);
+
+ // The max limit should be bounded by the timeclose value.
+ list ($min, $max) = mod_data_core_calendar_get_valid_event_timestart_range($event, $data);
+ $this->assertEquals($timeopen, $min[0]);
+ $this->assertNull($max);
+
+ // No timeavailableto value should result in no upper limit.
+ $data->timeavailablefrom = 0;
+ list ($min, $max) = mod_data_core_calendar_get_valid_event_timestart_range($event, $data);
+ $this->assertNull($min);
+ $this->assertNull($max);
+ }
}
$string['ongoingcustom'] = 'You have earned {$a->score} point(s) out of {$a->currenthigh} point(s) thus far.';
$string['ongoingnormal'] = 'You have answered {$a->correct} correctly out of {$a->viewed} attempts.';
$string['onpostperpage'] = 'Only one posting per grade';
+$string['openafterclose'] = 'You have specified an open date after the close date';
$string['options'] = 'Options';
$string['or'] = 'OR';
$string['ordered'] = 'Ordered';
defined('MOODLE_INTERNAL') || die();
+// Event types.
+define('LESSON_EVENT_TYPE_OPEN', 'open');
+define('LESSON_EVENT_TYPE_CLOSE', 'close');
+
/* Do not include any libraries here! */
/**
}
return $descriptions;
}
+
+/**
+ * This function calculates the minimum and maximum cutoff values for the timestart of
+ * the given event.
+ *
+ * It will return an array with two values, the first being the minimum cutoff value and
+ * the second being the maximum cutoff value. Either or both values can be null, which
+ * indicates there is no minimum or maximum, respectively.
+ *
+ * If a cutoff is required then the function must return an array containing the cutoff
+ * timestamp and error string to display to the user if the cutoff value is violated.
+ *
+ * A minimum and maximum cutoff return value will look like:
+ * [
+ * [1505704373, 'The due date must be after the start date'],
+ * [1506741172, 'The due date must be before the cutoff date']
+ * ]
+ *
+ * @param calendar_event $event The calendar event to get the time range for
+ * @param stdClass $instance The module instance to get the range from
+ * @return array
+ */
+function mod_lesson_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $instance) {
+ $mindate = null;
+ $maxdate = null;
+
+ if ($event->eventtype == LESSON_EVENT_TYPE_OPEN) {
+ // The start time of the open event can't be equal to or after the
+ // close time of the lesson activity.
+ if (!empty($instance->deadline)) {
+ $maxdate = [
+ $instance->deadline,
+ get_string('openafterclose', 'lesson')
+ ];
+ }
+ } else if ($event->eventtype == LESSON_EVENT_TYPE_CLOSE) {
+ // The start time of the close event can't be equal to or earlier than the
+ // open time of the lesson activity.
+ if (!empty($instance->available)) {
+ $mindate = [
+ $instance->available,
+ get_string('closebeforeopen', 'lesson')
+ ];
+ }
+ }
+
+ return [$mindate, $maxdate];
+}
+
+/**
+ * This function will update the lesson module according to the
+ * event that has been modified.
+ *
+ * It will set the available or deadline value of the lesson instance
+ * according to the type of event provided.
+ *
+ * @throws \moodle_exception
+ * @param \calendar_event $event
+ * @param stdClass $lesson The module instance to get the range from
+ */
+function mod_lesson_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $lesson) {
+ global $DB;
+
+ if (empty($event->instance) || $event->modulename != 'lesson') {
+ return;
+ }
+
+ if ($event->instance != $lesson->id) {
+ return;
+ }
+
+ if (!in_array($event->eventtype, [LESSON_EVENT_TYPE_OPEN, LESSON_EVENT_TYPE_CLOSE])) {
+ return;
+ }
+
+ $courseid = $event->courseid;
+ $modulename = $event->modulename;
+ $instanceid = $event->instance;
+ $modified = false;
+
+ $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid];
+ $context = context_module::instance($coursemodule->id);
+
+ // The user does not have the capability to modify this activity.
+ if (!has_capability('moodle/course:manageactivities', $context)) {
+ return;
+ }
+
+ if ($event->eventtype == LESSON_EVENT_TYPE_OPEN) {
+ // If the event is for the lesson activity opening then we should
+ // set the start time of the lesson activity to be the new start
+ // time of the event.
+ if ($lesson->available != $event->timestart) {
+ $lesson->available = $event->timestart;
+ $lesson->timemodified = time();
+ $modified = true;
+ }
+ } else if ($event->eventtype == LESSON_EVENT_TYPE_CLOSE) {
+ // If the event is for the lesson activity closing then we should
+ // set the end time of the lesson activity to be the new start
+ // time of the event.
+ if ($lesson->deadline != $event->timestart) {
+ $lesson->deadline = $event->timestart;
+ $modified = true;
+ }
+ }
+
+ if ($modified) {
+ $lesson->timemodified = time();
+ $DB->update_record('lesson', $lesson);
+ $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context);
+ $event->trigger();
+ }
+}
/** Answer format is HTML */
define("LESSON_ANSWER_HTML", "HTML");
-// Event types.
-define('LESSON_EVENT_TYPE_OPEN', 'open');
-define('LESSON_EVENT_TYPE_CLOSE', 'close');
-
//////////////////////////////////////////////////////////////////////////////////////
/// Any other lesson functions go here. Each of them must have a name that
/// starts with lesson_
$this->add_message(get_string('numberofpagesviewednotice', 'lesson', $a));
}
- if (!$reviewmode && !$this->properties->retake) {
+ if (!$reviewmode && $this->properties->ongoing) {
$this->add_message(get_string("numberofcorrectanswers", "lesson", $gradeinfo->earned), 'notify');
if ($this->properties->grade != GRADE_TYPE_NONE) {
$a = new stdClass;
$this->assertEquals(mod_lesson_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions);
$this->assertEquals(mod_lesson_get_completion_active_rule_descriptions(new stdClass()), []);
}
+
+ /**
+ * An unknown event type should not change the lesson instance.
+ */
+ public function test_mod_lesson_core_calendar_event_timestart_updated_unknown_event() {
+ global $CFG, $DB;
+ require_once($CFG->dirroot . "/calendar/lib.php");
+
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course();
+ $lessongenerator = $generator->get_plugin_generator('mod_lesson');
+ $timeopen = time();
+ $timeclose = $timeopen + DAYSECS;
+ $lesson = $lessongenerator->create_instance(['course' => $course->id]);
+ $lesson->available = $timeopen;
+ $lesson->deadline = $timeclose;
+ $DB->update_record('lesson', $lesson);
+
+ // Create a valid event.
+ $event = new \calendar_event([
+ 'name' => 'Test event',
+ 'description' => '',
+ 'format' => 1,
+ 'courseid' => $course->id,
+ 'groupid' => 0,
+ 'userid' => 2,
+ 'modulename' => 'lesson',
+ 'instance' => $lesson->id,
+ 'eventtype' => LESSON_EVENT_TYPE_OPEN . "SOMETHING ELSE",
+ 'timestart' => 1,
+ 'timeduration' => 86400,
+ 'visible' => 1
+ ]);
+
+ mod_lesson_core_calendar_event_timestart_updated($event, $lesson);
+ $lesson = $DB->get_record('lesson', ['id' => $lesson->id]);
+ $this->assertEquals($timeopen, $lesson->available);
+ $this->assertEquals($timeclose, $lesson->deadline);
+ }
+
+ /**
+ * A LESSON_EVENT_TYPE_OPEN event should update the available property of the lesson activity.
+ */
+ public function test_mod_lesson_core_calendar_event_timestart_updated_open_event() {
+ global $CFG, $DB;
+ require_once($CFG->dirroot . "/calendar/lib.php");
+
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course();
+ $lessongenerator = $generator->get_plugin_generator('mod_lesson');
+ $timeopen = time();
+ $timeclose = $timeopen + DAYSECS;
+ $timemodified = 1;
+ $newtimeopen = $timeopen - DAYSECS;
+ $lesson = $lessongenerator->create_instance(['course' => $course->id]);
+ $lesson->available = $timeopen;
+ $lesson->deadline = $timeclose;
+ $lesson->timemodified = $timemodified;
+ $DB->update_record('lesson', $lesson);
+
+ // Create a valid event.
+ $event = new \calendar_event([
+ 'name' => 'Test event',
+ 'description' => '',
+ 'format' => 1,
+ 'courseid' => $course->id,
+ 'groupid' => 0,
+ 'userid' => 2,
+ 'modulename' => 'lesson',
+ 'instance' => $lesson->id,
+ 'eventtype' => LESSON_EVENT_TYPE_OPEN,
+ 'timestart' => $newtimeopen,
+ 'timeduration' => 86400,
+ 'visible' => 1
+ ]);
+
+ // Trigger and capture the event when adding a contact.
+ $sink = $this->redirectEvents();
+ mod_lesson_core_calendar_event_timestart_updated($event, $lesson);
+ $triggeredevents = $sink->get_events();
+ $moduleupdatedevents = array_filter($triggeredevents, function($e) {
+ return is_a($e, 'core\event\course_module_updated');
+ });
+ $lesson = $DB->get_record('lesson', ['id' => $lesson->id]);
+
+ // Ensure the available property matches the event timestart.
+ $this->assertEquals($newtimeopen, $lesson->available);
+
+ // Ensure the deadline isn't changed.
+ $this->assertEquals($timeclose, $lesson->deadline);
+
+ // Ensure the timemodified property has been changed.
+ $this->assertNotEquals($timemodified, $lesson->timemodified);
+
+ // Confirm that a module updated event is fired when the module is changed.
+ $this->assertNotEmpty($moduleupdatedevents);
+ }
+
+ /**
+ * A LESSON_EVENT_TYPE_CLOSE event should update the deadline property of the lesson activity.
+ */
+ public function test_mod_lesson_core_calendar_event_timestart_updated_close_event() {
+ global $CFG, $DB;
+ require_once($CFG->dirroot . "/calendar/lib.php");
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course();
+ $lessongenerator = $generator->get_plugin_generator('mod_lesson');
+ $timeopen = time();
+ $timeclose = $timeopen + DAYSECS;
+ $timemodified = 1;
+ $newtimeclose = $timeclose + DAYSECS;
+ $lesson = $lessongenerator->create_instance(['course' => $course->id]);
+ $lesson->available = $timeopen;
+ $lesson->deadline = $timeclose;
+ $lesson->timemodified = $timemodified;
+ $DB->update_record('lesson', $lesson);
+ // Create a valid event.
+ $event = new \calendar_event([
+ 'name' => 'Test event',
+ 'description' => '',
+ 'format' => 1,
+ 'courseid' => $course->id,
+ 'groupid' => 0,
+ 'userid' => 2,
+ 'modulename' => 'lesson',
+ 'instance' => $lesson->id,
+ 'eventtype' => LESSON_EVENT_TYPE_CLOSE,
+ 'timestart' => $newtimeclose,
+ 'timeduration' => 86400,
+ 'visible' => 1
+ ]);
+ // Trigger and capture the event when adding a contact.
+ $sink = $this->redirectEvents();
+ mod_lesson_core_calendar_event_timestart_updated($event, $lesson);
+ $triggeredevents = $sink->get_events();
+ $moduleupdatedevents = array_filter($triggeredevents, function($e) {
+ return is_a($e, 'core\event\course_module_updated');
+ });
+ $lesson = $DB->get_record('lesson', ['id' => $lesson->id]);
+ // Ensure the deadline property matches the event timestart.
+ $this->assertEquals($newtimeclose, $lesson->deadline);
+ // Ensure the available isn't changed.
+ $this->assertEquals($timeopen, $lesson->available);
+ // Ensure the timemodified property has been changed.
+ $this->assertNotEquals($timemodified, $lesson->timemodified);
+ // Confirm that a module updated event is fired when the module is changed.
+ $this->assertNotEmpty($moduleupdatedevents);
+ }
+
+ /**
+ * An unknown event type should not have any limits.
+ */
+ public function test_mod_lesson_core_calendar_get_valid_event_timestart_range_unknown_event() {
+ global $CFG;
+ require_once($CFG->dirroot . "/calendar/lib.php");
+
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course();
+ $timeopen = time();
+ $timeclose = $timeopen + DAYSECS;
+ $lesson = new \stdClass();
+ $lesson->available = $timeopen;
+ $lesson->deadline = $timeclose;
+
+ // Create a valid event.
+ $event = new \calendar_event([
+ 'name' => 'Test event',
+ 'description' => '',
+ 'format' => 1,
+ 'courseid' => $course->id,
+ 'groupid' => 0,
+ 'userid' => 2,
+ 'modulename' => 'lesson',
+ 'instance' => 1,
+ 'eventtype' => LESSON_EVENT_TYPE_OPEN . "SOMETHING ELSE",
+ 'timestart' => 1,
+ 'timeduration' => 86400,
+ 'visible' => 1
+ ]);
+
+ list ($min, $max) = mod_lesson_core_calendar_get_valid_event_timestart_range($event, $lesson);
+ $this->assertNull($min);
+ $this->assertNull($max);
+ }
+
+ /**
+ * The open event should be limited by the lesson's deadline property, if it's set.
+ */
+ public function test_mod_lesson_core_calendar_get_valid_event_timestart_range_open_event() {
+ global $CFG;
+ require_once($CFG->dirroot . "/calendar/lib.php");
+
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course();
+ $timeopen = time();
+ $timeclose = $timeopen + DAYSECS;
+ $lesson = new \stdClass();
+ $lesson->available = $timeopen;
+ $lesson->deadline = $timeclose;
+
+ // Create a valid event.
+ $event = new \calendar_event([
+ 'name' => 'Test event',
+ 'description' => '',
+ 'format' => 1,
+ 'courseid' => $course->id,
+ 'groupid' => 0,
+ 'userid' => 2,
+ 'modulename' => 'lesson',
+ 'instance' => 1,
+ 'eventtype' => LESSON_EVENT_TYPE_OPEN,
+ 'timestart' => 1,
+ 'timeduration' => 86400,
+ 'visible' => 1
+ ]);
+
+ // The max limit should be bounded by the timeclose value.
+ list ($min, $max) = mod_lesson_core_calendar_get_valid_event_timestart_range($event, $lesson);
+ $this->assertNull($min);
+ $this->assertEquals($timeclose, $max[0]);
+
+ // No timeclose value should result in no upper limit.
+ $lesson->deadline = 0;
+ list ($min, $max) = mod_lesson_core_calendar_get_valid_event_timestart_range($event, $lesson);
+ $this->assertNull($min);
+ $this->assertNull($max);
+ }
+
+ /**
+ * The close event should be limited by the lesson's available property, if it's set.
+ */
+ public function test_mod_lesson_core_calendar_get_valid_event_timestart_range_close_event() {
+ global $CFG;
+ require_once($CFG->dirroot . "/calendar/lib.php");
+
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course();
+ $timeopen = time();
+ $timeclose = $timeopen + DAYSECS;
+ $lesson = new \stdClass();
+ $lesson->available = $timeopen;
+ $lesson->deadline = $timeclose;
+
+ // Create a valid event.
+ $event = new \calendar_event([
+ 'name' => 'Test event',
+ 'description' => '',
+ 'format' => 1,
+ 'courseid' => $course->id,
+ 'groupid' => 0,
+ 'userid' => 2,
+ 'modulename' => 'lesson',
+ 'instance' => 1,
+ 'eventtype' => LESSON_EVENT_TYPE_CLOSE,
+ 'timestart' => 1,
+ 'timeduration' => 86400,
+ 'visible' => 1
+ ]);
+
+ // The max limit should be bounded by the timeclose value.
+ list ($min, $max) = mod_lesson_core_calendar_get_valid_event_timestart_range($event, $lesson);
+ $this->assertEquals($timeopen, $min[0]);
+ $this->assertNull($max);
+
+ // No deadline value should result in no upper limit.
+ $lesson->available = 0;
+ list ($min, $max) = mod_lesson_core_calendar_get_valid_event_timestart_range($event, $lesson);
+ $this->assertNull($min);
+ $this->assertNull($max);
+ }
}
// Print the header.
$strquizzes = get_string("modulenameplural", "quiz");
-$streditquestions = '';
-$editqcontexts = new question_edit_contexts($coursecontext);
-if ($editqcontexts->have_one_edit_tab_cap('questions')) {
- $streditquestions =
- "<form target=\"_parent\" method=\"get\" action=\"$CFG->wwwroot/question/edit.php\">
- <div>
- <input type=\"hidden\" name=\"courseid\" value=\"$course->id\" />
- <input type=\"submit\" value=\"".get_string("editquestions", "quiz")."\" />
- </div>
- </form>";
-}
$PAGE->navbar->add($strquizzes);
$PAGE->set_title($strquizzes);
-$PAGE->set_button($streditquestions);
$PAGE->set_heading($course->fullname);
echo $OUTPUT->header();
echo $OUTPUT->heading($strquizzes, 2);
// Get relevant data indexed by positionkey from the multianswers table.
$sequence = $DB->get_field('question_multianswer', 'sequence',
- array('question' => $question->id), '*', MUST_EXIST);
+ array('question' => $question->id), MUST_EXIST);
$wrappedquestions = $DB->get_records_list('question', 'id',
explode(',', $sequence), 'id ASC');
*/
protected $skippeddocs = 0;
+ /**
+ * Solr server major version.
+ *
+ * @var int
+ */
+ protected $solrmajorversion = null;
+
/**
* Initialises the search engine configuration.
*
$url = $this->get_connection_url('/update/extract');
+ // Return results as XML.
+ $url->param('wt', 'xml');
+
// This will prevent solr from automatically making fields for every tika output.
$url->param('uprefix', 'ignored_');
* @return int
*/
public function get_solr_major_version() {
+ if ($this->solrmajorversion !== null) {
+ return $this->solrmajorversion;
+ }
+
// We should really ping first the server to see if the specified indexname is valid but
// we want to minimise solr server requests as they are expensive. system() emits a warning
// if it can not connect to the configured index in the configured server.
$systemdata = @$this->get_search_client()->system();
$solrversion = $systemdata->getResponse()->offsetGet('lucene')->offsetGet('solr-spec-version');
- return intval(substr($solrversion, 0, strpos($solrversion, '.')));
+ $this->solrmajorversion = intval(substr($solrversion, 0, strpos($solrversion, '.')));
+
+ return $this->solrmajorversion;
}
/**
*/
public function can_setup_server() {
- $engine = new \search_solr\engine();
- $status = $engine->is_server_configured();
+ $status = $this->engine->is_server_configured();
if ($status !== true) {
return $status;
}
// At this stage we know that the server is properly configured with a valid host:port and indexname.
// We're not too concerned about repeating the SolrClient::system() call (already called in
// is_server_configured) because this is just a setup script.
- if ($engine->get_solr_major_version() < 5) {
+ if ($this->engine->get_solr_major_version() < 5) {
// Schema setup script only available for 5.0 onwards.
return get_string('schemasetupfromsolr5', 'search_solr');
}
if (!isset($data['type']) || !isset($data['stored']) || !isset($data['indexed'])) {
throw new \coding_exception($fieldname . ' does not define all required field params: type, stored and indexed.');
}
+ $type = $this->doc_field_to_solr_field($data['type']);
+
// Changing default multiValued value to false as we want to match values easily.
$params = array(
'add-field' => array(
'name' => $fieldname,
- 'type' => ($data['type'] === 'text' ? 'text_general' : $data['type']),
+ 'type' => $type,
'stored' => $data['stored'],
'multiValued' => false,
'indexed' => $data['indexed']
// All these field attributes are set when fields are added through this script and should
// be returned and match the defined field's values.
+ $expectedsolrfield = $this->doc_field_to_solr_field($data['type']);
if (empty($results->field) || !isset($results->field->type) ||
!isset($results->field->multiValued) || !isset($results->field->indexed) ||
!isset($results->field->stored)) {
throw new \moodle_exception('errorcreatingschema', 'search_solr', '',
get_string('schemafieldautocreated', 'search_solr', $fieldname));
- } else if (($results->field->type !== $data['type'] &&
- ($data['type'] !== 'text' || $results->field->type !== 'text_general')) ||
- $results->field->multiValued !== false ||
- $results->field->indexed !== $data['indexed'] ||
- $results->field->stored !== $data['stored']) {
+ } else if ($results->field->type !== $expectedsolrfield ||
+ $results->field->multiValued !== false ||
+ $results->field->indexed !== $data['indexed'] ||
+ $results->field->stored !== $data['stored']) {
- throw new \moodle_exception('errorcreatingschema', 'search_solr', '',
- get_string('schemafieldautocreated', 'search_solr', $fieldname));
+ throw new \moodle_exception('errorcreatingschema', 'search_solr', '',
+ get_string('schemafieldautocreated', 'search_solr', $fieldname));
} else {
// The field already exists and it is properly defined, no need to create it.
unset($fields[$fieldname]);
}
}
+
+ /**
+ * Returns the solr field type from the document field type string.
+ *
+ * @param string $datatype
+ * @return string
+ */
+ private function doc_field_to_solr_field($datatype) {
+ $type = $datatype;
+
+ $solrversion = $this->engine->get_solr_major_version();
+
+ switch($datatype) {
+ case 'text':
+ $type = 'text_general';
+ break;
+ case 'int':
+ if ($solrversion >= 7) {
+ $type = 'pint';
+ }
+ break;
+ case 'tdate':
+ if ($solrversion >= 7) {
+ $type = 'pdate';
+ }
+ break;
+ }
+
+ return $type;
+ }
}
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2017111300;
+$plugin->version = 2017111700;
$plugin->requires = 2017110800;
$plugin->component = 'search_solr';
// Find the specified activity.
$idnumber = $input['idnumber'];
- $cmid = $DB->get_field('course_modules', 'id', ['idnumber' => $idnumber], '*', IGNORE_MISSING);
+ $cmid = $DB->get_field('course_modules', 'id', ['idnumber' => $idnumber], IGNORE_MISSING);
if (!$cmid) {
throw new Exception('Cannot find activity with idnumber: ' . $idnumber);
}
.path-mod-lesson .invisiblefieldset.fieldsetfix {
display: block;
}
+.path-mod-lesson .answeroption .checkbox label p {
+ display: inline;
+}
+#page-mod-lesson-view .branchbuttoncontainer .singlebutton button[type="submit"] {
+ white-space: normal;
+}
.path-mod-wiki .wiki_headingtitle,
.path-mod-wiki .midpad,
{{< core_form/element-template-inline }}
{{$element}}
{{^element.frozen}}
+ <input type="hidden" name="{{element.nameraw}}" value="_qf__force_multiselect_submission">
<select class="custom-select {{#error}}form-control-danger{{/error}}" name="{{element.name}}"
id="{{element.id}}"
{{#element.multiple}}multiple{{/element.multiple}}
{{< core_form/element-template }}
{{$element}}
{{^element.frozen}}
+ <input type="hidden" name="{{element.nameraw}}" value="_qf__force_multiselect_submission">
<select class="custom-select {{#error}}form-control-danger{{/error}}" name="{{element.name}}"
id="{{element.id}}"
{{#element.multiple}}multiple{{/element.multiple}}
.path-mod-lesson .answeroptiongroup .felement label p:last-child {
margin-bottom: 0;
}
+.path-mod-lesson .answeroption .fradio label p,
+.path-mod-lesson .answeroption .fcheckbox label p {
+ display: inline;
+}
/**
* Style for view.php
#page-mod-lesson-view .password-form .submitbutton {
display: inline;
}
+#page-mod-lesson-view .branchbuttoncontainer .singlebutton input[type="submit"] {
+ white-space: normal;
+}
.path-mod-lesson .reviewessay {
width: 40%;
border: 1px solid #ddd;
.path-mod-lesson .answeroptiongroup .felement label p:last-child {
margin-bottom: 0;
}
+.path-mod-lesson .answeroption .fradio label p,
+.path-mod-lesson .answeroption .fcheckbox label p {
+ display: inline;
+}
/**
* Style for view.php
**/
#page-mod-lesson-view .password-form .submitbutton {
display: inline;
}
+#page-mod-lesson-view .branchbuttoncontainer .singlebutton input[type="submit"] {
+ white-space: normal;
+}
.path-mod-lesson .reviewessay {
width: 40%;
border: 1px solid #ddd;
defined('MOODLE_INTERNAL') || die();
-$version = 2017120800.00; // YYYYMMDD = weekly release date of this DEV branch.
+$version = 2017121400.00; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
-$release = '3.5dev (Build: 20171208)'; // Human-friendly version name
+$release = '3.5dev (Build: 20171214)'; // Human-friendly version name
$branch = '35'; // This version's branch.
$maturity = MATURITY_ALPHA; // This version's maturity level.