lib/babel-polyfill/
lib/polyfills/
lib/emoji-data/
+lib/plist/
media/player/videojs/amd/src/video-lazy.js
media/player/videojs/amd/src/Youtube-lazy.js
media/player/videojs/videojs/
--- /dev/null
+# Primary donations pages.
+custom: ["https://moodle.com/donations/", moodle.org]
lib/babel-polyfill/
lib/polyfills/
lib/emoji-data/
+lib/plist/
media/player/videojs/amd/src/video-lazy.js
media/player/videojs/amd/src/Youtube-lazy.js
media/player/videojs/videojs/
# process (which uses our internal CI system) this file is here for the benefit
# of community developers git clones - see MDL-51458.
-sudo: required
-
# We currently disable Travis notifications entirely until https://github.com/travis-ci/travis-ci/issues/4976
# is fixed.
notifications:
language: php
+os: linux
+
dist: xenial
services:
php:
# We only run the highest and lowest supported versions to reduce the load on travis-ci.org.
- - 7.3
+ - 7.4
- 7.2
addons:
# Perform an upgrade test too.
- DB=pgsql TASK=UPGRADE
-matrix:
+jobs:
# Enable fast finish.
# This will fail the build if a single job fails (except those in allow_failures).
# It will not stop the jobs from running.
fast_finish: true
include:
- # Run mysql only on 7.3 - it's just too slow
- - php: 7.3
+ # Run mysql only on highest - it's just too slow
+ - php: 7.4
env: DB=mysqli TASK=PHPUNIT
- # Run grunt/npm install on highest version ('node' is an alias for the latest node.js version.)
- - php: 7.2
+ # Run grunt/npm install on highest version too ('node' is an alias for the latest node.js version.)
+ - php: 7.4
env: DB=none TASK=GRUNT NVM_VERSION='lts/carbon'
cache:
}
} else {
$newsettings = array();
- if (array_key_exists($componentprovidersetting, $form)) {
+ if (property_exists($form, $componentprovidersetting)) {
// We must be processing loggedin or loggedoff checkboxes.
// Store defained comma-separated processors as setting value.
// Using array_filter eliminates elements set to 0 above.
$output = '';
if ($checker->enabled()) {
- $output .= $this->container_start('checkforupdates');
+ $output .= $this->container_start('checkforupdates mb-4');
$output .= $this->single_button(
new moodle_url($reloadurl, array('fetchupdates' => 1)),
get_string('checkforupdates', 'core_plugin')
);
if ($timefetched = $checker->get_last_timefetched()) {
$timefetched = userdate($timefetched, get_string('strftimedatetime', 'core_langconfig'));
- $output .= $this->container(get_string('checkforupdateslast', 'core_plugin', $timefetched), 'lasttimefetched');
+ $output .= $this->container(get_string('checkforupdateslast', 'core_plugin', $timefetched),
+ 'lasttimefetched small text-muted mt-1');
}
$output .= $this->container_end();
}
}
}
- $updateinfo .= $this->container_start('checkforupdates');
+ $updateinfo .= $this->container_start('checkforupdates mt-1');
$fetchurl = new moodle_url('/admin/index.php', array('fetchupdates' => 1, 'sesskey' => sesskey(), 'cache' => 0));
$updateinfo .= $this->single_button($fetchurl, get_string('checkforupdates', 'core_plugin'));
if ($fetch) {
*/
protected function moodle_available_update_info(\core\update\info $updateinfo) {
- $boxclasses = 'moodleupdateinfo';
+ $boxclasses = 'moodleupdateinfo mb-2';
$info = array();
if (isset($updateinfo->release)) {
}
if (isset($updateinfo->download)) {
- $info[] = html_writer::link($updateinfo->download, get_string('download'), array('class' => 'info download'));
+ $info[] = html_writer::link($updateinfo->download, get_string('download'),
+ array('class' => 'info download btn btn-secondary'));
}
if (isset($updateinfo->url)) {
array('class' => 'info more'));
}
- $box = $this->output->box_start($boxclasses);
- $box .= $this->output->box(implode(html_writer::tag('span', ' ', array('class' => 'separator')), $info), '');
- $box .= $this->output->box_end();
+ $box = $this->output->container_start($boxclasses);
+ $box .= $this->output->container(implode(html_writer::tag('span', ' | ', array('class' => 'separator')), $info), '');
+ $box .= $this->output->container_end();
return $box;
}
$displayname = new html_table_cell(
$icon.
html_writer::span($plugin->displayname, 'pluginname').
- html_writer::div($plugin->get_dir(), 'plugindir')
+ html_writer::div($plugin->get_dir(), 'plugindir text-muted small')
);
$versiondb = new html_table_cell($plugin->versiondb);
break;
case core_plugin_manager::PLUGIN_STATUS_NODB:
case core_plugin_manager::PLUGIN_STATUS_UPTODATE:
- $statusclass .= $dependenciesok ? '' : 'badge-warning';
+ $statusclass .= $dependenciesok ? 'badge-light' : 'badge-warning';
break;
}
$status = html_writer::span(get_string('status_' . $statuscode, 'core_plugin'), $statusclass);
new moodle_url($this->page->url, array('abortinstall' => $plugin->component)),
get_string('cancelinstallone', 'core_plugin'),
'post',
- array('class' => 'actionbutton cancelinstallone')
+ array('class' => 'actionbutton cancelinstallone d-block mt-1')
);
}
new moodle_url($this->page->url, array('abortupgrade' => $plugin->component)),
get_string('cancelupgradeone', 'core_plugin'),
'post',
- array('class' => 'actionbutton cancelupgradeone')
+ array('class' => 'actionbutton cancelupgradeone d-block mt-1')
);
}
}
}
- $out .= $this->output->container_start('actions');
+ $out .= $this->output->container_start('actions mb-2');
$installableupdates = $pluginman->filter_installable($pluginman->available_updates());
if ($installableupdates) {
new moodle_url($this->page->url, array('installupdatex' => 1)),
get_string('updateavailableinstallall', 'core_admin', count($installableupdates)),
'post',
- array('class' => 'singlebutton updateavailableinstallall')
+ array('class' => 'singlebutton updateavailableinstallall mr-1')
);
}
new moodle_url($this->page->url, array('abortinstallx' => 1)),
get_string('cancelinstallall', 'core_plugin', count($installabortable)),
'post',
- array('class' => 'singlebutton cancelinstallall')
+ array('class' => 'singlebutton cancelinstallall mr-1')
);
}
new moodle_url($this->page->url, array('abortupgradex' => 1)),
get_string('cancelupgradeall', 'core_plugin', count($upgradeabortable)),
'post',
- array('class' => 'singlebutton cancelupgradeall')
+ array('class' => 'singlebutton cancelupgradeall mr-1')
);
}
$out .= html_writer::div(html_writer::link(new moodle_url($this->page->url, array('showallplugins' => 0)),
- get_string('plugincheckattention', 'core_plugin')).' '.html_writer::span($sumattention, 'badge'));
+ get_string('plugincheckattention', 'core_plugin')).' '.html_writer::span($sumattention, 'badge badge-light'),
+ 'btn btn-link mr-1');
$out .= html_writer::div(html_writer::link(new moodle_url($this->page->url, array('showallplugins' => 1)),
- get_string('plugincheckall', 'core_plugin')).' '.html_writer::span($sumtotal, 'badge'));
+ get_string('plugincheckall', 'core_plugin')).' '.html_writer::span($sumtotal, 'badge badge-light'),
+ 'btn btn-link mr-1');
$out .= $this->output->container_end(); // End of .actions container.
$out .= $this->output->container_end(); // End of #plugins-check-info container.
}
}
- $out = $this->output->container_start('plugins-check-dependencies');
+ $out = $this->output->container_start('plugins-check-dependencies mb-4');
if ($unavailable or $unknown) {
$out .= $this->output->heading(get_string('misdepsunavail', 'core_plugin'));
if ($unknown) {
- $out .= $this->output->notification(get_string('misdepsunknownlist', 'core_plugin', implode(', ', $unknown)));
+ $out .= $this->output->render((new \core\output\notification(get_string('misdepsunknownlist', 'core_plugin',
+ implode(', ', $unknown))))->set_show_closebutton(false));
}
if ($unavailable) {
$unavailablelist = array();
}
$unavailablelist[] = $unavailablelistitem;
}
- $out .= $this->output->notification(get_string('misdepsunavaillist', 'core_plugin',
- implode(', ', $unavailablelist)));
+ $out .= $this->output->render((new \core\output\notification(get_string('misdepsunavaillist', 'core_plugin',
+ implode(', ', $unavailablelist))))->set_show_closebutton(false));
}
- $out .= $this->output->container_start('plugins-check-dependencies-actions');
+ $out .= $this->output->container_start('plugins-check-dependencies-actions mb-4');
$out .= ' '.html_writer::link(new moodle_url('/admin/tool/installaddon/'),
- get_string('dependencyuploadmissing', 'core_plugin'));
+ get_string('dependencyuploadmissing', 'core_plugin'), array('class' => 'btn btn-secondary'));
$out .= $this->output->container_end(); // End of .plugins-check-dependencies-actions container.
}
if ($available) {
$out .= $this->output->heading(get_string('misdepsavail', 'core_plugin'));
- $out .= $this->output->container_start('plugins-check-dependencies-actions');
+ $out .= $this->output->container_start('plugins-check-dependencies-actions mb-2');
$installable = $pluginman->filter_installable($available);
if ($installable) {
new moodle_url($this->page->url, array('installdepx' => 1)),
get_string('dependencyinstallmissing', 'core_plugin', count($installable)),
'post',
- array('class' => 'singlebutton dependencyinstallmissing')
+ array('class' => 'singlebutton dependencyinstallmissing d-inline-block mr-1')
);
}
$out .= html_writer::div(html_writer::link(new moodle_url('/admin/tool/installaddon/'),
- get_string('dependencyuploadmissing', 'core_plugin')), 'dependencyuploadmissing');
+ get_string('dependencyuploadmissing', 'core_plugin'), array('class' => 'btn btn-link')),
+ 'dependencyuploadmissing d-inline-block mr-1');
$out .= $this->output->container_end(); // End of .plugins-check-dependencies-actions container.
if ($CFG->branch == str_replace('.', '', $moodle->release)) {
$supportedmoodles[] = html_writer::span($moodle->release, 'badge badge-success');
} else {
- $supportedmoodles[] = html_writer::span($moodle->release, 'label');
+ $supportedmoodles[] = html_writer::span($moodle->release, 'badge badge-light');
}
}
}
$info = html_writer::div(
get_string('requiredby', 'core_plugin', implode(', ', $requriedby)),
- 'requiredby'
+ 'requiredby mb-1'
);
} else {
$info = '';
$info .= html_writer::div(
html_writer::link('https://moodle.org/plugins/view.php?plugin='.$plugin->component,
get_string('misdepinfoplugin', 'core_plugin')),
- 'misdepinfoplugin'
+ 'misdepinfoplugin d-inline-block mr-3 mb-1'
);
$info .= html_writer::div(
html_writer::link('https://moodle.org/plugins/pluginversion.php?id='.$plugin->version->id,
get_string('misdepinfoversion', 'core_plugin')),
- 'misdepinfoversion'
+ 'misdepinfoversion d-inline-block mr-3 mb-1'
);
- $info .= html_writer::div(html_writer::link($plugin->version->downloadurl, get_string('download')), 'misdepdownload');
+ $info .= html_writer::div(html_writer::link($plugin->version->downloadurl, get_string('download')),
+ 'misdepdownload d-inline-block mr-3 mb-1');
if ($pluginman->is_remote_plugin_installable($plugin->component, $plugin->version->version, $reason)) {
$info .= $this->output->single_button(
new moodle_url($this->page->url, array('installdep' => $plugin->component)),
get_string('dependencyinstall', 'core_plugin'),
'post',
- array('class' => 'singlebutton dependencyinstall')
+ array('class' => 'singlebutton dependencyinstall mr-3 mb-1')
);
} else {
$reasonhelp = $this->info_remote_plugin_not_installable($reason);
if ($reasonhelp) {
- $info .= html_writer::div($reasonhelp, 'reasonhelp dependencyinstall');
+ $info .= html_writer::div($reasonhelp, 'reasonhelp dependencyinstall d-inline-block mr-3 mb-1');
}
}
$info .= $this->output->container_end(); // End of .actions container.
$table->data[] = array(
- html_writer::div($plugin->name, 'name').' '.html_writer::div($plugin->component, 'component'),
+ html_writer::div($plugin->name, 'name').' '.html_writer::div($plugin->component, 'component text-muted small'),
$plugin->version->release,
$plugin->version->version,
implode(' ', $supportedmoodles),
foreach ($requirements as $reqname => $reqinfo) {
if ($reqname === 'core') {
if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_OK) {
- $class = 'requires-ok';
+ $class = 'requires-ok text-muted';
$label = '';
} else {
$class = 'requires-failed';
if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_OK) {
$label = '';
- $class = 'requires-ok';
+ $class = 'requires-ok text-muted';
} else if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_MISSING) {
if ($reqinfo->availability == $pluginman::REQUIREMENT_AVAILABLE) {
return '';
}
- $out = html_writer::tag('ul', implode("\n", $requires));
+ $out = html_writer::tag('ul', implode("\n", $requires), array('class' => 'm-0'));
if ($displayuploadlink) {
$out .= html_writer::div(
html_writer::link(
new moodle_url('/admin/tool/installaddon/'),
- get_string('dependencyuploadmissing', 'core_plugin')
+ get_string('dependencyuploadmissing', 'core_plugin'),
+ array('class' => 'btn btn-secondary btn-sm m-1')
),
'dependencyuploadmissing'
);
$out .= html_writer::div(
html_writer::link(
new moodle_url($this->page->url, array('sesskey' => sesskey(), 'fetchupdates' => 1)),
- get_string('checkforupdates', 'core_plugin')
+ get_string('checkforupdates', 'core_plugin'),
+ array('class' => 'btn btn-secondary btn-sm m-1')
),
'checkforupdates'
);
$temp = new admin_settingpage('experimentalsettings', new lang_string('experimentalsettings', 'admin'));
//TODO: Re-enable cc-import once re-implemented in 2.0.x
//$temp->add(new admin_setting_configcheckbox('enableimsccimport', new lang_string('enable_cc_import', 'imscc'), new lang_string('enable_cc_import_description', 'imscc'), 0));
- $temp->add(new admin_setting_configcheckbox('enablesafebrowserintegration', new lang_string('enablesafebrowserintegration', 'admin'), new lang_string('configenablesafebrowserintegration', 'admin'), 0));
$temp->add(new admin_setting_configcheckbox('dndallowtextandlinks', new lang_string('dndallowtextandlinks', 'admin'), new lang_string('configdndallowtextandlinks', 'admin'), 0));
$string['classname'] = 'Class name';
$string['component'] = 'Component';
-$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, use [server]:[port], for example mail.example.com:587. If a port isn\'t specified, the default port for the type of mail server will be used.';
+$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, use [server]:[port], for example mail.example.com:993. If a port isn\'t specified, the default port for the type of mail server will be used.';
$string['defaultexpiration'] = 'Default address expiry period';
$string['defaultexpiration_help'] = 'When an email address is generated by the handler, it can be set to automatically expire after a period of time, so that it can no longer be used. It is advisable to set an expiry period.';
$string['description'] = 'Description';
const LOGIN_VIA_EMBEDDED_BROWSER = 3;
/** @var int seconds an auto-login key will expire. */
const LOGIN_KEY_TTL = 60;
+ /** @var string URL of the Moodle Apps Portal */
+ const MOODLE_APPS_PORTAL_URL = 'https://apps.moodle.com';
/**
* Returns a list of Moodle plugins supporting the mobile app.
$string['mobilefeatures'] = 'Mobile features';
$string['mobilenotificationsdisabledwarning'] = 'Mobile notifications are not enabled. They should be enabled in Notification settings.';
$string['mobilesettings'] = 'Mobile settings';
+$string['moodleappsportalfeatureswarning'] = 'Please note that some features may be restricted depending on your Moodle app subscription. For details, visit the <a href="{$a}" target="_blank">Moodle Apps Portal</a>.';
$string['oauth2identityproviders'] = 'OAuth 2 identity providers';
$string['offlineuse'] = 'Offline use';
$string['pluginname'] = 'Moodle app tools';
// Show only mobile settings if the mobile service is enabled.
if (!empty($CFG->enablemobilewebservice)) {
+ // General notification about limited features due to app restrictions.
+ $notify = new \core\output\notification(
+ get_string('moodleappsportalfeatureswarning', 'tool_mobile', tool_mobile\api::MOODLE_APPS_PORTAL_URL),
+ \core\output\notification::NOTIFY_WARNING);
+ $featuresnotice = $OUTPUT->render($notify);
+
// Type of login.
$temp = new admin_settingpage('mobileauthentication', new lang_string('mobileauthentication', 'tool_mobile'));
$options = array(
// Appearance related settings.
$temp = new admin_settingpage('mobileappearance', new lang_string('mobileappearance', 'tool_mobile'));
+ $temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeaturesappearance', '', $featuresnotice));
+
$temp->add(new admin_setting_configtext('mobilecssurl', new lang_string('mobilecssurl', 'tool_mobile'),
new lang_string('configmobilecssurl', 'tool_mobile'), '', PARAM_URL));
// Features related settings.
$temp = new admin_settingpage('mobilefeatures', new lang_string('mobilefeatures', 'tool_mobile'));
+ $temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeatures', '', $featuresnotice));
+
$temp->add(new admin_setting_heading('tool_mobile/logout',
new lang_string('logout'), ''));
$CFG->debugdisplay = 1;
set_config('debugauthdb', 1, 'auth_db');
set_config('debugdb', 1, 'enrol_database');
- $expectedissues = array('nohttpsformobilewarning', 'invaliduserquotawarning', 'adodbdebugwarning', 'displayerrorswarning',
- 'mobilenotificationsdisabledwarning');
-
- $processors = get_message_processors();
- foreach ($processors as $processor => $status) {
- if ($processor == 'airnotifier' && $status->enabled) {
- unset($expectedissues['mobilenotificationsdisabledwarning']);
- }
- }
+ $expectedissues = array('nohttpsformobilewarning', 'invaliduserquotawarning', 'adodbdebugwarning', 'displayerrorswarning');
$issues = api::get_potential_config_issues();
$this->assertCount(count($expectedissues), $issues);
And I press "Yes"
And I should see "Recycle bin has been emptied"
And I should see "There are no items in the recycle bin."
+
+ @javascript
+ Scenario: Show recycle bin on category action menu
+ Given I log in as "admin"
+ And I navigate to "Courses > Manage courses and categories" in site administration
+ And I click on "Actions menu" "link"
+ And I click on "Recycle bin" "link"
+ Then I should see "There are no items in the recycle bin."
+
+ @javascript
+ Scenario: Not show recycle bin empty on category action menu whit autohide enable
+ Given I log in as "admin"
+ And the following config values are set as admin:
+ | categorybinenable | 0 | tool_recyclebin |
+ And I navigate to "Courses > Manage courses and categories" in site administration
+ And I click on "Actions menu" "link"
+ Then I should not see "Recycle bin"
+
+ @javascript
+ Scenario: Show recycle bin not empty on category action menu whit autohide enable
+ Given I log in as "admin"
+ And the following config values are set as admin:
+ | autohide | 1 | tool_recyclebin |
+ And I navigate to "Courses > Manage courses and categories" in site administration
+ And I click on "Actions menu" "link"
+ Then I should not see "Recycle bin"
+ And I click on "delete" action for "Course 2" in management course listing
+ And I press "Delete"
+ And I should see "Deleting C2"
+ And I should see "C2 has been completely deleted"
+ And I press "Continue"
+ When I click on "Actions menu" "link"
+ Then I should see "Recycle bin"
*/
$string['asap'] = 'ASAP';
-$string['adhocempty'] = 'Adhoc task queue is empty';
-$string['adhocqueuesize'] = 'Adhoc task queue has {$a} tasks';
+$string['adhocempty'] = 'Ad hoc task queue is empty';
+$string['adhocqueuesize'] = 'Ad hoc task queue has {$a} tasks';
$string['adhocqueueold'] = 'Oldest task is {$a->age} which is more than {$a->max}';
$string['backtoscheduledtasks'] = 'Back to scheduled tasks';
$string['blocking'] = 'Blocking';
$string['cannotfindthepathtothecli'] = 'Cannot find the path to the PHP CLI executable so task execution aborted. Set the \'Path to PHP CLI\' setting in Site administration / Server / System paths.';
-$string['checkadhocqueue'] = 'Adhoc task queue';
+$string['checkadhocqueue'] = 'Ad hoc task queue';
$string['checkcronrunning'] = 'Cron running';
$string['checkmaxfaildelay'] = 'Tasks max fail delay';
$string['clearfaildelay_confirm'] = 'Are you sure you want to clear the fail delay for task \'{$a}\'? After clearing the delay, the task will run according to its normal schedule.';
$string['scheduledtasks'] = 'Scheduled tasks';
$string['scheduledtaskchangesdisabled'] = 'Modifications to the list of scheduled tasks have been prevented in Moodle configuration';
$string['taskdisabled'] = 'Task disabled';
-$string['taskfailures'] = 'There are {$a} task(s) failing';
+$string['taskfailures'] = '{$a} task(s) failing';
$string['tasklogs'] = 'Task logs';
$string['tasknofailures'] = 'There are no tasks failing';
$string['taskscheduleday'] = 'Day';
return $this->errors;
}
+ /**
+ * Return array of valid fields for default values
+ *
+ * @return array
+ */
+ protected function get_valid_fields() {
+ return array_merge(self::$validfields, \tool_uploadcourse_helper::get_custom_course_field_names());
+ }
+
/**
* Assemble the course data based on defaults.
*
* @return array
*/
protected function get_final_create_data($data) {
- foreach (self::$validfields as $field) {
+ foreach ($this->get_valid_fields() as $field) {
if (!isset($data[$field]) && isset($this->defaults[$field])) {
$data[$field] = $this->defaults[$field];
}
global $DB;
$newdata = array();
$existingdata = $DB->get_record('course', array('shortname' => $this->shortname));
- foreach (self::$validfields as $field) {
+ foreach ($this->get_valid_fields() as $field) {
if ($missingonly) {
- if (!is_null($existingdata->$field) and $existingdata->$field !== '') {
+ if (isset($existingdata->$field) and $existingdata->$field !== '') {
continue;
}
}
$coursedata[$rolekey] = $rolename;
}
+ // Custom fields. If the course already exists and mode isn't set to force creation, we can use its context.
+ if ($exists && $mode !== tool_uploadcourse_processor::MODE_CREATE_ALL) {
+ $context = context_course::instance($coursedata['id']);
+ } else {
+ // The category ID is taken from the defaults if it exists, otherwise from course data.
+ $context = context_coursecat::instance($this->defaults['category'] ?? $coursedata['category']);
+ }
+ $customfielddata = tool_uploadcourse_helper::get_custom_course_field_data($this->rawdata, $this->defaults, $context,
+ $errors);
+ if (!empty($errors)) {
+ foreach ($errors as $key => $message) {
+ $this->error($key, $message);
+ }
+
+ return false;
+ }
+
+ foreach ($customfielddata as $name => $value) {
+ $coursedata[$name] = $value;
+ }
+
// Some validation.
if (!empty($coursedata['format']) && !in_array($coursedata['format'], tool_uploadcourse_helper::get_course_formats())) {
$this->error('invalidcourseformat', new lang_string('invalidcourseformat', 'tool_uploadcourse'));
return $rolenames;
}
+ /**
+ * Return array of all custom course fields indexed by their shortname
+ *
+ * @return \core_customfield\field_controller[]
+ */
+ public static function get_custom_course_fields(): array {
+ $result = [];
+
+ $fields = \core_course\customfield\course_handler::create()->get_fields();
+ foreach ($fields as $field) {
+ $result[$field->get('shortname')] = $field;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Return array of custom field element names
+ *
+ * @return string[]
+ */
+ public static function get_custom_course_field_names(): array {
+ $result = [];
+
+ $fields = self::get_custom_course_fields();
+ foreach ($fields as $field) {
+ $controller = \core_customfield\data_controller::create(0, null, $field);
+ $result[] = $controller->get_form_element_name();
+ }
+
+ return $result;
+ }
+
+ /**
+ * Return any elements from passed $data whose key matches one of the custom course fields defined for the site
+ *
+ * @param array $data
+ * @param array $defaults
+ * @param context $context
+ * @param array $errors Will be populated with any errors
+ * @return array
+ */
+ public static function get_custom_course_field_data(array $data, array $defaults, context $context,
+ array &$errors = []): array {
+
+ $fields = self::get_custom_course_fields();
+ $result = [];
+
+ $canchangelockedfields = guess_if_creator_will_have_course_capability('moodle/course:changelockedcustomfields', $context);
+
+ foreach ($data as $name => $originalvalue) {
+ if (preg_match('/^customfield_(?<name>.*)?$/', $name, $matches)
+ && isset($fields[$matches['name']])) {
+
+ $fieldname = $matches['name'];
+ $field = $fields[$fieldname];
+
+ // Skip field if it's locked and user doesn't have capability to change locked fields.
+ if ($field->get_configdata_property('locked') && !$canchangelockedfields) {
+ continue;
+ }
+
+ // Create field data controller.
+ $controller = \core_customfield\data_controller::create(0, null, $field);
+ $controller->set('id', 1);
+
+ $defaultvalue = $defaults["customfield_{$fieldname}"] ?? $controller->get_default_value();
+ $value = (empty($originalvalue) ? $defaultvalue : $field->parse_value($originalvalue));
+
+ // If we initially had a value, but now don't, then reset it to the default.
+ if (!empty($originalvalue) && empty($value)) {
+ $value = $defaultvalue;
+ }
+
+ // Validate data with controller.
+ $fieldformdata = [$controller->get_form_element_name() => $value];
+ $validationerrors = $controller->instance_form_validation($fieldformdata, []);
+ if (count($validationerrors) > 0) {
+ $errors['customfieldinvalid'] = new lang_string('customfieldinvalid', 'tool_uploadcourse',
+ $field->get_formatted_name());
+
+ continue;
+ }
+
+ $controller->set($controller->datafield(), $value);
+
+ // Pass an empty object to the data controller, which will transform it to a correct name/value pair.
+ $instance = new stdClass();
+ $controller->instance_form_before_set_data($instance);
+
+ $result = array_merge($result, (array) $instance);
+ }
+ }
+
+ return $result;
+ }
+
/**
* Helper to increment an ID number.
*
}
return $id;
}
-
-}
+}
\ No newline at end of file
$mform->addHelpButton('defaults[enablecompletion]', 'enablecompletion', 'completion');
}
+ // Add custom fields to the form.
+ $handler = \core_course\customfield\course_handler::create();
+ $handler->instance_form_definition($mform, 0, 'defaultvaluescustomfieldcategory', 'tool_uploadcourse');
+
// Hidden fields.
$mform->addElement('hidden', 'importid');
$mform->setType('importid', PARAM_INT);
$this->add_action_buttons(true, get_string('uploadcourses', 'tool_uploadcourse'));
+ // Prepare custom fields data.
+ $data = (object) $data;
+ $handler->instance_form_before_set_data($data);
+
$this->set_data($data);
}
$enddate = $format->get_default_course_enddate($mform, array('startdate' => 'defaults[startdate]'));
$mform->setDefault('defaults[enddate]', $enddate);
}
+
+ // Tweak the form with values provided by custom fields in use.
+ \core_course\customfield\course_handler::create()->instance_form_definition_after_data($mform);
}
/**
$errors['defaults[enddate]'] = get_string($errorcode, 'error');
}
+ // Custom fields validation.
+ array_merge($errors, \core_course\customfield\course_handler::create()->instance_form_validation($data, $files));
+
return $errors;
}
}
$options = (array) $form2data->options;
$defaults = (array) $form2data->defaults;
+ // Custom field defaults.
+ $customfields = tool_uploadcourse_helper::get_custom_course_field_names();
+ foreach ($customfields as $customfield) {
+ $defaults[$customfield] = $form2data->{$customfield};
+ }
+
// Restorefile deserves its own logic because formslib does not really appreciate
// when the name of a filepicker is an array...
$options['restorefile'] = '';
$string['csvfileerror'] = 'There is something wrong with the format of the CSV file. Please check the number of headings and columns match, and that the delimiter and file encoding are correct: {$a}';
$string['csvline'] = 'Line';
$string['defaultvalues'] = 'Default course values';
+$string['defaultvaluescustomfieldcategory'] = 'Default values for \'{$a}\'';
$string['encoding'] = 'Encoding';
$string['encoding_help'] = 'Encoding of the CSV file.';
$string['errorwhilerestoringcourse'] = 'Error while restoring the course';
$string['nochanges'] = 'No changes';
$string['pluginname'] = 'Course upload';
$string['preview'] = 'Preview';
+$string['customfieldinvalid'] = 'Custom field \'{$a}\' is empty or contains invalid data';
$string['reset'] = 'Reset course after upload';
$string['reset_help'] = 'Whether to reset the course after creating/updating it.';
$string['result'] = 'Result';
And I should see "Course 1"
And I should see "Course 2"
And I should see "Course 3"
+
+ @javascript
+ Scenario: Creation of new courses with custom fields
+ Given the following "custom field categories" exist:
+ | name | component | area | itemid |
+ | Other | core_course | course | 0 |
+ And the following "custom fields" exist:
+ | name | category | type | shortname | configdata |
+ | Field 1 | Other | checkbox | checkbox | |
+ | Field 2 | Other | date | date | |
+ | Field 3 | Other | select | select | {"options":"a\nb\nc"} |
+ | Field 4 | Other | text | text | |
+ | Field 5 | Other | textarea | textarea | |
+ When I upload "admin/tool/uploadcourse/tests/fixtures/courses_custom_fields.csv" file to "File" filemanager
+ And I set the field "Upload mode" to "Create new courses only, skip existing ones"
+ And I click on "Preview" "button"
+ And I click on "Upload courses" "button"
+ Then I should see "Course created"
+ And I should see "Courses created: 1"
+ And I am on site homepage
+ And I should see "Course fields 1"
+ And I should see "Field 1: Yes"
+ And I should see "Field 2: Tuesday, 1 October 2019, 2:00"
+ And I should see "Field 3: b"
+ And I should see "Field 4: Hello"
+ And I should see "Field 5: Goodbye"
+
+ @javascript
+ Scenario: Creation of new courses with custom fields using defaults
+ Given the following "custom field categories" exist:
+ | name | component | area | itemid |
+ | Other | core_course | course | 0 |
+ And the following "custom fields" exist:
+ | name | category | type | shortname | configdata |
+ | Field 1 | Other | checkbox | checkbox | {"checkbydefault":1} |
+ | Field 2 | Other | date | date | {"includetime":0} |
+ | Field 3 | Other | select | select | {"options":"a\nb\nc","defaultvalue":"b"} |
+ | Field 4 | Other | text | text | {"defaultvalue":"Hello"} |
+ | Field 5 | Other | textarea | textarea | {"defaultvalue":"Some text","defaultvalueformat":1} |
+ When I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filemanager
+ And I set the field "Upload mode" to "Create all, increment shortname if needed"
+ And I click on "Preview" "button"
+ And I expand all fieldsets
+ And the field "Field 1" matches value "1"
+ And the field "Field 3" matches value "b"
+ And the field "Field 4" matches value "Hello"
+ And the field "Field 5" matches value "Some text"
+ # We have to enable the date field manually.
+ And I set the following fields to these values:
+ | customfield_date[enabled] | 1 |
+ | customfield_date[day] | 1 |
+ | customfield_date[month] | June |
+ | customfield_date[year] | 2020 |
+ And I click on "Upload courses" "button"
+ Then I should see "Course created"
+ And I should see "Courses created: 3"
+ And I am on site homepage
+ And I should see "Course 1"
+ And I should see "Field 1: Yes"
+ And I should see "Field 2: 1 June 2020"
+ And I should see "Field 3: b"
+ And I should see "Field 4: Hello"
+ And I should see "Field 5: Some text"
\ No newline at end of file
Background:
Given the following "courses" exist:
| fullname | shortname | category |
- | Some random name | C1 | 0 |
+ | Some random name | C1 | 0 |
+ | Another course | CF1 | 0 |
And I log in as "admin"
And I navigate to "Courses > Upload courses" in site administration
And I should see "Course 1"
And I should not see "Course 2"
And I should not see "Course 3"
+
+ @javascript
+ Scenario: Updating a course with custom fields
+ Given the following "custom field categories" exist:
+ | name | component | area | itemid |
+ | Other | core_course | course | 0 |
+ And the following "custom fields" exist:
+ | name | category | type | shortname | configdata |
+ | Field 1 | Other | checkbox | checkbox | |
+ | Field 2 | Other | date | date | |
+ | Field 3 | Other | select | select | {"options":"a\nb\nc"} |
+ | Field 4 | Other | text | text | |
+ | Field 5 | Other | textarea | textarea | |
+ When I upload "admin/tool/uploadcourse/tests/fixtures/courses_custom_fields.csv" file to "File" filemanager
+ And I set the following fields to these values:
+ | Upload mode | Only update existing courses |
+ | Update mode | Update with CSV data only |
+ And I click on "Preview" "button"
+ And I click on "Upload courses" "button"
+ Then I should see "Course updated"
+ And I should see "Courses updated: 1"
+ And I am on site homepage
+ And I should see "Course fields 1"
+ And I should see "Field 1: Yes"
+ And I should see "Field 2: Tuesday, 1 October 2019, 2:00"
+ And I should see "Field 3: b"
+ And I should see "Field 4: Hello"
+ And I should see "Field 5: Goodbye"
\ No newline at end of file
$this->assertEquals(strtotime('12th July 2013'), $enroldata['manual']->enrolenddate);
}
+ /**
+ * Test upload processing of course custom fields
+ */
+ public function test_custom_fields_data() {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ $course = $this->getDataGenerator()->create_course(['shortname' => 'C1']);
+
+ // Create our custom fields.
+ $category = $this->get_customfield_generator()->create_category();
+ $this->create_custom_field($category, 'date', 'mydatefield');
+ $this->create_custom_field($category, 'text', 'mytextfield');
+ $this->create_custom_field($category, 'textarea', 'mytextareafield');
+
+ // Perform upload.
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $dataupload = [
+ 'shortname' => $course->shortname,
+ 'customfield_mydatefield' => '2020-04-01 16:00',
+ 'customfield_mytextfield' => 'Hello',
+ 'customfield_mytextareafield' => 'Is it me you\'re looking for?',
+ ];
+
+ $uploader = new tool_uploadcourse_course($mode, $updatemode, $dataupload);
+ $this->assertTrue($uploader->prepare());
+ $uploader->proceed();
+
+ // Confirm presence of course custom fields.
+ $data = \core_course\customfield\course_handler::create()->export_instance_data_object($course->id);
+ $this->assertEquals('Wednesday, 1 April 2020, 4:00 PM', $data->mydatefield, '', 0.0, 10, false, true);
+ $this->assertEquals($dataupload['customfield_mytextfield'], $data->mytextfield);
+ $this->assertContains($dataupload['customfield_mytextareafield'], $data->mytextareafield);
+ }
+
+ /**
+ * Test upload processing of course custom field that is required but empty
+ */
+ public function test_custom_fields_data_required() {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ $course = $this->getDataGenerator()->create_course(['shortname' => 'C1']);
+
+ // Create our custom field.
+ $category = $this->get_customfield_generator()->create_category();
+ $this->create_custom_field($category, 'select', 'myselect', ['required' => true, 'options' => "Cat\nDog"]);
+
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $dataupload = [
+ 'shortname' => $course->shortname,
+ 'customfield_myselect' => null,
+ ];
+
+ $uploader = new tool_uploadcourse_course($mode, $updatemode, $dataupload);
+ $this->assertFalse($uploader->prepare());
+ $this->assertArrayHasKey('customfieldinvalid', $uploader->get_errors());
+
+ // Try again with a default value.
+ $defaults = [
+ 'customfield_myselect' => 2, // Our second option: Dog.
+ ];
+
+ $uploader = new tool_uploadcourse_course($mode, $updatemode, $dataupload, $defaults);
+ $this->assertTrue($uploader->prepare());
+ $uploader->proceed();
+
+ // Confirm presence of course custom fields.
+ $data = \core_course\customfield\course_handler::create()->export_instance_data_object($course->id);
+ $this->assertEquals('Dog', $data->myselect);
+ }
+
+ /**
+ * Test upload processing of course custom field with an invalid select option
+ */
+ public function test_custom_fields_data_invalid_select_option() {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ $course = $this->getDataGenerator()->create_course(['shortname' => 'C1']);
+
+ // Create our custom field.
+ $category = $this->get_customfield_generator()->create_category();
+ $this->create_custom_field($category, 'select', 'myselect',
+ ['required' => true, 'options' => "Cat\nDog", 'defaultvalue' => 'Cat']);
+
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $dataupload = [
+ 'shortname' => $course->shortname,
+ 'customfield_myselect' => 'Fish', // No, invalid.
+ ];
+
+ $uploader = new tool_uploadcourse_course($mode, $updatemode, $dataupload);
+ $this->assertTrue($uploader->prepare());
+ $uploader->proceed();
+
+ // Confirm presence of course custom fields.
+ $data = \core_course\customfield\course_handler::create()->export_instance_data_object($course->id);
+ $this->assertEquals('Cat', $data->myselect);
+ }
+
+ /**
+ * Test upload processing of course custom field with an out of range date
+ */
+ public function test_custom_fields_data_invalid_date() {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ $course = $this->getDataGenerator()->create_course(['shortname' => 'C1']);
+
+ // Create our custom field.
+ $category = $this->get_customfield_generator()->create_category();
+ $this->create_custom_field($category, 'date', 'mydate',
+ ['mindate' => strtotime('2020-04-01'), 'maxdate' => '2020-04-30']);
+
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $dataupload = [
+ 'shortname' => $course->shortname,
+ 'customfield_mydate' => '2020-05-06', // Out of range.
+ ];
+
+ $uploader = new tool_uploadcourse_course($mode, $updatemode, $dataupload);
+ $this->assertFalse($uploader->prepare());
+ $this->assertArrayHasKey('customfieldinvalid', $uploader->get_errors());
+ }
+
public function test_idnumber_problems() {
$this->resetAfterTest(true);
$co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
$this->assertFalse($co->prepare());
$this->assertArrayHasKey('cannotrenameshortnamealreadyinuse', $co->get_errors());
+ }
+ /**
+ * Get custom field plugin generator
+ *
+ * @return core_customfield_generator
+ */
+ protected function get_customfield_generator() : core_customfield_generator {
+ return $this->getDataGenerator()->get_plugin_generator('core_customfield');
}
-}
+ /**
+ * Helper method to create custom course field
+ *
+ * @param \core_customfield\category_controller $category
+ * @param string $type
+ * @param string $shortname
+ * @param array $configdata
+ * @return \core_customfield\field_controller
+ */
+ protected function create_custom_field(\core_customfield\category_controller $category, string $type, string $shortname,
+ array $configdata = []) : \core_customfield\field_controller {
+
+ return $this->get_customfield_generator()->create_field([
+ 'categoryid' => $category->get('id'),
+ 'type' => $type,
+ 'shortname' => $shortname,
+ 'configdata' => $configdata,
+ ]);
+ }
+}
\ No newline at end of file
--- /dev/null
+shortname,fullname,summary,category,customfield_checkbox,customfield_date,customfield_select,customfield_text,customfield_textarea
+CF1,Course fields 1,Testing course fields,1,1,2019-10-01 14:00,b,Hello,Goodbye
\ No newline at end of file
$this->assertArrayHasKey('invalidroles', $errors);
}
+ /**
+ * Test custom field data processing
+ */
+ public function test_get_custom_course_field_data() {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ // Create all the fields!
+ $category = $this->get_customfield_generator()->create_category();
+
+ $checkboxfield = $this->create_custom_field($category, 'checkbox', 'mycheckbox');
+ $datefield = $this->create_custom_field($category, 'date', 'mydate');
+ $selectfield = $this->create_custom_field($category, 'select', 'myselect', ['options' => "Red\nGreen\nBlue"]);
+ $textfield = $this->create_custom_field($category, 'text', 'mytext', ['locked' => 1]);
+ $textareafield = $this->create_custom_field($category, 'textarea', 'mytextarea');
+
+ $fields = tool_uploadcourse_helper::get_custom_course_fields();
+ $this->assertCount(5, $fields);
+
+ $this->assertArrayHasKey($checkboxfield->get('shortname'), $fields);
+ $this->assertInstanceOf(customfield_checkbox\field_controller::class, $fields[$checkboxfield->get('shortname')]);
+
+ $this->assertArrayHasKey($datefield->get('shortname'), $fields);
+ $this->assertInstanceOf(customfield_date\field_controller::class, $fields[$datefield->get('shortname')]);
+
+ $this->assertArrayHasKey($selectfield->get('shortname'), $fields);
+ $this->assertInstanceOf(customfield_select\field_controller::class, $fields[$selectfield->get('shortname')]);
+
+ $this->assertArrayHasKey($textfield->get('shortname'), $fields);
+ $this->assertInstanceOf(customfield_text\field_controller::class, $fields[$textfield->get('shortname')]);
+
+ $this->assertArrayHasKey($textareafield->get('shortname'), $fields);
+ $this->assertInstanceOf(customfield_textarea\field_controller::class, $fields[$textareafield->get('shortname')]);
+
+ $data = [
+ 'customfield_mycheckbox' => '1',
+ 'customfield_mydate' => '2019-10-01',
+ 'customfield_myselect' => 'Green',
+ 'customfield_mytext' => 'Hello',
+ 'customfield_myunknownfield' => 'Goodbye',
+ ];
+
+ $expected = [
+ 'customfield_mycheckbox' => '1',
+ 'customfield_mydate' => strtotime('2019-10-01'),
+ 'customfield_myselect' => 2,
+ 'customfield_mytext' => 'Hello',
+ ];
+
+ $course = $this->getDataGenerator()->create_course();
+ $user = $this->getDataGenerator()->create_and_enrol($course, 'manager');
+ $this->setUser($user);
+
+ $context = context_course::instance($course->id);
+
+ $this->assertEquals($expected, tool_uploadcourse_helper::get_custom_course_field_data($data, [], $context));
+
+ // Now add our custom textarea field (separately because the value of it's 'itemid' element is unknown).
+ $data['customfield_mytextarea'] = 'Something';
+ $fields = tool_uploadcourse_helper::get_custom_course_field_data($data, [], $context);
+ $this->assertArrayHasKey('customfield_mytextarea_editor', $fields);
+ $this->assertArrayHasKey('text', $fields['customfield_mytextarea_editor']);
+ $this->assertEquals('Something', $fields['customfield_mytextarea_editor']['text']);
+
+ // Now prohibit the capability to change locked fields for the manager role.
+ $managerrole = $DB->get_record('role', ['shortname' => 'manager']);
+ role_change_permission($managerrole->id, $context, 'moodle/course:changelockedcustomfields', CAP_PROHIBIT);
+
+ // The locked 'mytext' custom field should not be returned.
+ $fields = tool_uploadcourse_helper::get_custom_course_field_data($data, [], $context);
+ $this->assertCount(4, $fields);
+ $this->assertArrayNotHasKey('customfield_mytext', $fields);
+ }
+
public function test_increment_idnumber() {
$this->resetAfterTest(true);
$this->assertEquals($cat3_fakedouble->id, tool_uploadcourse_helper::resolve_category_by_path($path));
$this->assertEquals($cat3_fakedouble->id, tool_uploadcourse_helper::resolve_category_by_path($path));
}
-}
+
+ /**
+ * Get custom field plugin generator
+ *
+ * @return core_customfield_generator
+ */
+ protected function get_customfield_generator() : core_customfield_generator {
+ return $this->getDataGenerator()->get_plugin_generator('core_customfield');
+ }
+
+ /**
+ * Helper method to create custom course field
+ *
+ * @param \core_customfield\category_controller $category
+ * @param string $type
+ * @param string $shortname
+ * @param array $configdata
+ * @return \core_customfield\field_controller
+ */
+ protected function create_custom_field(\core_customfield\category_controller $category, string $type, string $shortname,
+ array $configdata = []) : \core_customfield\field_controller {
+
+ return $this->get_customfield_generator()->create_field([
+ 'categoryid' => $category->get('id'),
+ 'type' => $type,
+ 'shortname' => $shortname,
+ 'configdata' => $configdata,
+ ]);
+ }
+}
\ No newline at end of file
// Get needed strings
$this->loadStrings(array(
+ 'extraindexesfound' => 'tool_xmldb',
'missing' => 'tool_xmldb',
'key' => 'tool_xmldb',
'index' => 'tool_xmldb',
'missingindexes' => 'tool_xmldb',
- 'nomissingindexesfound' => 'tool_xmldb',
+ 'nomissingorextraindexesfound' => 'tool_xmldb',
+ 'yesextraindexesfound' => 'tool_xmldb',
'yesmissingindexesfound' => 'tool_xmldb',
));
}
$dbman = $DB->get_manager();
$o = '';
+ $dbindexes = $DB->get_indexes($xmldb_table->getName());
$missing_indexes = array();
// Keys
// Check if the index exists in DB
if ($dbman->index_exists($xmldb_table, $xmldb_index)) {
$o.='<font color="green">' . $this->str['ok'] . '</font>';
+ $this->remove_index_from_dbindex($dbindexes, $xmldb_index);
} else {
$o.='<font color="red">' . $this->str['missing'] . '</font>';
// Add the missing index to the list
// Check if the index exists in DB
if ($dbman->index_exists($xmldb_table, $xmldb_index)) {
$o.='<font color="green">' . $this->str['ok'] . '</font>';
+ $this->remove_index_from_dbindex($dbindexes, $xmldb_index);
} else {
$o.='<font color="red">' . $this->str['missing'] . '</font>';
// Add the missing index to the list
$o.=' </ul>';
}
+ // Hack - skip for table 'search_simpledb_index' as this plugin adds indexes dynamically on install
+ // which are not included in install.xml. See search/engine/simpledb/db/install.php.
+ if ($xmldb_table->getName() != 'search_simpledb_index') {
+ foreach ($dbindexes as $indexname => $index) {
+ $missing_indexes[] = $indexname;
+ }
+ }
+
return array($o, $missing_indexes);
}
global $DB;
$dbman = $DB->get_manager();
+ $missingindexes = [];
+ $extraindexes = [];
+
+ foreach ($missing_indexes as $missingindex) {
+ if (is_object($missingindex)) {
+ $missingindexes[] = $missingindex;
+ } else {
+ $extraindexes[] = $missingindex;
+ }
+ }
+
$s = '';
$r = '<table class="generaltable boxaligncenter boxwidthwide" border="0" cellpadding="5" cellspacing="0" id="results">';
$r.= ' <tr><td class="generalboxcontent">';
$r.= ' <h2 class="main">' . $this->str['searchresults'] . '</h2>';
- $r.= ' <p class="centerpara">' . $this->str['missingindexes'] . ': ' . count($missing_indexes) . '</p>';
+ $r .= ' <p class="centerpara">' . $this->str['missingindexes'] . ': ' . count($missingindexes) . '</p>';
+ $r .= ' <p class="centerpara">' . $this->str['extraindexesfound'] . ': ' . count($extraindexes) . '</p>';
$r.= ' </td></tr>';
$r.= ' <tr><td class="generalboxcontent">';
- // If we have found missing indexes inform about them
- if (count($missing_indexes)) {
- $r.= ' <p class="centerpara">' . $this->str['yesmissingindexesfound'] . '</p>';
- $r.= ' <ul>';
- foreach ($missing_indexes as $obj) {
- $xmldb_table = $obj->table;
- $xmldb_index = $obj->index;
- $sqlarr = $dbman->generator->getAddIndexSQL($xmldb_table, $xmldb_index);
- $r.= ' <li>' . $this->str['table'] . ': ' . $xmldb_table->getName() . '. ' .
- $this->str['index'] . ': ' . $xmldb_index->readableInfo() . '</li>';
- $sqlarr = $dbman->generator->getEndedStatements($sqlarr);
- $s.= '<code>' . str_replace("\n", '<br />', implode('<br />', $sqlarr)) . '</code><br />';
+ // If we have found missing indexes or extra indexes inform the user about them.
+ if (!empty($missingindexes) || !empty($extraindexes)) {
+ if ($missingindexes) {
+ $r.= ' <p class="centerpara">' . $this->str['yesmissingindexesfound'] . '</p>';
+ $r.= ' <ul>';
+ foreach ($missingindexes as $obj) {
+ $xmldb_table = $obj->table;
+ $xmldb_index = $obj->index;
+ $sqlarr = $dbman->generator->getAddIndexSQL($xmldb_table, $xmldb_index);
+ $r.= ' <li>' . $this->str['table'] . ': ' . $xmldb_table->getName() . '. ' .
+ $this->str['index'] . ': ' . $xmldb_index->readableInfo() . '</li>';
+ $sqlarr = $dbman->generator->getEndedStatements($sqlarr);
+ $s.= '<code>' . str_replace("\n", '<br />', implode('<br />', $sqlarr)) . '</code><br />';
+ }
+ $r.= ' </ul>';
+ // Add the SQL statements (all together)
+ $r.= '<hr />' . $s;
+ }
+ if ($extraindexes) {
+ $r .= '<p class="centerpara">' . $this->str['yesextraindexesfound'] . '</p>';
+ $r .= '<ul>';
+ foreach ($extraindexes as $ei) {
+ $r .= '<li>' . $ei . '</li>';
+ }
+ $r .= '</ul>';
+ $r .= '<hr />';
}
- $r.= ' </ul>';
- // Add the SQL statements (all together)
- $r.= '<hr />' . $s;
} else {
- $r.= ' <p class="centerpara">' . $this->str['nomissingindexesfound'] . '</p>';
+ $r .= '<p class="centerpara">' . $this->str['nomissingorextraindexesfound'] . '</p>';
}
$r.= ' </td></tr>';
$r.= ' <tr><td class="generalboxcontent">';
return $r;
}
+
+ /**
+ * Removes an index from the array $dbindexes if it is found.
+ *
+ * @param array $dbindexes
+ * @param xmldb_index $index
+ */
+ private function remove_index_from_dbindex(array &$dbindexes, xmldb_index $index) {
+ foreach ($dbindexes as $key => $dbindex) {
+ if ($dbindex['columns'] == $index->getFields()) {
+ unset($dbindexes[$key]);
+ }
+ }
+ }
}
$string['enumvaluesincorrect'] = 'Incorrect values for enum field';
$string['expected'] = 'Expected';
$string['extensionrequired'] = 'Sorry - the PHP extension \'{$a}\' is required for this action. Please install the extension if you want to use this feature.';
+$string['extraindexesfound'] = 'Extra indexes found';
$string['field'] = 'Field';
$string['fieldnameempty'] = 'Name field empty';
$string['fields'] = 'Fields';
$string['new_table_from_mysql'] = 'New table from MySQL';
$string['nofieldsspecified'] = 'No fields specified';
$string['nomasterprimaryuniquefound'] = 'The column(s) that your foreign key references must be included in a primary or unique KEY in the referenced table. Note that the column being in a UNIQUE INDEX is not good enough.';
-$string['nomissingindexesfound'] = 'No missing indexes have been found, your DB doesn\'t need further actions.';
+$string['nomissingorextraindexesfound'] = 'No missing or extra indexes have been found, so no further action is required.';
$string['noreffieldsspecified'] = 'No reference fields specified';
$string['noreftablespecified'] = 'Specified reference table not found';
$string['noviolatedforeignkeysfound'] = 'No violated foreign keys found';
$string['wrongnumberofreffields'] = 'Wrong number of reference fields';
$string['wrongreservedwords'] = 'Currently used reserved words<br />(note that table names aren\'t important if using $CFG->prefix)';
$string['wrongoraclesemantics'] = 'Wrong Oracle BYTE semantics found';
+$string['yesextraindexesfound'] = 'The following additional indexes were found.';
$string['yesmissingindexesfound'] = '<p>Some missing indexes have been found in your DB. Here are their details and the needed SQL statements to be executed with your favourite SQL interface to create all of them. Remember to backup your data first!</p>
<p>After doing that, it\'s highly recommended to execute this utility again to check that no more missing indexes are found.</p>';
$string['yeswrongdefaultsfound'] = '<p>Some inconsistent defaults have been found in your DB. Here are their details and the needed SQL statements to be executed with your favourite SQL interface to fix them all. Remember to backup your data first!</p>
define('NO_OUTPUT_BUFFERING', true);
require_once('../../config.php');
require_once($CFG->libdir.'/adminlib.php');
-require_once($CFG->libdir.'/dataformatlib.php');
require_once($CFG->dirroot.'/user/profile/lib.php');
$dataformat = optional_param('dataformat', '', PARAM_ALPHA);
$downloadusers = new ArrayObject($SESSION->bulk_users);
$iterator = $downloadusers->getIterator();
- download_as_dataformat($filename, $dataformat, $fields, $iterator, function($userid) use ($extrafields, $fields) {
+ \core\dataformat::download_data($filename, $dataformat, $fields, $iterator, function($userid) use ($extrafields, $fields) {
global $DB;
- $row = array();
+
if (!$user = $DB->get_record('user', array('id' => $userid))) {
return null;
}
namespace availability_completion;
+use cache;
+use core_availability\info;
+use core_availability\info_module;
+use core_availability\info_section;
+use stdClass;
+
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/completionlib.php');
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class condition extends \core_availability\condition {
+
+ /** @var int previous module cm value used to calculate relative completions */
+ public const OPTION_PREVIOUS = -1;
+
/** @var int ID of module that this depends on */
protected $cmid;
+ /** @var array IDs of the current module and section */
+ protected $selfids;
+
/** @var int Expected completion type (one of the COMPLETE_xx constants) */
protected $expectedcompletion;
+ /** @var array Array of previous cmids used to calculate relative completions */
+ protected $modfastprevious = [];
+
+ /** @var array Array of cmids previous to each course section */
+ protected $sectionfastprevious = [];
+
/** @var array Array of modules used in these conditions for course */
- protected static $modsusedincondition = array();
+ protected static $modsusedincondition = [];
/**
* Constructor.
} else {
throw new \coding_exception('Missing or invalid ->cm for completion condition');
}
-
// Get expected completion.
if (isset($structure->e) && in_array($structure->e,
- array(COMPLETION_COMPLETE, COMPLETION_INCOMPLETE,
- COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE_FAIL))) {
+ [COMPLETION_COMPLETE, COMPLETION_INCOMPLETE,
+ COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE_FAIL])) {
$this->expectedcompletion = $structure->e;
} else {
throw new \coding_exception('Missing or invalid ->e for completion condition');
}
}
- public function save() {
- return (object)array('type' => 'completion',
- 'cm' => $this->cmid, 'e' => $this->expectedcompletion);
+ /**
+ * Saves tree data back to a structure object.
+ *
+ * @return stdClass Structure object (ready to be made into JSON format)
+ */
+ public function save(): stdClass {
+ return (object) [
+ 'type' => 'completion',
+ 'cm' => $this->cmid,
+ 'e' => $this->expectedcompletion,
+ ];
}
/**
* @param int $expectedcompletion Expected completion value (COMPLETION_xx)
* @return stdClass Object representing condition
*/
- public static function get_json($cmid, $expectedcompletion) {
- return (object)array('type' => 'completion', 'cm' => (int)$cmid,
- 'e' => (int)$expectedcompletion);
+ public static function get_json(int $cmid, int $expectedcompletion): stdClass {
+ return (object) [
+ 'type' => 'completion',
+ 'cm' => (int)$cmid,
+ 'e' => (int)$expectedcompletion,
+ ];
}
- public function is_available($not, \core_availability\info $info, $grabthelot, $userid) {
+ /**
+ * Determines whether a particular item is currently available
+ * according to this availability condition.
+ *
+ * @see \core_availability\tree_node\update_after_restore
+ *
+ * @param bool $not Set true if we are inverting the condition
+ * @param info $info Item we're checking
+ * @param bool $grabthelot Performance hint: if true, caches information
+ * required for all course-modules, to make the front page and similar
+ * pages work more quickly (works only for current user)
+ * @param int $userid User ID to check availability for
+ * @return bool True if available
+ */
+ public function is_available($not, info $info, $grabthelot, $userid): bool {
+ list($selfcmid, $selfsectionid) = $this->get_selfids($info);
+ $cmid = $this->get_cmid($info->get_course(), $selfcmid, $selfsectionid);
$modinfo = $info->get_modinfo();
$completion = new \completion_info($modinfo->get_course());
- if (!array_key_exists($this->cmid, $modinfo->cms)) {
+ if (!array_key_exists($cmid, $modinfo->cms) || $modinfo->cms[$cmid]->deletioninprogress) {
// If the cmid cannot be found, always return false regardless
// of the condition or $not state. (Will be displayed in the
// information message.)
$allow = false;
} else {
// The completion system caches its own data so no caching needed here.
- $completiondata = $completion->get_data((object)array('id' => $this->cmid),
+ $completiondata = $completion->get_data((object)['id' => $cmid],
$grabthelot, $userid, $modinfo);
$allow = true;
return $allow;
}
+ /**
+ * Return current item IDs (cmid and sectionid).
+ *
+ * @param info $info
+ * @return int[] with [0] => cmid/null, [1] => sectionid/null
+ */
+ public function get_selfids(info $info): array {
+ if (isset($this->selfids)) {
+ return $this->selfids;
+ }
+ if ($info instanceof info_module) {
+ $cminfo = $info->get_course_module();
+ if (!empty($cminfo->id)) {
+ $this->selfids = [$cminfo->id, null];
+ return $this->selfids;
+ }
+ }
+ if ($info instanceof info_section) {
+ $section = $info->get_section();
+ if (!empty($section->id)) {
+ $this->selfids = [null, $section->id];
+ return $this->selfids;
+ }
+
+ }
+ return [null, null];
+ }
+
+ /**
+ * Get the cmid referenced in the access restriction.
+ *
+ * @param stdClass $course course object
+ * @param int|null $selfcmid current course-module ID or null
+ * @param int|null $selfsectionid current course-section ID or null
+ * @return int|null cmid or null if no referenced cm is found
+ */
+ public function get_cmid(stdClass $course, ?int $selfcmid, ?int $selfsectionid): ?int {
+ if ($this->cmid > 0) {
+ return $this->cmid;
+ }
+ // If it's a relative completion, load fast browsing.
+ if ($this->cmid == self::OPTION_PREVIOUS) {
+ $prevcmid = $this->get_previous_cmid($course, $selfcmid, $selfsectionid);
+ if ($prevcmid) {
+ return $prevcmid;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return the previous CM ID of an specific course-module or course-section.
+ *
+ * @param stdClass $course course object
+ * @param int|null $selfcmid course-module ID or null
+ * @param int|null $selfsectionid course-section ID or null
+ * @return int|null
+ */
+ private function get_previous_cmid(stdClass $course, ?int $selfcmid, ?int $selfsectionid): ?int {
+ $this->load_course_structure($course);
+ if (isset($this->modfastprevious[$selfcmid])) {
+ return $this->modfastprevious[$selfcmid];
+ }
+ if (isset($this->sectionfastprevious[$selfsectionid])) {
+ return $this->sectionfastprevious[$selfsectionid];
+ }
+ return null;
+ }
+
+ /**
+ * Loads static information about a course elements previous activities.
+ *
+ * Populates two variables:
+ * - $this->sectionprevious[] course-module previous to a cmid
+ * - $this->sectionfastprevious[] course-section previous to a cmid
+ *
+ * @param stdClass $course course object
+ */
+ private function load_course_structure(stdClass $course): void {
+ // If already loaded we don't need to do anything.
+ if (empty($this->modfastprevious)) {
+ $previouscache = cache::make('availability_completion', 'previous_cache');
+ $this->modfastprevious = $previouscache->get("mod_{$course->id}");
+ $this->sectionfastprevious = $previouscache->get("sec_{$course->id}");
+ }
+
+ if (!empty($this->modfastprevious)) {
+ return;
+ }
+
+ if (empty($this->modfastprevious)) {
+ $this->modfastprevious = [];
+ $sectionprevious = [];
+
+ $modinfo = get_fast_modinfo($course);
+ $lastcmid = 0;
+ foreach ($modinfo->cms as $othercm) {
+ if ($othercm->deletioninprogress) {
+ continue;
+ }
+ // Save first cm of every section.
+ if (!isset($sectionprevious[$othercm->section])) {
+ $sectionprevious[$othercm->section] = $lastcmid;
+ }
+ // Load previous to all cms with completion.
+ if ($othercm->completion == COMPLETION_TRACKING_NONE) {
+ continue;
+ }
+ if ($lastcmid) {
+ $this->modfastprevious[$othercm->id] = $lastcmid;
+ }
+ $lastcmid = $othercm->id;
+ }
+ // Fill empty sections index.
+ $isections = array_reverse($modinfo->get_section_info_all());
+ foreach ($isections as $section) {
+ if (isset($sectionprevious[$section->id])) {
+ $lastcmid = $sectionprevious[$section->id];
+ } else {
+ $sectionprevious[$section->id] = $lastcmid;
+ }
+ }
+ $this->sectionfastprevious = $sectionprevious;
+ $previouscache->set("mod_{$course->id}", $this->modfastprevious);
+ $previouscache->set("sec_{$course->id}", $this->sectionfastprevious);
+ }
+ }
+
/**
* Returns a more readable keyword corresponding to a completion state.
*
* @param int $completionstate COMPLETION_xx constant
* @return string Readable keyword
*/
- protected static function get_lang_string_keyword($completionstate) {
+ protected static function get_lang_string_keyword(int $completionstate): string {
switch($completionstate) {
case COMPLETION_INCOMPLETE:
return 'incomplete';
}
}
- public function get_description($full, $not, \core_availability\info $info) {
- // Get name for module.
- $modinfo = $info->get_modinfo();
- if (!array_key_exists($this->cmid, $modinfo->cms)) {
- $modname = get_string('missing', 'availability_completion');
+ /**
+ * Obtains a string describing this restriction (whether or not
+ * it actually applies).
+ *
+ * @param bool $full Set true if this is the 'full information' view
+ * @param bool $not Set true if we are inverting the condition
+ * @param info $info Item we're checking
+ * @return string Information string (for admin) about all restrictions on
+ * this item
+ */
+ public function get_description($full, $not, info $info): string {
+ global $USER;
+ $str = 'requires_';
+ $course = $info->get_course();
+ list($selfcmid, $selfsectionid) = $this->get_selfids($info);
+ $modname = '';
+ // On ajax duplicate get_fast_modinfo is called before $PAGE->set_context
+ // so we cannot use $PAGE->user_is_editing().
+ $coursecontext = \context_course::instance($course->id);
+ $editing = !empty($USER->editing) && has_capability('moodle/course:manageactivities', $coursecontext);
+ if ($this->cmid == self::OPTION_PREVIOUS && $editing) {
+ // Previous activity name could be inconsistent when editing due to partial page loadings.
+ $str .= 'previous_';
} else {
- $modname = '<AVAILABILITY_CMNAME_' . $modinfo->cms[$this->cmid]->id . '/>';
+ // Get name for module.
+ $cmid = $this->get_cmid($course, $selfcmid, $selfsectionid);
+ $modinfo = $info->get_modinfo();
+ if (!array_key_exists($cmid, $modinfo->cms) || $modinfo->cms[$cmid]->deletioninprogress) {
+ $modname = get_string('missing', 'availability_completion');
+ } else {
+ $modname = '<AVAILABILITY_CMNAME_' . $modinfo->cms[$cmid]->id . '/>';
+ }
}
- // Work out which lang string to use.
+ // Work out which lang string to use depending on required completion status.
if ($not) {
// Convert NOT strings to use the equivalent where possible.
switch ($this->expectedcompletion) {
case COMPLETION_INCOMPLETE:
- $str = 'requires_' . self::get_lang_string_keyword(COMPLETION_COMPLETE);
+ $str .= self::get_lang_string_keyword(COMPLETION_COMPLETE);
break;
case COMPLETION_COMPLETE:
- $str = 'requires_' . self::get_lang_string_keyword(COMPLETION_INCOMPLETE);
+ $str .= self::get_lang_string_keyword(COMPLETION_INCOMPLETE);
break;
default:
// The other two cases do not have direct opposites.
- $str = 'requires_not_' . self::get_lang_string_keyword($this->expectedcompletion);
+ $str .= 'not_' . self::get_lang_string_keyword($this->expectedcompletion);
break;
}
} else {
- $str = 'requires_' . self::get_lang_string_keyword($this->expectedcompletion);
+ $str .= self::get_lang_string_keyword($this->expectedcompletion);
}
return get_string($str, 'availability_completion', $modname);
}
- protected function get_debug_string() {
+ /**
+ * Obtains a representation of the options of this condition as a string,
+ * for debugging.
+ *
+ * @return string Text representation of parameters
+ */
+ protected function get_debug_string(): string {
switch ($this->expectedcompletion) {
case COMPLETION_COMPLETE :
$type = 'COMPLETE';
default:
throw new \coding_exception('Unexpected expected completion');
}
- return 'cm' . $this->cmid . ' ' . $type;
+ $cm = $this->cmid;
+ if ($this->cmid == self::OPTION_PREVIOUS) {
+ $cm = 'opprevious';
+ }
+ return 'cm' . $cm . ' ' . $type;
}
- public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name) {
+ /**
+ * Updates this node after restore, returning true if anything changed.
+ *
+ * @see \core_availability\tree_node\update_after_restore
+ *
+ * @param string $restoreid Restore ID
+ * @param int $courseid ID of target course
+ * @param \base_logger $logger Logger for any warnings
+ * @param string $name Name of this item (for use in warning messages)
+ * @return bool True if there was any change
+ */
+ public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name): bool {
global $DB;
+ $res = false;
+ // If we depend on the previous activity, no translation is needed.
+ if ($this->cmid == self::OPTION_PREVIOUS) {
+ return $res;
+ }
$rec = \restore_dbops::get_backup_ids_record($restoreid, 'course_module', $this->cmid);
if (!$rec || !$rec->newitemid) {
// If we are on the same course (e.g. duplicate) then we can just
// use the existing one.
if ($DB->record_exists('course_modules',
- array('id' => $this->cmid, 'course' => $courseid))) {
- return false;
+ ['id' => $this->cmid, 'course' => $courseid])) {
+ return $res;
}
// Otherwise it's a warning.
$this->cmid = 0;
* @param int $cmid Course-module id
* @return bool True if this is used in a condition, false otherwise
*/
- public static function completion_value_used($course, $cmid) {
+ public static function completion_value_used($course, $cmid): bool {
// Have we already worked out a list of required completion values
// for this course? If so just use that.
if (!array_key_exists($course->id, self::$modsusedincondition)) {
// We don't have data for this course, build it.
$modinfo = get_fast_modinfo($course);
- self::$modsusedincondition[$course->id] = array();
+ self::$modsusedincondition[$course->id] = [];
// Activities.
foreach ($modinfo->cms as $othercm) {
$ci = new \core_availability\info_module($othercm);
$tree = $ci->get_availability_tree();
foreach ($tree->get_all_children('availability_completion\condition') as $cond) {
- self::$modsusedincondition[$course->id][$cond->cmid] = true;
+ $condcmid = $cond->get_cmid($course, $othercm->id, null);
+ if (!empty($condcmid)) {
+ self::$modsusedincondition[$course->id][$condcmid] = true;
+ }
}
}
$ci = new \core_availability\info_section($section);
$tree = $ci->get_availability_tree();
foreach ($tree->get_all_children('availability_completion\condition') as $cond) {
- self::$modsusedincondition[$course->id][$cond->cmid] = true;
+ $condcmid = $cond->get_cmid($course, null, $section->id);
+ if (!empty($condcmid)) {
+ self::$modsusedincondition[$course->id][$condcmid] = true;
+ }
}
}
}
* Wipes the static cache of modules used in a condition (for unit testing).
*/
public static function wipe_static_cache() {
- self::$modsusedincondition = array();
+ self::$modsusedincondition = [];
}
public function update_dependency_id($table, $oldid, $newid) {
/**
* @var array Cached init parameters
*/
- protected $cacheparams = array();
+ protected $cacheparams = [];
/**
* @var string IDs of course, cm, and section for cache (if any)
protected $cachekey = '';
protected function get_javascript_strings() {
- return array('option_complete', 'option_fail', 'option_incomplete', 'option_pass',
- 'label_cm', 'label_completion');
+ return ['option_complete', 'option_fail', 'option_incomplete', 'option_pass',
+ 'label_cm', 'label_completion'];
}
protected function get_javascript_init_params($course, \cm_info $cm = null,
// Get list of activities on course which have completion values,
// to fill the dropdown.
$context = \context_course::instance($course->id);
- $cms = array();
+ $cms = [];
$modinfo = get_fast_modinfo($course);
+ $previouscm = false;
foreach ($modinfo->cms as $id => $othercm) {
// Add each course-module if it has completion turned on and is not
// the one currently being edited.
if ($othercm->completion && (empty($cm) || $cm->id != $id) && !$othercm->deletioninprogress) {
- $cms[] = (object)array('id' => $id,
- 'name' => format_string($othercm->name, true, array('context' => $context)),
- 'completiongradeitemnumber' => $othercm->completiongradeitemnumber);
+ $cms[] = (object)['id' => $id,
+ 'name' => format_string($othercm->name, true, ['context' => $context]),
+ 'completiongradeitemnumber' => $othercm->completiongradeitemnumber];
+ }
+ if (count($cms) && (empty($cm) || $cm->id == $id)) {
+ $previouscm = true;
}
}
-
+ if ($previouscm) {
+ $previous = (object)['id' => \availability_completion\condition::OPTION_PREVIOUS,
+ 'name' => get_string('option_previous', 'availability_completion'),
+ 'completiongradeitemnumber' => \availability_completion\condition::OPTION_PREVIOUS];
+ array_unshift($cms, $previous);
+ }
$this->cachekey = $cachekey;
- $this->cacheinitparams = array($cms);
+ $this->cacheinitparams = [$cms];
}
return $this->cacheinitparams;
}
--- /dev/null
+<?php
+// This file is part of Moodle - https://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/>.
+
+/**
+ * Defined caches used internally by the plugin.
+ *
+ * @package availability_completion
+ * @category cache
+ * @copyright 2020 Ferran Recio <ferran@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$definitions = [
+ 'previous_cache' => [
+ 'mode' => cache_store::MODE_REQUEST,
+ 'simplekeys' => true,
+ 'simpledata' => true,
+ 'staticacceleration' => true
+ ],
+];
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+$string['cachedef_previous_cache'] = 'Previous activity dependency information';
$string['description'] = 'Require students to complete (or not complete) another activity.';
$string['error_selectcmid'] = 'You must select an activity for the completion condition.';
$string['error_selectcmidpassfail'] = 'You must select an activity with "Require grade" completion condition set.';
$string['option_fail'] = 'must be complete with fail grade';
$string['option_incomplete'] = 'must not be marked complete';
$string['option_pass'] = 'must be complete with pass grade';
+$string['option_previous'] = 'Previous activity with completion';
$string['pluginname'] = 'Restriction by activity completion';
$string['requires_incomplete'] = 'The activity <strong>{$a}</strong> is incomplete';
$string['requires_complete'] = 'The activity <strong>{$a}</strong> is marked complete';
$string['requires_complete_fail'] = 'The activity <strong>{$a}</strong> is complete and failed';
$string['requires_not_complete_pass'] = 'The activity <strong>{$a}</strong> is not complete and passed';
$string['requires_not_complete_fail'] = 'The activity <strong>{$a}</strong> is not complete and failed';
+$string['requires_previous_incomplete'] = 'The <strong>previous activity with completion</strong> is incomplete';
+$string['requires_previous_complete'] = 'The <strong>previous activity with completion</strong> is marked complete';
+$string['requires_previous_complete_pass'] = 'The <strong>previous activity with completion</strong> is complete and passed';
+$string['requires_previous_complete_fail'] = 'The <strong>previous activity with completion</strong> is complete and failed';
+$string['requires_previous_not_complete_pass'] = 'The <strong>previous activity with completion</strong> is not complete and passed';
+$string['requires_previous_not_complete_fail'] = 'The <strong>previous activity with completion</strong> is not complete and failed';
$string['title'] = 'Activity completion';
$string['privacy:metadata'] = 'The Restriction by activity completion plugin does not store any personal data.';
--- /dev/null
+@availability @availability_completion
+Feature: Confirm that availability_completion works with previous activity setting
+ In order to control student access to activities
+ As a teacher
+ I need to set completion conditions which prevent student access
+
+ Background:
+ Given the following "courses" exist:
+ | fullname | shortname | format | enablecompletion | numsections |
+ | Course 1 | C1 | topics | 1 | 5 |
+ And the following "users" exist:
+ | username |
+ | teacher1 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ Given the following "activities" exist:
+ | activity | name | intro | course | idnumber | groupmode | completion | section |
+ | page | Page1 | Page 1 description | C1 | page1 | 1 | 1 | 1 |
+ | page | Page Ignored 1 | Page Ignored | C1 | pagei1 | 1 | 0 | 1 |
+ | page | Page2 | Page 2 description | C1 | page2 | 1 | 1 | 3 |
+ | page | Page3 | Page 3 description | C1 | page3 | 1 | 1 | 4 |
+
+ @javascript
+ Scenario: Test condition with previous activity on an activity
+ Given I log in as "teacher1"
+ And I am on "Course 1" course homepage with editing mode on
+
+ # Set Page3 restriction to Previous Activity with completion.
+ When I open "Page3" actions menu
+ And I click on "Edit settings" "link" in the "Page3" activity
+ And I expand all fieldsets
+ And I click on "Add restriction..." "button"
+ And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+ And I click on "Displayed greyed-out if user does not meet this condition • Click to hide" "link"
+ And I set the field "Activity or resource" to "Previous activity with completion"
+ And I press "Save and return to course"
+ Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
+
+ When I turn editing mode off
+ Then I should see "Not available unless: The activity Page2 is marked complete" in the "region-main" "region"
+
+ # Remove Page 2 and check Page3 depends now on Page1.
+ When I turn editing mode on
+ And I delete "Page2" activity
+ And I turn editing mode off
+ Then I should see "Not available unless: The activity Page1 is marked complete" in the "region-main" "region"
+
+ @javascript
+ Scenario: Test previous activity availability when duplicate an activity
+ Given I log in as "teacher1"
+ And I am on "Course 1" course homepage with editing mode on
+
+ # Set Page3 restriction to Previous Activity with completion.
+ When I open "Page3" actions menu
+ And I click on "Edit settings" "link" in the "Page3" activity
+ And I expand all fieldsets
+ And I click on "Add restriction..." "button"
+ And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+ And I click on "Displayed greyed-out if user does not meet this condition • Click to hide" "link"
+ And I set the field "Activity or resource" to "Previous activity with completion"
+ And I press "Save and return to course"
+ Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
+
+ When I turn editing mode off
+ Then I should see "Not available unless: The activity Page2 is marked complete" in the "region-main" "region"
+
+ # Duplicate Page3.
+ When I turn editing mode on
+ And I duplicate "Page3" activity
+ And I turn editing mode off
+ Then I should see "Not available unless: The activity Page3 is marked complete" in the "region-main" "region"
+
+ @javascript
+ Scenario: Test previous activity availability when modify completion tacking
+ Given I log in as "teacher1"
+ And I am on "Course 1" course homepage with editing mode on
+
+ # Set Page3 restriction to Previous Activity with completion.
+ When I open "Page3" actions menu
+ And I click on "Edit settings" "link" in the "Page3" activity
+ And I expand all fieldsets
+ And I click on "Add restriction..." "button"
+ And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+ And I click on "Displayed greyed-out if user does not meet this condition • Click to hide" "link"
+ And I set the field "Activity or resource" to "Previous activity with completion"
+ And I press "Save and return to course"
+ Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
+
+ When I turn editing mode off
+ Then I should see "Not available unless: The activity Page2 is marked complete" in the "region-main" "region"
+
+ # Test if I disable completion tracking on Page2 section 5 depends on Page2.
+ When I turn editing mode on
+ When I open "Page2" actions menu
+ And I click on "Edit settings" "link" in the "Page2" activity
+ And I set the following fields to these values:
+ | Completion tracking | Do not indicate activity completion |
+ And I press "Save and return to course"
+ When I turn editing mode off
+ Then I should see "Not available unless: The activity Page1 is marked complete" in the "region-main" "region"
+
+ @javascript
+ Scenario: Test condition with previous activity on a section
+ Given I log in as "teacher1"
+ And I am on "Course 1" course homepage with editing mode on
+
+ # Set section 4 restriction to Previous Activity with completion.
+ When I edit the section "4"
+ And I expand all fieldsets
+ And I click on "Add restriction..." "button"
+ And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+ And I click on "Displayed greyed-out if user does not meet this condition • Click to hide" "link"
+ And I set the field "Activity or resource" to "Previous activity with completion"
+ And I press "Save changes"
+ Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
+
+ When I turn editing mode off
+ Then I should see "Not available unless: The activity Page2 is marked complete" in the "region-main" "region"
+
+ # Remove Page 2 and check Section 4 depends now on Page1.
+ When I turn editing mode on
+ And I delete "Page2" activity
+ And I turn editing mode off
+ Then I should see "Not available unless: The activity Page1 is marked complete" in the "region-main" "region"
+
+ @javascript
+ Scenario: Test condition with previous activity on the first activity of the course
+ Given I log in as "teacher1"
+ And I am on "Course 1" course homepage with editing mode on
+
+ # Try to set Page1 restriction to Previous Activity with completion.
+ When I open "Page1" actions menu
+ And I click on "Edit settings" "link" in the "Page1" activity
+ And I expand all fieldsets
+ And I click on "Add restriction..." "button"
+ And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+ And I click on "Displayed greyed-out if user does not meet this condition • Click to hide" "link"
+ Then the "Activity or resource" select box should not contain "Previous activity with completion"
+
+ # Set Page2 restriction to Previous Activity with completion and delete Page1.
+ When I am on "Course 1" course homepage
+ When I open "Page2" actions menu
+ And I click on "Edit settings" "link" in the "Page2" activity
+ And I expand all fieldsets
+ And I click on "Add restriction..." "button"
+ And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+ And I click on "Displayed greyed-out if user does not meet this condition • Click to hide" "link"
+ And I set the field "Activity or resource" to "Previous activity with completion"
+ And I press "Save and return to course"
+ Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
+
+ # Delete Page 1 and check than Page2 now depends on a missing activity (no previous activity found).
+ When I am on "Course 1" course homepage
+ And I delete "Page1" activity
+ And I turn editing mode off
+ Then I should see "Not available unless: The activity (Missing activity)" in the "region-main" "region"
+
+ @javascript
+ Scenario: Test previous activities on empty sections
+ Given I log in as "teacher1"
+ And I am on "Course 1" course homepage with editing mode on
+
+ # Set section 2 restriction to Previous Activity with completion.
+ When I edit the section "2"
+ And I expand all fieldsets
+ And I click on "Add restriction..." "button"
+ And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+ And I click on "Displayed greyed-out if user does not meet this condition • Click to hide" "link"
+ And I set the field "Activity or resource" to "Previous activity with completion"
+ And I press "Save changes"
+ Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
+
+ When I turn editing mode off
+ Then I should see "Not available unless: The activity Page1 is marked complete" in the "region-main" "region"
+
+ # Set section 5 restriction to Previous Activity with completion.
+ When I turn editing mode on
+ And I edit the section "5"
+ And I expand all fieldsets
+ And I click on "Add restriction..." "button"
+ And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
+ And I click on "Displayed greyed-out if user does not meet this condition • Click to hide" "link"
+ And I set the field "Activity or resource" to "Previous activity with completion"
+ And I press "Save changes"
+ Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region"
+
+ When I turn editing mode off
+ Then I should see "Not available unless: The activity Page3 is marked complete" in the "region-main" "region"
+
+ # Test if I disable completion tracking on Page3 section 5 depends on Page2.
+ When I turn editing mode on
+ And I open "Page3" actions menu
+ And I click on "Edit settings" "link" in the "Page3" activity
+ And I set the following fields to these values:
+ | Completion tracking | Do not indicate activity completion |
+ And I press "Save and return to course"
+
+ When I turn editing mode off
+ Then I should see "Not available unless: The activity Page2 is marked complete" in the "region-main" "region"
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class availability_completion_condition_testcase extends advanced_testcase {
+
/**
- * Load required classes.
+ * Setup to ensure that fixtures are loaded.
*/
- public function setUp() {
- // Load the mock info class so that it can be used.
+ public static function setupBeforeClass(): void {
global $CFG;
+ // Load the mock info class so that it can be used.
require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info.php');
+ require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info_module.php');
+ require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info_section.php');
+ }
+
+ /**
+ * Load required classes.
+ */
+ public function setUp() {
+ availability_completion\condition::wipe_static_cache();
}
/**
$CFG->enablecompletion = true;
$CFG->enableavailability = true;
$generator = $this->getDataGenerator();
- $course = $generator->create_course(array('enablecompletion' => 1));
+ $course = $generator->create_course(['enablecompletion' => 1]);
$page = $generator->get_plugin_generator('mod_page')->create_instance(
- array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
+ ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
+ $selfpage = $generator->get_plugin_generator('mod_page')->create_instance(
+ ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
$modinfo = get_fast_modinfo($course);
$cm = $modinfo->get_cm($page->cmid);
$info = new \core_availability\mock_info($course, $USER->id);
- $structure = (object)array('op' => '|', 'show' => true, 'c' => array(
- (object)array('type' => 'completion', 'cm' => (int)$cm->id,
- 'e' => COMPLETION_COMPLETE)));
+ $structure = (object)[
+ 'op' => '|',
+ 'show' => true,
+ 'c' => [
+ (object)[
+ 'type' => 'completion',
+ 'cm' => (int)$cm->id,
+ 'e' => COMPLETION_COMPLETE
+ ]
+ ]
+ ];
$tree = new \core_availability\tree($structure);
// Initial check (user has not completed activity).
$structure->e = COMPLETION_INCOMPLETE;
$cond = new condition($structure);
$this->assertEquals('{completion:cm42 INCOMPLETE}', (string)$cond);
+
+ // Successful contruct with previous activity.
+ $structure->cm = condition::OPTION_PREVIOUS;
+ $cond = new condition($structure);
+ $this->assertEquals('{completion:cmopprevious INCOMPLETE}', (string)$cond);
+
}
/**
* Tests the save() function.
*/
public function test_save() {
- $structure = (object)array('cm' => 42, 'e' => COMPLETION_COMPLETE);
+ $structure = (object)['cm' => 42, 'e' => COMPLETION_COMPLETE];
$cond = new condition($structure);
$structure->type = 'completion';
$this->assertEquals($structure, $cond->save());
$CFG->enablecompletion = true;
$CFG->enableavailability = true;
$generator = $this->getDataGenerator();
- $course = $generator->create_course(array('enablecompletion' => 1));
+ $course = $generator->create_course(['enablecompletion' => 1]);
$user = $generator->create_user();
$generator->enrol_user($user->id, $course->id);
$this->setUser($user);
// Create a Page with manual completion for basic checks.
$page = $generator->get_plugin_generator('mod_page')->create_instance(
- array('course' => $course->id, 'name' => 'Page!',
- 'completion' => COMPLETION_TRACKING_MANUAL));
+ ['course' => $course->id, 'name' => 'Page!',
+ 'completion' => COMPLETION_TRACKING_MANUAL]);
// Create an assignment - we need to have something that can be graded
// so as to test the PASS/FAIL states. Set it up to be completed based
// on its grade item.
- $assignrow = $this->getDataGenerator()->create_module('assign', array(
- 'course' => $course->id, 'name' => 'Assign!',
- 'completion' => COMPLETION_TRACKING_AUTOMATIC));
+ $assignrow = $this->getDataGenerator()->create_module('assign', [
+ 'course' => $course->id, 'name' => 'Assign!',
+ 'completion' => COMPLETION_TRACKING_AUTOMATIC]);
$DB->set_field('course_modules', 'completiongradeitemnumber', 0,
- array('id' => $assignrow->cmid));
+ ['id' => $assignrow->cmid]);
$assign = new assign(context_module::instance($assignrow->cmid), false, false);
// Get basic details.
$info = new \core_availability\mock_info($course, $user->id);
// COMPLETE state (false), positive and NOT.
- $cond = new condition((object)array(
- 'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE));
+ $cond = new condition((object)[
+ 'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE
+ ]);
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$information = \core_availability\info::format_info($information, $course);
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
// INCOMPLETE state (true).
- $cond = new condition((object)array(
- 'cm' => (int)$pagecm->id, 'e' => COMPLETION_INCOMPLETE));
+ $cond = new condition((object)[
+ 'cm' => (int)$pagecm->id, 'e' => COMPLETION_INCOMPLETE
+ ]);
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
$information = $cond->get_description(false, true, $info);
$completion->update_state($pagecm, COMPLETION_COMPLETE);
// COMPLETE state (true).
- $cond = new condition((object)array(
- 'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE));
+ $cond = new condition((object)[
+ 'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE
+ ]);
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
$information = $cond->get_description(false, true, $info);
$this->assertRegExp('~Page!.*is incomplete~', $information);
// INCOMPLETE state (false).
- $cond = new condition((object)array(
- 'cm' => (int)$pagecm->id, 'e' => COMPLETION_INCOMPLETE));
+ $cond = new condition((object)[
+ 'cm' => (int)$pagecm->id, 'e' => COMPLETION_INCOMPLETE
+ ]);
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$information = \core_availability\info::format_info($information, $course);
// We are going to need the grade item so that we can get pass/fails.
$gradeitem = $assign->get_grade_item();
- grade_object::set_properties($gradeitem, array('gradepass' => 50.0));
+ grade_object::set_properties($gradeitem, ['gradepass' => 50.0]);
$gradeitem->update();
// With no grade, it should return true for INCOMPLETE and false for
// the other three.
- $cond = new condition((object)array(
- 'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE));
+ $cond = new condition((object)[
+ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE
+ ]);
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
- $cond = new condition((object)array(
- 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE));
+ $cond = new condition((object)[
+ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE
+ ]);
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
// Check $information for COMPLETE_PASS and _FAIL as we haven't yet.
- $cond = new condition((object)array(
- 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS));
+ $cond = new condition((object)[
+ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS
+ ]);
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$information = \core_availability\info::format_info($information, $course);
$this->assertRegExp('~Assign!.*is complete and passed~', $information);
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
- $cond = new condition((object)array(
- 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL));
+ $cond = new condition((object)[
+ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL
+ ]);
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$information = \core_availability\info::format_info($information, $course);
// Change the grade to be complete and failed.
self::set_grade($assignrow, $user->id, 40);
- $cond = new condition((object)array(
- 'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE));
+ $cond = new condition((object)[
+ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE
+ ]);
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
- $cond = new condition((object)array(
- 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE));
+ $cond = new condition((object)[
+ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE
+ ]);
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
- $cond = new condition((object)array(
- 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS));
+ $cond = new condition((object)[
+ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS
+ ]);
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$information = \core_availability\info::format_info($information, $course);
$this->assertRegExp('~Assign!.*is complete and passed~', $information);
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
- $cond = new condition((object)array(
- 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL));
+ $cond = new condition((object)[
+ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL
+ ]);
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
$information = $cond->get_description(false, true, $info);
// Now change it to pass.
self::set_grade($assignrow, $user->id, 60);
- $cond = new condition((object)array(
- 'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE));
+ $cond = new condition((object)[
+ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE
+ ]);
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
- $cond = new condition((object)array(
- 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE));
+ $cond = new condition((object)[
+ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE
+ ]);
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
- $cond = new condition((object)array(
- 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS));
+ $cond = new condition((object)[
+ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS
+ ]);
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
$information = $cond->get_description(false, true, $info);
$information = \core_availability\info::format_info($information, $course);
$this->assertRegExp('~Assign!.*is not complete and passed~', $information);
- $cond = new condition((object)array(
- 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL));
+ $cond = new condition((object)[
+ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL
+ ]);
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$information = \core_availability\info::format_info($information, $course);
// Simulate deletion of an activity by using an invalid cmid. These
// conditions always fail, regardless of NOT flag or INCOMPLETE.
- $cond = new condition((object)array(
- 'cm' => ($assigncm->id + 100), 'e' => COMPLETION_COMPLETE));
+ $cond = new condition((object)[
+ 'cm' => ($assigncm->id + 100), 'e' => COMPLETION_COMPLETE
+ ]);
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$information = \core_availability\info::format_info($information, $course);
$this->assertRegExp('~(Missing activity).*is marked complete~', $information);
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
- $cond = new condition((object)array(
- 'cm' => ($assigncm->id + 100), 'e' => COMPLETION_INCOMPLETE));
+ $cond = new condition((object)[
+ 'cm' => ($assigncm->id + 100), 'e' => COMPLETION_INCOMPLETE
+ ]);
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
}
+ /**
+ * Tests the is_available and get_description functions for previous activity option.
+ *
+ * @dataProvider test_previous_activity_data
+ * @param int $grade the current assign grade (0 for none)
+ * @param int $condition true for complete, false for incomplete
+ * @param string $mark activity to mark as complete
+ * @param string $activity activity name to test
+ * @param bool $result if it must be available or not
+ * @param bool $resultnot if it must be available when the condition is inverted
+ * @param string $description the availabiklity text to check
+ */
+ public function test_previous_activity(int $grade, int $condition, string $mark, string $activity,
+ bool $result, bool $resultnot, string $description): void {
+ global $CFG, $DB;
+ require_once($CFG->dirroot . '/mod/assign/locallib.php');
+ $this->resetAfterTest();
+
+ // Create course with completion turned on.
+ $CFG->enablecompletion = true;
+ $CFG->enableavailability = true;
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course(['enablecompletion' => 1]);
+ $user = $generator->create_user();
+ $generator->enrol_user($user->id, $course->id);
+ $this->setUser($user);
+
+ // Page 1 (manual completion).
+ $page1 = $generator->get_plugin_generator('mod_page')->create_instance(
+ ['course' => $course->id, 'name' => 'Page1!',
+ 'completion' => COMPLETION_TRACKING_MANUAL]);
+
+ // Page 2 (manual completion).
+ $page2 = $generator->get_plugin_generator('mod_page')->create_instance(
+ ['course' => $course->id, 'name' => 'Page2!',
+ 'completion' => COMPLETION_TRACKING_MANUAL]);
+
+ // Page ignored (no completion).
+ $pagenocompletion = $generator->get_plugin_generator('mod_page')->create_instance(
+ ['course' => $course->id, 'name' => 'Page ignored!']);
+
+ // Create an assignment - we need to have something that can be graded
+ // so as to test the PASS/FAIL states. Set it up to be completed based
+ // on its grade item.
+ $assignrow = $this->getDataGenerator()->create_module('assign', [
+ 'course' => $course->id, 'name' => 'Assign!',
+ 'completion' => COMPLETION_TRACKING_AUTOMATIC
+ ]);
+ $DB->set_field('course_modules', 'completiongradeitemnumber', 0,
+ ['id' => $assignrow->cmid]);
+ $assign = new assign(context_module::instance($assignrow->cmid), false, false);
+
+ // Page 3 (manual completion).
+ $page3 = $generator->get_plugin_generator('mod_page')->create_instance(
+ ['course' => $course->id, 'name' => 'Page3!',
+ 'completion' => COMPLETION_TRACKING_MANUAL]);
+
+ // Get basic details.
+ $activities = [];
+ $modinfo = get_fast_modinfo($course);
+ $activities['page1'] = $modinfo->get_cm($page1->cmid);
+ $activities['page2'] = $modinfo->get_cm($page2->cmid);
+ $activities['assign'] = $assign->get_course_module();
+ $activities['page3'] = $modinfo->get_cm($page3->cmid);
+ $prevvalue = condition::OPTION_PREVIOUS;
+
+ // Setup gradings and completion.
+ if ($grade) {
+ $gradeitem = $assign->get_grade_item();
+ grade_object::set_properties($gradeitem, ['gradepass' => 50.0]);
+ $gradeitem->update();
+ self::set_grade($assignrow, $user->id, $grade);
+ }
+ if ($mark) {
+ $completion = new completion_info($course);
+ $completion->update_state($activities[$mark], COMPLETION_COMPLETE);
+ }
+
+ // Set opprevious WITH non existent previous activity.
+ $info = new \core_availability\mock_info_module($user->id, $activities[$activity]);
+ $cond = new condition((object)[
+ 'cm' => (int)$prevvalue, 'e' => $condition
+ ]);
+
+ // Do the checks.
+ $this->assertEquals($result, $cond->is_available(false, $info, true, $user->id));
+ $this->assertEquals($resultnot, $cond->is_available(true, $info, true, $user->id));
+ $information = $cond->get_description(false, false, $info);
+ $information = \core_availability\info::format_info($information, $course);
+ $this->assertRegExp($description, $information);
+ }
+
+ public function test_previous_activity_data(): array {
+ // Assign grade, condition, activity to complete, activity to test, result, resultnot, description.
+ return [
+ 'Missing previous activity complete' => [
+ 0, COMPLETION_COMPLETE, '', 'page1', false, false, '~Missing activity.*is marked complete~'
+ ],
+ 'Missing previous activity incomplete' => [
+ 0, COMPLETION_INCOMPLETE, '', 'page1', false, false, '~Missing activity.*is incomplete~'
+ ],
+ 'Previous complete condition with previous activity incompleted' => [
+ 0, COMPLETION_COMPLETE, '', 'page2', false, true, '~Page1!.*is marked complete~'
+ ],
+ 'Previous incomplete condition with previous activity incompleted' => [
+ 0, COMPLETION_INCOMPLETE, '', 'page2', true, false, '~Page1!.*is incomplete~'
+ ],
+ 'Previous complete condition with previous activity completed' => [
+ 0, COMPLETION_COMPLETE, 'page1', 'page2', true, false, '~Page1!.*is marked complete~'
+ ],
+ 'Previous incomplete condition with previous activity completed' => [
+ 0, COMPLETION_INCOMPLETE, 'page1', 'page2', false, true, '~Page1!.*is incomplete~'
+ ],
+ // Depenging on page pass fail (pages are not gradable).
+ 'Previous complete pass condition with previous no gradable activity incompleted' => [
+ 0, COMPLETION_COMPLETE_PASS, '', 'page2', false, true, '~Page1!.*is complete and passed~'
+ ],
+ 'Previous complete fail condition with previous no gradable activity incompleted' => [
+ 0, COMPLETION_COMPLETE_FAIL, '', 'page2', false, true, '~Page1!.*is complete and failed~'
+ ],
+ 'Previous complete pass condition with previous no gradable activity completed' => [
+ 0, COMPLETION_COMPLETE_PASS, 'page1', 'page2', false, true, '~Page1!.*is complete and passed~'
+ ],
+ 'Previous complete fail condition with previous no gradable activity completed' => [
+ 0, COMPLETION_COMPLETE_FAIL, 'page1', 'page2', false, true, '~Page1!.*is complete and failed~'
+ ],
+ // There's an page without completion between page2 ans assign.
+ 'Previous complete condition with sibling activity incompleted' => [
+ 0, COMPLETION_COMPLETE, '', 'assign', false, true, '~Page2!.*is marked complete~'
+ ],
+ 'Previous incomplete condition with sibling activity incompleted' => [
+ 0, COMPLETION_INCOMPLETE, '', 'assign', true, false, '~Page2!.*is incomplete~'
+ ],
+ 'Previous complete condition with sibling activity completed' => [
+ 0, COMPLETION_COMPLETE, 'page2', 'assign', true, false, '~Page2!.*is marked complete~'
+ ],
+ 'Previous incomplete condition with sibling activity completed' => [
+ 0, COMPLETION_INCOMPLETE, 'page2', 'assign', false, true, '~Page2!.*is incomplete~'
+ ],
+ // Depending on assign without grade.
+ 'Previous complete condition with previous without grade' => [
+ 0, COMPLETION_COMPLETE, '', 'page3', false, true, '~Assign!.*is marked complete~'
+ ],
+ 'Previous incomplete condition with previous without grade' => [
+ 0, COMPLETION_INCOMPLETE, '', 'page3', true, false, '~Assign!.*is incomplete~'
+ ],
+ 'Previous complete pass condition with previous without grade' => [
+ 0, COMPLETION_COMPLETE_PASS, '', 'page3', false, true, '~Assign!.*is complete and passed~'
+ ],
+ 'Previous complete fail condition with previous without grade' => [
+ 0, COMPLETION_COMPLETE_FAIL, '', 'page3', false, true, '~Assign!.*is complete and failed~'
+ ],
+ // Depending on assign with grade.
+ 'Previous complete condition with previous fail grade' => [
+ 40, COMPLETION_COMPLETE, '', 'page3', true, false, '~Assign!.*is marked complete~'
+ ],
+ 'Previous incomplete condition with previous fail grade' => [
+ 40, COMPLETION_INCOMPLETE, '', 'page3', false, true, '~Assign!.*is incomplete~'
+ ],
+ 'Previous complete pass condition with previous fail grade' => [
+ 40, COMPLETION_COMPLETE_PASS, '', 'page3', false, true, '~Assign!.*is complete and passed~'
+ ],
+ 'Previous complete fail condition with previous fail grade' => [
+ 40, COMPLETION_COMPLETE_FAIL, '', 'page3', true, false, '~Assign!.*is complete and failed~'
+ ],
+ 'Previous complete condition with previous pass grade' => [
+ 60, COMPLETION_COMPLETE, '', 'page3', true, false, '~Assign!.*is marked complete~'
+ ],
+ 'Previous incomplete condition with previous pass grade' => [
+ 60, COMPLETION_INCOMPLETE, '', 'page3', false, true, '~Assign!.*is incomplete~'
+ ],
+ 'Previous complete pass condition with previous pass grade' => [
+ 60, COMPLETION_COMPLETE_PASS, '', 'page3', true, false, '~Assign!.*is complete and passed~'
+ ],
+ 'Previous complete fail condition with previous pass grade' => [
+ 60, COMPLETION_COMPLETE_FAIL, '', 'page3', false, true, '~Assign!.*is complete and failed~'
+ ],
+ ];
+ }
+
+ /**
+ * Tests the is_available and get_description functions for
+ * previous activity option in course sections.
+ *
+ * @dataProvider test_section_previous_activity_data
+ * @param int $condition condition value
+ * @param bool $mark if Page 1 must be mark as completed
+ * @param string $section section to add the availability
+ * @param bool $result expected result
+ * @param bool $resultnot expected negated result
+ * @param string $description description to match
+ */
+ public function test_section_previous_activity(int $condition, bool $mark, string $section,
+ bool $result, bool $resultnot, string $description): void {
+ global $CFG, $DB;
+ require_once($CFG->dirroot . '/mod/assign/locallib.php');
+ $this->resetAfterTest();
+
+ // Create course with completion turned on.
+ $CFG->enablecompletion = true;
+ $CFG->enableavailability = true;
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course(
+ ['numsections' => 4, 'enablecompletion' => 1],
+ ['createsections' => true]);
+ $user = $generator->create_user();
+ $generator->enrol_user($user->id, $course->id);
+ $this->setUser($user);
+
+ // Section 1 - page1 (manual completion).
+ $page1 = $generator->get_plugin_generator('mod_page')->create_instance(
+ ['course' => $course->id, 'name' => 'Page1!', 'section' => 1,
+ 'completion' => COMPLETION_TRACKING_MANUAL]);
+
+ // Section 1 - page ignored 1 (no completion).
+ $pagenocompletion1 = $generator->get_plugin_generator('mod_page')->create_instance(
+ ['course' => $course, 'name' => 'Page ignored!', 'section' => 1]);
+
+ // Section 2 - page ignored 2 (no completion).
+ $pagenocompletion2 = $generator->get_plugin_generator('mod_page')->create_instance(
+ ['course' => $course, 'name' => 'Page ignored!', 'section' => 2]);
+
+ // Section 3 - page2 (manual completion).
+ $page2 = $generator->get_plugin_generator('mod_page')->create_instance(
+ ['course' => $course->id, 'name' => 'Page2!', 'section' => 3,
+ 'completion' => COMPLETION_TRACKING_MANUAL]);
+
+ // Section 4 is empty.
+
+ // Get basic details.
+ get_fast_modinfo(0, 0, true);
+ $modinfo = get_fast_modinfo($course);
+ $sections['section1'] = $modinfo->get_section_info(1);
+ $sections['section2'] = $modinfo->get_section_info(2);
+ $sections['section3'] = $modinfo->get_section_info(3);
+ $sections['section4'] = $modinfo->get_section_info(4);
+ $page1cm = $modinfo->get_cm($page1->cmid);
+ $prevvalue = condition::OPTION_PREVIOUS;
+
+ if ($mark) {
+ // Mark page1 complete.
+ $completion = new completion_info($course);
+ $completion->update_state($page1cm, COMPLETION_COMPLETE);
+ }
+
+ $info = new \core_availability\mock_info_section($user->id, $sections[$section]);
+ $cond = new condition((object)[
+ 'cm' => (int)$prevvalue, 'e' => $condition
+ ]);
+ $this->assertEquals($result, $cond->is_available(false, $info, true, $user->id));
+ $this->assertEquals($resultnot, $cond->is_available(true, $info, true, $user->id));
+ $information = $cond->get_description(false, false, $info);
+ $information = \core_availability\info::format_info($information, $course);
+ $this->assertRegExp($description, $information);
+
+ }
+
+ public function test_section_previous_activity_data(): array {
+ return [
+ // Condition, Activity completion, section to test, result, resultnot, description.
+ 'Completion complete Section with no previous activity' => [
+ COMPLETION_COMPLETE, false, 'section1', false, false, '~Missing activity.*is marked complete~'
+ ],
+ 'Completion incomplete Section with no previous activity' => [
+ COMPLETION_INCOMPLETE, false, 'section1', false, false, '~Missing activity.*is incomplete~'
+ ],
+ // Section 2 depending on section 1 -> Page 1 (no grading).
+ 'Completion complete Section with previous activity incompleted' => [
+ COMPLETION_COMPLETE, false, 'section2', false, true, '~Page1!.*is marked complete~'
+ ],
+ 'Completion incomplete Section with previous activity incompleted' => [
+ COMPLETION_INCOMPLETE, false, 'section2', true, false, '~Page1!.*is incomplete~'
+ ],
+ 'Completion complete Section with previous activity completed' => [
+ COMPLETION_COMPLETE, true, 'section2', true, false, '~Page1!.*is marked complete~'
+ ],
+ 'Completion incomplete Section with previous activity completed' => [
+ COMPLETION_INCOMPLETE, true, 'section2', false, true, '~Page1!.*is incomplete~'
+ ],
+ // Section 3 depending on section 1 -> Page 1 (no grading).
+ 'Completion complete Section ignoring empty sections and activity incompleted' => [
+ COMPLETION_COMPLETE, false, 'section3', false, true, '~Page1!.*is marked complete~'
+ ],
+ 'Completion incomplete Section ignoring empty sections and activity incompleted' => [
+ COMPLETION_INCOMPLETE, false, 'section3', true, false, '~Page1!.*is incomplete~'
+ ],
+ 'Completion complete Section ignoring empty sections and activity completed' => [
+ COMPLETION_COMPLETE, true, 'section3', true, false, '~Page1!.*is marked complete~'
+ ],
+ 'Completion incomplete Section ignoring empty sections and activity completed' => [
+ COMPLETION_INCOMPLETE, true, 'section3', false, true, '~Page1!.*is incomplete~'
+ ],
+ // Section 4 depending on section 3 -> Page 2 (no grading).
+ 'Completion complete Last section with previous activity incompleted' => [
+ COMPLETION_COMPLETE, false, 'section4', false, true, '~Page2!.*is marked complete~'
+ ],
+ 'Completion incomplete Last section with previous activity incompleted' => [
+ COMPLETION_INCOMPLETE, false, 'section4', true, false, '~Page2!.*is incomplete~'
+ ],
+ 'Completion complete Last section with previous activity completed' => [
+ COMPLETION_COMPLETE, true, 'section4', false, true, '~Page2!.*is marked complete~'
+ ],
+ 'Completion incomplete Last section with previous activity completed' => [
+ COMPLETION_INCOMPLETE, true, 'section4', true, false, '~Page2!.*is incomplete~'
+ ],
+ ];
+ }
+
/**
* Tests completion_value_used static function.
*/
public function test_completion_value_used() {
global $CFG, $DB;
$this->resetAfterTest();
+ $prevvalue = condition::OPTION_PREVIOUS;
// Create course with completion turned on and some sections.
$CFG->enablecompletion = true;
$CFG->enableavailability = true;
$generator = $this->getDataGenerator();
$course = $generator->create_course(
- array('numsections' => 1, 'enablecompletion' => 1),
- array('createsections' => true));
- availability_completion\condition::wipe_static_cache();
+ ['numsections' => 1, 'enablecompletion' => 1],
+ ['createsections' => true]);
- // Create three pages with manual completion.
+ // Create six pages with manual completion.
$page1 = $generator->get_plugin_generator('mod_page')->create_instance(
- array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
+ ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
$page2 = $generator->get_plugin_generator('mod_page')->create_instance(
- array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
+ ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
$page3 = $generator->get_plugin_generator('mod_page')->create_instance(
- array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
+ ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
+ $page4 = $generator->get_plugin_generator('mod_page')->create_instance(
+ ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
+ $page5 = $generator->get_plugin_generator('mod_page')->create_instance(
+ ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
+ $page6 = $generator->get_plugin_generator('mod_page')->create_instance(
+ ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]);
// Set up page3 to depend on page1, and section1 to depend on page2.
$DB->set_field('course_modules', 'availability',
'{"op":"|","show":true,"c":[' .
'{"type":"completion","e":1,"cm":' . $page1->cmid . '}]}',
- array('id' => $page3->cmid));
+ ['id' => $page3->cmid]);
$DB->set_field('course_sections', 'availability',
'{"op":"|","show":true,"c":[' .
'{"type":"completion","e":1,"cm":' . $page2->cmid . '}]}',
- array('course' => $course->id, 'section' => 1));
+ ['course' => $course->id, 'section' => 1]);
+ // Set up page5 and page6 to depend on previous activity.
+ $DB->set_field('course_modules', 'availability',
+ '{"op":"|","show":true,"c":[' .
+ '{"type":"completion","e":1,"cm":' . $prevvalue . '}]}',
+ ['id' => $page5->cmid]);
+ $DB->set_field('course_modules', 'availability',
+ '{"op":"|","show":true,"c":[' .
+ '{"type":"completion","e":1,"cm":' . $prevvalue . '}]}',
+ ['id' => $page6->cmid]);
- // Now check: nothing depends on page3 but something does on the others.
+ // Check 1: nothing depends on page3 and page6 but something does on the others.
$this->assertTrue(availability_completion\condition::completion_value_used(
$course, $page1->cmid));
$this->assertTrue(availability_completion\condition::completion_value_used(
$course, $page2->cmid));
$this->assertFalse(availability_completion\condition::completion_value_used(
$course, $page3->cmid));
+ $this->assertTrue(availability_completion\condition::completion_value_used(
+ $course, $page4->cmid));
+ $this->assertTrue(availability_completion\condition::completion_value_used(
+ $course, $page5->cmid));
+ $this->assertFalse(availability_completion\condition::completion_value_used(
+ $course, $page6->cmid));
}
/**
* @param float $grade Grade
*/
protected static function set_grade($assignrow, $userid, $grade) {
- $grades = array();
- $grades[$userid] = (object)array(
- 'rawgrade' => $grade, 'userid' => $userid);
+ $grades = [];
+ $grades[$userid] = (object)[
+ 'rawgrade' => $grade, 'userid' => $userid
+ ];
$assignrow->cmidnumber = null;
assign_grade_item_update($assignrow, $grades);
}
* Tests the update_dependency_id() function.
*/
public function test_update_dependency_id() {
- $cond = new condition((object)array(
- 'cm' => 123, 'e' => COMPLETION_COMPLETE));
- $this->assertFalse($cond->update_dependency_id('frogs', 123, 456));
+ $cond = new condition((object)[
+ 'cm' => 42, 'e' => COMPLETION_COMPLETE, 'selfid' => 43
+ ]);
+ $this->assertFalse($cond->update_dependency_id('frogs', 42, 540));
$this->assertFalse($cond->update_dependency_id('course_modules', 12, 34));
- $this->assertTrue($cond->update_dependency_id('course_modules', 123, 456));
+ $this->assertTrue($cond->update_dependency_id('course_modules', 42, 456));
$after = $cond->save();
$this->assertEquals(456, $after->cm);
+
+ // Test selfid updating.
+ $cond = new condition((object)[
+ 'cm' => 42, 'e' => COMPLETION_COMPLETE
+ ]);
+ $this->assertFalse($cond->update_dependency_id('frogs', 43, 540));
+ $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34));
+ $after = $cond->save();
+ $this->assertEquals(42, $after->cm);
+
+ // Test on previous activity.
+ $cond = new condition((object)[
+ 'cm' => condition::OPTION_PREVIOUS,
+ 'e' => COMPLETION_COMPLETE
+ ]);
+ $this->assertFalse($cond->update_dependency_id('frogs', 43, 80));
+ $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34));
+ $after = $cond->save();
+ $this->assertEquals(condition::OPTION_PREVIOUS, $after->cm);
}
}
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2019111800;
+$plugin->version = 2020032600;
$plugin->requires = 2019111200;
$plugin->component = 'availability_completion';
--- /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/>.
+
+/**
+ * For use in unit tests that require an info module which isn't really used.
+ *
+ * @package core_availability
+ * @copyright 2019 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_availability;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * For use in unit tests that require an info module which isn't really used.
+ *
+ * @package core_availability
+ * @copyright 2019 Ferran Recio <ferran@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mock_info_module extends info_module {
+ /** @var int User id for modinfo */
+ protected $userid;
+
+ /** @var \cm_info Activity. */
+ protected $cm;
+
+ /**
+ * Constructs with item details.
+ *
+ * @param int $userid Userid for modinfo (if used)
+ * @param \cm_info $cm Course-module object
+ */
+ public function __construct($userid = 0, \cm_info $cm = null) {
+ parent::__construct($cm);
+ $this->userid = $userid;
+ $this->cm = $cm;
+ }
+
+ /**
+ * Just returns a mock name.
+ *
+ * @return string Name of item
+ */
+ protected function get_thing_name() {
+ return 'Mock Module';
+ }
+
+ /**
+ * Returns the current context.
+ *
+ * @return \context Context for this item
+ */
+ public function get_context() {
+ return \context_course::instance($this->get_course()->id);
+ }
+
+ /**
+ * Returns the cappability used to ignore access restrictions.
+ *
+ * @return string Name of capability used to view hidden items of this type
+ */
+ protected function get_view_hidden_capability() {
+ return 'moodle/course:ignoreavailabilityrestrictions';
+ }
+
+ /**
+ * Mocks don't need to save anything into DB.
+ *
+ * @param string $availability New JSON value
+ */
+ protected function set_in_database($availability) {
+ }
+
+ /**
+ * Obtains the modinfo associated with this availability information.
+ *
+ * Note: This field is available ONLY for use by conditions when calculating
+ * availability or information.
+ *
+ * @return \course_modinfo Modinfo
+ * @throws \coding_exception If called at incorrect times
+ */
+ public function get_modinfo() {
+ // Allow modinfo usage outside is_available etc., so we can use this
+ // to directly call into condition is_available.
+ if (!$this->userid) {
+ throw new \coding_exception('Need to set mock_info userid');
+ }
+ return get_fast_modinfo($this->course, $this->userid);
+ }
+
+ /**
+ * Override course-module info.
+ * @param \cm_info $cm
+ */
+ public function set_cm (\cm_info $cm) {
+ $this->cm = $cm;
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * For use in unit tests that require an info section which isn't really used.
+ *
+ * @package core_availability
+ * @copyright 2019 Ferran Recio <ferran@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_availability;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * For use in unit tests that require an info section which isn't really used.
+ *
+ * @package core_availability
+ * @copyright 2019 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mock_info_section extends info_section {
+ /** @var int User id for modinfo */
+ protected $userid;
+
+ /** @var \section_info Section. */
+ protected $section;
+
+ /**
+ * Constructs with item details.
+ *
+ * @param int $userid Userid for modinfo (if used)
+ * @param \section_info $section Section object
+ */
+ public function __construct($userid = 0, \section_info $section = null) {
+ parent::__construct($section);
+ $this->userid = $userid;
+ $this->section = $section;
+ }
+
+ /**
+ * Just returns a mock name.
+ *
+ * @return string Name of item
+ */
+ protected function get_thing_name() {
+ return 'Mock Section';
+ }
+
+ /**
+ * Returns the current context.
+ *
+ * @return \context Context for this item
+ */
+ public function get_context() {
+ return \context_course::instance($this->get_course()->id);
+ }
+
+ /**
+ * Returns the cappability used to ignore access restrictions.
+ *
+ * @return string Name of capability used to view hidden items of this type
+ */
+ protected function get_view_hidden_capability() {
+ return 'moodle/course:ignoreavailabilityrestrictions';
+ }
+
+ /**
+ * Mocks don't need to save anything into DB.
+ *
+ * @param string $availability New JSON value
+ */
+ protected function set_in_database($availability) {
+ }
+
+ /**
+ * Obtains the modinfo associated with this availability information.
+ *
+ * Note: This field is available ONLY for use by conditions when calculating
+ * availability or information.
+ *
+ * @return \course_modinfo Modinfo
+ * @throws \coding_exception If called at incorrect times
+ */
+ public function get_modinfo() {
+ // Allow modinfo usage outside is_available etc., so we can use this
+ // to directly call into condition is_available.
+ if (!$this->userid) {
+ throw new \coding_exception('Need to set mock_info userid');
+ }
+ return get_fast_modinfo($this->course, $this->userid);
+ }
+
+ /**
+ * Override section info.
+ *
+ * @param \section_info $section
+ */
+ public function set_section (\section_info $section) {
+ $this->section = $section;
+ }
+}
$output .= html_writer::tag('div',
html_writer::link(new moodle_url('/course/recent.php', array('id' => $course->id)),
get_string('recentactivityreport')),
- array('class' => 'activityhead'));
+ array('class' => 'activityhead mb-3'));
$content = false;
// Next, have there been any modifications to the course structure?
if (!empty($structuralchanges)) {
$content = true;
- $output .= $this->heading(get_string("courseupdates").':', 3);
+ $output .= $this->heading(get_string("courseupdates") . ':', 6);
foreach ($structuralchanges as $changeinfo => $change) {
$output .= $this->structural_change($change);
}
-.block_recent_activity .activitydate,
-.block_recent_activity .activityhead {
- text-align: center;
-}
-
.block_recent_activity .unlist li {
margin-bottom: 1em;
}
| External URL | http://www.google.com |
| id_display | In pop-up |
Then "google" "link" should exist in the "Main menu" "block"
- And "Add an activity or resource" "button" should exist in the "Main menu" "block"
+ And "Add an activity" "button" should exist in the "Main menu" "block"
/**
* Ensure that the stats array is ready to collect information for the given store and definition.
* @param string $store
+ * @param string $storeclass
* @param string $definition A string that identifies the definition.
* @param int $mode One of cache_store::MODE_*. Since 2.9.
*/
- protected static function ensure_ready_for_stats($store, $definition, $mode = cache_store::MODE_APPLICATION) {
+ protected static function ensure_ready_for_stats($store, $storeclass, $definition, $mode = cache_store::MODE_APPLICATION) {
// This function is performance-sensitive, so exit as quickly as possible
// if we do not need to do anything.
if (isset(self::$stats[$definition]['stores'][$store])) {
return;
}
+
if (!array_key_exists($definition, self::$stats)) {
self::$stats[$definition] = array(
'mode' => $mode,
'stores' => array(
$store => array(
+ 'class' => $storeclass,
'hits' => 0,
'misses' => 0,
'sets' => 0,
);
} else if (!array_key_exists($store, self::$stats[$definition]['stores'])) {
self::$stats[$definition]['stores'][$store] = array(
+ 'class' => $storeclass,
'hits' => 0,
'misses' => 0,
'sets' => 0,
* In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
* cache_definition instance. It is preferable to pass a cache definition instance.
*
+ * In Moodle 3.9 the first argument changed to also accept a cache_store.
+ *
* @internal
- * @param cache_definition $store
+ * @param string|cache_store $store
* @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
* actual cache_definition object now.
* @param int $hits The number of hits to record (by default 1)
*/
public static function record_cache_hit($store, $definition, $hits = 1) {
+ $storeclass = '';
+ if ($store instanceof cache_store) {
+ $storeclass = get_class($store);
+ $store = $store->my_name();
+ }
list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
- self::ensure_ready_for_stats($store, $definitionstr, $mode);
+ self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode);
self::$stats[$definitionstr]['stores'][$store]['hits'] += $hits;
}
* In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
* cache_definition instance. It is preferable to pass a cache definition instance.
*
+ * In Moodle 3.9 the first argument changed to also accept a cache_store.
+ *
* @internal
- * @param string $store
+ * @param string|cache_store $store
* @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
* actual cache_definition object now.
&nb