--adminpass=PASSWORD Password for the moodle admin account,
required in non-interactive mode.
--adminemail=STRING Email address for the moodle admin account.
+--upgradekey=STRING The upgrade key to be set in the config.php, leave empty to not set it.
--non-interactive No interactive questions, installation fails if any
problem encountered.
--agree-license Indicates agreement with software license,
'adminuser' => 'admin',
'adminpass' => '',
'adminemail' => '',
+ 'upgradekey' => '',
'non-interactive' => false,
'agree-license' => false,
'allow-unstable' => false,
cli_error(get_string('cliincorrectvalueerror', 'admin', $a));
}
+// Ask for the upgrade key.
+if ($interactive) {
+ cli_separator();
+ cli_heading(get_string('upgradekeyset', 'admin'));
+ if ($options['upgradekey'] !== '') {
+ $prompt = get_string('clitypevaluedefault', 'admin', $options['upgradekey']);
+ $options['upgradekey'] = cli_input($prompt, $options['upgradekey']);
+ } else {
+ $prompt = get_string('clitypevalue', 'admin');
+ $options['upgradekey'] = cli_input($prompt);
+ }
+}
+
+// Set the upgrade key if it was provided.
+if ($options['upgradekey'] !== '') {
+ $CFG->upgradekey = $options['upgradekey'];
+}
+
if ($interactive) {
if (!$options['agree-license']) {
cli_separator();
define('NO_OUTPUT_BUFFERING', true);
+if (isset($_POST['upgradekey'])) {
+ // Before you start reporting issues about the collision attacks against
+ // SHA-1, you should understand that we are not actually attempting to do
+ // any cryptography here. This is hashed purely so that the key is not
+ // that apparent in the address bar itself. Anyone who catches the HTTP
+ // traffic can immediately use it as a valid admin key.
+ header('Location: index.php?cache=0&upgradekeyhash='.sha1($_POST['upgradekey']));
+ die();
+}
+
if ((isset($_GET['cache']) and $_GET['cache'] === '0')
or (isset($_POST['cache']) and $_POST['cache'] === '0')
or (!isset($_POST['cache']) and !isset($_GET['cache']) and empty($_GET['sesskey']) and empty($_POST['sesskey']))) {
$agreelicense = optional_param('agreelicense', 0, PARAM_BOOL);
$fetchupdates = optional_param('fetchupdates', 0, PARAM_BOOL);
$newaddonreq = optional_param('installaddonrequest', null, PARAM_RAW);
+$upgradekeyhash = optional_param('upgradekeyhash', null, PARAM_ALPHANUM);
// Set up PAGE.
$url = new moodle_url('/admin/index.php');
$url->param('cache', $cache);
+if (isset($upgradekeyhash)) {
+ $url->param('upgradekeyhash', $upgradekeyhash);
+}
$PAGE->set_url($url);
unset($url);
$PAGE->set_heading($strinstallation . ' - Moodle ' . $CFG->target_release);
$output = $PAGE->get_renderer('core', 'admin');
- $url = new moodle_url('/admin/index.php', array('agreelicense' => 1, 'confirmrelease' => 1, 'lang' => $CFG->lang));
+ $url = new moodle_url($PAGE->url, array('agreelicense' => 1, 'confirmrelease' => 1, 'lang' => $CFG->lang));
echo $output->unsatisfied_dependencies_page($version, $failed, $url);
die();
}
// Detect config cache inconsistency, this happens when you switch branches on dev servers.
if ($CFG->version != $DB->get_field('config', 'value', array('name'=>'version'))) {
purge_all_caches();
- redirect(new moodle_url('/admin/index.php'), 'Config cache inconsistency detected, resetting caches...');
+ redirect(new moodle_url($PAGE->url), 'Config cache inconsistency detected, resetting caches...');
}
if (!$cache and $version > $CFG->version) { // upgrade
+ check_upgrade_key($upgradekeyhash);
+
// Warning about upgrading a test site.
$testsite = false;
if (defined('BEHAT_SITE_RUNNING')) {
$PAGE->set_heading($strplugincheck);
$PAGE->set_cacheable(false);
- $reloadurl = new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1, 'cache' => 0));
+ $reloadurl = new moodle_url($PAGE->url, array('confirmupgrade' => 1, 'confirmrelease' => 1, 'cache' => 0));
if ($fetchupdates) {
// No sesskey support guaranteed here, because sessions might not work yet.
}
echo $output->upgrade_plugin_check_page(core_plugin_manager::instance(), \core\update\checker::instance(),
- $version, $showallplugins, $reloadurl,
- new moodle_url('/admin/index.php', array('confirmupgrade'=>1, 'confirmrelease'=>1, 'confirmplugincheck'=>1, 'cache'=>0)));
+ $version, $showallplugins, $reloadurl, new moodle_url($PAGE->url, array(
+ 'confirmupgrade' => 1, 'confirmrelease' => 1, 'confirmplugincheck' => 1, 'cache' => 0)));
die();
} else {
// Always verify plugin dependencies!
$failed = array();
if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
- $reloadurl = new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1, 'cache' => 0));
+ $reloadurl = new moodle_url($PAGE->url, array('confirmupgrade' => 1, 'confirmrelease' => 1, 'cache' => 0));
echo $output->unsatisfied_dependencies_page($version, $failed, $reloadurl);
die();
}
}
if (!$cache and moodle_needs_upgrading()) {
+
+ check_upgrade_key($upgradekeyhash);
+
if (!$PAGE->headerprinted) {
// means core upgrade or installation was not already done
echo $output->upgrade_plugin_check_page(core_plugin_manager::instance(), \core\update\checker::instance(),
$version, $showallplugins,
new moodle_url($PAGE->url),
- new moodle_url('/admin/index.php', array('confirmplugincheck'=>1, 'cache'=>0)));
+ new moodle_url($PAGE->url, array('confirmplugincheck' => 1, 'cache' => 0)));
die();
}
if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
/** @var core_admin_renderer $output */
$output = $PAGE->get_renderer('core', 'admin');
- $reloadurl = new moodle_url('/admin/index.php', array('cache' => 0));
+ $reloadurl = new moodle_url($PAGE->url, array('cache' => 0));
echo $output->unsatisfied_dependencies_page($version, $failed, $reloadurl);
die();
}
$copyrightnotice = text_to_html(get_string('gpl3'));
$copyrightnotice = str_replace('target="_blank"', 'onclick="this.target=\'_blank\'"', $copyrightnotice); // extremely ugly validation hack
- $continue = new single_button(new moodle_url('/admin/index.php', array('lang'=>$CFG->lang, 'agreelicense'=>1)), get_string('continue'), 'get');
+ $continue = new single_button(new moodle_url($this->page->url, array(
+ 'lang' => $CFG->lang, 'agreelicense' => 1)), get_string('continue'), 'get');
$output .= $this->header();
$output .= $this->heading('<a href="http://moodle.org">Moodle</a> - Modular Object-Oriented Dynamic Learning Environment');
$output .= $this->environment_check_table($envstatus, $environment_results);
if (!$envstatus) {
- $output .= $this->upgrade_reload(new moodle_url('/admin/index.php', array('agreelicense' => 1, 'lang' => $CFG->lang)));
+ $output .= $this->upgrade_reload(new moodle_url($this->page->url, array('agreelicense' => 1, 'lang' => $CFG->lang)));
} else {
$output .= $this->notification(get_string('environmentok', 'admin'), 'notifysuccess');
- $output .= $this->continue_button(new moodle_url('/admin/index.php', array('agreelicense'=>1, 'confirmrelease'=>1, 'lang'=>$CFG->lang)));
+ $output .= $this->continue_button(new moodle_url($this->page->url, array(
+ 'agreelicense' => 1, 'confirmrelease' => 1, 'lang' => $CFG->lang)));
}
$output .= $this->footer();
public function upgrade_confirm_page($strnewversion, $maturity, $testsite) {
$output = '';
- $continueurl = new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'cache' => 0));
+ $continueurl = new moodle_url($this->page->url, array('confirmupgrade' => 1, 'cache' => 0));
$continue = new single_button($continueurl, get_string('continue'), 'get');
$cancelurl = new moodle_url('/admin/index.php');
$output .= $this->environment_check_table($envstatus, $environment_results);
if (!$envstatus) {
- $output .= $this->upgrade_reload(new moodle_url('/admin/index.php'), array('confirmupgrade' => 1, 'cache' => 0));
+ $output .= $this->upgrade_reload(new moodle_url($this->page->url, array('confirmupgrade' => 1, 'cache' => 0)));
} else {
$output .= $this->notification(get_string('environmentok', 'admin'), 'notifysuccess');
$output .= $this->box(get_string('langpackwillbeupdated', 'admin'), 'generalbox', 'notice');
}
- $output .= $this->continue_button(new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1, 'cache' => 0)));
+ $output .= $this->continue_button(new moodle_url($this->page->url, array(
+ 'confirmupgrade' => 1, 'confirmrelease' => 1, 'cache' => 0)));
}
$output .= $this->footer();
$out = $this->output->container_start('nonehighlighted', 'plugins-check-info');
$out .= $this->output->heading(get_string('nonehighlighted', 'core_plugin'));
if (empty($options['full'])) {
- $out .= html_writer::link(new moodle_url('/admin/index.php',
+ $out .= html_writer::link(new moodle_url($this->page->url,
array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 1, 'cache' => 0)),
get_string('nonehighlightedinfo', 'core_plugin'));
}
} else {
$out = $this->output->container_start('somehighlighted', 'plugins-check-info');
- $out .= $this->output->heading(get_string('somehighlighted', 'core_plugin', $sumofhighlighted));
if (empty($options['full'])) {
- $out .= html_writer::link(new moodle_url('/admin/index.php',
+ $out .= $this->output->heading(get_string('somehighlighted', 'core_plugin', $sumofhighlighted));
+ $out .= html_writer::link(new moodle_url($this->page->url,
array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 1, 'cache' => 0)),
get_string('somehighlightedinfo', 'core_plugin'));
} else {
- $out .= html_writer::link(new moodle_url('/admin/index.php',
+ $out .= $this->output->heading(get_string('somehighlightedall', 'core_plugin', $sumofhighlighted));
+ $out .= html_writer::link(new moodle_url($this->page->url,
array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 0, 'cache' => 0)),
get_string('somehighlightedonly', 'core_plugin'));
}
return $output;
}
+
+ /**
+ * Render a simple page for providing the upgrade key.
+ *
+ * @param moodle_url|string $url
+ * @return string
+ */
+ public function upgradekey_form_page($url) {
+
+ $output = '';
+ $output .= $this->header();
+ $output .= $this->container_start('upgradekeyreq');
+ $output .= $this->heading(get_string('upgradekeyreq', 'core_admin'));
+ $output .= html_writer::start_tag('form', array('method' => 'POST', 'action' => $url));
+ $output .= html_writer::empty_tag('input', array('name' => 'upgradekey', 'type' => 'password'));
+ $output .= html_writer::empty_tag('input', array('value' => get_string('submit'), 'type' => 'submit'));
+ $output .= html_writer::end_tag('form');
+ $output .= $this->container_end();
+ $output .= $this->footer();
+
+ return $output;
+ }
}
);
$temp->add(new admin_setting_configselect('backup/backup_auto_storage', new lang_string('automatedstorage', 'backup'), new lang_string('automatedstoragehelp', 'backup'), 0, $storageoptions));
$temp->add(new admin_setting_special_backup_auto_destination());
- $keepoptoins = array(
+
+ $maxkeptoptions = array(
0 => new lang_string('all'), 1 => '1',
2 => '2',
5 => '5',
300 => '300',
400 => '400',
500 => '500');
- $temp->add(new admin_setting_configselect('backup/backup_auto_keep', new lang_string('keep'), new lang_string('backupkeephelp'), 1, $keepoptoins));
+ $temp->add(new admin_setting_configselect('backup/backup_auto_max_kept', new lang_string('automatedmaxkept', 'backup'),
+ new lang_string('automatedmaxkepthelp', 'backup'), 1, $maxkeptoptions));
+
+ $automateddeletedaysoptions = array(
+ 0 => new lang_string('never'),
+ 1000 => new lang_string('numdays', '', 1000),
+ 365 => new lang_string('numdays', '', 365),
+ 180 => new lang_string('numdays', '', 180),
+ 150 => new lang_string('numdays', '', 150),
+ 120 => new lang_string('numdays', '', 120),
+ 90 => new lang_string('numdays', '', 90),
+ 60 => new lang_string('numdays', '', 60),
+ 35 => new lang_string('numdays', '', 35),
+ 10 => new lang_string('numdays', '', 10),
+ 5 => new lang_string('numdays', '', 5),
+ 2 => new lang_string('numdays', '', 2)
+ );
+ $temp->add(new admin_setting_configselect('backup/backup_auto_delete_days', new lang_string('automateddeletedays', 'backup'),
+ '', 0, $automateddeletedaysoptions));
+
+ $minkeptoptions = array(
+ 0 => new lang_string('none'),
+ 1 => '1',
+ 2 => '2',
+ 5 => '5',
+ 10 => '10',
+ 20 => '20',
+ 30 => '30',
+ 40 => '40',
+ 50 => '50',
+ 100 => '100',
+ 200 => '200',
+ 300 => '300',
+ 400 => '400'
+ );
+ $temp->add(new admin_setting_configselect('backup/backup_auto_min_kept', new lang_string('automatedminkept', 'backup'),
+ new lang_string('automatedminkepthelp', 'backup'), 0, $minkeptoptions));
+
$temp->add(new admin_setting_configcheckbox('backup/backup_shortname', new lang_string('backup_shortname', 'admin'), new lang_string('backup_shortnamehelp', 'admin'), 0));
$temp->add(new admin_setting_configcheckbox('backup/backup_auto_skip_hidden', new lang_string('skiphidden', 'backup'), new lang_string('skiphiddenhelp', 'backup'), 1));
$temp->add(new admin_setting_configselect('backup/backup_auto_skip_modif_days', new lang_string('skipmodifdays', 'backup'), new lang_string('skipmodifdayshelp', 'backup'), 30, array(
$ADMIN->add('modules', new admin_category('webservicesettings', new lang_string('webservices', 'webservice')));
// Mobile
$temp = new admin_settingpage('mobile', new lang_string('mobile','admin'), 'moodle/site:config', false);
- $enablemobiledocurl = new moodle_url(get_docs_url('Enable_mobile_web_services'));
- $enablemobiledoclink = html_writer::link($enablemobiledocurl, new lang_string('documentation'));
- $temp->add(new admin_setting_enablemobileservice('enablemobilewebservice',
- new lang_string('enablemobilewebservice', 'admin'),
- new lang_string('configenablemobilewebservice', 'admin', $enablemobiledoclink), 0));
+
+ // We should wait to the installation to finish since we depend on some configuration values that are set once
+ // the admin user profile is configured.
+ if (!during_initial_install()) {
+ $enablemobiledocurl = new moodle_url(get_docs_url('Enable_mobile_web_services'));
+ $enablemobiledoclink = html_writer::link($enablemobiledocurl, new lang_string('documentation'));
+ $default = is_https() ? 1 : 0;
+ $temp->add(new admin_setting_enablemobileservice('enablemobilewebservice',
+ new lang_string('enablemobilewebservice', 'admin'),
+ new lang_string('configenablemobilewebservice', 'admin', $enablemobiledoclink), $default));
+ }
+
$temp->add(new admin_setting_configtext('mobilecssurl', new lang_string('mobilecssurl', 'admin'), new lang_string('configmobilecssurl','admin'), '', PARAM_URL));
$ADMIN->add('webservicesettings', $temp);
/// overview page
} else {
phpCAS::client($this->config->casversion, $this->config->hostname, (int) $this->config->port, $this->config->baseuri, false);
}
+ // Some CAS installs require SSLv3 that should be explicitly set.
+ if (!empty($this->config->curl_ssl_version)) {
+ phpCAS::setExtraCurlOption(CURLOPT_SSLVERSION, $this->config->curl_ssl_version);
+ }
+
$connected = true;
}
if (!isset($config->certificate_path)) {
$config->certificate_path = '';
}
+ if (!isset($config->curl_ssl_version)) {
+ $config->curl_ssl_version = '';
+ }
if (!isset($config->logout_return_url)) {
$config->logout_return_url = '';
}
set_config('multiauth', $config->multiauth, $this->pluginconfig);
set_config('certificate_check', $config->certificate_check, $this->pluginconfig);
set_config('certificate_path', $config->certificate_path, $this->pluginconfig);
+ set_config('curl_ssl_version', $config->curl_ssl_version, $this->pluginconfig);
set_config('logout_return_url', $config->logout_return_url, $this->pluginconfig);
// save LDAP settings
if (!isset ($config->certificate_path)) {
$config->certificate_path = '';
}
+if (!isset($config->curl_ssl_version)) {
+ $config->curl_ssl_version = '';
+}
if (!isset($config->logout_return_url)) {
$config->logout_return_url = '';
}
<?php print_string('auth_cas_certificate_path', 'auth_cas') ?>
</td>
</tr>
+<tr valign="top" class="required">
+ <td align="right"><label for="curl_ ssl_version"><?php print_string('auth_cas_curl_ssl_version_key', 'auth_cas') ?>: </label></td>
+ <td>
+ <?php
+ $sslversions = array();
+ $sslversions[''] = get_string('auth_cas_curl_ssl_version_default', 'auth_cas');
+ if (defined('CURL_SSLVERSION_TLSv1')) {
+ $sslversions[CURL_SSLVERSION_TLSv1] = get_string('auth_cas_curl_ssl_version_TLSv1x', 'auth_cas');
+ }
+ if (defined('CURL_SSLVERSION_TLSv1_0')) {
+ $sslversions[CURL_SSLVERSION_TLSv1_0] = get_string('auth_cas_curl_ssl_version_TLSv10', 'auth_cas');
+ }
+ if (defined('CURL_SSLVERSION_TLSv1_1')) {
+ $sslversions[CURL_SSLVERSION_TLSv1_1] = get_string('auth_cas_curl_ssl_version_TLSv11', 'auth_cas');
+ }
+ if (defined('CURL_SSLVERSION_TLSv1_2')) {
+ $sslversions[CURL_SSLVERSION_TLSv1_2] = get_string('auth_cas_curl_ssl_version_TLSv12', 'auth_cas');
+ }
+ if (defined('CURL_SSLVERSION_SSLv2')) {
+ $sslversions[CURL_SSLVERSION_SSLv2] = get_string('auth_cas_curl_ssl_version_SSLv2', 'auth_cas');
+ }
+ if (defined('CURL_SSLVERSION_SSLv3')) {
+ $sslversions[CURL_SSLVERSION_SSLv3] = get_string('auth_cas_curl_ssl_version_SSLv3', 'auth_cas');
+ }
+ echo html_writer::select($sslversions, 'curl_ssl_version', $config->curl_ssl_version, false);
+ if (isset($err['curl_ssl_version'])) echo $OUTPUT->error_text($err['curl_ssl_version']);
+ ?>
+ </td>
+ <td>
+ <?php print_string('auth_cas_curl_ssl_version', 'auth_cas') ?>
+ </td>
+</tr>
<tr valign="top" class="required">
<td align="right"><?php print_string('auth_cas_logout_return_url_key', 'auth_cas') ?>:</td>
<td>
$string['auth_cas_certificate_path_key'] = 'Certificate path';
$string['auth_cas_create_user'] = 'Turn this on if you want to insert CAS-authenticated users in Moodle database. If not then only users who already exist in the Moodle database can log in.';
$string['auth_cas_create_user_key'] = 'Create user';
+$string['auth_cas_curl_ssl_version'] = 'The SSL version (2 or 3) to use. By default PHP will try to determine this itself, although in some cases this must be set manually.';
+$string['auth_cas_curl_ssl_version_default'] = 'Default';
+$string['auth_cas_curl_ssl_version_key'] = 'cURL SSL Version';
+$string['auth_cas_curl_ssl_version_SSLv2'] = 'SSLv2';
+$string['auth_cas_curl_ssl_version_SSLv3'] = 'SSLv3';
+$string['auth_cas_curl_ssl_version_TLSv1x'] = 'TLSv1.x';
+$string['auth_cas_curl_ssl_version_TLSv10'] = 'TLSv1.0';
+$string['auth_cas_curl_ssl_version_TLSv11'] = 'TLSv1.1';
+$string['auth_cas_curl_ssl_version_TLSv12'] = 'TLSv1.2';
$string['auth_casdescription'] = 'This method uses a CAS server (Central Authentication Service) to authenticate users in a Single Sign On environment (SSO). You can also use a simple LDAP authentication. If the given username and password are valid according to CAS, Moodle creates a new user entry in its database, taking user attributes from LDAP if required. On following logins only the username and password are checked.';
$string['auth_cas_enabled'] = 'Turn this on if you want to use CAS authentication.';
$string['auth_cas_hostname'] = 'Hostname of the CAS server <br />eg: host.domain.fr';
</tr>
<tr valign="top" >
- <td align="right"><?php echo html_writer::label(get_string('auth_radiustype_key', 'auth_radius'), 'menuradiustype'); ?>: </td>
+ <td align="right"><?php echo html_writer::label(get_string('auth_radiustype_key', 'auth_radius') . ':', 'menuradiustype'); ?> </td>
<td>
<?php
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class info_testcase extends \advanced_testcase {
+class info_testcase extends advanced_testcase {
public function setUp() {
// Load the mock condition so that it can be used.
require_once(__DIR__ . '/fixtures/mock_condition.php');
// Check invalid one.
$info = new info_module($cm3);
$this->assertFalse($info->is_available($information));
- $debugging = phpunit_util::get_debugging_messages();
- phpunit_util::reset_debugging();
+ $debugging = $this->getDebuggingMessages();
+ $this->resetDebugging();
$this->assertEquals(1, count($debugging));
$this->assertContains('Invalid availability', $debugging[0]->message);
// Check invalid one.
$info = new info_section($sections[3]);
$this->assertFalse($info->is_available($information));
- $debugging = phpunit_util::get_debugging_messages();
- phpunit_util::reset_debugging();
+ $debugging = $this->getDebuggingMessages();
+ $this->resetDebugging();
$this->assertEquals(1, count($debugging));
$this->assertContains('Invalid availability', $debugging[0]->message);
* Test course format that has 1 option.
*/
class format_test_cs_options extends format_topics {
+ /**
+ * Override method format_topics::get_default_section_name to prevent PHPUnit errors related to the nonexistent
+ * format_test_cs_options lang file.
+ *
+ * @param stdClass $section The section in question.
+ * @return string The section's name for display.
+ */
+ public function get_default_section_name($section) {
+ if ($section->section == 0) {
+ return parent::get_default_section_name($section);
+ } else {
+ return get_string('sectionname', 'format_topics') . ' ' . $section->section;
+ }
+ }
+
public function section_format_options($foreditform = false) {
return array(
'numdaystocomplete' => array(
This files describes API changes in /backup/*,
information provided here is intended especially for developers.
+=== 3.0 ===
+
+* The backup_auto_keep setting, in automated backups configuration, is now
+ renamed to backup_auto_max_kept as part of a rationalise of naming (see MDL-50602)
+
=== 2.6 ===
* The backup_controller_dbops::create_temptable_from_real_table()
const AUTO_BACKUP_ENABLED = 1;
const AUTO_BACKUP_MANUAL = 2;
+ /** Automated backup storage in course backup filearea */
+ const STORAGE_COURSE = 0;
+ /** Automated backup storage in specified directory */
+ const STORAGE_DIRECTORY = 1;
+ /** Automated backup storage in course backup filearea and specified directory */
+ const STORAGE_COURSE_AND_DIRECTORY = 2;
+
/**
* Runs the automated backups if required
*
$backupcourse->nextstarttime = $nextstarttime;
$DB->update_record('backup_courses', $backupcourse);
mtrace('Skipping ' . $course->fullname . ' (Not scheduled for backup until ' . $showtime . ')');
- } else if ($skipped) { // Must have been skipped for a reason.
- $backupcourse->laststatus = self::BACKUP_STATUS_SKIPPED;
- $backupcourse->nextstarttime = $nextstarttime;
- $DB->update_record('backup_courses', $backupcourse);
- mtrace('Skipping ' . $course->fullname . ' (' . $skippedmessage . ')');
- mtrace('Backup of \'' . $course->fullname . '\' is scheduled on ' . $showtime);
} else {
- // Backup every non-skipped courses.
- mtrace('Backing up '.$course->fullname.'...');
+ if ($skipped) { // Must have been skipped for a reason.
+ $backupcourse->laststatus = self::BACKUP_STATUS_SKIPPED;
+ $backupcourse->nextstarttime = $nextstarttime;
+ $DB->update_record('backup_courses', $backupcourse);
+ mtrace('Skipping ' . $course->fullname . ' (' . $skippedmessage . ')');
+ mtrace('Backup of \'' . $course->fullname . '\' is scheduled on ' . $showtime);
+ } else {
+ // Backup every non-skipped courses.
+ mtrace('Backing up '.$course->fullname.'...');
- // We have to send an email because we have included at least one backup.
- $emailpending = true;
+ // We have to send an email because we have included at least one backup.
+ $emailpending = true;
- // Only make the backup if laststatus isn't 2-UNFINISHED (uncontrolled error).
- if ($backupcourse->laststatus != self::BACKUP_STATUS_UNFINISHED) {
- // Set laststarttime.
- $starttime = time();
+ // Only make the backup if laststatus isn't 2-UNFINISHED (uncontrolled error).
+ if ($backupcourse->laststatus != self::BACKUP_STATUS_UNFINISHED) {
+ // Set laststarttime.
+ $starttime = time();
- $backupcourse->laststarttime = time();
- $backupcourse->laststatus = self::BACKUP_STATUS_UNFINISHED;
- $DB->update_record('backup_courses', $backupcourse);
+ $backupcourse->laststarttime = time();
+ $backupcourse->laststatus = self::BACKUP_STATUS_UNFINISHED;
+ $DB->update_record('backup_courses', $backupcourse);
- $backupcourse->laststatus = backup_cron_automated_helper::launch_automated_backup($course, $backupcourse->laststarttime, $admin->id);
- $backupcourse->lastendtime = time();
- $backupcourse->nextstarttime = $nextstarttime;
+ $backupcourse->laststatus = self::launch_automated_backup($course, $backupcourse->laststarttime,
+ $admin->id);
+ $backupcourse->lastendtime = time();
+ $backupcourse->nextstarttime = $nextstarttime;
- $DB->update_record('backup_courses', $backupcourse);
+ $DB->update_record('backup_courses', $backupcourse);
- if ($backupcourse->laststatus === self::BACKUP_STATUS_OK) {
- // Clean up any excess course backups now that we have
- // taken a successful backup.
- $removedcount = backup_cron_automated_helper::remove_excess_backups($course);
+ mtrace("complete - next execution: $showtime");
}
}
- mtrace("complete - next execution: $showtime");
+ // Remove excess backups.
+ $removedcount = self::remove_excess_backups($course, $now);
}
}
$rs->close();
}
/**
- * Removes excess backups from the external system and the local file system.
+ * Removes excess backups from a specified course.
*
- * The number of backups keep comes from $config->backup_auto_keep.
- *
- * @param stdClass $course object
- * @return bool
+ * @param stdClass $course Course object
+ * @param int $now Starting time of the process
+ * @return bool Whether or not backups is being removed
*/
- public static function remove_excess_backups($course) {
+ public static function remove_excess_backups($course, $now = null) {
$config = get_config('backup');
- $keep = (int)$config->backup_auto_keep;
- $storage = $config->backup_auto_storage;
- $dir = $config->backup_auto_destination;
+ $maxkept = (int)$config->backup_auto_max_kept;
+ $storage = $config->backup_auto_storage;
+ $deletedays = (int)$config->backup_auto_delete_days;
- if ($keep == 0) {
- // Means keep all backup files.
+ if ($maxkept == 0 && $deletedays == 0) {
+ // Means keep all backup files and never delete backup after x days.
return true;
}
- if (!file_exists($dir) || !is_dir($dir) || !is_writable($dir)) {
- $dir = null;
+ if (!isset($now)) {
+ $now = time();
}
// Clean up excess backups in the course backup filearea.
- if ($storage == 0 || $storage == 2) {
- $fs = get_file_storage();
- $context = context_course::instance($course->id);
- $component = 'backup';
- $filearea = 'automated';
- $itemid = 0;
- $files = array();
- // Store all the matching files into timemodified => stored_file array.
- foreach ($fs->get_area_files($context->id, $component, $filearea, $itemid) as $file) {
- $files[$file->get_timemodified()] = $file;
+ $deletedcoursebackups = false;
+ if ($storage == self::STORAGE_COURSE || $storage == self::STORAGE_COURSE_AND_DIRECTORY) {
+ $deletedcoursebackups = self::remove_excess_backups_from_course($course, $now);
+ }
+
+ // Clean up excess backups in the specified external directory.
+ $deleteddirectorybackups = false;
+ if ($storage == self::STORAGE_DIRECTORY || $storage == self::STORAGE_COURSE_AND_DIRECTORY) {
+ $deleteddirectorybackups = self::remove_excess_backups_from_directory($course, $now);
+ }
+
+ if ($deletedcoursebackups || $deleteddirectorybackups) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Removes excess backups in the course backup filearea from a specified course.
+ *
+ * @param stdClass $course Course object
+ * @param int $now Starting time of the process
+ * @return bool Whether or not backups are being removed
+ */
+ protected static function remove_excess_backups_from_course($course, $now) {
+ $fs = get_file_storage();
+ $context = context_course::instance($course->id);
+ $component = 'backup';
+ $filearea = 'automated';
+ $itemid = 0;
+ $backupfiles = array();
+ $backupfilesarea = $fs->get_area_files($context->id, $component, $filearea, $itemid, 'timemodified DESC', false);
+ // Store all the matching files into timemodified => stored_file array.
+ foreach ($backupfilesarea as $backupfile) {
+ $backupfiles[$backupfile->get_timemodified()] = $backupfile;
+ }
+
+ $backupstodelete = self::get_backups_to_delete($backupfiles, $now);
+ if ($backupstodelete) {
+ foreach ($backupstodelete as $backuptodelete) {
+ $backuptodelete->delete();
}
- if (count($files) <= $keep) {
- // There are less matching files than the desired number to keep there is nothing to clean up.
- return 0;
+ mtrace('Deleted ' . count($backupstodelete) . ' old backup file(s) from the automated filearea');
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Removes excess backups in the specified external directory from a specified course.
+ *
+ * @param stdClass $course Course object
+ * @param int $now Starting time of the process
+ * @return bool Whether or not backups are being removed
+ */
+ protected static function remove_excess_backups_from_directory($course, $now) {
+ $config = get_config('backup');
+ $dir = $config->backup_auto_destination;
+
+ $isnotvaliddir = !file_exists($dir) || !is_dir($dir) || !is_writable($dir);
+ if ($isnotvaliddir) {
+ mtrace('Error: ' . $dir . ' does not appear to be a valid directory');
+ return false;
+ }
+
+ // Calculate backup filename regex, ignoring the date/time/info parts that can be
+ // variable, depending of languages, formats and automated backup settings.
+ $filename = backup::FORMAT_MOODLE . '-' . backup::TYPE_1COURSE . '-' . $course->id . '-';
+ $regex = '#' . preg_quote($filename, '#') . '.*\.mbz$#';
+
+ // Store all the matching files into filename => timemodified array.
+ $backupfiles = array();
+ foreach (scandir($dir) as $backupfile) {
+ // Skip files not matching the naming convention.
+ if (!preg_match($regex, $backupfile)) {
+ continue;
}
- // Sort by keys descending (newer to older filemodified).
- krsort($files);
- $remove = array_splice($files, $keep);
- foreach ($remove as $file) {
- $file->delete();
+
+ // Read the information contained in the backup itself.
+ try {
+ $bcinfo = backup_general_helper::get_backup_information_from_mbz($dir . '/' . $backupfile);
+ } catch (backup_helper_exception $e) {
+ mtrace('Error: ' . $backupfile . ' does not appear to be a valid backup (' . $e->errorcode . ')');
+ continue;
}
- //mtrace('Removed '.count($remove).' old backup file(s) from the automated filearea');
- }
- // Clean up excess backups in the specified external directory.
- if (!empty($dir) && ($storage == 1 || $storage == 2)) {
- // Calculate backup filename regex, ignoring the date/time/info parts that can be
- // variable, depending of languages, formats and automated backup settings.
- $filename = backup::FORMAT_MOODLE . '-' . backup::TYPE_1COURSE . '-' . $course->id . '-';
- $regex = '#' . preg_quote($filename, '#') . '.*\.mbz$#';
-
- // Store all the matching files into filename => timemodified array.
- $files = array();
- foreach (scandir($dir) as $file) {
- // Skip files not matching the naming convention.
- if (!preg_match($regex, $file, $matches)) {
- continue;
- }
+ // Make sure this backup concerns the course and site we are looking for.
+ if ($bcinfo->format === backup::FORMAT_MOODLE &&
+ $bcinfo->type === backup::TYPE_1COURSE &&
+ $bcinfo->original_course_id == $course->id &&
+ backup_general_helper::backup_is_samesite($bcinfo)) {
+ $backupfiles[$bcinfo->backup_date] = $backupfile;
+ }
+ }
- // Read the information contained in the backup itself.
- try {
- $bcinfo = backup_general_helper::get_backup_information_from_mbz($dir . '/' . $file);
- } catch (backup_helper_exception $e) {
- mtrace('Error: ' . $file . ' does not appear to be a valid backup (' . $e->errorcode . ')');
- continue;
- }
+ $backupstodelete = self::get_backups_to_delete($backupfiles, $now);
+ if ($backupstodelete) {
+ foreach ($backupstodelete as $backuptodelete) {
+ unlink($dir . '/' . $backuptodelete);
+ }
+ mtrace('Deleted ' . count($backupstodelete) . ' old backup file(s) from external directory');
+ return true;
+ } else {
+ return false;
+ }
+ }
- // Make sure this backup concerns the course and site we are looking for.
- if ($bcinfo->format === backup::FORMAT_MOODLE &&
- $bcinfo->type === backup::TYPE_1COURSE &&
- $bcinfo->original_course_id == $course->id &&
- backup_general_helper::backup_is_samesite($bcinfo)) {
- $files[$file] = $bcinfo->backup_date;
+ /**
+ * Get the list of backup files to delete depending on the automated backup settings.
+ *
+ * @param array $backupfiles Existing backup files
+ * @param int $now Starting time of the process
+ * @return array Backup files to delete
+ */
+ protected static function get_backups_to_delete($backupfiles, $now) {
+ $config = get_config('backup');
+ $maxkept = (int)$config->backup_auto_max_kept;
+ $deletedays = (int)$config->backup_auto_delete_days;
+ $minkept = (int)$config->backup_auto_min_kept;
+
+ // Sort by keys descending (newer to older filemodified).
+ krsort($backupfiles);
+ $tokeep = $maxkept;
+ if ($deletedays > 0) {
+ $deletedayssecs = $deletedays * DAYSECS;
+ $tokeep = 0;
+ $backupfileskeys = array_keys($backupfiles);
+ foreach ($backupfileskeys as $timemodified) {
+ $mustdeletebackup = $timemodified < ($now - $deletedayssecs);
+ if ($mustdeletebackup || $tokeep >= $maxkept) {
+ break;
}
+ $tokeep++;
}
- if (count($files) <= $keep) {
- // There are less matching files than the desired number to keep there is nothing to clean up.
- return 0;
- }
- // Sort by values descending (newer to older filemodified).
- arsort($files);
- $remove = array_splice($files, $keep);
- foreach (array_keys($remove) as $file) {
- unlink($dir . '/' . $file);
+
+ if ($tokeep < $minkept) {
+ $tokeep = $minkept;
}
- //mtrace('Removed '.count($remove).' old backup file(s) from external directory');
}
- return true;
+ if (count($backupfiles) <= $tokeep) {
+ // There are less or equal matching files than the desired number to keep, there is nothing to clean up.
+ return false;
+ } else {
+ $backupstodelete = array_splice($backupfiles, $tokeep);
+ return $backupstodelete;
+ }
}
/**
$next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
$this->assertEquals(date('w-20:00'), date('w-H:i', $next));
}
+
+ /**
+ * Test {@link backup_cron_automated_helper::get_backups_to_delete}.
+ */
+ public function test_get_backups_to_delete() {
+ $this->resetAfterTest();
+ // Active only backup_auto_max_kept config to 2 days.
+ set_config('backup_auto_max_kept', '2', 'backup');
+ set_config('backup_auto_delete_days', '0', 'backup');
+ set_config('backup_auto_min_kept', '0', 'backup');
+
+ // No backups to delete.
+ $backupfiles = array(
+ '1000000000' => 'file1.mbz',
+ '1000432000' => 'file3.mbz'
+ );
+ $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1000432000);
+ $this->assertFalse($deletedbackups);
+
+ // Older backup to delete.
+ $backupfiles['1000172800'] = 'file2.mbz';
+ $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1000432000);
+ $this->assertEquals(1, count($deletedbackups));
+ $this->assertArrayHasKey('1000000000', $backupfiles);
+ $this->assertEquals('file1.mbz', $backupfiles['1000000000']);
+
+ // Activate backup_auto_max_kept to 5 days and backup_auto_delete_days to 10 days.
+ set_config('backup_auto_max_kept', '5', 'backup');
+ set_config('backup_auto_delete_days', '10', 'backup');
+ set_config('backup_auto_min_kept', '0', 'backup');
+
+ // No backups to delete. Timestamp is 1000000000 + 10 days.
+ $backupfiles['1000432001'] = 'file4.mbz';
+ $backupfiles['1000864000'] = 'file5.mbz';
+ $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1000864000);
+ $this->assertFalse($deletedbackups);
+
+ // One old backup to delete. Timestamp is 1000000000 + 10 days + 1 second.
+ $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1000864001);
+ $this->assertEquals(1, count($deletedbackups));
+ $this->assertArrayHasKey('1000000000', $backupfiles);
+ $this->assertEquals('file1.mbz', $backupfiles['1000000000']);
+
+ // Two old backups to delete. Timestamp is 1000000000 + 12 days + 1 second.
+ $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1001036801);
+ $this->assertEquals(2, count($deletedbackups));
+ $this->assertArrayHasKey('1000000000', $backupfiles);
+ $this->assertEquals('file1.mbz', $backupfiles['1000000000']);
+ $this->assertArrayHasKey('1000172800', $backupfiles);
+ $this->assertEquals('file2.mbz', $backupfiles['1000172800']);
+
+ // Activate backup_auto_max_kept to 5 days, backup_auto_delete_days to 10 days and backup_auto_min_kept to 2.
+ set_config('backup_auto_max_kept', '5', 'backup');
+ set_config('backup_auto_delete_days', '10', 'backup');
+ set_config('backup_auto_min_kept', '2', 'backup');
+
+ // Three instead of four old backups are deleted. Timestamp is 1000000000 + 16 days.
+ $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1001382400);
+ $this->assertEquals(3, count($deletedbackups));
+ $this->assertArrayHasKey('1000000000', $backupfiles);
+ $this->assertEquals('file1.mbz', $backupfiles['1000000000']);
+ $this->assertArrayHasKey('1000172800', $backupfiles);
+ $this->assertEquals('file2.mbz', $backupfiles['1000172800']);
+ $this->assertArrayHasKey('1000432000', $backupfiles);
+ $this->assertEquals('file3.mbz', $backupfiles['1000432000']);
+
+ // Three instead of all five backups are deleted. Timestamp is 1000000000 + 60 days.
+ $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1005184000);
+ $this->assertEquals(3, count($deletedbackups));
+ $this->assertArrayHasKey('1000000000', $backupfiles);
+ $this->assertEquals('file1.mbz', $backupfiles['1000000000']);
+ $this->assertArrayHasKey('1000172800', $backupfiles);
+ $this->assertEquals('file2.mbz', $backupfiles['1000172800']);
+ $this->assertArrayHasKey('1000432000', $backupfiles);
+ $this->assertEquals('file3.mbz', $backupfiles['1000432000']);
+ }
+}
+
+/**
+ * Provides access to protected methods we want to explicitly test
+ *
+ * @copyright 2015 Jean-Philippe Gaudreau <jp.gaudreau@umontreal.ca>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testable_backup_cron_automated_helper extends backup_cron_automated_helper {
+
+ /**
+ * Provides access to protected method get_backups_to_remove.
+ *
+ * @param array $backupfiles Existing backup files
+ * @param int $now Starting time of the process
+ * @return array Backup files to remove
+ */
+ public static function testable_get_backups_to_delete($backupfiles, $now) {
+ return parent::get_backups_to_delete($backupfiles, $now);
+ }
}
$mform->addElement('html', html_writer::tag('span', '', array('class' => 'notconnected', 'id' => 'connection-error')));
$mform->addElement('header', 'backpackheader', get_string('backpackconnection', 'badges'));
$mform->addHelpButton('backpackheader', 'backpackconnection', 'badges');
- $mform->addElement('static', 'url', get_string('url'), 'http://' . BADGE_BACKPACKURL);
+ $mform->addElement('static', 'url', get_string('url'), BADGE_BACKPACKURL);
$status = html_writer::tag('span', get_string('notconnected', 'badges'),
array('class' => 'notconnected', 'id' => 'connection-status'));
$mform->addElement('static', 'status', get_string('status'), $status);
$mform->addElement('hidden', 'userid', $USER->id);
$mform->setType('userid', PARAM_INT);
- $mform->addElement('hidden', 'backpackurl', 'http://' . BADGE_BACKPACKURL);
+ $mform->addElement('hidden', 'backpackurl', BADGE_BACKPACKURL);
$mform->setType('backpackurl', PARAM_URL);
}
$mform->addElement('header', 'backpackheader', get_string('backpackconnection', 'badges'));
$mform->addHelpButton('backpackheader', 'backpackconnection', 'badges');
- $mform->addElement('static', 'url', get_string('url'), 'http://' . BADGE_BACKPACKURL);
+ $mform->addElement('static', 'url', get_string('url'), BADGE_BACKPACKURL);
$status = html_writer::tag('span', get_string('connected', 'badges'), array('class' => 'connected'));
$mform->addElement('static', 'status', get_string('status'), $status);
// Make sure email matches a backpack.
$check = new stdClass();
-$check->backpackurl = 'http://' . BADGE_BACKPACKURL;
+$check->backpackurl = BADGE_BACKPACKURL;
$check->email = $data->email;
$bp = new OpenBadgesBackpackHandler($check);
$obj = new stdClass();
$obj->userid = $USER->id;
$obj->email = $data->email;
-$obj->backpackurl = 'http://' . BADGE_BACKPACKURL;
+$obj->backpackurl = BADGE_BACKPACKURL;
$obj->backpackuid = $backpackuid;
$obj->autosync = 0;
$obj->password = '';
}
$join = '';
- $where = '';
+ $whereparts = array();
$sqlparams = array();
$rule = ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) ? ' OR ' : ' AND ';
foreach ($this->params as $param) {
if (is_numeric($param['field'])) {
- $infodata[] = " uid.fieldid = :fieldid{$param['field']} ";
- $sqlparams["fieldid{$param['field']}"] = $param['field'];
+ // This is a custom field.
+ $idx = count($whereparts) + 1;
+ $join .= " LEFT JOIN {user_info_data} uid{$idx} ON uid{$idx}.userid = u.id AND uid{$idx}.fieldid = :fieldid{$idx} ";
+ $sqlparams["fieldid{$idx}"] = $param['field'];
+ $whereparts[] = "uid{$idx}.id IS NOT NULL";
} else {
- $userdata[] = $DB->sql_isnotempty('u', "u.{$param['field']}", false, true);
+ // This is a field from {user} table.
+ $whereparts[] = $DB->sql_isnotempty('u', "u.{$param['field']}", false, true);
}
}
- // Add user custom field parameters if there are any.
- if (!empty($infodata)) {
- $extraon = implode($rule, $infodata);
- $join = " LEFT JOIN {user_info_data} uid ON uid.userid = u.id AND ({$extraon})";
- }
+ $sqlparams['userid'] = $userid;
- // Add user table field parameters if there are any.
- if (!empty($userdata)) {
- $extraon = implode($rule, $userdata);
- $where = " AND ({$extraon})";
+ if ($whereparts) {
+ $where = " AND (" . implode($rule, $whereparts) . ")";
+ } else {
+ $where = '';
}
-
- $sqlparams['userid'] = $userid;
- $sql = "SELECT u.* FROM {user} u " . $join . " WHERE u.id = :userid " . $where;
+ $sql = "SELECT 1 FROM {user} u " . $join . " WHERE u.id = :userid $where";
$overall = $DB->record_exists_sql($sql, $sqlparams);
return $overall;
global $DB;
$join = '';
- $where = '';
+ $whereparts = array();
$params = array();
$rule = ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) ? ' OR ' : ' AND ';
foreach ($this->params as $param) {
if (is_numeric($param['field'])) {
- $infodata[] = " uid.fieldid = :fieldid{$param['field']} ";
- $params["fieldid{$param['field']}"] = $param['field'];
+ // This is a custom field.
+ $idx = count($whereparts);
+ $join .= " LEFT JOIN {user_info_data} uid{$idx} ON uid{$idx}.userid = u.id AND uid{$idx}.fieldid = :fieldid{$idx} ";
+ $params["fieldid{$idx}"] = $param['field'];
+ $whereparts[] = "uid{$idx}.id IS NOT NULL";
} else {
- $userdata[] = $DB->sql_isnotempty('u', "u.{$param['field']}", false, true);
+ $whereparts[] = $DB->sql_isnotempty('u', "u.{$param['field']}", false, true);
}
}
- // Add user custom fields if there are any.
- if (!empty($infodata)) {
- $extraon = implode($rule, $infodata);
- $join = " LEFT JOIN {user_info_data} uid ON uid.userid = u.id AND ({$extraon})";
- }
-
- // Add user table fields if there are any.
- if (!empty($userdata)) {
- $extraon = implode($rule, $userdata);
- $where = " AND ({$extraon})";
+ if ($whereparts) {
+ $where = " AND (" . implode($rule, $whereparts) . ")";
+ } else {
+ $where = '';
}
return array($join, $where, $params);
}
$criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_ACTIVITY, 'badgeid' => $badge->id));
$criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY, 'module_'.$this->module->cmid => $this->module->cmid));
+ // Assert the badge will not be issued to the user as is.
+ $badge = new badge($this->coursebadge);
+ $badge->review_all_criteria();
+ $this->assertFalse($badge->is_issued($this->user->id));
+
// Set completion for forum activity.
$c = new completion_info($this->course);
$activities = $c->get_activities();
$ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
+ // Assert the badge will not be issued to the user as is.
+ $badge = new badge($this->coursebadge);
+ $badge->review_all_criteria();
+ $this->assertFalse($badge->is_issued($this->user->id));
+
// Mark course as complete.
$sink = $this->redirectEmails();
$ccompletion->mark_complete();
* Test badges observer when user_updated event is fired.
*/
public function test_badges_observer_profile_criteria_review() {
+ global $CFG, $DB;
+ require_once($CFG->dirroot.'/user/profile/lib.php');
+
+ // Add a custom field of textarea type.
+ $customprofileid = $DB->insert_record('user_info_field', array(
+ 'shortname' => 'newfield', 'name' => 'Description of new field', 'categoryid' => 1,
+ 'datatype' => 'textarea'));
+
$this->preventResetByRollback(); // Messaging is not compatible with transactions.
$badge = new badge($this->coursebadge);
- $this->assertFalse($badge->is_issued($this->user->id));
$criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
$criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
$criteria_overall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $badge->id));
- $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address', 'field_aim' => 'aim'));
+ $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address', 'field_aim' => 'aim',
+ 'field_' . $customprofileid => $customprofileid));
+
+ // Assert the badge will not be issued to the user as is.
+ $badge = new badge($this->coursebadge);
+ $badge->review_all_criteria();
+ $this->assertFalse($badge->is_issued($this->user->id));
+ // Set the required fields and make sure the badge got issued.
$this->user->address = 'Test address';
$this->user->aim = '999999999';
$sink = $this->redirectEmails();
+ profile_save_data((object)array('id' => $this->user->id, 'profile_field_newfield' => 'X'));
user_update_user($this->user, false);
$this->assertCount(1, $sink->get_messages());
$sink->close();
return true;
}
+ /**
+ * Copy any block-specific data when copying to a new block instance.
+ * @param int $fromid the id number of the block instance to copy from
+ * @return boolean
+ */
+ public function instance_copy($fromid) {
+ $fromcontext = context_block::instance($fromid);
+ $fs = get_file_storage();
+ // This extra check if file area is empty adds one query if it is not empty but saves several if it is.
+ if (!$fs->is_area_empty($fromcontext->id, 'block_html', 'content', 0, false)) {
+ $draftitemid = 0;
+ file_prepare_draft_area($draftitemid, $fromcontext->id, 'block_html', 'content', 0, array('subdirs' => true));
+ file_save_draft_area_files($draftitemid, $this->context->id, 'block_html', 'content', 0, array('subdirs' => true));
+ }
+ return true;
+ }
+
function content_is_trusted() {
global $SCRIPT;
Background:
Given the following "users" exist:
- | username | firstname | lastname | email |
- | teacher1 | Teacher | 1 | teacher1@example.com |
- | student1 | Student | 1 | student1@example.com |
+ | username | firstname | lastname | email | interests |
+ | teacher1 | Teacher | 1 | teacher1@example.com | Dogs, Cats |
+ | student1 | Student | 1 | student1@example.com | |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | c1 |
| user | course | role |
| teacher1 | c1 | editingteacher |
| student1 | c1 | student |
- And I log in as "teacher1"
- And I follow "Preferences" in the user menu
- And I follow "Edit profile"
- And I expand all fieldsets
- And I set the field "Enter tags separated by commas" to "Dogs, Cats"
- And I press "Update profile"
- And I log out
Scenario: Add Tags block on a front page
When I log in as "admin"
$groupbysql = '';
$havingsql = '';
if ($withmembers) {
- $groupbysql = " GROUP BY $fieldssql";
+ $fieldssql .= ', s.memberscnt';
+ $subfields = "c.id, COUNT(DISTINCT cm.userid) AS memberscnt";
+ $groupbysql = " GROUP BY c.id";
$fromsql = " LEFT JOIN {cohort_members} cm ON cm.cohortid = c.id ";
- $fieldssql .= ', COUNT(DISTINCT cm.userid) AS memberscnt';
if (in_array($withmembers,
array(COHORT_COUNT_ENROLLED_MEMBERS, COHORT_WITH_ENROLLED_MEMBERS_ONLY, COHORT_WITH_NOTENROLLED_MEMBERS_ONLY))) {
list($esql, $params2) = get_enrolled_sql($currentcontext);
$fromsql .= " LEFT JOIN ($esql) u ON u.id = cm.userid ";
$params = array_merge($params2, $params);
- $fieldssql .= ', COUNT(DISTINCT u.id) AS enrolledcnt';
+ $fieldssql .= ', s.enrolledcnt';
+ $subfields .= ', COUNT(DISTINCT u.id) AS enrolledcnt';
}
if ($withmembers == COHORT_WITH_MEMBERS_ONLY) {
$havingsql = " HAVING COUNT(DISTINCT cm.userid) > 0";
$params = array_merge($params, $searchparams);
}
- $sql = "SELECT $fieldssql
- FROM {cohort} c
- $fromsql
- WHERE $wheresql
- $groupbysql
- $havingsql
- ORDER BY c.name, c.idnumber";
+ if ($withmembers) {
+ $sql = "SELECT " . str_replace('c.', 'cohort.', $fieldssql) . "
+ FROM {cohort} cohort
+ JOIN (SELECT $subfields
+ FROM {cohort} c $fromsql
+ WHERE $wheresql $groupbysql $havingsql
+ ) s ON cohort.id = s.id
+ ORDER BY cohort.name, cohort.idnumber";
+ } else {
+ $sql = "SELECT $fieldssql
+ FROM {cohort} c $fromsql
+ WHERE $wheresql
+ ORDER BY c.name, c.idnumber";
+ }
return $DB->get_records_sql($sql, $params, $offset, $limit);
}
$params = self::validate_parameters(self::get_activities_completion_status_parameters(), $arrayparams);
$course = get_course($params['courseid']);
- $user = core_user::get_user($params['userid'], 'id', MUST_EXIST);
+ $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
+ core_user::require_active_user($user);
$context = context_course::instance($course->id);
self::validate_context($context);
$params = self::validate_parameters(self::get_course_completion_status_parameters(), $arrayparams);
$course = get_course($params['courseid']);
- $user = core_user::get_user($params['userid'], 'id', MUST_EXIST);
+ $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
+ core_user::require_active_user($user);
+
$context = context_course::instance($course->id);
self::validate_context($context);
// any icon inside the pix/f folder. You can also set the customdescription field
// (shown above) and (for advanced use) the groups, string, and defaulticon fields.
//
+// Upgrade key
+//
+// If the upgrade key is defined here, then the value must be provided every time
+// the site is being upgraded though the web interface, regardless of whether the
+// administrator is logged in or not. This prevents anonymous access to the upgrade
+// screens where the real authentication and authorization mechanisms can not be
+// relied on.
+//
+// It is strongly recommended to use a value different from your real account
+// password.
+//
+// $CFG->upgradekey = 'put_some_password-like_value_here';
+//
//=========================================================================
// 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
//=========================================================================
}
$editoroptions = array('context'=>$context ,'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes'=>$CFG->maxbytes, 'trusttext'=>false, 'noclean'=>true);
-$mform = course_get_format($course->id)->editsection_form($PAGE->url,
- array('cs' => $sectioninfo, 'editoroptions' => $editoroptions));
+
+$courseformat = course_get_format($course);
+$defaultsectionname = $courseformat->get_default_section_name($section);
+
+$customdata = array(
+ 'cs' => $sectioninfo,
+ 'editoroptions' => $editoroptions,
+ 'defaultsectionname' => $defaultsectionname
+);
+$mform = $courseformat->editsection_form($PAGE->url, $customdata);
+
// set current value, make an editable copy of section_info object
// this will retrieve all format-specific options as well
$initialdata = convert_to_array($sectioninfo);
$elementgroup = array();
$elementgroup[] = $mform->createElement('text', 'name', '', array('size' => '30', 'maxlength' => '255'));
- $elementgroup[] = $mform->createElement('checkbox', 'usedefaultname', '', get_string('sectionusedefaultname'));
+
+ // Get default section name.
+ $defaultsectionname = $this->_customdata['defaultsectionname'];
+ if ($defaultsectionname) {
+ $defaultsectionname = ' [' . $defaultsectionname . ']';
+ }
+
+ $elementgroup[] = $mform->createElement('checkbox', 'usedefaultname', '',
+ get_string('sectionusedefaultname') . $defaultsectionname);
+
$mform->addGroup($elementgroup, 'name_group', get_string('sectionname'), ' ', false);
$mform->addGroupRule('name_group', array('name' => array(array(get_string('maximumchars', '', 255), 'maxlength', 255))));
$data = parent::get_data();
if ($data !== null) {
$editoroptions = $this->_customdata['editoroptions'];
- if (!empty($data->usedefaultname)) {
+ $trimmedname = $data->name;
+ if (!empty($data->usedefaultname) || empty($trimmedname)) {
$data->name = null;
}
$data = file_postupdate_standard_editor($data, 'summary', $editoroptions,
$module = array();
+ $modcontext = context_module::instance($cm->id);
+
//common info (for people being able to see the module or availability dates)
$module['id'] = $cm->id;
- $module['name'] = format_string($cm->name, true);
+ $module['name'] = external_format_string($cm->name, $modcontext->id);
$module['instance'] = $cm->instance;
$module['modname'] = $cm->modname;
$module['modplural'] = $cm->modplural;
$module['modicon'] = $cm->get_icon_url()->out(false);
$module['indent'] = $cm->indent;
- $modcontext = context_module::instance($cm->id);
-
if (!empty($cm->showdescription) or $cm->modname == 'label') {
// We want to use the external format. However from reading get_formatted_content(), $cm->content format is always FORMAT_HTML.
list($module['description'], $descriptionformat) = external_format_text($cm->content,
$info->completion = $cm->completion;
}
// Format name.
- $info->name = format_string($cm->name, true, array('context' => $context));
+ $info->name = external_format_string($cm->name, $context->id);
$result = array();
$result['cm'] = $info;
'id' => new external_value(PARAM_INT, 'The course module id'),
'course' => new external_value(PARAM_INT, 'The course id'),
'module' => new external_value(PARAM_INT, 'The module type id'),
- 'name' => new external_value(PARAM_TEXT, 'The activity name'),
+ 'name' => new external_value(PARAM_RAW, 'The activity name'),
'modname' => new external_value(PARAM_COMPONENT, 'The module component name (forum, assign, etc..)'),
'instance' => new external_value(PARAM_INT, 'The activity instance id'),
'section' => new external_value(PARAM_INT, 'The module section id'),
} else {
$sectionnum = $section;
}
- return get_string('sectionname', 'format_'.$this->format) . ' ' . $sectionnum;
+
+ if (get_string_manager()->string_exists('sectionname', 'format_' . $this->format)) {
+ return get_string('sectionname', 'format_' . $this->format) . ' ' . $sectionnum;
+ }
+
+ // Return an empty string if there's no available section name string for the given format.
+ return '';
+ }
+
+ /**
+ * Returns the default section using format_base's implementation of get_section_name.
+ *
+ * @param int|stdClass $section Section object from database or just field course_sections section
+ * @return string The default value for the section name based on the given course format.
+ */
+ public function get_default_section_name($section) {
+ return self::get_section_name($section);
}
/**
return $title;
}
+ /**
+ * Generate the edit control action menu
+ *
+ * @param array $controls The edit control items from section_edit_control_items
+ * @param stdClass $course The course entry from DB
+ * @param stdClass $section The course_section entry from DB
+ * @return string HTML to output.
+ */
+ protected function section_edit_control_menu($controls, $course, $section) {
+ $o = "";
+ if (!empty($controls)) {
+ $menu = new action_menu();
+ if ($section->section && get_string_manager()->string_exists('sectionmenu', 'format_'.$course->format)) {
+ $menu->set_menu_trigger(get_string('sectionmenu', 'format_'.$course->format));
+ } else {
+ $menu->set_menu_trigger(get_string('sectionmenu'));
+ }
+ $menu->attributes['class'] .= ' section-actions';
+ foreach ($controls as $value) {
+ $url = empty($value['url']) ? '' : $value['url'];
+ $icon = empty($value['icon']) ? '' : $value['icon'];
+ $name = empty($value['name']) ? '' : $value['name'];
+ $attr = empty($value['attr']) ? '' : $value['attr'];
+ $class = empty($item['pixattr']['class']) ? '' : $item['pixattr']['class'];
+ $alt = empty($item['pixattr']['alt']) ? '' : $item['pixattr']['alt'];
+ $al = new action_menu_link_secondary(
+ new moodle_url($url),
+ new pix_icon($icon, $name, null, array('class' => "smallicon " . $class, 'alt' => $alt)),
+ $name,
+ $attr
+ );
+ $menu->add($al);
+ }
+
+ $o .= html_writer::div($this->render($menu), 'section_action_menu');
+ }
+
+ return $o;
+ }
+
/**
* Generate the content to displayed on the right part of a section
* before course modules are included
protected function section_right_content($section, $course, $onsectionpage) {
$o = $this->output->spacer();
- if ($section->section != 0) {
- $controls = $this->section_edit_controls($course, $section, $onsectionpage);
- if (!empty($controls)) {
- $o = implode('<br />', $controls);
- }
- }
+ $controls = $this->section_edit_control_items($course, $section, $onsectionpage);
+ $o .= $this->section_edit_control_menu($controls, $course, $section);
return $o;
}
'class' => 'section main clearfix'.$sectionstyle, 'role'=>'region',
'aria-label'=> get_section_name($course, $section)));
+ // Create a span that contains the section title to be used to create the keyboard section move menu.
+ $o .= html_writer::tag('span', $this->section_title($section, $course), array('class' => 'hidden sectionname'));
+
$leftcontent = $this->section_left_content($section, $course, $onsectionpage);
$o.= html_writer::tag('div', $leftcontent, array('class' => 'left side'));
if ($hasnamenotsecpg || $hasnamesecpg) {
$classes = '';
}
- $o.= $this->output->heading($this->section_title($section, $course), 3, 'sectionname' . $classes);
+ $sectionname = html_writer::tag('span', $this->section_title($section, $course));
+ $o.= $this->output->heading($sectionname, 3, 'sectionname' . $classes);
$o.= html_writer::start_tag('div', array('class' => 'summary'));
$o.= $this->format_summary_text($section);
-
- $context = context_course::instance($course->id);
- if ($PAGE->user_is_editing() && has_capability('moodle/course:update', $context)) {
- $url = new moodle_url('/course/editsection.php', array('id'=>$section->id, 'sr'=>$sectionreturn));
- $o.= html_writer::link($url,
- html_writer::empty_tag('img', array('src' => $this->output->pix_url('i/settings'),
- 'class' => 'iconsmall edit', 'alt' => get_string('edit'))),
- array('title' => get_string('editsummary')));
- }
$o.= html_writer::end_tag('div');
+ $context = context_course::instance($course->id);
$o .= $this->section_availability_message($section,
has_capability('moodle/course:viewhiddensections', $context));
* @param stdClass $section The course_section entry from DB
* @param bool $onsectionpage true if being printed on a section page
* @return array of links with edit controls
+ * @deprecated since Moodle 3.0 MDL-48947 - please do not use this function any more.
+ * @see format_section_renderer_base::section_edit_control_items()
*/
protected function section_edit_controls($course, $section, $onsectionpage = false) {
global $PAGE;
return array();
}
+ $controls = array();
+ $items = $this->section_edit_control_items($course, $section, $onsectionpage);
+
+ foreach ($items as $key => $item) {
+ $url = empty($item['url']) ? '' : $item['url'];
+ $icon = empty($item['icon']) ? '' : $item['icon'];
+ $name = empty($item['name']) ? '' : $item['name'];
+ $attr = empty($item['attr']) ? '' : $item['attr'];
+ $class = empty($item['pixattr']['class']) ? '' : $item['pixattr']['class'];
+ $alt = empty($item['pixattr']['alt']) ? '' : $item['pixattr']['alt'];
+ $controls[$key] = html_writer::link(
+ new moodle_url($url),
+ html_writer::empty_tag('img', array(
+ 'src' => $this->output->pix_url($icon),
+ 'class' => "icon " . $class,
+ 'alt' => $alt
+ )),
+ $attr);
+ }
+
+ debugging('section_edit_controls() is deprecated, please use section_edit_control_items() instead.', DEBUG_DEVELOPER);
+ return $controls;
+ }
+
+ /**
+ * Generate the edit control items of a section
+ *
+ * @param stdClass $course The course entry from DB
+ * @param stdClass $section The course_section entry from DB
+ * @param bool $onsectionpage true if being printed on a section page
+ * @return array of edit control items
+ */
+ protected function section_edit_control_items($course, $section, $onsectionpage = false) {
+ global $PAGE;
+
+ if (!$PAGE->user_is_editing()) {
+ return array();
+ }
+
$coursecontext = context_course::instance($course->id);
$isstealth = isset($course->numsections) && ($section->section > $course->numsections);
$controls = array();
- $url = clone($baseurl);
- if (!$isstealth && has_capability('moodle/course:sectionvisibility', $coursecontext)) {
- if ($section->visible) { // Show the hide/show eye.
- $strhidefromothers = get_string('hidefromothers', 'format_'.$course->format);
- $url->param('hide', $section->section);
- $controls[] = html_writer::link($url,
- html_writer::empty_tag('img', array('src' => $this->output->pix_url('i/hide'),
- 'class' => 'icon hide', 'alt' => $strhidefromothers)),
- array('title' => $strhidefromothers, 'class' => 'editing_showhide'));
+ if (!$isstealth && has_capability('moodle/course:update', $coursecontext)) {
+ if ($section->section > 0
+ && get_string_manager()->string_exists('editsection', 'format_'.$course->format)) {
+ $streditsection = get_string('editsection', 'format_'.$course->format);
} else {
- $strshowfromothers = get_string('showfromothers', 'format_'.$course->format);
- $url->param('show', $section->section);
- $controls[] = html_writer::link($url,
- html_writer::empty_tag('img', array('src' => $this->output->pix_url('i/show'),
- 'class' => 'icon hide', 'alt' => $strshowfromothers)),
- array('title' => $strshowfromothers, 'class' => 'editing_showhide'));
+ $streditsection = get_string('editsection');
}
- }
- if (course_can_delete_section($course, $section)) {
- if (get_string_manager()->string_exists('deletesection', 'format_'.$course->format)) {
- $strdelete = get_string('deletesection', 'format_'.$course->format);
- } else {
- $strdelete = get_string('deletesection');
- }
- $url = new moodle_url('/course/editsection.php', array('id' => $section->id,
- 'sr' => $onsectionpage ? $section->section : 0, 'delete' => 1));
- $controls[] = html_writer::link($url,
- html_writer::empty_tag('img', array('src' => $this->output->pix_url('t/delete'),
- 'class' => 'icon delete', 'alt' => $strdelete)),
- array('title' => $strdelete));
+ $controls['edit'] = array(
+ 'url' => new moodle_url('/course/editsection.php', array('id' => $section->id, 'sr' => $onsectionpage)),
+ 'icon' => 'i/settings',
+ 'name' => $streditsection,
+ 'pixattr' => array('class' => '', 'alt' => $streditsection),
+ 'attr' => array('class' => 'icon edit', 'title' => $streditsection));
}
- if (!$isstealth && !$onsectionpage && has_capability('moodle/course:movesections', $coursecontext)) {
+ if ($section->section) {
$url = clone($baseurl);
- if ($section->section > 1) { // Add a arrow to move section up.
- $url->param('section', $section->section);
- $url->param('move', -1);
- $strmoveup = get_string('moveup');
-
- $controls[] = html_writer::link($url,
- html_writer::empty_tag('img', array('src' => $this->output->pix_url('i/up'),
- 'class' => 'icon up', 'alt' => $strmoveup)),
- array('title' => $strmoveup, 'class' => 'moveup'));
+ if (!$isstealth) {
+ if (has_capability('moodle/course:sectionvisibility', $coursecontext)) {
+ if ($section->visible) { // Show the hide/show eye.
+ $strhidefromothers = get_string('hidefromothers', 'format_'.$course->format);
+ $url->param('hide', $section->section);
+ $controls['visiblity'] = array(
+ 'url' => $url,
+ 'icon' => 'i/hide',
+ 'name' => $strhidefromothers,
+ 'pixattr' => array('class' => '', 'alt' => $strhidefromothers),
+ 'attr' => array('class' => 'icon editing_showhide', 'title' => $strhidefromothers));
+ } else {
+ $strshowfromothers = get_string('showfromothers', 'format_'.$course->format);
+ $url->param('show', $section->section);
+ $controls['visiblity'] = array(
+ 'url' => $url,
+ 'icon' => 'i/show',
+ 'name' => $strshowfromothers,
+ 'pixattr' => array('class' => '', 'alt' => $strshowfromothers),
+ 'attr' => array('class' => 'icon editing_showhide', 'title' => $strshowfromothers));
+ }
+ }
+
+ if (!$onsectionpage) {
+ if (has_capability('moodle/course:movesections', $coursecontext)) {
+ $url = clone($baseurl);
+ if ($section->section > 1) { // Add a arrow to move section up.
+ $url->param('section', $section->section);
+ $url->param('move', -1);
+ $strmoveup = get_string('moveup');
+ $controls['moveup'] = array(
+ 'url' => $url,
+ 'icon' => 'i/up',
+ 'name' => $strmoveup,
+ 'pixattr' => array('class' => '', 'alt' => $strmoveup),
+ 'attr' => array('class' => 'icon moveup', 'title' => $strmoveup));
+ }
+
+ $url = clone($baseurl);
+ if ($section->section < $course->numsections) { // Add a arrow to move section down.
+ $url->param('section', $section->section);
+ $url->param('move', 1);
+ $strmovedown = get_string('movedown');
+ $controls['movedown'] = array(
+ 'url' => $url,
+ 'icon' => 'i/down',
+ 'name' => $strmovedown,
+ 'pixattr' => array('class' => '', 'alt' => $strmovedown),
+ 'attr' => array('class' => 'icon movedown', 'title' => $strmovedown));
+ }
+ }
+ }
}
- $url = clone($baseurl);
- if ($section->section < $course->numsections) { // Add a arrow to move section down.
- $url->param('section', $section->section);
- $url->param('move', 1);
- $strmovedown = get_string('movedown');
-
- $controls[] = html_writer::link($url,
- html_writer::empty_tag('img', array('src' => $this->output->pix_url('i/down'),
- 'class' => 'icon down', 'alt' => $strmovedown)),
- array('title' => $strmovedown, 'class' => 'movedown'));
+ if (course_can_delete_section($course, $section)) {
+ if (get_string_manager()->string_exists('deletesection', 'format_'.$course->format)) {
+ $strdelete = get_string('deletesection', 'format_'.$course->format);
+ } else {
+ $strdelete = get_string('deletesection');
+ }
+ $url = new moodle_url('/course/editsection.php', array(
+ 'id' => $section->id,
+ 'sr' => $onsectionpage ? $section->section : 0,
+ 'delete' => 1));
+ $controls['delete'] = array(
+ 'url' => $url,
+ 'icon' => 'i/delete',
+ 'name' => $strdelete,
+ 'pixattr' => array('class' => '', 'alt' => $strdelete),
+ 'attr' => array('class' => 'icon delete', 'title' => $strdelete));
}
}
if (!$thissection->visible) {
$classes .= ' dimmed_text';
}
- $sectiontitle .= $this->output->heading(get_section_name($course, $displaysection), 3, $classes);
+ $sectionname = html_writer::tag('span', get_section_name($course, $displaysection));
+ $sectiontitle .= $this->output->heading($sectionname, 3, $classes);
$sectiontitle .= html_writer::end_tag('div');
echo $sectiontitle;
for (var i = sectionfrom; i <= sectionto; i++) {
// Update section title.
- sectionlist.item(i).one('.'+CSS.SECTIONNAME).setContent(response.sectiontitles[i]);
+ var content = Y.Node.create('<span>' + response.sectiontitles[i] + '</span>');
+ sectionlist.item(i).all('.'+CSS.SECTIONNAME).setHTML(content);
// Update move icon.
ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE);
str = ele.getAttribute('alt');
*/
$string['currentsection'] = 'This topic';
+$string['editsection'] = 'Edit topic';
$string['deletesection'] = 'Delete topic';
$string['sectionname'] = 'Topic';
$string['pluginname'] = 'Topics format';
+$string['sectionmenu'] = 'Topic menu';
$string['section0name'] = 'General';
$string['page-course-view-topics'] = 'Any course main page in topics format';
$string['page-course-view-topics-x'] = 'Any course page in topics format';
if ((string)$section->name !== '') {
return format_string($section->name, true,
array('context' => context_course::instance($this->courseid)));
- } else if ($section->section == 0) {
+ } else {
+ return $this->get_default_section_name($section);
+ }
+ }
+
+ /**
+ * Returns the default section name for the topics course format.
+ *
+ * If the section number is 0, it will use the string with key = section0name from the course format's lang file.
+ * If the section number is not 0, the base implementation of format_base::get_default_section_name which uses
+ * the string with the key = 'sectionname' from the course format's lang file + the section number will be used.
+ *
+ * @param stdClass $section Section object from database or just field course_sections section
+ * @return string The default value for the section name.
+ */
+ public function get_default_section_name($section) {
+ if ($section->section == 0) {
+ // Return the general section.
return get_string('section0name', 'format_topics');
} else {
- return get_string('topic').' '.$section->section;
+ // Use format_base::get_default_section_name implementation which
+ // will display the section name in "Topic n" format.
+ return parent::get_default_section_name($section);
}
}
}
/**
- * Generate the edit controls of a section
+ * Generate the edit control items of a section
*
* @param stdClass $course The course entry from DB
* @param stdClass $section The course_section entry from DB
* @param bool $onsectionpage true if being printed on a section page
- * @return array of links with edit controls
+ * @return array of edit control items
*/
- protected function section_edit_controls($course, $section, $onsectionpage = false) {
+ protected function section_edit_control_items($course, $section, $onsectionpage = false) {
global $PAGE;
if (!$PAGE->user_is_editing()) {
$isstealth = $section->section > $course->numsections;
$controls = array();
- if (!$isstealth && has_capability('moodle/course:setcurrentsection', $coursecontext)) {
+ if (!$isstealth && $section->section && has_capability('moodle/course:setcurrentsection', $coursecontext)) {
if ($course->marker == $section->section) { // Show the "light globe" on/off.
$url->param('marker', 0);
- $controls[] = html_writer::link($url,
- html_writer::empty_tag('img', array('src' => $this->output->pix_url('i/marked'),
- 'class' => 'icon ', 'alt' => get_string('markedthistopic'))),
- array('title' => get_string('markedthistopic'), 'class' => 'editing_highlight'));
+ $markedthistopic = get_string('markedthistopic');
+ $highlightoff = get_string('highlightoff');
+ $controls[] = array("url" => $url, "icon" => 'i/marked',
+ "name" => $highlightoff,
+ 'pixattr' => array('class' => '', 'alt' => $markedthistopic),
+ "attr" => array('class' => 'editing_highlight', 'title' => $markedthistopic));
} else {
$url->param('marker', $section->section);
- $controls[] = html_writer::link($url,
- html_writer::empty_tag('img', array('src' => $this->output->pix_url('i/marker'),
- 'class' => 'icon', 'alt' => get_string('markthistopic'))),
- array('title' => get_string('markthistopic'), 'class' => 'editing_highlight'));
+ $markthistopic = get_string('markthistopic');
+ $highlight = get_string('highlight');
+ $controls[] = array("url" => $url, "icon" => 'i/marker',
+ "name" => $highlight,
+ 'pixattr' => array('class' => '', 'alt' => $markthistopic),
+ "attr" => array('class' => 'editing_highlight', 'title' => $markthistopic));
}
}
- return array_merge($controls, parent::section_edit_controls($course, $section, $onsectionpage));
+ return array_merge($controls, parent::section_edit_control_items($course, $section, $onsectionpage));
}
}
.course-content ul.topics {margin:0;}
.course-content ul.topics li.section {list-style: none;margin:0 0 5px 0;padding:0;}
.course-content ul.topics li.section .content {margin:0 40px;}
-.course-content ul.topics li.section .left {float:left;}
-.course-content ul.topics li.section .right {float:right;}
.course-content ul.topics li.section .left,
-.course-content ul.topics li.section .right {width:40px;text-align:center;padding: 6px 0;}
+.course-content ul.topics li.section .right {width:40px;padding: 0 6px;}
.course-content ul.topics li.section .right img.icon { padding: 0 0 4px 0;}
+.course-content ul.topics li.section .left {padding-top:22px;text-align: right;}
+.jsenabled .course-content ul.topics li.section .left,
+.jsenabled .course-content ul.topics li.section .right {width:auto;}
.course-content ul.topics li.section .left .section-handle img.icon { padding:0; vertical-align: baseline; }
+.course-content ul.topics li.section .section_action_menu .textmenu,
+.course-content ul.topics li.section .section_action_menu .menu-action-text { white-space: nowrap; }
\ No newline at end of file
And I follow "Course 1"
And I turn editing mode on
+ Scenario: View the default name of the general section in topics format
+ When I click on "Edit section" "link" in the "li#section-0" "css_element"
+ Then I should see "Use default section name [General]"
+
+ Scenario: Edit the default name of the general section in topics format
+ When I click on "Edit section" "link" in the "li#section-0" "css_element"
+ And I set the following fields to these values:
+ | Use default section name | 0 |
+ | name | This is the general section |
+ And I press "Save changes"
+ Then I should see "This is the general section" in the "li#section-0" "css_element"
+
+ Scenario: View the default name of the second section in topics format
+ When I click on "Edit topic" "link" in the "li#section-2" "css_element"
+ Then I should see "Use default section name [Topic 2]"
+
Scenario: Edit section summary in topics format
- When I click on "Edit summary" "link" in the "li#section-2" "css_element"
+ When I edit the section "2"
And I set the following fields to these values:
| Summary | Welcome to section 2 |
And I press "Save changes"
Then I should see "Welcome to section 2" in the "li#section-2" "css_element"
Scenario: Edit section default name in topics format
- When I click on "Edit summary" "link" in the "li#section-2" "css_element"
+ When I edit the section "2"
And I set the following fields to these values:
| Use default section name | 0 |
| name | This is the second topic |
And I should not see "Topic 2" in the "li#section-2" "css_element"
Scenario: Deleting the last section in topics format
- When I click on "Delete topic" "link" in the "li#section-5" "css_element"
+ When I delete section "5"
Then I should see "Are you absolutely sure you want to completely delete \"Topic 5\" and all the activities it contains?"
And I press "Delete"
And I should not see "Topic 5"
And the field "Number of sections" matches value "4"
Scenario: Deleting the middle section in topics format
- When I click on "Delete topic" "link" in the "li#section-4" "css_element"
+ When I delete section "4"
And I press "Delete"
Then I should not see "Topic 5"
And I should not see "Test chat name"
Scenario: Deleting the orphaned section in topics format
When I follow "Reduce the number of sections"
Then I should see "Orphaned activities (section 5)" in the "li#section-5" "css_element"
- And I click on "Delete topic" "link" in the "li#section-5" "css_element"
+ And I delete section "5"
And I press "Delete"
And I should not see "Topic 5"
And I should not see "Orphaned activities"
Then I should see "Orphaned activities (section 5)" in the "li#section-5" "css_element"
And "li#section-5.orphaned" "css_element" should exist
And "li#section-4.orphaned" "css_element" should not exist
- And I click on "Delete topic" "link" in the "li#section-1" "css_element"
+ And I delete section "1"
And I press "Delete"
And I should not see "Test book name"
And I should see "Orphaned activities (section 4)" in the "li#section-4" "css_element"
$this->assertEquals(8, count(get_fast_modinfo($course)->get_section_info_all()));
$this->assertEquals(6, course_get_format($course)->get_course()->numsections);
}
+
+ /**
+ * Tests for format_topics::get_section_name method with default section names.
+ */
+ public function test_get_section_name() {
+ global $DB;
+ $this->resetAfterTest(true);
+
+ // Generate a course with 5 sections.
+ $generator = $this->getDataGenerator();
+ $numsections = 5;
+ $course = $generator->create_course(array('numsections' => $numsections, 'format' => 'topics'),
+ array('createsections' => true));
+
+ // Get section names for course.
+ $coursesections = $DB->get_records('course_sections', array('course' => $course->id));
+
+ // Test get_section_name with default section names.
+ $courseformat = course_get_format($course);
+ foreach ($coursesections as $section) {
+ // Assert that with unmodified section names, get_section_name returns the same result as get_default_section_name.
+ $this->assertEquals($courseformat->get_default_section_name($section), $courseformat->get_section_name($section));
+ }
+ }
+
+ /**
+ * Tests for format_topics::get_section_name method with modified section names.
+ */
+ public function test_get_section_name_customised() {
+ global $DB;
+ $this->resetAfterTest(true);
+
+ // Generate a course with 5 sections.
+ $generator = $this->getDataGenerator();
+ $numsections = 5;
+ $course = $generator->create_course(array('numsections' => $numsections, 'format' => 'topics'),
+ array('createsections' => true));
+
+ // Get section names for course.
+ $coursesections = $DB->get_records('course_sections', array('course' => $course->id));
+
+ // Modify section names.
+ $customname = "Custom Section";
+ foreach ($coursesections as $section) {
+ $section->name = "$customname $section->section";
+ $DB->update_record('course_sections', $section);
+ }
+
+ // Requery updated section names then test get_section_name.
+ $coursesections = $DB->get_records('course_sections', array('course' => $course->id));
+ $courseformat = course_get_format($course);
+ foreach ($coursesections as $section) {
+ // Assert that with modified section names, get_section_name returns the modified section name.
+ $this->assertEquals($section->name, $courseformat->get_section_name($section));
+ }
+ }
+
+ /**
+ * Tests for format_topics::get_default_section_name.
+ */
+ public function test_get_default_section_name() {
+ global $DB;
+ $this->resetAfterTest(true);
+
+ // Generate a course with 5 sections.
+ $generator = $this->getDataGenerator();
+ $numsections = 5;
+ $course = $generator->create_course(array('numsections' => $numsections, 'format' => 'topics'),
+ array('createsections' => true));
+
+ // Get section names for course.
+ $coursesections = $DB->get_records('course_sections', array('course' => $course->id));
+
+ // Test get_default_section_name with default section names.
+ $courseformat = course_get_format($course);
+ foreach ($coursesections as $section) {
+ if ($section->section == 0) {
+ $sectionname = get_string('section0name', 'format_topics');
+ $this->assertEquals($sectionname, $courseformat->get_default_section_name($section));
+ } else {
+ $sectionname = get_string('sectionname', 'format_topics') . ' ' . $section->section;
+ $this->assertEquals($sectionname, $courseformat->get_default_section_name($section));
+ }
+ }
+ }
}
Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
+=== 3.0 ===
+* Course formats should now use section_edit_control_items and use the returned array of controls items and their attributes to create a
+ renderable menu or array of links. Plugin calls to section_edit_controls will now include the section edit control in the returned array.
+* The section name is now wrapped in a new span (.sectionname > span), process_sections method in format.js should be updated so .sectionname
+ DOM node's wraps the section title in a span. You can look at how to implement the change in course/format/topics/format.js or MDL-48947.
+* New method format_base::get_default_section_name retrieves the default section name for the given course format. The base
+ implementation basically uses the implementation of format_base::get_section_name. The method can be overridden in
+ format_base subclasses that use sections (i.e. format_topics, format_weeks). In relation to the changes made for the default
+ section name, the default section name is now being shown when editing the section information.
+
=== 2.9 ===
* Course formats may support deleting sections, see MDL-10405 for more details.
format_section_renderer_base::section_edit_controls() is now also called for
for (var i = sectionfrom; i <= sectionto; i++) {
// Update section title.
- sectionlist.item(i).one('.'+CSS.SECTIONNAME).setContent(response.sectiontitles[i]);
+ var content = Y.Node.create('<span>' + response.sectiontitles[i] + '</span>');
+ sectionlist.item(i).all('.'+CSS.SECTIONNAME).setHTML(content);
// Update move icon.
ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE);
*/
$string['currentsection'] = 'This week';
+$string['editsection'] = 'Edit week';
$string['deletesection'] = 'Delete week';
$string['sectionname'] = 'Week';
$string['pluginname'] = 'Weekly format';
+$string['sectionmenu'] = 'Week menu';
$string['section0name'] = 'General';
$string['page-course-view-weeks'] = 'Any course main page in weeks format';
$string['page-course-view-weeks-x'] = 'Any course page in weeks format';
if ((string)$section->name !== '') {
// Return the name the user set.
return format_string($section->name, true, array('context' => context_course::instance($this->courseid)));
- } else if ($section->section == 0) {
+ } else {
+ return $this->get_default_section_name($section);
+ }
+ }
+
+ /**
+ * Returns the default section name for the weekly course format.
+ *
+ * If the section number is 0, it will use the string with key = section0name from the course format's lang file.
+ * Otherwise, the default format of "[start date] - [end date]" will be returned.
+ *
+ * @param stdClass $section Section object from database or just field course_sections section
+ * @return string The default value for the section name.
+ */
+ public function get_default_section_name($section) {
+ if ($section->section == 0) {
// Return the general section.
return get_string('section0name', 'format_weeks');
} else {
.course-content ul.weeks {margin:0;}
.course-content ul.weeks li.section {list-style: none;margin:0 0 5px 0;padding:0;}
.course-content ul.weeks li.section .content {margin:0 40px;}
-.course-content ul.weeks li.section .left {float:left;}
-.course-content ul.weeks li.section .right {float:right;}
.course-content ul.weeks li.section .left,
-.course-content ul.weeks li.section .right {width:40px;text-align:center;padding: 6px 0;}
+.course-content ul.weeks li.section .right {width:40px;padding: 0 6px;}
.course-content ul.weeks li.section .right img.icon { padding: 0 0 4px 0;}
+.course-content ul.weeks li.section .left {padding-top:22px;text-align: right;}
+.jsenabled .course-content ul.weeks li.section .left,
+.jsenabled .course-content ul.weeks li.section .right {width:auto;}
.course-content ul.weeks li.section .left .section-handle img.icon { padding:0; vertical-align: baseline; }
+.course-content ul.weeks li.section .section_action_menu .textmenu,
+.course-content ul.weeks li.section .section_action_menu .menu-action-text { white-space: nowrap; }
\ No newline at end of file
And I follow "Course 1"
And I turn editing mode on
+ Scenario: View the default name of the general section in weeks format
+ When I click on "Edit section" "link" in the "li#section-0" "css_element"
+ Then I should see "Use default section name [General]"
+
+ Scenario: Edit the default name of the general section in weeks format
+ When I click on "Edit section" "link" in the "li#section-0" "css_element"
+ And I set the following fields to these values:
+ | Use default section name | 0 |
+ | name | This is the general section |
+ And I press "Save changes"
+ Then I should see "This is the general section" in the "li#section-0" "css_element"
+
+ Scenario: View the default name of the second section in weeks format
+ When I click on "Edit week" "link" in the "li#section-2" "css_element"
+ Then I should see "Use default section name [8 May - 14 May]"
+
Scenario: Edit section summary in weeks format
- When I click on "Edit summary" "link" in the "li#section-2" "css_element"
+ When I click on "Edit week" "link" in the "li#section-2" "css_element"
And I set the following fields to these values:
| Summary | Welcome to section 2 |
And I press "Save changes"
Scenario: Edit section default name in weeks format
Given I should see "8 May - 14 May" in the "li#section-2" "css_element"
- When I click on "Edit summary" "link" in the "li#section-2" "css_element"
+ When I click on "Edit week" "link" in the "li#section-2" "css_element"
And I set the following fields to these values:
| Use default section name | 0 |
| name | This is the second week |
Scenario: Deleting the last section in weeks format
Given I should see "29 May - 4 June" in the "li#section-5" "css_element"
- When I click on "Delete week" "link" in the "li#section-5" "css_element"
+ When I delete section "5"
Then I should see "Are you absolutely sure you want to completely delete \"29 May - 4 June\" and all the activities it contains?"
And I press "Delete"
And I should not see "29 May - 4 June"
Scenario: Deleting the middle section in weeks format
Given I should see "29 May - 4 June" in the "li#section-5" "css_element"
- When I click on "Delete week" "link" in the "li#section-4" "css_element"
+ When I delete section "4"
And I press "Delete"
Then I should not see "29 May - 4 June"
And I should not see "Test chat name"
Scenario: Deleting the orphaned section in weeks format
When I follow "Reduce the number of sections"
Then I should see "Orphaned activities (section 5)" in the "li#section-5" "css_element"
- And I click on "Delete week" "link" in the "li#section-5" "css_element"
+ And I delete section "5"
And I press "Delete"
And I should not see "29 May - 4 June"
And I should not see "Orphaned activities"
Then I should see "Orphaned activities (section 5)" in the "li#section-5" "css_element"
And "li#section-5.orphaned" "css_element" should exist
And "li#section-4.orphaned" "css_element" should not exist
- And I click on "Delete week" "link" in the "li#section-1" "css_element"
+ And I delete section "1"
And I press "Delete"
And I should not see "Test book name"
And I should see "Orphaned activities (section 4)" in the "li#section-4" "css_element"
$this->assertEquals(8, count(get_fast_modinfo($course)->get_section_info_all()));
$this->assertEquals(6, course_get_format($course)->get_course()->numsections);
}
+
+ /**
+ * Tests for format_weeks::get_section_name method with default section names.
+ */
+ public function test_get_section_name() {
+ global $DB;
+ $this->resetAfterTest(true);
+
+ // Generate a course with 5 sections.
+ $generator = $this->getDataGenerator();
+ $numsections = 5;
+ $course = $generator->create_course(array('numsections' => $numsections, 'format' => 'weeks'),
+ array('createsections' => true));
+
+ // Get section names for course.
+ $coursesections = $DB->get_records('course_sections', array('course' => $course->id));
+
+ // Test get_section_name with default section names.
+ $courseformat = course_get_format($course);
+ foreach ($coursesections as $section) {
+ // Assert that with unmodified section names, get_section_name returns the same result as get_default_section_name.
+ $this->assertEquals($courseformat->get_default_section_name($section), $courseformat->get_section_name($section));
+ }
+ }
+
+ /**
+ * Tests for format_weeks::get_section_name method with modified section names.
+ */
+ public function test_get_section_name_customised() {
+ global $DB;
+ $this->resetAfterTest(true);
+
+ // Generate a course with 5 sections.
+ $generator = $this->getDataGenerator();
+ $numsections = 5;
+ $course = $generator->create_course(array('numsections' => $numsections, 'format' => 'weeks'),
+ array('createsections' => true));
+
+ // Get section names for course.
+ $coursesections = $DB->get_records('course_sections', array('course' => $course->id));
+
+ // Modify section names.
+ $customname = "Custom Section";
+ foreach ($coursesections as $section) {
+ $section->name = "$customname $section->section";
+ $DB->update_record('course_sections', $section);
+ }
+
+ // Requery updated section names then test get_section_name.
+ $coursesections = $DB->get_records('course_sections', array('course' => $course->id));
+ $courseformat = course_get_format($course);
+ foreach ($coursesections as $section) {
+ // Assert that with modified section names, get_section_name returns the modified section name.
+ $this->assertEquals($section->name, $courseformat->get_section_name($section));
+ }
+ }
+
+ /**
+ * Tests for format_weeks::get_default_section_name.
+ */
+ public function test_get_default_section_name() {
+ global $DB;
+ $this->resetAfterTest(true);
+
+ // Generate a course with 5 sections.
+ $generator = $this->getDataGenerator();
+ $numsections = 5;
+ $course = $generator->create_course(array('numsections' => $numsections, 'format' => 'weeks'),
+ array('createsections' => true));
+
+ // Get section names for course.
+ $coursesections = $DB->get_records('course_sections', array('course' => $course->id));
+
+ // Test get_default_section_name with default section names.
+ $courseformat = course_get_format($course);
+ foreach ($coursesections as $section) {
+ if ($section->section == 0) {
+ $sectionname = get_string('section0name', 'format_weeks');
+ $this->assertEquals($sectionname, $courseformat->get_default_section_name($section));
+ } else {
+ $dates = $courseformat->get_section_dates($section);
+ $dates->end = ($dates->end - 86400);
+ $dateformat = get_string('strftimedateshort');
+ $weekday = userdate($dates->start, $dateformat);
+ $endweekday = userdate($dates->end, $dateformat);
+ $sectionname = $weekday.' - '.$endweekday;
+
+ $this->assertEquals($sectionname, $courseformat->get_default_section_name($section));
+ }
+ }
+ }
}
'edittitleinstructions',
'show',
'hide',
+ 'highlight',
+ 'highlightoff',
'groupsnone',
'groupsvisible',
'groupsseparate',
}
+
+ /**
+ * Opens a section edit menu if it is not already opened.
+ *
+ * @Given /^I open section "(?P<section_number>\d+)" edit menu$/
+ * @throws DriverException The step is not available when Javascript is disabled
+ * @param string $sectionnumber
+ */
+ public function i_open_section_edit_menu($sectionnumber) {
+ if (!$this->running_javascript()) {
+ throw new DriverException('Section edit menu not available when Javascript is disabled');
+ }
+
+ // If it is already opened we do nothing.
+ $xpath = $this->section_exists($sectionnumber);
+ $xpath .= "/descendant::div[contains(@class, 'section-actions')]/descendant::a[contains(@class, 'textmenu')]";
+
+ $exception = new ExpectationException('Section "' . $sectionnumber . '" was not found', $this->getSession());
+ $menu = $this->find('xpath', $xpath, $exception);
+ $menu->click();
+ $this->i_wait_until_section_is_available($sectionnumber);
+ }
+
+ /**
+ * Deletes course section.
+ *
+ * @Given /^I delete section "(?P<section_number>\d+)"$/
+ * @param int $sectionnumber The section number
+ * @return Given[]
+ */
+ public function i_delete_section($sectionnumber) {
+ // Ensures the section exists.
+ $xpath = $this->section_exists($sectionnumber);
+
+ // We need to know the course format as the text strings depends on them.
+ $courseformat = $this->get_course_format();
+ if (get_string_manager()->string_exists('deletesection', $courseformat)) {
+ $strdelete = get_string('deletesection', $courseformat);
+ } else {
+ $strdelete = get_string('deletesection');
+ }
+
+ // If javascript is on, link is inside a menu.
+ if ($this->running_javascript()) {
+ $this->i_open_section_edit_menu($sectionnumber);
+ }
+
+ return new Given('I click on "' . $strdelete . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"');
+ }
+
/**
* Turns course section highlighting on.
*
// Ensures the section exists.
$xpath = $this->section_exists($sectionnumber);
+ // If javascript is on, link is inside a menu.
+ if ($this->running_javascript()) {
+ $this->i_open_section_edit_menu($sectionnumber);
+ }
+
return new Given('I click on "' . get_string('markthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"');
}
// Ensures the section exists.
$xpath = $this->section_exists($sectionnumber);
+ // If javascript is on, link is inside a menu.
+ if ($this->running_javascript()) {
+ $this->i_open_section_edit_menu($sectionnumber);
+ }
+
return new Given('I click on "' . get_string('markedthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"');
}
* @param int $sectionnumber
*/
public function i_edit_the_section($sectionnumber) {
- return new Given('I click on "' . get_string('editsummary') . '" "link" in the "#section-' . $sectionnumber . '" "css_element"');
+ // If javascript is on, link is inside a menu.
+ if ($this->running_javascript()) {
+ $this->i_open_section_edit_menu($sectionnumber);
+ }
+
+ // We need to know the course format as the text strings depends on them.
+ $courseformat = $this->get_course_format();
+ if (get_string_manager()->string_exists('editsection', $courseformat)) {
+ $stredit = get_string('editsection', $courseformat);
+ } else {
+ $stredit = get_string('editsection');
+ }
+
+ return new Given('I click on "' . $stredit . '" "link" in the "#section-' . $sectionnumber . '" "css_element"');
}
/**
$xpath = $this->section_exists($sectionnumber);
// The important checking, we can not check the img.
- $xpath = $xpath . "/descendant::img[@alt='" . get_string('markedthistopic') . "'][contains(@src, 'marked')]";
+ $xpath = $xpath . "/descendant::img[contains(@src, 'marked')]";
$exception = new ExpectationException('The "' . $sectionnumber . '" section is not highlighted', $this->getSession());
$this->find('xpath', $xpath, $exception);
}
throw new ExpectationException('The section is hidden', $this->getSession());
}
- // Hide section button should be visible.
+ // Edit menu should be visible.
if ($this->is_course_editor()) {
- $this->hide_section_icon_exists($sectionnumber);
+ $xpath = $sectionxpath .
+ "/descendant::div[contains(@class, 'section-actions')]" .
+ "/descendant::a[contains(@class, 'textmenu')]";
+ if (!$this->getSession()->getPage()->find('xpath', $xpath)) {
+ throw new ExpectationException('The section edit menu is not available', $this->getSession());
+ }
}
}
// Ensures the section exists.
$sectionxpath = $this->section_exists($sectionnumber);
+ // If javascript is on, link is inside a menu.
+ if ($this->running_javascript()) {
+ $this->i_open_section_edit_menu($sectionnumber);
+ }
+
// Follows the link
$moveuplink = $this->get_node_in_container('link', get_string('moveup'), 'xpath_element', $sectionxpath);
$moveuplink->click();
// Ensures the section exists.
$sectionxpath = $this->section_exists($sectionnumber);
+ // If javascript is on, link is inside a menu.
+ if ($this->running_javascript()) {
+ $this->i_open_section_edit_menu($sectionnumber);
+ }
+
// Follows the link
$movedownlink = $this->get_node_in_container('link', get_string('movedown'), 'xpath_element', $sectionxpath);
$movedownlink->click();
// We need to know the course format as the text strings depends on them.
$courseformat = $this->get_course_format();
+ // If javascript is on, link is inside a menu.
+ if ($this->running_javascript()) {
+ $this->i_open_section_edit_menu($sectionnumber);
+ }
+
// Checking the show button alt text and show icon.
$showtext = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('showfromothers', $courseformat));
$linkxpath = $xpath . "/descendant::a[@title=$showtext]";
- $imgxpath = $linkxpath . "/descendant::img[@alt=$showtext][contains(@src, 'show')]";
+ $imgxpath = $linkxpath . "/descendant::img[contains(@src, 'show')]";
$exception = new ElementNotFoundException($this->getSession(), 'Show section icon ');
$this->find('xpath', $imgxpath, $exception);
// We need to know the course format as the text strings depends on them.
$courseformat = $this->get_course_format();
+ // If javascript is on, link is inside a menu.
+ if ($this->running_javascript()) {
+ $this->i_open_section_edit_menu($sectionnumber);
+ }
+
// Checking the hide button alt text and hide icon.
$hidetext = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('hidefromothers', $courseformat));
$linkxpath = $xpath . "/descendant::a[@title=$hidetext]";
- $imgxpath = $linkxpath . "/descendant::img[@alt=$hidetext][contains(@src, 'hide')]";
+ $imgxpath = $linkxpath . "/descendant::img[contains(@src, 'hide')]";
$exception = new ElementNotFoundException($this->getSession(), 'Hide section icon ');
$this->find('xpath', $imgxpath, $exception);
$sink->close();
// Validate the event.
- $event = $events[0];
+ $event = array_pop($events);
$this->assertInstanceOf('\core\event\course_created', $event);
$this->assertEquals('course', $event->objecttable);
$this->assertEquals($course->id, $event->objectid);
$imstestcase->imsplugin->cron();
$events = $sink->get_events();
$sink->close();
- $event = $events[0];
+ $event = array_pop($events);
// Validate the event triggered is \core\event\course_created. There is no need to validate the other values
// as they have already been validated in the previous steps. Here we only want to make sure that when the
$sink->close();
// Validate the event.
- $event = $events[1];
+ $event = array_pop($events);
$this->assertInstanceOf('\core\event\course_deleted', $event);
$this->assertEquals('course', $event->objecttable);
$this->assertEquals($course->id, $event->objectid);
$sink->close();
// Validate the event.
- $event = $events[0];
+ $event = array_pop($events);
$this->assertInstanceOf('\core\event\course_content_deleted', $event);
$this->assertEquals('course', $event->objecttable);
$this->assertEquals($course->id, $event->objectid);
$sink->close();
// Validate the event.
- $event = $events[0];
+ $event = array_pop($events);
$this->assertInstanceOf('\core\event\course_restored', $event);
$this->assertEquals('course', $event->objecttable);
$this->assertEquals($rc->get_courseid(), $event->objectid);
cssleft.appendChild(this.get_drag_handle(title, CSS.SECTIONHANDLE, 'icon', true));
if (moveup) {
- moveup.remove();
+ if (moveup.previous('br')) {
+ moveup.previous('br').remove();
+ } else if (moveup.next('br')) {
+ moveup.next('br').remove();
+ }
+
+ if (moveup.ancestor('.section_action_menu')) {
+ moveup.ancestor('li').remove();
+ } else {
+ moveup.remove();
+ }
}
if (movedown) {
- movedown.remove();
+ if (movedown.previous('br')) {
+ movedown.previous('br').remove();
+ } else if (movedown.next('br')) {
+ movedown.next('br').remove();
+ }
+
+ if (movedown.ancestor('.section_action_menu')) {
+ movedown.ancestor('li').remove();
+ } else {
+ movedown.remove();
+ }
}
// This section can be moved - add the class to indicate this to Y.DD.
var section = e.target.ancestor(M.course.format.get_section_selector(Y)),
button = e.target.ancestor('a', true),
hideicon = button.one('img'),
+ buttontext = button.one('span'),
// The value to submit
value,
'src' : M.util.image_url('i/' + nextaction)
});
button.set('title', newstring);
+ if (buttontext) {
+ buttontext.set('text', newstring);
+ }
- // Change the highlight status
+ // Change the show/hide status
var data = {
'class' : 'section',
'field' : 'visible',
var section = e.target.ancestor(M.course.format.get_section_selector(Y));
var button = e.target.ancestor('a', true);
var buttonicon = button.one('img');
+ var buttontext = button.one('span');
// Determine whether the marker is currently set.
var togglestatus = section.hasClass('current');
// Set the current highlighted item text.
var old_string = M.util.get_string('markthistopic', 'moodle');
- Y.one(SELECTOR.PAGECONTENT)
+
+ var selectedpage = Y.one(SELECTOR.PAGECONTENT);
+ selectedpage
.all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT)
.set('title', old_string);
- Y.one(SELECTOR.PAGECONTENT)
+ selectedpage
+ .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT + ' span')
+ .set('text', M.util.get_string('highlight', 'moodle'));
+ selectedpage
.all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT + ' img')
.set('alt', old_string)
.set('src', M.util.image_url('i/marker'));
// Remove the highlighting from all sections.
- Y.one(SELECTOR.PAGECONTENT).all(M.course.format.get_section_selector(Y))
+ selectedpage.all(M.course.format.get_section_selector(Y))
.removeClass('current');
// Then add it if required to the selected section.
buttonicon
.set('alt', new_string)
.set('src', M.util.image_url('i/marked'));
+ if (buttontext) {
+ buttontext
+ .set('text', M.util.get_string('highlightoff', 'moodle'));
+ }
}
// Change the highlight status.
$instance->customint2 = $groupid;
}
$DB->update_record('enrol', $instance);
+ \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
} else {
// Create a new group for the cohort if requested.
if ($data->customint2 == COHORT_CREATE_GROUP) {
$instance->password = $data->{'enrol_guest_password_'.$i};
}
$DB->update_record('enrol', $instance);
+ \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
if ($reset) {
$context = context_course::instance($course->id);
$instance->notifyall = $data->notifyall;
$instance->expirythreshold = $data->expirythreshold;
$instance->timemodified = time();
+ $markdirty = ($instance->status != $data->status);
+ $instance->status = $data->status;
$DB->update_record('enrol', $instance);
+ \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
- // Use standard API to update instance status.
- if ($instance->status != $data->status) {
- $instance = $DB->get_record('enrol', array('id'=>$instance->id));
- $plugin->update_status($instance, $data->status);
+ if ($markdirty) {
$context->mark_dirty();
}
return true;
}
+
+ /**
+ * Triggered via enrol_instance_updated event.
+ *
+ * @param \core\event\enrol_instance_updated $event
+ * @return boolean
+ */
+ public static function enrol_instance_updated(\core\event\enrol_instance_updated $event) {
+ global $DB;
+
+ if (!enrol_is_enabled('meta')) {
+ // This is slow, let enrol_meta_sync() deal with disabled plugin.
+ return true;
+ }
+
+ // Does anything want to sync with this parent?
+ $affectedcourses = $DB->get_fieldset_sql('SELECT DISTINCT courseid FROM {enrol} '.
+ 'WHERE customint1 = ? AND enrol = ?',
+ array($event->courseid, 'meta'));
+
+ foreach ($affectedcourses as $courseid) {
+ enrol_meta_sync($courseid);
+ }
+
+ return true;
+ }
}
'eventname' => '\core\event\course_deleted',
'callback' => 'enrol_meta_observer::course_deleted',
),
+ array(
+ 'eventname' => '\core\event\enrol_instance_updated',
+ 'callback' => 'enrol_meta_observer::enrol_instance_updated',
+ ),
);
// Disable manual enrolment in course1 and make sure all user enrolments in course2 are suspended.
$manplugin->update_status($manual1, ENROL_INSTANCE_DISABLED);
$allsuspendedenrolemnts = array_combine(array_keys($expectedenrolments), array_fill(0, 5, ENROL_USER_SUSPENDED));
+ $enrolmentstatuses = $DB->get_records_menu('user_enrolments', array('enrolid' => $meta2id), '', 'userid, status');
+ $this->assertEquals($allsuspendedenrolemnts, $enrolmentstatuses);
+
+ $manplugin->update_status($manual1, ENROL_INSTANCE_ENABLED);
+ $enrolments = $DB->get_records('user_enrolments', array('enrolid' => $meta2id), '', 'userid, timestart, timeend, status');
+ $this->assertEquals($expectedenrolments, $enrolments);
+
+ // Disable events and repeat the same for course3 (testing sync):
+ $sink = $this->redirectEvents();
+ $manplugin->update_status($manual1, ENROL_INSTANCE_DISABLED);
enrol_meta_sync($course3->id);
$enrolmentstatuses = $DB->get_records_menu('user_enrolments', array('enrolid' => $meta3id), '', 'userid, status');
$this->assertEquals($allsuspendedenrolemnts, $enrolmentstatuses);
enrol_meta_sync($course3->id);
$enrolments = $DB->get_records('user_enrolments', array('enrolid' => $meta3id), '', 'userid, timestart, timeend, status');
$this->assertEquals($expectedenrolments, $enrolments);
+ $sink->close();
}
}
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2015051100; // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version = 2015082400; // The current plugin version (Date: YYYYMMDDXX)
$plugin->requires = 2015050500; // Requires this Moodle version
$plugin->component = 'enrol_meta'; // Full name of the plugin (used for diagnostics)
$plugin->cron = 60*60; // run cron every hour by default, it is not out-of-sync often
$instance->enrolenddate = $data->enrolenddate;
$instance->timemodified = time();
$DB->update_record('enrol', $instance);
+ \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
if ($reset) {
$context->mark_dirty();
$instance->enrolenddate = $data->enrolenddate;
$instance->timemodified = time();
$DB->update_record('enrol', $instance);
+ \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
if ($reset) {
$context->mark_dirty();
*/
$string['canntenrol'] = 'Enrolment is disabled or inactive';
+$string['canntenrolearly'] = 'You cannot enrol yet; enrolment starts on {$a}.';
+$string['canntenrollate'] = 'You cannot enrol any more, since enrolment ended on {$a}.';
$string['cohortnonmemberinfo'] = 'Only members of cohort \'{$a}\' can self-enrol.';
$string['cohortonly'] = 'Only cohort members';
$string['cohortonly_help'] = 'Self enrolment may be restricted to members of a specified cohort only. Note that changing this setting has no effect on existing enrolments.';
}
if ($instance->enrolstartdate != 0 and $instance->enrolstartdate > time()) {
- return get_string('canntenrol', 'enrol_self');
+ return get_string('canntenrolearly', 'enrol_self', userdate($instance->enrolstartdate));
}
if ($instance->enrolenddate != 0 and $instance->enrolenddate < time()) {
- return get_string('canntenrol', 'enrol_self');
+ return get_string('canntenrollate', 'enrol_self', userdate($instance->enrolenddate));
}
if (!$instance->customint6) {
$this->assertEventLegacyLogData($expected, $event);
$this->assertEventContextNotUsed($event);
}
+
+ /**
+ * Test enrol_instance_created, enrol_instance_updated and enrol_instance_deleted events.
+ */
+ public function test_instance_events() {
+ global $DB;
+
+ $this->resetAfterTest(true);
+
+ $selfplugin = enrol_get_plugin('self');
+ $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+
+ $course = $this->getDataGenerator()->create_course();
+
+ // Creating enrol instance.
+ $sink = $this->redirectEvents();
+ $instanceid = $selfplugin->add_instance($course, array('status' => ENROL_INSTANCE_ENABLED,
+ 'name' => 'Test instance 1',
+ 'customint6' => 1,
+ 'roleid' => $studentrole->id));
+ $events = $sink->get_events();
+ $sink->close();
+
+ $this->assertCount(1, $events);
+ $event = array_pop($events);
+ $this->assertInstanceOf('\core\event\enrol_instance_created', $event);
+ $this->assertEquals(context_course::instance($course->id), $event->get_context());
+ $this->assertEquals('self', $event->other['enrol']);
+ $this->assertEventContextNotUsed($event);
+
+ // Updating enrol instance.
+ $instance = $DB->get_record('enrol', array('id' => $instanceid));
+ $sink = $this->redirectEvents();
+ $selfplugin->update_status($instance, ENROL_INSTANCE_DISABLED);
+
+ $events = $sink->get_events();
+ $sink->close();
+
+ $this->assertCount(1, $events);
+ $event = array_pop($events);
+ $this->assertInstanceOf('\core\event\enrol_instance_updated', $event);
+ $this->assertEquals(context_course::instance($course->id), $event->get_context());
+ $this->assertEquals('self', $event->other['enrol']);
+ $this->assertEventContextNotUsed($event);
+
+ // Deleting enrol instance.
+ $instance = $DB->get_record('enrol', array('id' => $instanceid));
+ $sink = $this->redirectEvents();
+ $selfplugin->delete_instance($instance);
+
+ $events = $sink->get_events();
+ $sink->close();
+
+ $this->assertCount(1, $events);
+ $event = array_pop($events);
+ $this->assertInstanceOf('\core\event\enrol_instance_deleted', $event);
+ $this->assertEquals(context_course::instance($course->id), $event->get_context());
+ $this->assertEquals('self', $event->other['enrol']);
+ $this->assertEventContextNotUsed($event);
+ }
}
This files describes API changes in /enrol/* - plugins,
information provided here is intended especially for developers.
+=== 3.0 ===
+
+* Added new events enrol_instance_created, enrol_instance_updated and
+ enrol_instance_deleted . Always trigger them when changing records in the
+ DB table 'enrol'.
+
=== 2.9 ===
* External function core_enrol_external::get_users_courses now returns additional optional fields:
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
-require_once(__DIR__ . "../../../../config.php");
+require_once(__DIR__ . "/../../../config.php");
require_once($CFG->libdir.'/gradelib.php');
require_once($CFG->dirroot.'/grade/lib.php');
require_once($CFG->dirroot.'/grade/import/lib.php');
$grades[$grade_item->id] =
new grade_grade(array('userid'=>$user->id, 'itemid'=>$grade_item->id), false);
}
+ $grades[$grade_item->id]->grade_item = $grade_item;
}
}
require_capability('moodle/grade:viewall', $context);
} else {
$user = core_user::get_user($userid, '*', MUST_EXIST);
+ core_user::require_active_user($user);
}
$access = false;
$userid = $USER->id;
} else {
$user = core_user::get_user($userid, '*', MUST_EXIST);
- if ($user->deleted) {
- throw new moodle_exception('userdeleted');
- }
- if (isguestuser($user)) {
- // Can not view profile of guest - thre is nothing to see there.
- throw new moodle_exception('invaliduserid');
- }
+ core_user::require_active_user($user);
}
$access = false;
// Validate course and user. get_course throws an exception if the course does not exists.
$course = get_course($courseid);
- $user = core_user::get_user($userid, 'id', MUST_EXIST);
+ $user = core_user::get_user($userid, '*', MUST_EXIST);
+ core_user::require_active_user($user);
// Security checks.
$context = context_course::instance($course->id);
$userid = $USER->id;
}
- $user = core_user::get_user($userid, 'id, deleted', MUST_EXIST);
- if ($user->deleted) {
- throw new moodle_exception('userdeleted');
- }
- if (isguestuser($user)) {
- throw new moodle_exception('invaliduserid');
- }
+ $user = core_user::get_user($userid, '*', MUST_EXIST);
+ core_user::require_active_user($user);
// Check if we have permissions for retrieve the information.
if ($user->id != $USER->id) {
}
// Validate if the user is enrolled in the course.
- if (!is_enrolled($coursecontext, $user->id)) {
+ $course = get_course($cm->course);
+ if (!can_access_course($course, $user, '', true)) {
// We return a warning because the function does not fail for not enrolled users.
$warning = array();
$warning['item'] = 'course';
$warning['itemid'] = $cm->course;
$warning['warningcode'] = '1';
- $warning['message'] = "User $user->id is not enrolled in course $cm->course";
+ $warning['message'] = "User $user->id cannot access course $cm->course";
$warnings[] = $warning;
}
}
$newsforumcontext = context_module::instance($newsforumcm->id, MUST_EXIST);
$forumname = format_string($newsforum->name, true, array('context' => $newsforumcontext));
- echo html_writer::tag('a',
+ echo html_writer::link('#',
get_string('skipa', 'access', core_text::strtolower(strip_tags($forumname))),
- array('href' => '#skipsitenews', 'class' => 'skip-block'));
+ array('data-target' => '#skipsitenews', 'class' => 'skip-block skip'));
// Wraps site news forum in div container.
echo html_writer::start_tag('div', array('id' => 'site-news-forum'));
// End site news forum div container.
echo html_writer::end_tag('div');
- echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipsitenews'));
+ echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipsitenews', 'tabindex' => '-1'));
}
break;
case FRONTPAGEENROLLEDCOURSELIST:
$mycourseshtml = $courserenderer->frontpage_my_courses();
if (!empty($mycourseshtml)) {
- echo html_writer::tag('a',
+ echo html_writer::link('#',
get_string('skipa', 'access', core_text::strtolower(get_string('mycourses'))),
- array('href' => '#skipmycourses', 'class' => 'skip-block'));
+ array('data-target' => '#skipmycourses', 'class' => 'skip skip-block'));
// Wrap frontpage course list in div container.
echo html_writer::start_tag('div', array('id' => 'frontpage-course-list'));
// End frontpage course list div container.
echo html_writer::end_tag('div');
- echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipmycourses'));
+ echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipmycourses', 'tabindex' => '-1'));
break;
}
// No "break" here. If there are no enrolled courses - continue to 'Available courses'.
case FRONTPAGEALLCOURSELIST:
$availablecourseshtml = $courserenderer->frontpage_available_courses();
if (!empty($availablecourseshtml)) {
- echo html_writer::tag('a',
+ echo html_writer::link('#',
get_string('skipa', 'access', core_text::strtolower(get_string('availablecourses'))),
- array('href' => '#skipavailablecourses', 'class' => 'skip-block'));
+ array('data-target' => '#skipavailablecourses', 'class' => 'skip skip-block'));
// Wrap frontpage course list in div container.
echo html_writer::start_tag('div', array('id' => 'frontpage-course-list'));
// End frontpage course list div container.
echo html_writer::end_tag('div');
- echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipavailablecourses'));
+ echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipavailablecourses', 'tabindex' => '-1'));
}
break;
case FRONTPAGECATEGORYNAMES:
- echo html_writer::tag('a',
+ echo html_writer::link('#',
get_string('skipa', 'access', core_text::strtolower(get_string('categories'))),
- array('href' => '#skipcategories', 'class' => 'skip-block'));
+ array('data-target' => '#skipcategories', 'class' => 'skip skip-block'));
// Wrap frontpage category names in div container.
echo html_writer::start_tag('div', array('id' => 'frontpage-category-names'));
// End frontpage category names div container.
echo html_writer::end_tag('div');
- echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipcategories'));
+ echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipcategories', 'tabindex' => '-1'));
break;
case FRONTPAGECATEGORYCOMBO:
- echo html_writer::tag('a',
+ echo html_writer::link('#',
get_string('skipa', 'access', core_text::strtolower(get_string('courses'))),
- array('href' => '#skipcourses', 'class' => 'skip-block'));
+ array('data-target' => '#skipcourses', 'class' => 'skip skip-block'));
// Wrap frontpage category combo in div container.
echo html_writer::start_tag('div', array('id' => 'frontpage-category-combo'));
// End frontpage category combo div container.
echo html_writer::end_tag('div');
- echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipcourses'));
+ echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipcourses', 'tabindex' => '-1'));
break;
case FRONTPAGECOURSESEARCH:
$string['cliinstallheader'] = 'Moodle {$a} inštalačný program z príkazového riadku';
$string['databasehost'] = 'Databázový server';
$string['databasename'] = 'Názov databázy';
-$string['databasetypehead'] = 'Vyberte driver pre databázu';
+$string['databasetypehead'] = 'Vyberte ovládač pre databázu';
$string['dataroot'] = 'Adresár pre údaje';
$string['dbprefix'] = 'Predpona tabuliek';
$string['dirroot'] = 'Adresár Moodle';
remotedownloaderror,error
thisdirection,langconfig
thislanguage,langconfig
+upgradekeyset,admin
welcomep10,install
welcomep20,install
welcomep30,install
$string['upgradeerror'] = 'Unknown error upgrading {$a->plugin} to version {$a->version}, can not continue.';
$string['upgradeforumread'] = 'A new feature has been added in Moodle 1.5 to track read/unread forum posts.<br />To use this functionality you need to <a href="{$a}">update your tables</a>.';
$string['upgradeforumreadinfo'] = 'A new feature has been added in Moodle 1.5 to track read/unread forum posts. To use this functionality you need to update your tables with all the tracking information for existing posts. Depending on the size of your site this can take a long time (hours) and can be quite taxing on the database, so it\'s best to do it during a quiet period. However, your site will continue functioning during this upgrade and users won\'t be affected. Once you start this process you should let it finish (keep your browser window open). However, if you stop the process by closing the window: don\'t worry, you can start over.<br /><br />Do you want to start the upgrading process now?';
+$string['upgradekeyreq'] = 'Upgrade key required';
+$string['upgradekeyset'] = 'Upgrade key (leave empty to not set it)';
$string['upgradelogs'] = 'For full functionality, your old logs need to be upgraded. <a href="{$a}">More information</a>';
$string['upgradelogsinfo'] = 'Some changes have recently been made in the way logs are stored. To be able to view all of your old logs on a per-activity basis, your old logs need to be upgraded. Depending on your site this can take a long time (eg several hours) and can be quite taxing on the database for large sites. Once you start this process you should let it finish (by keeping the browser window open). Don\'t worry - your site will work fine for other people while the logs are being upgraded.<br /><br />Do you want to upgrade your logs now?';
$string['upgradesettings'] = 'New settings';
$string['automatedbackupschedulehelp'] = 'Choose which days of the week to perform automated backups.';
$string['automatedbackupsinactive'] = 'Automated backups haven\'t been enabled by the site admin';
$string['automatedbackupstatus'] = 'Automated backup status';
+$string['automateddeletedays'] = 'Delete backups older than';
+$string['automatedmaxkept'] = 'Maximum number of backups kept';
+$string['automatedmaxkepthelp'] = 'This specifies the maximum number of recent automated backups to be kept for each course. Older backups will be deleted automatically.';
+$string['automatedminkept'] = 'Minimum number of backups kept';
+$string['automatedminkepthelp'] = 'If backups older than a specified number of days are deleted, it can happen that an inactive course ends up with no backup. To prevent this, a minimum number of backups kept should be specified.';
$string['automatedsetup'] = 'Automated backup setup';
$string['automatedsettings'] = 'Automated backup settings';
$string['automatedstorage'] = 'Automated backup storage';
$string['enrolcandidatesmatching'] = 'Matching not enrolled users';
$string['enrolcohort'] = 'Enrol cohort';
$string['enrolcohortusers'] = 'Enrol users';
+$string['eventenrolinstancecreated'] = 'Enrolment instance created';
+$string['eventenrolinstancedeleted'] = 'Enrolment instance deleted';
+$string['eventenrolinstanceupdated'] = 'Enrolment instance updated';
$string['enrollednewusers'] = 'Successfully enrolled {$a} new users';
$string['enrolledusers'] = 'Enrolled users';
$string['enrolledusersmatching'] = 'Matching enrolled users';
$string['eventmessagecontactblocked'] = 'Message contact blocked';
$string['eventmessagecontactremoved'] = 'Message contact removed';
$string['eventmessagecontactunblocked'] = 'Message contact unblocked';
+$string['eventmessagedeleted'] = 'Message deleted';
$string['eventmessageviewed'] = 'Message viewed';
$string['eventmessagesent'] = 'Message sent';
$string['forced'] = 'Forced';
$string['backupgradebookhistoryhelp'] = 'If enabled then gradebook history will be included in automated backups. Note that grade history must not be disabled in server settings (disablegradehistory) in order for this to work';
$string['backupincludemoduleshelp'] = 'Choose whether you want to include course modules, with or without user data, in automated backups';
$string['backupincludemoduleuserdatahelp'] = 'Choose whether you want to include module user data in automated backups.';
-$string['backupkeephelp'] = 'How many recent backups for each course do you want to keep? (older ones will be deleted automatically)';
$string['backuplogdetailed'] = 'Detailed execution log';
$string['backuploglaststatus'] = 'Last execution log';
$string['backupmissinguserinfoperms'] = 'Note: This backup contains no user data. Exercise and Workshop activities will not be included in the backup, since these modules are not compatible with this type of backup.';
$string['editorresettodefaults'] = 'Reset to default values';
$string['editorsettings'] = 'Editor settings';
$string['editorshortcutkeys'] = 'Editor shortcut keys';
-$string['editsettings'] = 'Edit settings';
+$string['editsection'] = 'Edit section';
$string['editsummary'] = 'Edit summary';
$string['edittitle'] = 'Edit title';
$string['edittitleinstructions'] = 'Escape to cancel, Enter when finished';
$string['hidesection'] = 'Hide section {$a}';
$string['hidesettings'] = 'Hide settings';
$string['hideshowblocks'] = 'Hide or show blocks';
+$string['highlight'] = 'Highlight';
+$string['highlightoff'] = 'Remove highlight';
$string['hits'] = 'Hits';
$string['hitsoncourse'] = 'Hits on {$a->coursename} by {$a->username}';
$string['hitsoncoursetoday'] = 'Today\'s hits on {$a->coursename} by {$a->username}';
$string['notice'] = 'Notice';
$string['noticenewerbackup'] = 'This backup file has been created with Moodle {$a->backuprelease} ({$a->backupversion}) and it\'s newer than your currently installed Moodle {$a->serverrelease} ({$a->serverversion}). This could cause some inconsistencies because backwards compatibility of backup files cannot be guaranteed.';
$string['notifications'] = 'Notifications';
-$string['notifyloginfailuresmessage'] = '{$a->time}, IP: {$a->ip}, User: {$a->info}';
+$string['notifyloginfailuresmessage'] = '{$a->time}, IP: {$a->ip}, User: {$a->info}, User full name: {$a->name}';
$string['notifyloginfailuresmessageend'] = 'You can view these logs at {$a}';
$string['notifyloginfailuresmessagestart'] = 'Here is a list of failed login attempts at {$a} since you were last notified';
$string['notifyloginfailuressubject'] = '{$a} :: Failed logins notification';
$string['secretalreadyused'] = 'Change password confirmation link was already used, password was not changed.';
$string['secs'] = 'secs';
$string['section'] = 'Section';
+$string['sectionmenu'] = 'Section menu';
$string['sectionname'] = 'Section name';
$string['sections'] = 'Sections';
$string['sectionusedefaultname'] = 'Use default section name';
$string['unfinished'] = 'Unfinished';
$string['unknowncategory'] = 'Unknown category';
$string['unknownerror'] = 'Unknown error';
+$string['unknownuser'] = 'Unknown user';
$string['unlimited'] = 'Unlimited';
$string['unpacking'] = 'Unpacking {$a}';
$string['unsafepassword'] = 'Unsafe password - try something else';
$string['rootdir'] = 'Directory';
$string['settings'] = 'Settings';
$string['somehighlighted'] = 'Number of plugins requiring your attention: {$a}';
+$string['somehighlightedall'] = 'Number of installed plugins: {$a}';
$string['somehighlightedinfo'] = 'Display the full list of installed plugins';
$string['somehighlightedonly'] = 'Display only plugins requiring your attention';
$string['source'] = 'Source';
public function get_setting() {
global $CFG;
+ // First check if is not set.
+ $result = $this->config_read($this->name);
+ if (is_null($result)) {
+ return null;
+ }
+
// For install cli script, $CFG->defaultuserroleid is not set so return 0
// Or if web services aren't enabled this can't be,
if (empty($CFG->defaultuserroleid) || empty($CFG->enablewebservices)) {
$webservicemanager = new webservice();
$mobileservice = $webservicemanager->get_external_service_by_shortname(MOODLE_OFFICIAL_MOBILE_SERVICE);
if ($mobileservice->enabled and $this->is_protocol_cap_allowed()) {
- return $this->config_read($this->name); //same as returning 1
+ return $result;
} else {
return 0;
}
// Disable cache if debugging.
return false;
}
- if (typeof(window.localStorage) !== "undefined") {
- try {
- localStorage = window.localStorage;
- return localStorage !== null;
- } catch (ex) {
+ if (typeof(window.localStorage) === "undefined") {
+ return false;
+ }
+ var testKey = 'test';
+ try {
+ localStorage = window.localStorage;
+ if (localStorage === null) {
return false;
}
+ // MDL-51461 - Some browsers misreport availability of local storage
+ // so check it is actually usable.
+ localStorage.setItem(testKey, '1');
+ localStorage.removeItem(testKey);
+ return true;
+ } catch (ex) {
+ return false;
}
};
// Copy loglevel.js into lib/amd/src/ in Moodle folder.\r
// Add the license as a comment to the file and these instructions.\r
// Add the jshint ignore:start and ignore:end comments.\r
+// Delete the jshint validthis:true comments.\r
\r
/* jshint ignore:start */\r
-/*! loglevel - v1.2.0 - https://github.com/pimterry/loglevel - (c) 2014 Tim Perry - licensed MIT */\r
+/*! loglevel - v1.4.0 - https://github.com/pimterry/loglevel - (c) 2015 Tim Perry - licensed MIT */\r
(function (root, definition) {\r
+ "use strict";\r
if (typeof module === 'object' && module.exports && typeof require === 'function') {\r
module.exports = definition();\r
} else if (typeof define === 'function' && typeof define.amd === 'object') {\r
root.log = definition();\r
}\r
}(this, function () {\r
- var self = {};\r
+ "use strict";\r
var noop = function() {};\r
var undefinedType = "undefined";\r
\r
}\r
}\r
\r
- function enableLoggingWhenConsoleArrives(methodName, level) {\r
+ // these private functions always need `this` to be set properly\r
+\r
+ function enableLoggingWhenConsoleArrives(methodName, level, loggerName) {\r
return function () {\r
if (typeof console !== undefinedType) {\r
- replaceLoggingMethods(level);\r
- self[methodName].apply(self, arguments);\r
+ replaceLoggingMethods.call(this, level, loggerName);\r
+ this[methodName].apply(this, arguments);\r
}\r
};\r
}\r
\r
- var logMethods = [\r
- "trace",\r
- "debug",\r
- "info",\r
- "warn",\r
- "error"\r
- ];\r
-\r
- function replaceLoggingMethods(level) {\r
+ function replaceLoggingMethods(level, loggerName) {\r
for (var i = 0; i < logMethods.length; i++) {\r
var methodName = logMethods[i];\r
- self[methodName] = (i < level) ? noop : self.methodFactory(methodName, level);\r
+ this[methodName] = (i < level) ?\r
+ noop :\r
+ this.methodFactory(methodName, level, loggerName);\r
}\r
}\r
\r
- function persistLevelIfPossible(levelNum) {\r
- var levelName = (logMethods[levelNum] || 'silent').toUpperCase();\r
-\r
- // Use localStorage if available\r
- try {\r
- window.localStorage['loglevel'] = levelName;\r
- return;\r
- } catch (ignore) {}\r
-\r
- // Use session cookie as fallback\r
- try {\r
- window.document.cookie = "loglevel=" + levelName + ";";\r
- } catch (ignore) {}\r
+ function defaultMethodFactory(methodName, level, loggerName) {\r
+ return realMethod(methodName) ||\r
+ enableLoggingWhenConsoleArrives.apply(this, arguments);\r
}\r
\r
- function loadPersistedLevel() {\r
- var storedLevel;\r
-\r
- try {\r
- storedLevel = window.localStorage['loglevel'];\r
- } catch (ignore) {}\r
-\r
- if (typeof storedLevel === undefinedType) {\r
- try {\r
- storedLevel = /loglevel=([^;]+)/.exec(window.document.cookie)[1];\r
- } catch (ignore) {}\r
- }\r
- \r
- if (self.levels[storedLevel] === undefined) {\r
- storedLevel = "WARN";\r
- }\r
+ var logMethods = [\r
+ "trace",\r
+ "debug",\r
+ "info",\r
+ "warn",\r
+ "error"\r
+ ];\r
\r
- self.setLevel(self.levels[storedLevel]);\r
+ function Logger(name, defaultLevel, factory) {\r
+ var self = this;\r
+ var currentLevel;\r
+ var storageKey = "loglevel";\r
+ if (name) {\r
+ storageKey += ":" + name;\r
+ }\r
+\r
+ function persistLevelIfPossible(levelNum) {\r
+ var levelName = (logMethods[levelNum] || 'silent').toUpperCase();\r
+\r
+ // Use localStorage if available\r
+ try {\r
+ window.localStorage[storageKey] = levelName;\r
+ return;\r
+ } catch (ignore) {}\r
+\r
+ // Use session cookie as fallback\r
+ try {\r
+ window.document.cookie =\r
+ encodeURIComponent(storageKey) + "=" + levelName + ";";\r
+ } catch (ignore) {}\r
+ }\r
+\r
+ function getPersistedLevel() {\r
+ var storedLevel;\r
+\r
+ try {\r
+ storedLevel = window.localStorage[storageKey];\r
+ } catch (ignore) {}\r
+\r
+ if (typeof storedLevel === undefinedType) {\r
+ try {\r
+ var cookie = window.document.cookie;\r
+ var location = cookie.indexOf(\r
+ encodeURIComponent(storageKey) + "=");\r
+ if (location) {\r
+ storedLevel = /^([^;]+)/.exec(cookie.slice(location))[1];\r
+ }\r
+ } catch (ignore) {}\r
+ }\r
+\r
+ // If the stored level is not valid, treat it as if nothing was stored.\r
+ if (self.levels[storedLevel] === undefined) {\r
+ storedLevel = undefined;\r
+ }\r
+\r
+ return storedLevel;\r
+ }\r
+\r
+ /*\r
+ *\r
+ * Public API\r
+ *\r
+ */\r
+\r
+ self.levels = { "TRACE": 0, "DEBUG": 1, "INFO": 2, "WARN": 3,\r
+ "ERROR": 4, "SILENT": 5};\r
+\r
+ self.methodFactory = factory || defaultMethodFactory;\r
+\r
+ self.getLevel = function () {\r
+ return currentLevel;\r
+ };\r
+\r
+ self.setLevel = function (level, persist) {\r
+ if (typeof level === "string" && self.levels[level.toUpperCase()] !== undefined) {\r
+ level = self.levels[level.toUpperCase()];\r
+ }\r
+ if (typeof level === "number" && level >= 0 && level <= self.levels.SILENT) {\r
+ currentLevel = level;\r
+ if (persist !== false) { // defaults to true\r
+ persistLevelIfPossible(level);\r
+ }\r
+ replaceLoggingMethods.call(self, level, name);\r
+ if (typeof console === undefinedType && level < self.levels.SILENT) {\r
+ return "No console available for logging";\r
+ }\r
+ } else {\r
+ throw "log.setLevel() called with invalid level: " + level;\r
+ }\r
+ };\r
+\r
+ self.setDefaultLevel = function (level) {\r
+ if (!getPersistedLevel()) {\r
+ self.setLevel(level, false);\r
+ }\r
+ };\r
+\r
+ self.enableAll = function(persist) {\r
+ self.setLevel(self.levels.TRACE, persist);\r
+ };\r
+\r
+ self.disableAll = function(persist) {\r
+ self.setLevel(self.levels.SILENT, persist);\r
+ };\r
+\r
+ // Initialize with the right level\r
+ var initialLevel = getPersistedLevel();\r
+ if (initialLevel == null) {\r
+ initialLevel = defaultLevel == null ? "WARN" : defaultLevel;\r
+ }\r
+ self.setLevel(initialLevel, false);\r
}\r
\r
/*\r
*\r
- * Public API\r
+ * Package-level API\r
*\r
*/\r
\r
- self.levels = { "TRACE": 0, "DEBUG": 1, "INFO": 2, "WARN": 3,\r
- "ERROR": 4, "SILENT": 5};\r
+ var defaultLogger = new Logger();\r
\r
- self.methodFactory = function (methodName, level) {\r
- return realMethod(methodName) ||\r
- enableLoggingWhenConsoleArrives(methodName, level);\r
- };\r
-\r
- self.setLevel = function (level) {\r
- if (typeof level === "string" && self.levels[level.toUpperCase()] !== undefined) {\r
- level = self.levels[level.toUpperCase()];\r
- }\r
- if (typeof level === "number" && level >= 0 && level <= self.levels.SILENT) {\r
- persistLevelIfPossible(level);\r
- replaceLoggingMethods(level);\r
- if (typeof console === undefinedType && level < self.levels.SILENT) {\r
- return "No console available for logging";\r
- }\r
- } else {\r
- throw "log.setLevel() called with invalid level: " + level;\r
+ var _loggersByName = {};\r
+ defaultLogger.getLogger = function getLogger(name) {\r
+ if (typeof name !== "string" || name === "") {\r
+ throw new TypeError("You must supply a name when creating a logger.");\r
}\r
- };\r
-\r
- self.enableAll = function() {\r
- self.setLevel(self.levels.TRACE);\r
- };\r
\r
- self.disableAll = function() {\r
- self.setLevel(self.levels.SILENT);\r
+ var logger = _loggersByName[name];\r
+ if (!logger) {\r
+ logger = _loggersByName[name] = new Logger(\r
+ name, defaultLogger.getLevel(), defaultLogger.methodFactory);\r
+ }\r
+ return logger;\r
};\r
\r
// Grab the current global log variable in case of overwrite\r
var _log = (typeof window !== undefinedType) ? window.log : undefined;\r
- self.noConflict = function() {\r
+ defaultLogger.noConflict = function() {\r
if (typeof window !== undefinedType &&\r
- window.log === self) {\r
+ window.log === defaultLogger) {\r
window.log = _log;\r
}\r
\r
- return self;\r
+ return defaultLogger;\r
};\r
\r
- loadPersistedLevel();\r
- return self;\r
+ return defaultLogger;\r
}));\r
/* jshint ignore:end */\r
/*
* URL of backpack. Currently only the Open Badges backpack is supported.
*/
-define('BADGE_BACKPACKURL', 'backpack.openbadges.org');
+define('BADGE_BACKPACKURL', 'https://backpack.openbadges.org');
/**
* Class that represents badge.
'HEADER' => 0,
'CONNECTTIMEOUT' => 2,
);
- $location = 'http://' . BADGE_BACKPACKURL . '/baker';
+ $location = BADGE_BACKPACKURL . '/baker';
$out = $curl->get($location, array('assertion' => $fakeassertion->out(false)), $options);
$data = json_decode($out);
global $CFG, $PAGE;
if (!empty($CFG->badges_allowexternalbackpack)) {
$PAGE->requires->string_for_js('error:backpackproblem', 'badges');
- $protocol = (is_https()) ? 'https://' : 'http://';
- $PAGE->requires->js(new moodle_url($protocol . BADGE_BACKPACKURL . '/issuer.js'), true);
+ $PAGE->requires->js(new moodle_url(BADGE_BACKPACKURL . '/issuer.js'), true);
$PAGE->requires->js('/badges/backpack.js', true);
}
}
--- /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/>.
+
+/**
+ * Enrol instance created event.
+ *
+ * @package core
+ * @copyright 2015 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Enrol instance created event class.
+ *
+ * @property-read array $other {
+ * Extra information about event.
+ *
+ * - string enrol: name of enrol method
+ * }
+ *
+ * @package core
+ * @since Moodle 2.9
+ * @copyright 2015 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class enrol_instance_created extends base {
+
+ /**
+ * Api to Create new event from enrol object.
+ *
+ * @param \stdClass $enrol record from DB table 'enrol'
+ * @return \core\event\base returns instance of new event
+ */
+ public static final function create_from_record($enrol) {
+ $event = static::create(array(
+ 'context' => \context_course::instance($enrol->courseid),
+ 'objectid' => $enrol->id,
+ 'other' => array('enrol' => $enrol->enrol)
+ ));
+ return $event;
+ }
+
+ /**
+ * Returns description of what happened.
+ *
+ * @return string
+ */
+ public function get_description() {
+ return "The user with id '$this->userid' created the instance of enrolment method '" .
+ $this->other['enrol'] . "' with id '$this->objectid'.";
+ }
+
+ /**
+ * Return localised event name.
+ *
+ * @return string
+ */
+ public static function get_name() {
+ return get_string('eventenrolinstancecreated', 'enrol');
+ }
+
+ /**
+ * Get URL related to the action
+ *
+ * @return \moodle_url
+ */
+ public function get_url() {
+ return new \moodle_url('/enrol/instances.php', array('id' => $this->courseid));
+ }
+
+ /**
+ * Init method.
+ *
+ * @return void
+ */
+ protected function init() {
+ $this->data['crud'] = 'c';
+ $this->data['edulevel'] = self::LEVEL_OTHER;
+ $this->data['objecttable'] = 'enrol';
+ }
+
+ /**
+ * custom validations
+ *
+ * Throw \coding_exception notice in case of any problems.
+ */
+ protected function validate_data() {
+ parent::validate_data();
+ if (!isset($this->other['enrol'])) {
+ throw new \coding_exception('The \'enrol\' value must be set in other.');
+ }
+ }
+}
--- /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/>.
+
+/**
+ * Enrol instance deleted event.
+ *
+ * @package core
+ * @copyright 2015 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Enrol instance deleted event class.
+ *
+ * @property-read array $other {
+ * Extra information about event.
+ *
+ * - string enrol: name of enrol method
+ * }
+ *
+ * @package core
+ * @since Moodle 2.9
+ * @copyright 2015 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class enrol_instance_deleted extends base {
+
+ /**
+ * Api to Create new event from enrol object.
+ *
+ * @param \stdClass $enrol record from DB table 'enrol'
+ * @return \core\event\base returns instance of new event
+ */
+ public static final function create_from_record($enrol) {
+ $event = static::create(array(
+ 'context' => \context_course::instance($enrol->courseid),
+ 'objectid' => $enrol->id,
+ 'other' => array('enrol' => $enrol->enrol)
+ ));
+ $event->add_record_snapshot('enrol', $enrol);
+ return $event;
+ }
+
+ /**
+ * Returns description of what happened.
+ *
+ * @return string
+ */
+ public function get_description() {
+ return "The user with id '$this->userid' deleted the instance of enrolment method '" .
+ $this->other['enrol'] . "' with id '$this->objectid'.";
+ }
+
+ /**
+ * Return localised event name.
+ *
+ * @return string
+ */
+ public static function get_name() {
+ return get_string('eventgroupingdeleted', 'group');
+ }
+
+ /**
+ * Get URL related to the action
+ *
+ * @return \moodle_url
+ */
+ public function get_url() {
+ return new \moodle_url('/enrol/instances.php', array('id' => $this->courseid));
+ }
+
+ /**
+ * Init method.
+ *
+ * @return void
+ */
+ protected function init() {
+ $this->data['crud'] = 'd';
+ $this->data['edulevel'] = self::LEVEL_OTHER;
+ $this->data['objecttable'] = 'enrol';
+ }
+
+ /**
+ * custom validations
+ *
+ * Throw \coding_exception notice in case of any problems.
+ */
+ protected function validate_data() {
+ parent::validate_data();
+ if (!isset($this->other['enrol'])) {
+ throw new \coding_exception('The \'enrol\' value must be set in other.');
+ }
+ }
+}
--- /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/>.
+
+/**
+ * Enrol instance updated event.
+ *
+ * @package core
+ * @copyright 2015 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Enrol instance updated event class.
+ *
+ * @property-read array $other {
+ * Extra information about event.
+ *
+ * - string enrol: name of enrol method
+ * }
+ *
+ * @package core
+ * @since Moodle 2.9
+ * @copyright 2015 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class enrol_instance_updated extends base {
+
+ /**
+ * Api to Create new event from enrol object.
+ *
+ * @param \stdClass $enrol record from DB table 'enrol'
+ * @return \core\event\base returns instance of new event
+ */
+ public static final function create_from_record($enrol) {
+ $event = static::create(array(
+ 'context' => \context_course::instance($enrol->courseid),
+ 'objectid' => $enrol->id,
+ 'other' => array('enrol' => $enrol->enrol)
+ ));
+ $event->add_record_snapshot('enrol', $enrol);
+ return $event;
+ }
+
+ /**
+ * Returns description of what happened.
+ *
+ * @return string
+ */
+ public function get_description() {
+ return "The user with id '$this->userid' updated the instance of enrolment method '" .
+ $this->other['enrol'] . "' with id '$this->objectid'.";
+ }
+
+ /**
+ * Return localised event name.
+ *
+ * @return string
+ */
+ public static function get_name() {
+ return get_string('eventenrolinstanceupdated', 'enrol');
+ }
+
+ /**
+ * Get URL related to the action
+ *
+ * @return \moodle_url
+ */
+ public function get_url() {
+ return new \moodle_url('/enrol/instances.php', array('id' => $this->courseid));
+ }
+
+ /**
+ * Init method.
+ *
+ * @return void
+ */
+ protected function init() {
+ $this->data['crud'] = 'u';
+ $this->data['edulevel'] = self::LEVEL_OTHER;
+ $this->data['objecttable'] = 'enrol';
+ }
+
+ /**
+ * custom validations
+ *
+ * Throw \coding_exception notice in case of any problems.
+ */
+ protected function validate_data() {
+ parent::validate_data();
+ if (!isset($this->other['enrol'])) {
+ throw new \coding_exception('The \'enrol\' value must be set in other.');
+ }
+ }
+}
--- /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/>.
+
+/**
+ * Message deleted event.
+ *
+ * @package core
+ * @copyright 2015 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Message deleted event class.
+ *
+ * @property-read array $other {
+ * Extra information about event.
+ *
+ * - string $messagetable: the table we marked the message as deleted from (message/message_read).
+ * - int messageid: the id of the message.
+ * - int useridfrom: the id of the user who received the message.
+ * - int useridto: the id of the user who sent the message.
+ * }
+ *
+ * @package core
+ * @since Moodle 3.0
+ * @copyright 2015 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class message_deleted extends base {
+
+ /**
+ * Create event using ids.
+ *
+ * @param int $userfromid the user who the message was from.
+ * @param int $usertoid the user who the message was sent to.
+ * @param int $userdeleted the user who deleted it.
+ * @param string $messagetable the table we are marking the message as deleted in.
+ * @param int $messageid the id of the message that was deleted.
+ * @return message_deleted
+ */
+ public static function create_from_ids($userfromid, $usertoid, $userdeleted, $messagetable, $messageid) {
+ // Check who was deleting the message.
+ if ($userdeleted == $userfromid) {
+ $relateduserid = $usertoid;
+ } else {
+ $relateduserid = $userfromid;
+ }
+
+ // We set the userid to the user who deleted the message, nothing to do
+ // with whether or not they sent or received the message.
+ $event = self::create(array(
+ 'userid' => $userdeleted,
+ 'context' => \context_system::instance(),
+ 'relateduserid' =