<ON_CHECK message="unoconvwarning" />
</FEEDBACK>
</CUSTOM_CHECK>
- <CUSTOM_CHECK file="lib/upgradelib.php" function="check_tls_libraries" level="optional">
- <FEEDBACK>
- <ON_CHECK message="tlswarning" />
- </FEEDBACK>
- </CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
<FEEDBACK>
<ON_CHECK message="libcurlwarning" />
$primaryadminname = NULL;
}
$temp->add(new admin_setting_configtext('supportname', new lang_string('supportname', 'admin'),
- new lang_string('configsupportname', 'admin'), $primaryadminname, PARAM_NOTAGS));
+ new lang_string('configsupportname', 'admin'), $primaryadminname, PARAM_NOTAGS));
$setting = new admin_setting_configtext('supportemail', new lang_string('supportemail', 'admin'),
- new lang_string('configsupportemail', 'admin'), $primaryadminemail, PARAM_NOTAGS);
+ new lang_string('configsupportemail', 'admin'), $primaryadminemail, PARAM_EMAIL);
$setting->set_force_ltr(true);
$temp->add($setting);
$temp->add(new admin_setting_configtext('supportpage', new lang_string('supportpage', 'admin'), new lang_string('configsupportpage', 'admin'), '', PARAM_URL));
$temp->add(new admin_setting_heading('noreplydomainheading', new lang_string('noreplydomain', 'admin'),
new lang_string('noreplydomaindetail', 'admin')));
$temp->add(new admin_setting_configtext('noreplyaddress', new lang_string('noreplyaddress', 'admin'),
- new lang_string('confignoreplyaddress', 'admin'), 'noreply@' . get_host_from_url($CFG->wwwroot), PARAM_NOTAGS));
+ new lang_string('confignoreplyaddress', 'admin'), 'noreply@' . get_host_from_url($CFG->wwwroot), PARAM_EMAIL));
$temp->add(new admin_setting_configtextarea('allowedemaildomains',
new lang_string('allowedemaildomains', 'admin'),
new lang_string('configallowedemaildomains', 'admin'),
--- /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/>.
+
+/**
+ * Language import page.
+ *
+ * @package tool_langimport
+ * @copyright 2016 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_langimport\output;
+defined('MOODLE_INTERNAL') || die();
+
+use moodle_url;
+use renderable;
+use renderer_base;
+use stdClass;
+use templatable;
+
+/**
+ * Language import page class.
+ *
+ * @package tool_langimport
+ * @copyright 2016 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class langimport_page implements renderable, templatable {
+
+ /** @var array Array of currently installed languages. */
+ protected $installedlanguages;
+
+ /** @var array Array of languages that can be installed. */
+ protected $availablelanguages;
+
+ /** @var moodle_url The URL to be used for uninstalling the selected existing language packs. */
+ protected $uninstallurl;
+
+ /** @var moodle_url The URL to be used for updating the installed language packs. */
+ protected $updateurl;
+
+ /** @var moodle_url The URL to be used for installing the selected language packs to be installed. */
+ protected $installurl;
+
+
+ /**
+ * langimport_page constructor.
+ *
+ * @param array $installedlanguages Array of currently installed languages.
+ * @param array $availablelanguages Array of languages that can be installed.
+ * @param moodle_url $uninstallurl The URL to be used for uninstalling the selected existing language packs.
+ * @param moodle_url $updateurl The URL to be used for updating the installed language packs.
+ * @param moodle_url $installurl The URL to be used for installing the selected language packs to be installed.
+ */
+ public function __construct($installedlanguages, $availablelanguages, $uninstallurl, $updateurl, $installurl) {
+ $this->installedlanguages = $installedlanguages;
+ $this->availablelanguages = $availablelanguages;
+ $this->uninstallurl = $uninstallurl;
+ $this->updateurl = $updateurl;
+ $this->installurl = $installurl;
+ }
+
+ /**
+ * Export the data.
+ *
+ * @param renderer_base $output
+ * @return stdClass
+ */
+ public function export_for_template(renderer_base $output) {
+ $data = new stdClass();
+ $data->uninstallurl = $this->uninstallurl;
+ $data->sesskey = sesskey();
+
+ $data->installedoptions = [];
+ foreach ($this->installedlanguages as $code => $language) {
+ $option = new stdClass();
+ $option->value = $code;
+ $option->text = $language;
+ $data->installedoptions[] = $option;
+ }
+
+ $data->updateurl = $this->updateurl;
+
+ if (!empty($this->availablelanguages)) {
+ $data->toinstalloptions = [];
+ foreach ($this->availablelanguages as $code => $language) {
+ $option = new stdClass();
+ $option->value = $code;
+ $option->text = $language;
+ $data->toinstalloptions[] = $option;
+ }
+ $data->installurl = $this->installurl;
+ $data->caninstall = true;
+ }
+
+ return $data;
+ }
+}
--- /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/>.
+
+/**
+ * Renderers.
+ *
+ * @package tool_langimport
+ * @copyright 2016 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_langimport\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+use plugin_renderer_base;
+
+/**
+ * Renderer class.
+ *
+ * @package tool_langimport
+ * @copyright 2016 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends plugin_renderer_base {
+
+ /**
+ * Defer to template.
+ *
+ * @param langimport_page $page
+ * @return string
+ */
+ public function render_langimport_page(langimport_page $page) {
+ $data = $page->export_for_template($this);
+ return parent::render_from_template('tool_langimport/langimport', $data);
+ }
+}
} else {
$remote = false;
$availablelangs = array();
- echo $OUTPUT->box_start();
$a = [
'src' => $controller->lang_pack_url(),
'dest' => $CFG->dataroot.'/lang/',
];
- print_string('downloadnotavailable', 'tool_langimport', $a);
- echo $OUTPUT->box_end();
+ $errormessage = get_string('downloadnotavailable', 'tool_langimport', $a);
+ \core\notification::error($errormessage);
}
if ($controller->info) {
$info = implode('<br />', $controller->info);
- echo $OUTPUT->notification($info, 'notifysuccess');
+ \core\notification::success($info);
}
if ($controller->errors) {
$info = implode('<br />', $controller->errors);
- echo $OUTPUT->notification($info, 'notifyproblem');
+ \core\notification::error($info);
}
if ($missingparents) {
}
}
$info = get_string('missinglangparent', 'tool_langimport', $a);
- echo $OUTPUT->notification($info, 'notifyproblem');
+ \core\notification::error($info);
}
}
-echo $OUTPUT->box_start();
-
-echo html_writer::start_tag('table');
-echo html_writer::start_tag('tr');
-
-// list of installed languages
-$url = new moodle_url('/admin/tool/langimport/index.php', array('mode' => DELETION_OF_SELECTED_LANG));
-echo html_writer::start_tag('td', array('valign' => 'top'));
-echo html_writer::start_tag('form', array('id' => 'uninstallform', 'action' => $url->out(), 'method' => 'post'));
-echo html_writer::start_tag('fieldset');
-echo html_writer::label(get_string('installedlangs', 'tool_langimport'), 'menuuninstalllang');
-echo html_writer::empty_tag('br');
-echo html_writer::select($installedlangs, 'uninstalllang[]', '', false, array('size' => 15, 'multiple' => 'multiple'));
-echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
-echo html_writer::empty_tag('br');
-echo html_writer::empty_tag('input', array('id' => 'languninstallbutton',
- 'type' => 'submit',
- 'value' => get_string('uninstall', 'tool_langimport'))
- );
-echo html_writer::end_tag('fieldset');
-echo html_writer::end_tag('form');
+$uninstallurl = new moodle_url('/admin/tool/langimport/index.php', array('mode' => DELETION_OF_SELECTED_LANG));
+$updateurl = null;
if ($remote) {
- $url = new moodle_url('/admin/tool/langimport/index.php', array('mode' => UPDATE_ALL_LANG));
- echo html_writer::start_tag('form', array('id' => 'updateform', 'action' => $url->out(), 'method' => 'post'));
- echo html_writer::tag('fieldset', html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('updatelangs','tool_langimport'))));
- echo html_writer::end_tag('form');
+ $updateurl = new moodle_url('/admin/tool/langimport/index.php', array('mode' => UPDATE_ALL_LANG));
}
-echo html_writer::end_tag('td');
+$installurl = new moodle_url('/admin/tool/langimport/index.php', array('mode' => INSTALLATION_OF_SELECTED_LANG));
-// list of available languages
+// List of available languages.
$options = array();
foreach ($availablelangs as $alang) {
if (!empty($alang[0]) and trim($alang[0]) !== 'en' and !$controller->is_installed_lang($alang[0], $alang[1])) {
$options[$alang[0]] = $alang[2].' ‎('.$alang[0].')‎';
}
}
-if (!empty($options)) {
- echo html_writer::start_tag('td', array('valign' => 'top'));
- $url = new moodle_url('/admin/tool/langimport/index.php', array('mode' => INSTALLATION_OF_SELECTED_LANG));
- echo html_writer::start_tag('form', array('id' => 'installform', 'action' => $url->out(), 'method' => 'post'));
- echo html_writer::start_tag('fieldset');
- echo html_writer::label(get_string('availablelangs','install'), 'menupack');
- echo html_writer::empty_tag('br');
- echo html_writer::select($options, 'pack[]', '', false, array('size' => 15, 'multiple' => 'multiple'));
- echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
- echo html_writer::empty_tag('br');
- echo html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('install','tool_langimport')));
- echo html_writer::end_tag('fieldset');
- echo html_writer::end_tag('form');
- echo html_writer::end_tag('td');
-}
-echo html_writer::end_tag('tr');
-echo html_writer::end_tag('table');
-echo $OUTPUT->box_end();
+$renderable = new \tool_langimport\output\langimport_page($installedlangs, $options, $uninstallurl, $updateurl, $installurl);
+$output = $PAGE->get_renderer('tool_langimport');
+echo $output->render($renderable);
-$uninstallurl = new moodle_url('/admin/tool/langimport/index.php');
$PAGE->requires->strings_for_js(array('uninstallconfirm', 'uninstall', 'selectlangs', 'noenglishuninstall'),
'tool_langimport');
$PAGE->requires->yui_module('moodle-core-languninstallconfirm',
array(array('uninstallUrl' => $uninstallurl->out()))
);
echo $OUTPUT->footer();
-die();
-#page-admin-tool-langimport-index .generalbox table {
+#page-admin-tool-langimport-index .langimport {
margin: auto;
+ float: none;
width: 100%;
}
-
-#page-admin-tool-langimport-index .generalbox,
-#page-admin-tool-langimport-index .generalbox table {
- text-align: center;
-}
--- /dev/null
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+ @template tool_langimport/langimport
+
+ Template for the language import page.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Context variables required for this template:
+ * sesskey string The session key.
+ * uninstallurl string The URL for the uninstall action.
+ * updateurl string The URL for the update-language-packs action.
+ * installurl string The URL for the install action.
+ * installedoptions array The list of languages installed.
+ * toinstalloptions array The list of languages to be installed.
+ * caninstall boolean Flag to indicate if there are language packs that can be installed.
+
+ Example context (json):
+ {
+ "sesskey": "sesskey",
+ "uninstallurl": "#",
+ "updateurl": "#",
+ "installurl": "#",
+ "installedoptions": [
+ {
+ "value": "en",
+ "text": "English",
+ "selected": true
+ }
+ ],
+ "toinstalloptions": [
+ {
+ "value": "ja",
+ "text": "Japanese"
+ },
+ {
+ "value": "fr",
+ "text": "French"
+ },
+ {
+ "value": "es",
+ "text": "Spanish"
+ }
+ ],
+ "caninstall": true
+ }
+}}
+<div class="container-fluid langimport">
+ <div class="row row-fluid rtl-compatible">
+ <div class="col-md-{{#caninstall}}6{{/caninstall}}{{^caninstall}}12{{/caninstall}} span{{#caninstall}}6{{/caninstall}}{{^caninstall}}12{{/caninstall}} m-b-1">
+ <form id="uninstallform" action="{{uninstallurl}}" method="post">
+ <fieldset>
+ <div class="form-group">
+ <label for="menuuninstalllang">{{#str}}installedlangs, tool_langimport{{/str}}</label>
+ <select size="15" multiple="multiple" id="menuuninstalllang" class="form-control input-block-level" name="uninstalllang[]">
+ {{#installedoptions}}
+ <option value="{{value}}" {{#selected}}selected="selected"{{/selected}}>{{{text}}}</option>
+ {{/installedoptions}}
+ </select>
+ </div>
+ <div class="form-group">
+ <input type="hidden" name="sesskey" value="{{sesskey}}">
+ <input id="languninstallbutton" type="submit" value="{{#str}}uninstall, tool_langimport{{/str}}" class="btn btn-default">
+ </div>
+ </fieldset>
+ </form>
+ {{#updateurl}}
+ <div>
+ <form id="updateform" action="{{updateurl}}" method="post">
+ <fieldset>
+ <input type="submit" value="{{#str}}updatelangs, tool_langimport{{/str}}" class="btn btn-default">
+ </fieldset>
+ </form>
+ </div>
+ {{/updateurl}}
+ </div>
+ {{#caninstall}}
+ <div class="col-md-6 span6 m-b-1">
+ <form id="installform" action="{{installurl}}" method="post">
+ <fieldset>
+ <div class="form-group">
+ <label for="menupack">{{#str}}availablelangs, install{{/str}}</label>
+ <select size="15" multiple="multiple" class="form-control input-block-level" id="menupack" name="pack[]">
+ {{#toinstalloptions}}
+ <option value="{{value}}" {{#selected}}selected="selected"{{/selected}}>{{{text}}}</option>
+ {{/toinstalloptions}}
+ </select>
+ </div>
+ <div class="form-group">
+ <input type="hidden" name="sesskey" value="{{sesskey}}">
+ <input type="submit" value="{{#str}}install, tool_langimport{{/str}}" class="btn btn-default">
+ </div>
+ </fieldset>
+ </form>
+ </div>
+ {{/caninstall}}
+ </div>
+</div>
protected $importer = null;
protected $foundheaders = array();
protected $scalecache = array();
+ /** @var bool $useprogressbar Control whether importing should use progress bars or not. */
+ protected $useprogressbar = false;
+ /** @var \core\progress\display_if_slow|null $progress The progress bar instance. */
+ protected $progress = null;
/**
* Store an error message for display later
* @param string delimiter The specified delimiter for the file.
* @param string importid The id of the csv import.
* @param array mappingdata The mapping data from the import form.
+ * @param bool $useprogressbar Whether progress bar should be displayed, to avoid html output on CLI.
*/
- public function __construct($text = null, $encoding = null, $delimiter = null, $importid = 0, $mappingdata = null) {
+ public function __construct($text = null, $encoding = null, $delimiter = null, $importid = 0, $mappingdata = null,
+ $useprogressbar = false) {
+
global $CFG;
// The format of our records is:
}
$this->foundheaders = $this->importer->get_columns();
-
+ $this->useprogressbar = $useprogressbar;
$domainid = 1;
$flat = array();
$this->fail(get_string('invalidimportfile', 'tool_lpimportcsv'));
return;
} else {
+ // We are calling from browser, display progress bar.
+ if ($this->useprogressbar === true) {
+ $this->progress = new \core\progress\display_if_slow(get_string('processingfile', 'tool_lpimportcsv'));
+ $this->progress->start_html();
+ } else {
+ // Avoid html output on CLI scripts.
+ $this->progress = new \core\progress\none();
+ }
+ $this->progress->start_progress('', count($this->flat));
// Build a tree from this flat list.
+ raise_memory_limit(MEMORY_EXTRA);
$this->add_children($this->framework, '');
+ $this->progress->end_progress();
}
}
public function add_children(& $node, $parentidnumber) {
foreach ($this->flat as $competency) {
if ($competency->parentidnumber == $parentidnumber) {
+ $this->progress->increment_progress();
$node->children[] = $competency;
$this->add_children($competency, $competency->idnumber);
}
$record->contextid = context_system::instance()->id;
$framework = api::create_framework($record);
+ if ($this->useprogressbar === true) {
+ $this->progress = new \core\progress\display_if_slow(get_string('importingfile', 'tool_lpimportcsv'));
+ $this->progress->start_html();
+ } else {
+ $this->progress = new \core\progress\none();
+ }
+ $this->progress->start_progress('', (count($this->framework->children) * 2));
+ raise_memory_limit(MEMORY_EXTRA);
// Now all the children.
foreach ($this->framework->children as $comp) {
+ $this->progress->increment_progress();
$this->create_competency($comp, null, $framework);
}
// Now create the rules.
foreach ($this->framework->children as $record) {
+ $this->progress->increment_progress();
$this->set_rules($record);
$this->set_related($record);
}
+ $this->progress->end_progress();
$this->importer->cleanup();
return $framework;
+++ /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/>.
-
-/**
- * Page to continue after an action.
- *
- * @package tool_lpimportcsv
- * @copyright 2015 Damyon Wiese
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-require_once(__DIR__ . '/../../../config.php');
-require_once($CFG->libdir.'/adminlib.php');
-
-$pagetitle = get_string('pluginname', 'tool_lpimportcsv');
-
-$context = context_system::instance();
-
-$id = required_param('id', PARAM_INT);
-$url = new moodle_url("/admin/tool/lpimportcsv/index.php");
-$PAGE->set_context($context);
-$PAGE->set_url($url);
-$PAGE->set_title($pagetitle);
-$PAGE->set_pagelayout('admin');
-$PAGE->set_heading($pagetitle);
-
-echo $OUTPUT->header();
-echo $OUTPUT->heading($pagetitle);
-$urlparams = ['competencyframeworkid' => $id, 'pagecontextid' => $context->id];
-$frameworksurl = new moodle_url('/admin/tool/lp/competencies.php', $urlparams);
-echo $OUTPUT->notification(get_string('competencyframeworkcreated', 'tool_lp'), 'notifysuccess');
-echo $OUTPUT->continue_button($frameworksurl);
-
-echo $OUTPUT->footer();
$PAGE->set_heading($pagetitle);
$form = null;
+echo $OUTPUT->header();
if (optional_param('needsconfirm', 0, PARAM_BOOL)) {
$form = new \tool_lpimportcsv\form\import($url->out(false));
} else if (optional_param('confirm', 0, PARAM_BOOL)) {
if ($data->confirm) {
$importid = $data->importid;
- $importer = new \tool_lpimportcsv\framework_importer(null, null, null, $importid, $data);
+ $importer = new \tool_lpimportcsv\framework_importer(null, null, null, $importid, $data, true);
$error = $importer->get_error();
if ($error) {
$form->set_import_error($error);
} else {
$framework = $importer->import();
- redirect(new moodle_url('continue.php', array('id' => $framework->get_id())));
+ $urlparams = ['competencyframeworkid' => $framework->get_id(), 'pagecontextid' => $context->id];
+ $frameworksurl = new moodle_url('/admin/tool/lp/competencies.php', $urlparams);
+ echo $OUTPUT->notification(get_string('competencyframeworkcreated', 'tool_lp'), 'notifysuccess');
+ echo $OUTPUT->continue_button($frameworksurl);
die();
}
} else {
$text = $form->get_file_content('importfile');
$encoding = $data->encoding;
$delimiter = $data->delimiter_name;
- $importer = new \tool_lpimportcsv\framework_importer($text, $encoding, $delimiter);
+ $importer = new \tool_lpimportcsv\framework_importer($text, $encoding, $delimiter, 0, null, true);
$confirmform = new \tool_lpimportcsv\form\import_confirm(null, $importer);
$form = $confirmform;
$pagetitle = get_string('confirmcolumnmappings', 'tool_lpimportcsv');
}
}
-echo $OUTPUT->header();
echo $OUTPUT->heading($pagetitle);
$form->display();
$string['importfile_help'] = 'A competency framework may be imported via text file. The format of the file can be determined by creating a new competency framework on the site and then exporting it.';
$string['importfile_link'] = 'admin/tool/lpimportcsv';
$string['import'] = 'Import';
+$string['importingfile'] = 'Importing file data';
$string['invalidimportfile'] = 'File format is invalid.';
$string['isframework'] = 'Is framework';
$string['noframeworks'] = 'No competency frameworks have been created yet';
$string['parentidnumber'] = 'Parent ID number';
$string['pluginname'] = 'Import competency framework';
+$string['processingfile'] = 'Processing file';
$string['relatedidnumbers'] = 'Cross-referenced competency ID numbers';
$string['ruleconfig'] = 'Rule config (optional)';
$string['ruleoutcome'] = 'Rule outcome (optional)';
'enablemobilewebservice' => $CFG->enablemobilewebservice,
'maintenanceenabled' => $CFG->maintenance_enabled,
'maintenancemessage' => $maintenancemessage,
+ 'mobilecssurl' => !empty($CFG->mobilecssurl) ? $CFG->mobilecssurl : '',
);
$typeoflogin = get_config('tool_mobile', 'typeoflogin');
'compactlogourl' => new external_value(PARAM_URL, 'The site compact logo URL', VALUE_OPTIONAL),
'typeoflogin' => new external_value(PARAM_INT, 'The type of login. 1 for app, 2 for browser, 3 for embedded.'),
'launchurl' => new external_value(PARAM_URL, 'SSO login launch URL. Empty if it won\'t be used.', VALUE_OPTIONAL),
+ 'mobilecssurl' => new external_value(PARAM_URL, 'Mobile custom CSS theme', VALUE_OPTIONAL),
'warnings' => new external_warnings(),
)
);
'maintenanceenabled' => $CFG->maintenance_enabled,
'maintenancemessage' => $maintenancemessage,
'typeoflogin' => api::LOGIN_VIA_APP,
+ 'mobilecssurl' => '',
'warnings' => array()
);
$this->assertEquals($expected, $result);
--- /dev/null
+This files describes changes in tool_mobile code.
+Information provided here is intended especially for developers.
+
+=== 3.3 ===
+
+ * External function tool_mobile::get_public_config now returns the mobilecssurl field (Mobile custom CSS theme).
+
$string['encoding'] = 'Encoding';
$string['errormnetadd'] = 'Can not add remote users';
$string['errors'] = 'Errors';
-$string['invalidupdatetype'] = 'You can not select this option with the chosen \'Upload type\'';
+$string['invalidupdatetype'] = 'This option cannot be selected with the chosen upload type.';
$string['invaliduserdata'] = 'Invalid data detected for user {$a} and it has been automatically cleaned.';
$string['nochanges'] = 'No changes';
$string['pluginname'] = 'User upload';
$string['tour1_title_customisation'] = 'Customisation';
$string['tour1_content_customisation'] = 'To customise the look of your site and the front page, use the settings menu in the corner of this header. Try turning editing on right now.';
$string['tour1_title_blockregion'] = 'Block region';
-$string['tour1_content_blockregion'] = 'There is still a block region over here. We recommend removing the Navigation and Settings blocks completely, as all the functionality is elsewhere in the Boost theme.';
+$string['tour1_content_blockregion'] = 'There is still a block region over here. We recommend removing the Navigation and Administration blocks completely, as all the functionality is elsewhere in the Boost theme.';
$string['tour1_title_addingblocks'] = 'Adding blocks';
-$string['tour1_content_addingblocks'] = 'In fact, think carefully about including any blocks on your pages. Blocks are not shown on the Moodle mobile app, so as a general rule it\'s much better to make sure your site works well without any blocks.';
+$string['tour1_content_addingblocks'] = 'In fact, think carefully about including any blocks on your pages. Blocks are not shown on the Moodle Mobile app, so as a general rule it\'s much better to make sure your site works well without any blocks.';
$string['tour1_title_end'] = 'End of tour';
$string['tour1_content_end'] = 'This has been a user tour, a new feature in Moodle 3.2. It won\'t show again unless you reset it using the link in the footer. As an admin you can also create your own tours like this!';
$string['tour2_title_customisation'] = 'Customisation';
$string['tour2_content_customisation'] = 'To change any course settings, use the settings menu in the corner of this header. You will find a similar settings menu on the home page of every activity, too. Try turning editing on right now.';
$string['tour2_title_navigation'] = 'Navigation';
-$string['tour2_content_navigation'] = 'Major navigation is now though this nav drawer. Use the button at the top to hide or show it. You will see that there are links for major sections of your course.';
+$string['tour2_content_navigation'] = 'Navigation is now through this nav drawer. Use the button at the top to hide or show it. You will see that there are links for sections of your course.';
$string['tour2_title_opendrawer'] = 'Open the nav drawer';
$string['tour2_content_opendrawer'] = 'Try opening the nav drawer now.';
$string['tour2_title_participants'] = 'Course participants';
$string['tour2_content_participants'] = 'View participants here. This is also where you go to add or remove students.';
$string['tour2_title_addblock'] = 'Add a block';
-$string['tour2_content_addblock'] = 'If you enable editing mode you can add blocks from the nav drawer. However, think carefully about including any blocks on your pages. Blocks are not shown on Moodle mobile app, so for the best student experience it is much better to make sure your course works well without any blocks.';
+$string['tour2_content_addblock'] = 'If you turn editing on you can add blocks from the nav drawer. However, think carefully about including any blocks on your pages. Blocks are not shown on the Moodle Mobile app, so for the best user experience it is better to make sure your course works well without any blocks.';
$string['tour2_title_addingblocks'] = 'Adding blocks';
-$string['tour2_content_addingblocks'] = 'You can add blocks to this page using this button. However, think carefully about including any blocks on your pages. Blocks are not shown on Moodle mobile app, so for the best student experience it is much better to make sure your course works well without any blocks.';
+$string['tour2_content_addingblocks'] = 'You can add blocks to this page using this button. However, think carefully about including any blocks on your pages. Blocks are not shown on the Moodle Mobile app, so for the best user experience it is better to make sure your course works well without any blocks.';
$string['tour2_title_end'] = 'End of tour';
$string['tour2_content_end'] = 'This has been a user tour, a new feature in Moodle 3.2. It won\'t show again unless you reset it using the link in the footer. The site admin can also create further tours for this site if required.';
$data->rememberusername = $this->rememberusername;
$data->passwordautocomplete = $this->passwordautocomplete;
$data->signupurl = $this->signupurl->out(false);
+ $data->username = $this->username;
return $data;
}
--- /dev/null
+@core @core_auth
+Feature: Test the 'remember username' feature works.
+ In order to see my saved username on the login form
+ As a user
+ I need to have logged in once before and clicked 'Remember username'
+
+ Background:
+ Given the following "users" exist:
+ | username |
+ | teacher1 |
+
+ # Given the user has logged in and selected 'Remember username', when they log in again, then their username should be remembered.
+ Scenario: Check that 'remember username' works without javascript for teachers.
+ # Log in the first time and check the 'remember username' box.
+ Given I am on homepage
+ And I click on "Log in" "link" in the ".logininfo" "css_element"
+ And I set the field "Username" to "teacher1"
+ And I set the field "Password" to "teacher1"
+ And I set the field "Remember username" to "1"
+ And I press "Log in"
+ And I log out
+ # Log out and check that the username was remembered.
+ When I am on homepage
+ And I click on "Log in" "link" in the ".logininfo" "css_element"
+ Then the field "username" matches value "teacher1"
+ And the field "Remember username" matches value "1"
+
+ # Given the user has logged in before and selected 'Remember username', when they log in again and unset 'Remember username', then
+ # their username should be forgotten for future log in attempts.
+ Scenario: Check that 'remember username' unsetting works without javascript for teachers.
+ # Log in the first time and check the 'remember username' box.
+ Given I am on homepage
+ And I click on "Log in" "link" in the ".logininfo" "css_element"
+ And I set the field "Username" to "teacher1"
+ And I set the field "Password" to "teacher1"
+ And I set the field "Remember username" to "1"
+ And I press "Log in"
+ And I log out
+ # Log in again, unsetting the 'remember username' field.
+ When I am on homepage
+ And I click on "Log in" "link" in the ".logininfo" "css_element"
+ And I set the field "Password" to "teacher1"
+ And I set the field "Remember username" to "0"
+ And I press "Log in"
+ And I log out
+ # Check username has been forgotten.
+ Then I am on homepage
+ And I click on "Log in" "link" in the ".logininfo" "css_element"
+ Then the field "username" matches value ""
+ And the field "Remember username" matches value "0"
+++ /dev/null
-#fitem_id_availabilityconditionsjson .availability_grade input[type=text] {
- width: 3em;
-}
The information here is intended only for developers.
+=== 3.2 ===
+
+* Condition plugins must replace the CSS selector "#fitem_id_availabilityconditionsjson" with ".availability-field".
+ This selector is often used in your plugin's yui/src/form/js/form.js file.
+
=== 2.9 ===
* Condition plugins can now implement a new include_after_restore function to
$title = get_string('backpackdetails', 'badges');
$PAGE->set_title($title);
$PAGE->set_heading(fullname($USER));
-$PAGE->set_pagelayout('mydashboard');
+$PAGE->set_pagelayout('standard');
$backpack = $DB->get_record('badge_backpack', array('userid' => $USER->id));
$badgescache = cache::make('core', 'externalbadges');
$title = get_string('badges', 'badges');
$PAGE->set_title($title);
$PAGE->set_heading(fullname($USER));
-$PAGE->set_pagelayout('mydashboard');
+$PAGE->set_pagelayout('standard');
// Include JS files for backpack support.
badges_setup_backpack_js();
And I create a course with:
| Course full name | Course 1 |
| Course short name | C1 |
- | News items to show | 5 |
+ | Number of announcements | 5 |
And I enrol "Teacher 1" user as "Teacher"
And I log out
And I log in as "teacher1"
And I should see "Discussion Three" in the "Latest announcements" "block"
And I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
- | News items to show | 2 |
+ | Number of announcements | 2 |
And I press "Save and display"
And I should not see "Discussion One" in the "Latest announcements" "block"
And I should see "Discussion Two" in the "Latest announcements" "block"
And I should see "Discussion Three" in the "Latest announcements" "block"
And I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
- | News items to show | 0 |
+ | Number of announcements | 0 |
And I press "Save and display"
And "Latest announcements" "block" should not exist
$this->definition = $definition;
$this->store = $store;
$this->storetype = get_class($store);
- $this->perfdebug = !empty($CFG->perfdebug);
+ $this->perfdebug = (!empty($CFG->perfdebug) and $CFG->perfdebug > 7);
if ($loader instanceof cache_loader) {
$this->loader = $loader;
// Mark the loader as a sub (chained) loader.
$this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['sets'] -
$startstats[$requestid]['stores']['cachestore_static']['sets']);
}
+
+ public function test_performance_debug_off() {
+ global $CFG;
+ $this->resetAfterTest(true);
+ $CFG->perfdebug = 7;
+
+ $instance = cache_config_testing::instance();
+ $applicationid = 'phpunit/applicationperfoff';
+ $instance->phpunit_add_definition($applicationid, array(
+ 'mode' => cache_store::MODE_APPLICATION,
+ 'component' => 'phpunit',
+ 'area' => 'applicationperfoff'
+ ));
+ $sessionid = 'phpunit/sessionperfoff';
+ $instance->phpunit_add_definition($sessionid, array(
+ 'mode' => cache_store::MODE_SESSION,
+ 'component' => 'phpunit',
+ 'area' => 'sessionperfoff'
+ ));
+ $requestid = 'phpunit/requestperfoff';
+ $instance->phpunit_add_definition($requestid, array(
+ 'mode' => cache_store::MODE_REQUEST,
+ 'component' => 'phpunit',
+ 'area' => 'requestperfoff'
+ ));
+
+ $application = cache::make('phpunit', 'applicationperfoff');
+ $session = cache::make('phpunit', 'sessionperfoff');
+ $request = cache::make('phpunit', 'requestperfoff');
+
+ // Check that no stats are recorded for these definitions yet.
+ $stats = cache_helper::get_stats();
+ $this->assertArrayNotHasKey($applicationid, $stats);
+ $this->assertArrayNotHasKey($sessionid, $stats);
+ $this->assertArrayNotHasKey($requestid, $stats);
+
+ // Trigger cache misses, cache sets and cache hits.
+ $this->assertFalse($application->get('missMe'));
+ $this->assertTrue($application->set('setMe', 1));
+ $this->assertEquals(1, $application->get('setMe'));
+ $this->assertFalse($session->get('missMe'));
+ $this->assertTrue($session->set('setMe', 3));
+ $this->assertEquals(3, $session->get('setMe'));
+ $this->assertFalse($request->get('missMe'));
+ $this->assertTrue($request->set('setMe', 4));
+ $this->assertEquals(4, $request->get('setMe'));
+
+ // Check that no stats are being recorded for these definitions.
+ $endstats = cache_helper::get_stats();
+ $this->assertArrayNotHasKey($applicationid, $endstats);
+ $this->assertArrayNotHasKey($sessionid, $endstats);
+ $this->assertArrayNotHasKey($requestid, $endstats);
+ }
}
} else {
// Assemble pollinterval control.
$html .= html_writer::start_tag('div', array('style' => 'float:left;'));
- $html .= html_writer::start_tag('select', array('name' => 'pollinterval'));
+ $html .= html_writer::start_tag('select', array('name' => 'pollinterval', 'class' => 'custom-select'));
foreach (calendar_get_pollinterval_choices() as $k => $v) {
$attributes = array();
if ($k == $subscription->pollinterval) {
$html .= html_writer::end_tag('select');
$html .= html_writer::end_tag('div');
}
- $html .= html_writer::start_tag('div', array('style' => 'float:right;'));
$html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
$html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'course', 'value' => $courseid));
$html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'id', 'value' => $subscription->id));
+ $html .= html_writer::start_tag('div', array('class' => 'btn-group pull-right'));
if (!empty($subscription->url)) {
$html .= html_writer::tag('button', get_string('update'), array('type' => 'submit', 'name' => 'action',
+ 'class' => 'btn btn-secondary',
'value' => CALENDAR_SUBSCRIPTION_UPDATE));
}
$html .= html_writer::tag('button', get_string('remove'), array('type' => 'submit', 'name' => 'action',
+ 'class' => 'btn btn-secondary',
'value' => CALENDAR_SUBSCRIPTION_REMOVE));
$html .= html_writer::end_tag('div');
$html .= html_writer::end_tag('form');
public function get_icon($alt, array $attributes = null) {
return new pix_icon('i/calendar', $alt, 'moodle', $attributes);
}
+
+ /**
+ * Shift the date when resetting course.
+ *
+ * @param int $courseid the course id
+ * @param int $timeshift number of seconds to shift date
+ * @return boolean was the operation successful?
+ */
+ public static function update_date($courseid, $timeshift) {
+ if ($criteria = self::fetch(array('course' => $courseid))) {
+ $criteria->timeend = $criteria->timeend + $timeshift;
+ $criteria->update();
+ }
+ }
}
resel.li.outerHTML = unescape(resel.li.outerHTML);
}
self.add_editing(result.elementid);
+ // Fire the content updated event.
+ require(['core/event', 'jquery'], function(event, $) {
+ event.notifyFilterContentUpdated($(result.fullcontent));
+ });
} else {
// Error - remove the dummy element
resel.parent.removeChild(resel.li);
}
}
- // Check category depth is <= maxdepth (do not check for user who can manage categories).
- if ((!empty($CFG->maxcategorydepth) && count($parents) > $CFG->maxcategorydepth)
- and !has_capability('moodle/category:manage', $context)) {
- $excludedcats[$category->id] = 'depth';
- }
-
// Check the user can use the category context.
$context = context_coursecat::instance($category->id);
try {
'timemodified' => new external_value(PARAM_INT, 'Last time the course was updated', VALUE_OPTIONAL),
'requested' => new external_value(PARAM_INT, 'If is a requested course', VALUE_OPTIONAL),
'cacherev' => new external_value(PARAM_INT, 'Cache revision number', VALUE_OPTIONAL),
+ 'filters' => new external_multiple_structure(
+ new external_single_structure(
+ array(
+ 'filter' => new external_value(PARAM_PLUGIN, 'Filter plugin name'),
+ 'localstate' => new external_value(PARAM_INT, 'Filter state: 1 for on, -1 for off, 0 if inherit'),
+ 'inheritedstate' => new external_value(PARAM_INT, '1 or 0 to use when localstate is set to inherit'),
+ )
+ ),
+ 'Course filters', VALUE_OPTIONAL
+ ),
);
$coursestructure = array_merge($coursestructure, $extra);
}
public static function get_courses_by_field($field = '', $value = '') {
global $DB, $CFG;
require_once($CFG->libdir . '/coursecatlib.php');
+ require_once($CFG->libdir . '/filterlib.php');
$params = self::validate_parameters(self::get_courses_by_field_parameters(),
array(
'groupmode', 'groupmodeforce', 'defaultgroupingid', 'enablecompletion', 'completionnotify', 'lang', 'theme',
'sortorder', 'marker');
+ // Course filters.
+ $coursesdata[$course->id]['filters'] = filter_get_available_in_context($context);
+
// Information for managers only.
if ($canupdatecourse) {
$managerfields = array('idnumber', 'legacyfiles', 'calendartype', 'timecreated', 'timemodified', 'requested',
)
);
}
+
+ /**
+ * Returns description of method parameters
+ *
+ * @return external_function_parameters
+ * @since Moodle 3.3
+ */
+ public static function get_updates_since_parameters() {
+ return new external_function_parameters(
+ array(
+ 'courseid' => new external_value(PARAM_INT, 'Course id to check'),
+ 'since' => new external_value(PARAM_INT, 'Check updates since this time stamp'),
+ 'filter' => new external_multiple_structure(
+ new external_value(PARAM_ALPHANUM, 'Area name: configuration, fileareas, completion, ratings, comments,
+ gradeitems, outcomes'),
+ 'Check only for updates in these areas', VALUE_DEFAULT, array()
+ )
+ )
+ );
+ }
+
+ /**
+ * Check if there are updates affecting the user for the given course since the given time stamp.
+ *
+ * This function is a wrapper of self::check_updates for retrieving all the updates since a given time for all the activities.
+ *
+ * @param int $courseid the list of modules to check
+ * @param int $since check updates since this time stamp
+ * @param array $filter check only for updates in these areas
+ * @return array list of updates and warnings
+ * @throws moodle_exception
+ * @since Moodle 3.3
+ */
+ public static function get_updates_since($courseid, $since, $filter = array()) {
+ global $CFG, $DB;
+
+ $params = self::validate_parameters(
+ self::get_updates_since_parameters(),
+ array(
+ 'courseid' => $courseid,
+ 'since' => $since,
+ 'filter' => $filter,
+ )
+ );
+
+ $course = get_course($params['courseid']);
+ $modinfo = get_fast_modinfo($course);
+ $tocheck = array();
+
+ // Retrieve all the visible course modules for the current user.
+ $cms = $modinfo->get_cms();
+ foreach ($cms as $cm) {
+ if (!$cm->uservisible) {
+ continue;
+ }
+ $tocheck[] = array(
+ 'id' => $cm->id,
+ 'contextlevel' => 'module',
+ 'since' => $params['since'],
+ );
+ }
+
+ return self::check_updates($course->id, $tocheck, $params['filter']);
+ }
+
+ /**
+ * Returns description of method result value
+ *
+ * @return external_description
+ * @since Moodle 3.3
+ */
+ public static function get_updates_since_returns() {
+ return self::check_updates_returns();
+ }
}
// Create the course with sections.
$course = $this->getDataGenerator()->create_course(array('numsections' => 10), array('createsections' => true));
- $sections = $DB->get_records('course_sections', array('course' => $course->id));
+ $sections = $DB->get_records('course_sections', array('course' => $course->id), 'section');
$coursecontext = context_course::instance($course->id);
$section = array_pop($sections);
course_delete_section($course, $section);
* @param int $resultingenddate
*/
public function test_course_dates_reset($startdate, $enddate, $resetstartdate, $resetenddate, $resultingstartdate, $resultingenddate) {
- global $DB;
+ global $CFG, $DB;
+
+ require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php');
$this->resetAfterTest(true);
+ $CFG->enablecompletion = true;
+
$this->setTimezone('UTC');
- $record = array('startdate' => $startdate, 'enddate' => $enddate);
+ $record = array('startdate' => $startdate, 'enddate' => $enddate, 'enablecompletion' => 1);
$originalcourse = $this->getDataGenerator()->create_course($record);
+ $coursecriteria = new completion_criteria_date(array('course' => $originalcourse->id, 'timeend' => $startdate + DAYSECS));
+ $coursecriteria->insert();
+
+ $activitycompletiondate = $startdate + DAYSECS;
+ $data = $this->getDataGenerator()->create_module('data', array('course' => $originalcourse->id),
+ array('completion' => 1, 'completionexpected' => $activitycompletiondate));
$resetdata = new stdClass();
$resetdata->id = $originalcourse->id;
$this->assertEquals($resultingstartdate, $course->startdate);
$this->assertEquals($resultingenddate, $course->enddate);
+
+ $coursecompletioncriteria = completion_criteria_date::fetch(array('course' => $originalcourse->id));
+ $this->assertEquals($resultingstartdate + DAYSECS, $coursecompletioncriteria->timeend);
+
+ $this->assertEquals($resultingstartdate + DAYSECS, $DB->get_field('course_modules', 'completionexpected',
+ array('id' => $data->cmid)));
}
/**
$this->assertEquals($DB->count_records('course_categories'), count($categories));
- // Call without required capability (it will fail cause of the search on idnumber).
$this->unassignUserCapability('moodle/category:manage', $context->id, $roleid);
+
+ // Ensure maxdepthcategory is 2 and retrieve all categories without category:manage capability. It should retrieve all
+ // visible categories as well.
+ set_config('maxcategorydepth', 2);
+ $categories = core_course_external::get_categories();
+
+ // We need to execute the return values cleaning process to simulate the web service server.
+ $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
+
+ $this->assertEquals($DB->count_records('course_categories', array('visible' => 1)), count($categories));
+
+ // Call without required capability (it will fail cause of the search on idnumber).
$this->expectException('moodle_exception');
$categories = core_course_external::get_categories(array(
array('key' => 'id', 'value' => $category1->id),
$result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
$this->assertCount(3, $result['courses']);
// Expect to receive all the fields.
- $this->assertCount(35, $result['courses'][0]);
- $this->assertCount(35, $result['courses'][1]);
- $this->assertCount(35, $result['courses'][2]);
+ $this->assertCount(36, $result['courses'][0]);
+ $this->assertCount(36, $result['courses'][1]);
+ $this->assertCount(36, $result['courses'][2]);
$result = core_course_external::get_courses_by_field('id', $course1->id);
$result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
$this->assertCount(1, $result['courses']);
$this->assertEquals($course1->id, $result['courses'][0]['id']);
// Expect to receive all the fields.
- $this->assertCount(35, $result['courses'][0]);
+ $this->assertCount(36, $result['courses'][0]);
$result = core_course_external::get_courses_by_field('id', $course2->id);
$result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
$result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
$this->assertCount(2, $result['courses']);
+ // Check default filters.
+ $this->assertCount(3, $result['courses'][0]['filters']);
+ $this->assertCount(3, $result['courses'][1]['filters']);
+
$result = core_course_external::get_courses_by_field('category', $category1->id);
$result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
$this->assertCount(1, $result['courses']);
$result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
$this->assertCount(0, $result['courses']);
+ // Change filter value.
+ filter_set_local_state('mediaplugin', context_course::instance($course1->id)->id, TEXTFILTER_OFF);
+
self::setUser($student1);
// All visible courses (including front page) for normal student.
$result = core_course_external::get_courses_by_field();
$result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
$this->assertCount(2, $result['courses']);
- $this->assertCount(28, $result['courses'][0]);
- $this->assertCount(28, $result['courses'][1]);
+ $this->assertCount(29, $result['courses'][0]);
+ $this->assertCount(29, $result['courses'][1]);
$result = core_course_external::get_courses_by_field('id', $course1->id);
$result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
$this->assertCount(1, $result['courses']);
$this->assertEquals($course1->id, $result['courses'][0]['id']);
// Expect to receive all the files that a student can see.
- $this->assertCount(28, $result['courses'][0]);
+ $this->assertCount(29, $result['courses'][0]);
+
+ // Check default filters.
+ $filters = $result['courses'][0]['filters'];
+ $this->assertCount(3, $filters);
+ $found = false;
+ foreach ($filters as $filter) {
+ if ($filter['filter'] == 'mediaplugin' and $filter['localstate'] == TEXTFILTER_OFF) {
+ $found = true;
+ }
+ }
+ $this->assertTrue($found);
// Course 2 is not visible.
$result = core_course_external::get_courses_by_field('id', $course2->id);
$result = core_course_external::get_courses_by_field();
$result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
$this->assertCount(2, $result['courses']);
- $this->assertCount(28, $result['courses'][0]); // Site course.
+ $this->assertCount(29, $result['courses'][0]); // Site course.
$this->assertCount(12, $result['courses'][1]); // Only public information, not enrolled.
$result = core_course_external::get_courses_by_field('id', $course1->id);
$this->assertCount(0, $result['instances']);
$this->assertCount(0, $result['warnings']);
+ // Test with get_updates_since the same data.
+ $result = core_course_external::get_updates_since($course->id, $since);
+ $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result);
+ $this->assertCount(0, $result['instances']);
+ $this->assertCount(0, $result['warnings']);
+
// Update a module after a second.
$this->waitForSecond();
set_coursemodule_name($modules['forum']['cm']->id, 'New forum name');
}
$this->assertTrue($found);
+ // Test with get_updates_since the same data.
+ $result = core_course_external::get_updates_since($course->id, $since);
+ $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result);
+ $this->assertCount(1, $result['instances']);
+ $this->assertCount(0, $result['warnings']);
+ $found = false;
+ $this->assertCount(1, $result['instances']);
+ $this->assertCount(0, $result['warnings']);
+ foreach ($result['instances'] as $module) {
+ foreach ($module['updates'] as $update) {
+ if ($module['id'] == $modules['forum']['cm']->id and $update['name'] == 'configuration') {
+ $found = true;
+ }
+ }
+ }
+ $this->assertTrue($found);
+
// Do not retrieve the configuration field.
$filter = array('files');
$found = false;
This files describes API changes in /course/*,
information provided here is intended especially for developers.
+=== 3.3 ===
+
+ * External function core_course_external::get_courses_by_field now returns the course filters list and status.
+
=== 3.2 ===
* External function core_course_external::get_course_contents now returns the section's number in the course (new section field).
--- /dev/null
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package installer
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['parentlanguage'] = 'es_mx';
+$string['thislanguage'] = 'Español - México - kids';
defined('MOODLE_INTERNAL') || die();
$string['thisdirection'] = 'ltr';
-$string['thislanguage'] = 'िहनà¥\8dदी';
+$string['thislanguage'] = 'हिà¤\82दी';
$string['cannotfindcomponent'] = 'コンポーネントを見つけることができません。';
$string['cannotsavemd5file'] = 'mp5ファイルを保存できません。';
$string['cannotsavezipfile'] = 'ZIPファイルを保存できません。';
-$string['cannotunzipfile'] = 'ZIPファイルを解凍できません。';
+$string['cannotunzipfile'] = 'ZIPファイルを展開できません。';
$string['componentisuptodate'] = 'コンポーネントは最新です。';
$string['dmlexceptiononinstall'] = '<p>データベースエラーが発生しました: [{$a->errorcode}]<br />{$a->debuginfo}</p>';
$string['downloadedfilecheckfailed'] = 'ダウンロードファイルのチェックに失敗しました。';
$string['invalidmd5'] = 'チェック変数が正しくありません - 再度お試しください。';
$string['missingrequiredfield'] = 'いくつかの必須入力フィールドが入力されていません。';
$string['remotedownloaderror'] = '<p>あなたのサーバへのコンポーネントのダウンロードに失敗しました。プロクシ設定を確認してください。PHP cURL拡張モジュールの使用を強くお勧めします。</p>
-<p><a href="{$a->url}">{$a->url}</a>ファイルを手動でダウンロードした後、あなたのサーバの「{$a->dest}」にコピーおよび解凍してください。</p>';
+<p><a href="{$a->url}">{$a->url}</a>ファイルを手動でダウンロードした後、あなたのサーバの「{$a->dest}」にコピーおよび展開してください。</p>';
$string['wrongdestpath'] = '宛先パスが正しくありません。';
$string['wrongsourcebase'] = 'ソースURLベースが正しくありません。';
$string['wrongzipfilename'] = 'ZIPファイル名が正しくありません。';
<p>ウェブからは直接アクセスできないようにしてください。</p>
<p>現在ディレクトリが存在しない場合、インストレーションプロセスは作成を試みます。</p';
$string['pathssubdirroot'] = '<p>Moodleコードを含むディレクトリに関するフルパスです。</p>';
-$string['pathssubwwwroot'] = '<p>Moodleã\81«ã\82¢ã\82¯ã\82»ã\82¹ã\81\99ã\82\8bã\81\93ã\81¨ã\81®ã\81§ã\81\8dã\82\8bã\83\95ã\83«ã\82¦ã\82§ã\83\96ã\82¢ã\83\89ã\83¬ã\82¹ã\81§ã\81\99ã\80\82ä¾\8bã\81\88ã\81°ã\80\81ã\83¦ã\83¼ã\82¶ã\81\8cã\83\96ã\83©ã\82¦ã\82¶ã\81®ã\82¢ã\83\89ã\83¬ã\82¹ã\83\90ã\83¼ã\81«å\85¥å\8a\9bã\81\97ã\81¦Moodleã\81«ã\82¢ã\82¯ã\82»ã\82¹ã\81\99ã\82\8bã\81\9fã\82\81ã\81®ã\82¢ã\83\89ã\83¬ã\82¹ã\81§ã\81\99ã\80\82</p>
+$string['pathssubwwwroot'] = '<p>Moodleにアクセスすることのできるフルウェブアドレスです。例えばユーザがブラウザのアドレスバーに入力してMoodleにアクセスするためのアドレスです。</p>
<p>複数アドレスを使用してMoodleにアクセスすることはできません。あなたのサイトに複数アドレスからアクセスできる場合、最も簡単なアドレスを選択して、すべてのアドレスにパーマネントリダイレクトを設定してください。</p>
$string['cliyesnoprompt'] = 'e (evet) veya h (hayır) yazın';
$string['environmentrequireinstall'] = 'yüklenmiş ve etkinleştirilmiş olmalıdır';
$string['environmentrequireversion'] = 'sürüm {$a->needed} gerekli ve şu anda {$a->current} çalışıyor';
+$string['upgradekeyset'] = 'Yükseltme tuşu (ayarlanmak istenmiyorsa boş bırakın)';
$string['addcategory'] = 'Add a category';
$string['additionalhtml'] = 'Additional HTML';
$string['additionalhtml_heading'] = 'Additional HTML to be added to every page.';
-$string['additionalhtml_desc'] = 'These settings allow you to specify HTML that you want added to every page. You can set HTML that will be added within the HEAD tag for the page, immediately after the BODY tag has been opened, or immediately before the body tag is closed.<br />Doing this allows you add custom headers or footers on every page, or add support for services like Google Analytics very easily and independent of your chosen theme.';
+$string['additionalhtml_desc'] = 'These settings allow you to specify HTML that you want added to every page. You can set HTML that will be added within the HEAD tag for the page, immediately after the BODY tag has been opened, or immediately before the body tag is closed.<br />Doing this allows you to add custom headers or footers on every page, or add support for services like Google Analytics very easily, independent of your chosen theme.';
$string['additionalhtmlhead'] = 'Within HEAD';
$string['additionalhtmlhead_desc'] = 'Content here will be added to the bottom of the HEAD tag for every page.';
$string['additionalhtmltopofbody'] = 'When BODY is opened';
$string['cronwarningcli'] = 'The cli/cron.php maintenance script has not been run for at least 24 hours.';
$string['ctyperequired'] = 'The ctype PHP extension is now required by Moodle, in order to improve site performance and to offer multilingual compatibility.';
$string['curlsecurityallowedport'] = 'cURL allowed ports list';
-$string['curlsecurityallowedportsyntax'] = 'Put every entry on one line. Valid entries are integer numbers only.';
+$string['curlsecurityallowedportsyntax'] = 'List of port numbers that cURL can connect to. Valid entries are integer numbers only. Put each entry on a new line. If left empty, then all ports are allowed. If set, in almost all cases, both 443 and 80 should be specified for cURL to connect to standard HTTPS and HTTP ports.';
$string['curlsecurityblockedhosts'] = 'cURL blocked hosts list';
$string['curlsecurityblockedhostssyntax'] = 'Put each entry on a new line. Valid entries are either full IPv4 or IPv6 addresses (such as 192.168.10.1, 0:0:0:0:0:0:0:1, ::1, fe80::) which match a single host; or CIDR notation (such as 231.54.211.0/20 or fe80::/64); or a range of IP addresses (such as 231.3.56.10-20 or fe80::1111-bbbb) where the range applies to the last group of the address; or domain names (such as localhost or example.com); or wildcard domain names (such as *.example.com or *.sub.example.com). Blank lines are not allowed.';
$string['curlsecurityurlblocked'] = 'The URL is blocked.';
$string['rssglobaldisabled'] = 'Disabled at server level';
$string['save'] = 'Save';
$string['savechanges'] = 'Save changes';
+$string['scssinvalid'] = 'SCSS code is not valid, fails with: {$a}';
$string['search'] = 'Search';
$string['searchalldeleted'] = 'All indexed contents have been deleted';
$string['searchareaenabled'] = 'Search area enabled';
$string['groupmode'] = 'Group mode';
$string['groupmode_help'] = 'This setting has 3 options:
-* No groups - There are no sub groups, everyone is part of one big community
+* No groups
* Separate groups - Each group member can only see their own group, others are invisible
* Visible groups - Each group member works in their own group, but can also see other groups
$string['coursehelpformat'] = 'The course main page will be displayed in this format.';
$string['coursehelphiddensections'] = 'How the hidden sections in the course are displayed to students.';
$string['coursehelpmaximumupload'] = 'Define the largest size of file that can be uploaded in this course, limited by the site-wide setting.';
-$string['coursehelpnewsitemsnumber'] = 'Number of recent items from the news forum appearing in the latest news block on the course page. If set to zero, the latest news block will not be displayed.';
+$string['coursehelpnewsitemsnumber'] = 'Number of recent announcements appearing in the latest announcements block on the course page. If set to zero, the announcements forum will not be created.';
$string['coursehelpnumberweeks'] = 'Number of sections in the course (applies to certain course formats only).';
$string['coursehelpshowgrades'] = 'Enable the display of the gradebook. It does not prevent grades from being displayed within the individual activities.';
$string['coursehidden'] = 'This course is currently unavailable to students';
{$a->admin}';
$string['emailpasswordchangeinfodisabled'] = 'Hi {$a->firstname},
-Someone (probably you) has requested a new password for your
-account on \'{$a->sitename}\'.
+Someone (probably you) has requested a new password for your account on \'{$a->sitename}\'.
-Unfortunately your account on this site is disabled and can not be reset,
-please contact the site administrator,
-{$a->admin}';
+Unfortunately your account on this site is disabled, so the password cannot be reset. Please contact the site administrator {$a->admin}.';
$string['emailpasswordchangeinfofail'] = 'Hi {$a->firstname},
-Someone (probably you) has requested a new password for your
-account on \'{$a->sitename}\'.
+Someone (probably you) has requested a new password for your account on \'{$a->sitename}\'.
-Unfortunately passwords can not be reset on this site,
-please contact the site administrator,
-{$a->admin}';
+Unfortunately passwords cannot be reset on this site. Please contact the site administrator {$a->admin}.';
$string['emailpasswordchangeinfosubject'] = '{$a}: Change password information';
$string['emailpasswordsent'] = 'Thank you for confirming the change of password.
An email containing your new password has been sent to your address at<br /><b>{$a->email}</b>.<br />
$string['frontpagedescriptionhelp'] = 'This summary can be displayed on the front page using the course/site summary block.';
$string['frontpageformat'] = 'Front page format';
$string['frontpageformatloggedin'] = 'Front page format when logged in';
-$string['frontpagenews'] = 'News items';
+$string['frontpagenews'] = 'Announcements';
$string['frontpagesettings'] = 'Front page settings';
$string['fulllistofcourses'] = 'All courses';
$string['fullname'] = 'Full name'; /* @deprecated - Use fullnamecourse or fullnameuser or some own context specific string. */
$string['newpictureusernotsetup'] = 'A profile picture can only be added once all required profile information has been saved.';
$string['newsectionname'] = 'New name for section {$a}';
$string['newsitem'] = 'news item';
-$string['newsitems'] = 'news items';
-$string['newsitemsnumber'] = 'News items to show';
-$string['newsitemsnumber_help'] = 'This setting determines how many recent items appear in the latest news block on the course page. If set to "0 news items" then the latest news block will not be displayed.';
+$string['newsitems'] = 'announcements';
+$string['newsitemsnumber'] = 'Number of announcements';
+$string['newsitemsnumber_help'] = 'The announcements forum is a special forum which is created automatically in the course, has forced subscription set by default, and only users with appropriate permissions (by default teachers) can post in it.
+
+This setting determines how many recent announcements appear in the latest announcements block.
+
+If an announcements forum is not required in the course, this setting should be set to zero.';
$string['newuser'] = 'New user';
$string['newusernewpasswordsubj'] = 'New user account';
$string['newusernewpasswordtext'] = 'Hi {$a->firstname},
$string['sitelegacyfiles'] = 'Legacy site files';
$string['sitelogs'] = 'Site logs';
$string['sitemessage'] = 'Message users';
-$string['sitenews'] = 'Site news';
+$string['sitenews'] = 'Site announcements';
$string['sitepages'] = 'Site pages';
$string['sitepartlist'] = 'You do not have the required permissions to view the participants list';
$string['sitepartlist0'] = 'You must be a site teacher to be allowed to see the site participants list';
}
}
+
+/**
+ * Used to validate the contents of SCSS code and ensuring they are parsable.
+ *
+ * It does not attempt to detect undefined SCSS variables because it is designed
+ * to be used without knowledge of other config/scss included.
+ *
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2016 Dan Poltawski <dan@moodle.com>
+ */
+class admin_setting_scsscode extends admin_setting_configtextarea {
+
+ /**
+ * Validate the contents of the SCSS to ensure its parsable. Does not
+ * attempt to detect undefined scss variables.
+ *
+ * @param string $data The scss code from text field.
+ * @return mixed bool true for success or string:error on failure.
+ */
+ public function validate($data) {
+ if (empty($data)) {
+ return true;
+ }
+
+ $scss = new core_scss();
+ try {
+ $scss->compile($data);
+ } catch (Leafo\ScssPhp\Exception\ParserException $e) {
+ return get_string('scssinvalid', 'admin', $e->getMessage());
+ } catch (Leafo\ScssPhp\Exception\CompilerException $e) {
+ // Silently ignore this - it could be a scss variable defined from somewhere
+ // else which we are not examining here.
+ return true;
+ }
+
+ return true;
+ }
+}
// Set editor autosave to high value, so as to avoid unwanted ajax.
set_config('autosavefrequency', '604800', 'editor_atto');
+ // Set noreplyaddress to an example domain, as it should be valid email address and test site can be a localhost.
+ set_config('noreplyaddress', 'noreply@example.com');
+
// Keeps the current version of database and dataroot.
self::store_versions_hash();
// First check the theme.
$dirs[] = $CFG->dirroot . '/theme/' . $themename . '/templates/' . $component . '/';
+ if (isset($CFG->themedir)) {
+ $dirs[] = $CFG->themedir . '/' . $themename . '/templates/' . $component . '/';
+ }
// Now check the parent themes.
// Search each of the parent themes second.
foreach ($parents as $parent) {
$dirs[] = $CFG->dirroot . '/theme/' . $parent . '/templates/' . $component . '/';
+ if (isset($CFG->themedir)) {
+ $dirs[] = $CFG->themedir . '/' . $parent . '/templates/' . $component . '/';
+ }
}
$dirs[] = $compdirectory . '/templates/';
return $this->compile($content);
}
+ /**
+ * Compile child; returns a value to halt execution
+ *
+ * @param array $child
+ * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
+ *
+ * @return array|null
+ */
+ protected function compileChild($child, \Leafo\ScssPhp\Formatter\OutputBlock $out) {
+ switch($child[0]) {
+ case \Leafo\ScssPhp\Type::T_SCSSPHP_IMPORT_ONCE:
+ case \Leafo\ScssPhp\Type::T_IMPORT:
+ list(, $rawpath) = $child;
+ $rawpath = $this->reduce($rawpath);
+ $path = $this->compileStringContent($rawpath);
+ if ($path = $this->findImport($path)) {
+ if ($this->is_valid_file($path)) {
+ return parent::compileChild($child, $out);
+ } else {
+ // Sneaky stuff, don't let non scss file in.
+ debugging("Can't import scss file - " . $path, DEBUG_DEVELOPER);
+ }
+ }
+ break;
+ default:
+ return parent::compileChild($child, $out);
+ }
+ }
+
+ /**
+ * Is the given file valid for import ?
+ *
+ * @param $path
+ * @return bool
+ */
+ protected function is_valid_file($path) {
+ global $CFG;
+
+ $realpath = realpath($path);
+
+ // Additional theme directory.
+ $addthemedirectory = core_component::get_plugin_types()['theme'];
+ $addrealroot = realpath($addthemedirectory);
+
+ // Original theme directory.
+ $themedirectory = $CFG->dirroot . "/theme";
+ $realroot = realpath($themedirectory);
+
+ // File should end in .scss and must be in sites theme directory, else ignore it.
+ $pathvalid = $realpath !== false;
+ $pathvalid = $pathvalid && (substr($path, -5) === '.scss');
+ $pathvalid = $pathvalid && (strpos($realpath, $realroot) === 0 || strpos($realpath, $addrealroot) === 0);
+ return $pathvalid;
+ }
}
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => array(
'manager' => CAP_ALLOW,
+ 'coursecreator' => CAP_ALLOW,
)
),
'ajax' => true,
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
),
+ 'core_course_get_updates_since' => array(
+ 'classname' => 'core_course_external',
+ 'methodname' => 'get_updates_since',
+ 'classpath' => 'course/externallib.php',
+ 'description' => 'Check if there are updates affecting the user for the given course since the given time stamp.',
+ 'type' => 'read',
+ 'ajax' => true,
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+ ),
'core_enrol_get_course_enrolment_methods' => array(
'classname' => 'core_enrol_external',
'methodname' => 'get_course_enrolment_methods',
// Automatically generated Moodle v3.2.0 release upgrade line.
// Put any upgrade step following this.
+ if ($oldversion < 2016122800.00) {
+ // Find all roles with the coursecreator archetype.
+ $coursecreatorroleids = $DB->get_records('role', array('archetype' => 'coursecreator'), '', 'id');
+
+ $context = context_system::instance();
+ $capability = 'moodle/site:configview';
+
+ foreach ($coursecreatorroleids as $roleid => $notused) {
+
+ // Check that the capability has not already been assigned. If it has then it's either already set
+ // to allow or specifically set to prohibit or prevent.
+ if (!$DB->record_exists('role_capabilities', array('roleid' => $roleid, 'capability' => $capability))) {
+ // Assign the capability.
+ $cap = new stdClass();
+ $cap->contextid = $context->id;
+ $cap->roleid = $roleid;
+ $cap->capability = $capability;
+ $cap->permission = CAP_ALLOW;
+ $cap->timemodified = time();
+ $cap->modifierid = 0;
+
+ $DB->insert_record('role_capabilities', $cap);
+ }
+ }
+
+ // Main savepoint reached.
+ upgrade_main_savepoint(true, 2016122800.00);
+ }
+
return true;
}
*/
private $inorequaluniqueindex = 1;
+ /**
+ * @var boolean variable use to temporarily disable logging.
+ */
+ protected $skiplogging = false;
+
/**
* Constructor - Instantiates the database, specifying if it's external (connect to other systems) or not (Moodle DB).
* Note that this affects the decision of whether prefix checks must be performed or not.
* @return void
*/
public function query_log($error=false) {
+ // Logging disabled by the driver.
+ if ($this->skiplogging) {
+ return;
+ }
+
$logall = !empty($this->dboptions['logall']);
$logslow = !empty($this->dboptions['logslow']) ? $this->dboptions['logslow'] : false;
$logerrors = !empty($this->dboptions['logerrors']);
}
}
+ /**
+ * Disable logging temporarily.
+ */
+ protected function query_log_prevent() {
+ $this->skiplogging = true;
+ }
+
+ /**
+ * Restore old logging behavior.
+ */
+ protected function query_log_allow() {
+ $this->skiplogging = false;
+ }
+
/**
* Returns the time elapsed since the query started.
* @return float Seconds with microseconds
throw new dml_connection_exception($dberr);
}
+ // Disable logging until we are fully setup.
+ $this->query_log_prevent();
+
// already connected, select database and set some env. variables
$this->query_start("--mssql_select_db", null, SQL_QUERY_AUX);
$result = mssql_select_db($this->dbname, $this->mssql);
// Fetch/offset is supported staring from SQL Server 2012.
$this->supportsoffsetfetch = $serverinfo['version'] > '11';
+ // We can enable logging now.
+ $this->query_log_allow();
+
// Connection stabilised and configured, going to instantiate the temptables controller
$this->temptables = new mssql_native_moodle_temptables($this);
throw new dml_connection_exception($dberr);
}
+ // Disable logging until we are fully setup.
+ $this->query_log_prevent();
+
$this->query_start("--set_charset()", null, SQL_QUERY_AUX);
$this->mysqli->set_charset('utf8');
$this->query_end(true);
$this->query_end($result);
}
+ // We can enable logging now.
+ $this->query_log_allow();
+
// Connection stabilised and configured, going to instantiate the temptables controller
$this->temptables = new mysqli_native_moodle_temptables($this);
throw new dml_connection_exception($dberr);
}
+ // Disable logging until we are fully setup.
+ $this->query_log_prevent();
+
// Make sure moodle package is installed - now required.
if (!$this->oci_package_installed()) {
try {
//note: do not send "ALTER SESSION SET NLS_NUMERIC_CHARACTERS='.,'" !
// instead fix our PHP code to convert "," to "." properly!
+ // We can enable logging now.
+ $this->query_log_allow();
+
// Connection stabilised and configured, going to instantiate the temptables controller
$this->temptables = new oci_native_moodle_temptables($this, $this->unique_session_id);
throw new dml_connection_exception($dberr);
}
+ // Disable logging until we are fully setup.
+ $this->query_log_prevent();
+
// Allow quoted identifiers
$sql = "SET QUOTED_IDENTIFIER ON";
$this->query_start($sql, null, SQL_QUERY_AUX);
// Fetch/offset is supported staring from SQL Server 2012.
$this->supportsoffsetfetch = $serverinfo['version'] > '11';
+ // We can enable logging now.
+ $this->query_log_allow();
+
// Connection established and configured, going to instantiate the temptables controller
$this->temptables = new sqlsrv_native_moodle_temptables($this);
while (equation.charAt(currentPos) === '\\' && currentPos >= 0) {
currentPos -= 1;
}
- isChar = /[a-zA-Z\{\}]/;
+ isChar = /[a-zA-Z\{]/;
if (currentPos !== 0) {
- // Now match to the end of the line.
- while (isChar.test(equation.charAt(currentPos)) &&
- currentPos < equation.length &&
- isChar.test(equation.charAt(currentPos - 1))) {
- currentPos += 1;
+ if (equation.charAt(currentPos - 1) != '{') {
+ // Now match to the end of the line.
+ while (isChar.test(equation.charAt(currentPos)) &&
+ currentPos < equation.length &&
+ isChar.test(equation.charAt(currentPos - 1))) {
+ currentPos += 1;
+ }
}
}
// Save the cursor position - for insertion from the library.
$string['link'] = 'Link';
$string['loop'] = 'Loop';
$string['metadata'] = 'Metadata';
-$string['metadata_help'] = 'Metadata tracks, for use from a script, may be used only if the player supports metadata';
+$string['metadata_help'] = 'Metadata tracks, for use from a script, may be used only if the player supports metadata.';
$string['metadatasourcelabel'] = 'Metadata track URL';
$string['mute'] = 'Muted';
$string['pluginname'] = 'Media';
$this->responsefinished = false;
$this->response = array();
}
- list($key, $value) = explode(" ", rtrim($header, "\r\n"), 2);
- $key = rtrim($key, ':');
+ $parts = explode(" ", rtrim($header, "\r\n"), 2);
+ $key = rtrim($parts[0], ':');
+ $value = isset($parts[1]) ? $parts[1] : null;
if (!empty($this->response[$key])) {
if (is_array($this->response[$key])) {
$this->response[$key][] = $value;
* $enhancement = 'smartselect';
* $options = array('selectablecategories' => true|false)
*
- * @since Moodle 2.0
* @param string|element $element form element for which Javascript needs to be initalized
* @param string $enhancement which init function should be called
* @param array $options options passed to javascript
* @param array $strings strings for javascript
+ * @deprecated since Moodle 3.3 MDL-57471
*/
function init_javascript_enhancement($element, $enhancement, array $options=array(), array $strings=null) {
- global $PAGE;
- if (is_string($element)) {
- $element = $this->_form->getElement($element);
- }
- if (is_object($element)) {
- $element->_generateId();
- $elementid = $element->getAttribute('id');
- $PAGE->requires->js_init_call('M.form.init_'.$enhancement, array($elementid, $options));
- if (is_array($strings)) {
- foreach ($strings as $string) {
- if (is_array($string)) {
- call_user_func_array(array($PAGE->requires, 'string_for_js'), $string);
- } else {
- $PAGE->requires->string_for_js($string, 'moodle');
- }
- }
- }
- }
+ debugging('$mform->init_javascript_enhancement() is deprecated and no longer does anything. '.
+ 'smartselect uses should be converted to the searchableselector form element.', DEBUG_DEVELOPER);
}
/**
}
//for editor element, [text] is appended to the name.
$fullelementname = $elementName;
- if ($element->getType() == 'editor') {
- $fullelementname .= '[text]';
- //Add format to rule as moodleform check which format is supported by browser
- //it is not set anywhere... So small hack to make sure we pass it down to quickform
- if (is_null($rule['format'])) {
- $rule['format'] = $element->getFormat();
+ if (is_object($element) && $element->getType() == 'editor') {
+ if ($element->getType() == 'editor') {
+ $fullelementname .= '[text]';
+ // Add format to rule as moodleform check which format is supported by browser
+ // it is not set anywhere... So small hack to make sure we pass it down to quickform.
+ if (is_null($rule['format'])) {
+ $rule['format'] = $element->getFormat();
+ }
}
}
// Fix for bug displaying errors for elements in a group
$elementName);
$valFunc = 'validate_' . $this->_formName . '_' . $escapedElementName . '(ev.target, \''.$escapedElementName.'\')';
- $js .= '
+ if (!is_array($element)) {
+ $element = [$element];
+ }
+ foreach ($element as $elem) {
+ if (key_exists('id', $elem->_attributes)) {
+ $js .= '
function validate_' . $this->_formName . '_' . $escapedElementName . '(element, escapedName) {
if (undefined == element) {
//required element was not found, then let form be submitted without client side validation
}
}
- document.getElementById(\'' . $element->_attributes['id'] . '\').addEventListener(\'blur\', function(ev) {
+ document.getElementById(\'' . $elem->_attributes['id'] . '\').addEventListener(\'blur\', function(ev) {
' . $valFunc . '
});
- document.getElementById(\'' . $element->_attributes['id'] . '\').addEventListener(\'change\', function(ev) {
+ document.getElementById(\'' . $elem->_attributes['id'] . '\').addEventListener(\'change\', function(ev) {
' . $valFunc . '
});
';
+ }
+ }
$validateJS .= '
ret = validate_' . $this->_formName . '_' . $escapedElementName.'(frm.elements[\''.$elementName.'\'], \''.$escapedElementName.'\') && ret;
if (!ret && !first_focus) {
+++ /dev/null
-# On some PHP servers it may help if this file is copied
-# to the main moodle directory and renamed .htaccess
-#
-# As soon as you do this, check your web site. Is it
-# still working OK? If you are getting a "configuration
-# error" then you may need to enable overrides by editing
-# the main httpd.conf for Apache and in the main server
-# or virtual server area, adding something like:
-#
-# <Directory /web/moodle>
-# AllowOverride All
-# </Directory>
-#
-
-### Firstly, if you are using Apache 2, you need the following
-### three lines to allow Apache to pass a PATH_INFO variable
-### correctly for URLs like http://server/file.php/arg1/arg2
-
-<IfDefine APACHE2>
- AcceptPathInfo on
-</IfDefine>
-
-### Secondly, you can define the default files in the Moodle
-### directories as follows:
-
-DirectoryIndex index.php index.html index.htm
-
-### Thirdly, set up some PHP variables that Moodle needs
-
-php_flag file_uploads 1
-php_flag short_open_tag 1
-php_flag session.auto_start 0
-php_flag session.bug_compat_warn 0
-
-### Fourthly, sometimes Apache limits the size of uploaded files
-### (this is a separate limit to the one in PHP, see below).
-### The setting here turns off this limitation
-
-LimitRequestBody 0
-
-
-### These are optional - you may not want to override php.ini
-### To enable them, remove the leading hash (#)
-
-#php_value upload_max_filesize 2M
-#php_value post_max_size 2M
-#php_value session.gc_maxlifetime 7200
-
-
-### You can change the following line to point to the
-### error/index.php file in your Moodle distribution.
-### It provides a form which emails you (the admin)
-### about 404 errors (URL not found).
-
-#ErrorDocument 404 http://example.org/moodle/error/index.php
-
-
-### People have reported that these can help in some cases
-### (unusual) when you see errors about undefined functions
-
-#php_value auto_prepend_file none
-#php_value include_path .
-
-
return filteredCollection;
}
-/*
- All this is here just so that IE gets to handle oversized blocks
- in a visually pleasing manner. It does a browser detect. So sue me.
-*/
-
+/**
+ * @deprecated since Moodle 3.3, but shouldn't be used in earlier versions either.
+ */
function fix_column_widths() {
- var agt = navigator.userAgent.toLowerCase();
- if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
- fix_column_width('left-column');
- fix_column_width('right-column');
- }
+ Y.log('fix_column_widths() no longer does anything. Please remove it from your code.', 'warn', 'javascript-static.js');
}
+/**
+ * @deprecated since Moodle 3.3, but shouldn't be used in earlier versions either.
+ */
function fix_column_width(colName) {
- if(column = document.getElementById(colName)) {
- if(!column.offsetWidth) {
- setTimeout("fix_column_width('" + colName + "')", 20);
- return;
- }
-
- var width = 0;
- var nodes = column.childNodes;
-
- for(i = 0; i < nodes.length; ++i) {
- if(nodes[i].className.indexOf("block") != -1 ) {
- if(width < nodes[i].offsetWidth) {
- width = nodes[i].offsetWidth;
- }
- }
- }
-
- for(i = 0; i < nodes.length; ++i) {
- if(nodes[i].className.indexOf("block") != -1 ) {
- nodes[i].style.width = width + 'px';
- }
- }
- }
+ Y.log('fix_column_width() no longer does anything. Please remove it from your code.', 'warn', 'javascript-static.js');
}
/**
* Converts a nbsp indented select box into a multi drop down custom control much
- * like the custom menu. It also selectable categories on or off.
- *
- * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
- *
- * @param {YUI} Y
- * @param {string} id
- * @param {Array} options
+ * like the custom menu. Can no longer be used.
+ * @deprecated since Moodle 3.3
*/
-M.form.init_smartselect = function(Y, id, options) {
- if (!id.match(/^id_/)) {
- id = 'id_'+id;
- }
- var select = Y.one('select#'+id);
- if (!select) {
- return false;
- }
- Y.use('event-delegate',function(){
- var smartselect = {
- id : id,
- structure : [],
- options : [],
- submenucount : 0,
- currentvalue : null,
- currenttext : null,
- shownevent : null,
- cfg : {
- selectablecategories : true,
- mode : null
- },
- nodes : {
- select : null,
- loading : null,
- menu : null
- },
- init : function(Y, id, args, nodes) {
- if (typeof(args)=='object') {
- for (var i in this.cfg) {
- if (args[i] || args[i]===false) {
- this.cfg[i] = args[i];
- }
- }
- }
-
- // Display a loading message first up
- this.nodes.select = nodes.select;
-
- this.currentvalue = this.nodes.select.get('selectedIndex');
- this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
-
- var options = Array();
- options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
- this.nodes.select.all('option').each(function(option, index) {
- var rawtext = option.get('innerHTML');
- var text = rawtext.replace(/^( )*/, '');
- if (rawtext === text) {
- text = rawtext.replace(/^(\s)*/, '');
- var depth = (rawtext.length - text.length ) + 1;
- } else {
- var depth = ((rawtext.length - text.length )/12)+1;
- }
- option.set('innerHTML', text);
- options['i'+index] = {text:text,depth:depth,index:index,children:[]};
- }, this);
-
- this.structure = [];
- var structcount = 0;
- for (var i in options) {
- var o = options[i];
- if (o.depth == 0) {
- this.structure.push(o);
- structcount++;
- } else {
- var d = o.depth;
- var current = this.structure[structcount-1];
- for (var j = 0; j < o.depth-1;j++) {
- if (current && current.children) {
- current = current.children[current.children.length-1];
- }
- }
- if (current && current.children) {
- current.children.push(o);
- }
- }
- }
-
- this.nodes.menu = Y.Node.create(this.generate_menu_content());
- this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
- this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
- this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
-
- if (this.cfg.mode == null) {
- var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
- if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
- this.cfg.mode = 'compact';
- } else {
- this.cfg.mode = 'spanning';
- }
- }
-
- if (this.cfg.mode == 'compact') {
- this.nodes.menu.addClass('compactmenu');
- } else {
- this.nodes.menu.addClass('spanningmenu');
- this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
- }
-
- Y.one(document.body).append(this.nodes.menu);
- var pos = this.nodes.select.getXY();
- pos[0] += 1;
- this.nodes.menu.setXY(pos);
- this.nodes.menu.on('click', this.handle_click, this);
-
- Y.one(window).on('resize', function(){
- var pos = this.nodes.select.getXY();
- pos[0] += 1;
- this.nodes.menu.setXY(pos);
- }, this);
- },
- generate_menu_content : function() {
- var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
- content += this.generate_submenu_content(this.structure[0], true);
- content += '</ul></div>';
- return content;
- },
- generate_submenu_content : function(item, rootelement) {
- this.submenucount++;
- var content = '';
- if (item.children.length > 0) {
- if (rootelement) {
- content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'"> </div>';
- content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
- content += '<div class="smartselect_menu_content">';
- } else {
- content += '<li class="smartselect_submenuitem">';
- var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
- content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
- content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
- content += '<div class="smartselect_submenu_content">';
- }
- content += '<ul>';
- for (var i in item.children) {
- content += this.generate_submenu_content(item.children[i],false);
- }
- content += '</ul>';
- content += '</div>';
- content += '</div>';
- if (rootelement) {
- } else {
- content += '</li>';
- }
- } else {
- content += '<li class="smartselect_menuitem">';
- content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
- content += '</li>';
- }
- return content;
- },
- select : function(e) {
- var t = e.target;
- e.halt();
- this.currenttext = t.get('innerHTML');
- this.currentvalue = t.getAttribute('value');
- this.nodes.select.set('selectedIndex', this.currentvalue);
- this.hide_menu();
- },
- handle_click : function(e) {
- var target = e.target;
- if (target.hasClass('smartselect_mask')) {
- this.show_menu(e);
- } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
- this.select(e);
- } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
- this.show_sub_menu(e);
- }
- },
- show_menu : function(e) {
- e.halt();
- var menu = e.target.ancestor().one('.smartselect_menu');
- menu.addClass('visible');
- this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
- },
- show_sub_menu : function(e) {
- e.halt();
- var target = e.target;
- if (!target.hasClass('smartselect_submenuitem')) {
- target = target.ancestor('.smartselect_submenuitem');
- }
- if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
- target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
- return;
- }
- target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
- target.one('.smartselect_submenu').addClass('visible');
- },
- hide_menu : function() {
- this.nodes.menu.all('.visible').removeClass('visible');
- if (this.shownevent) {
- this.shownevent.detach();
- }
- }
- };
- smartselect.init(Y, id, options, {select:select});
- });
+M.form.init_smartselect = function() {
+ throw new Error('M.form.init_smartselect can not be used any more.');
};
/**
global $CFG, $DB;
require_once($CFG->libdir.'/gradelib.php');
require_once($CFG->libdir.'/completionlib.php');
+ require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php');
require_once($CFG->dirroot.'/group/lib.php');
$data->courseid = $data->id;
\availability_date\condition::update_all_dates($data->courseid, $data->timeshift);
}
+ // Update completion expected dates.
+ if ($CFG->enablecompletion) {
+ $modinfo = get_fast_modinfo($data->courseid);
+ $changed = false;
+ foreach ($modinfo->get_cms() as $cm) {
+ if ($cm->completion && !empty($cm->completionexpected)) {
+ $DB->set_field('course_modules', 'completionexpected', $cm->completionexpected + $data->timeshift,
+ array('id' => $cm->id));
+ $changed = true;
+ }
+ }
+
+ // Clear course cache if changes made.
+ if ($changed) {
+ rebuild_course_cache($data->courseid, true);
+ }
+
+ // Update course date completion criteria.
+ \completion_criteria_date::update_date($data->courseid, $data->timeshift);
+ }
+
$status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false);
}
$tempreplyto = array();
// Make sure that we fall back onto some reasonable no-reply address.
- $noreplyaddress = empty($CFG->noreplyaddress) ? 'noreply@' . get_host_from_url($CFG->wwwroot) : $CFG->noreplyaddress;
+ $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot);
+ $noreplyaddress = empty($CFG->noreplyaddress) ? $noreplyaddressdefault : $CFG->noreplyaddress;
+
+ if (!validate_email($noreplyaddress)) {
+ debugging('email_to_user: Invalid noreply-email '.s($noreplyaddress));
+ $noreplyaddress = $noreplyaddressdefault;
+ }
// Make up an email address for handling bounces.
if (!empty($CFG->handlebounces)) {
$mail->Sender = $noreplyaddress;
}
+ // Make sure that the explicit replyto is valid, fall back to the implicit one.
+ if (!empty($replyto) && !validate_email($replyto)) {
+ debugging('email_to_user: Invalid replyto-email '.s($replyto));
+ $replyto = $noreplyaddress;
+ }
+
$alloweddomains = null;
if (!empty($CFG->allowedemaildomains)) {
$alloweddomains = explode(PHP_EOL, $CFG->allowedemaildomains);
// and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled
// in a course with the sender.
} else if ($usetrueaddress && can_send_from_real_email_address($from, $user, $alloweddomains)) {
+ if (!validate_email($from->email)) {
+ debugging('email_to_user: Invalid from-email '.s($from->email).' - not sending');
+ // Better not to use $noreplyaddress in this case.
+ return false;
+ }
$mail->From = $from->email;
$fromdetails = new stdClass();
$fromdetails->name = fullname($from);
$onclick = htmlspecialchars_decode($activity->onclick, ENT_QUOTES);
// Build the JS function the click event will call
$jscode = "function {$functionname}(e) { $propogrationhandler $onclick }";
- $this->page->requires->js_init_code($jscode);
+ $this->page->requires->js_amd_inline($jscode);
// Override the default url with the new action link
$action = new action_link($action, $activityname, new component_action('click', $functionname));
}
if ($course->id > 1) {
// It's a real course.
$url = new moodle_url('/course/view.php', array('id' => $course->id));
- $flat = new flat_navigation_node(navigation_node::create($course->shortname, $url), 0);
+
+ $coursecontext = context_course::instance($course->id, MUST_EXIST);
+ // This is the name that will be shown for the course.
+ $coursename = empty($CFG->navshowfullcoursenames) ?
+ format_string($course->shortname, true, array('context' => $coursecontext)) :
+ format_string($course->fullname, true, array('context' => $coursecontext));
+
+ $flat = new flat_navigation_node(navigation_node::create($coursename, $url), 0);
$flat->key = 'coursehome';
$courseformat = course_get_format($course);
if (isloggedin() && !isguestuser() && (!isset($SESSION->load_navigation_admin) || $SESSION->load_navigation_admin)) {
$isadminpage = $this->is_admin_tree_needed();
- if (has_capability('moodle/site:config', context_system::instance())) {
- // Make sure this works even if config capability changes on the fly
- // and also make it fast for admin right after login.
- $SESSION->load_navigation_admin = 1;
- if ($isadminpage) {
- $adminsettings = $this->load_administration_settings();
- }
-
- } else if (!isset($SESSION->load_navigation_admin)) {
- $adminsettings = $this->load_administration_settings();
- $SESSION->load_navigation_admin = (int)($adminsettings->children->count() > 0);
+ if (has_capability('moodle/site:configview', context_system::instance())) {
+ if (has_capability('moodle/site:config', context_system::instance())) {
+ // Make sure this works even if config capability changes on the fly
+ // and also make it fast for admin right after login.
+ $SESSION->load_navigation_admin = 1;
+ if ($isadminpage) {
+ $adminsettings = $this->load_administration_settings();
+ }
- } else if ($SESSION->load_navigation_admin) {
- if ($isadminpage) {
+ } else if (!isset($SESSION->load_navigation_admin)) {
$adminsettings = $this->load_administration_settings();
+ $SESSION->load_navigation_admin = (int)($adminsettings->children->count() > 0);
+
+ } else if ($SESSION->load_navigation_admin) {
+ if ($isadminpage) {
+ $adminsettings = $this->load_administration_settings();
+ }
}
- }
- // Print empty navigation node, if needed.
- if ($SESSION->load_navigation_admin && !$isadminpage) {
- if ($adminsettings) {
- // Do not print settings tree on pages that do not need it, this helps with performance.
- $adminsettings->remove();
- $adminsettings = false;
+ // Print empty navigation node, if needed.
+ if ($SESSION->load_navigation_admin && !$isadminpage) {
+ if ($adminsettings) {
+ // Do not print settings tree on pages that do not need it, this helps with performance.
+ $adminsettings->remove();
+ $adminsettings = false;
+ }
+ $siteadminnode = $this->add(get_string('administrationsite'), new moodle_url('/admin/search.php'),
+ self::TYPE_SITE_ADMIN, null, 'siteadministration');
+ $siteadminnode->id = 'expandable_branch_' . $siteadminnode->type . '_' .
+ clean_param($siteadminnode->key, PARAM_ALPHANUMEXT);
+ $siteadminnode->requiresajaxloading = 'true';
}
- $siteadminnode = $this->add(get_string('administrationsite'), new moodle_url('/admin/search.php'), self::TYPE_SITE_ADMIN, null, 'siteadministration');
- $siteadminnode->id = 'expandable_branch_'.$siteadminnode->type.'_'.clean_param($siteadminnode->key, PARAM_ALPHANUMEXT);
- $siteadminnode->requiresajaxloading = 'true';
}
}
'loadinghelp',
), 'moodle');
- $this->page->requires->js_function_call('setTimeout', array('fix_column_widths()', 20));
-
$focus = $this->page->focuscontrol;
if (!empty($focus)) {
if (preg_match("#forms\['([a-zA-Z0-9]+)'\].elements\['([a-zA-Z0-9]+)'\]#", $focus, $matches)) {
*/
public function addCustomHeader($custom_header, $value = null) {
if ($value === null and preg_match('/message-id:(.*)/i', $custom_header, $matches)) {
- $this->MessageID = $matches[1];
+ $this->MessageID = trim($matches[1]);
return true;
} else if ($value !== null and strcasecmp($custom_header, 'message-id') === 0) {
- $this->MessageID = $value;
+ $this->MessageID = trim($value);
return true;
} else {
return parent::addCustomHeader($custom_header, $value);
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
+ @template core/action_menu
+
Action menu.
+
+ Example context (json):
+ {
+ "classes": "",
+ "primary": {
+ "items": [{"rawhtml": "<p>Item in primary menu</p>"}]
+ },
+ "secondary": {
+ "items": [{"rawhtml": "<p>Item in secondary menu</p>"}]
+ }
+ }
}}
<div class="{{classes}}" {{#attributes}}{{name}}="{{value}}"{{/attributes}}>
{{#primary}}
{{#items}}<li role="presentation">{{> core/action_menu_item }}</li>{{/items}}
</ul>
{{/secondary}}
-
- </span>
</div>
{{#js}}
require(['core/yui'], function(Y) {
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
+ @template core/action_menu_item
+
Action menu item.
+
+ Example context (json):
+ {
+ "rawhtml": "<p>[rawhtml]</p>"
+ }
}}
{{#actionmenulink}}{{> core/action_menu_link }}{{/actionmenulink}}
{{#actionmenufiller}}<span class="filler"> </span>{{/actionmenufiller}}
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
+ @template core/action_menu_link
+
Action menu link.
+
+ Example context (json):
+ {
+ "text": "Example link text",
+ "showtext": true,
+ "url": "http://example.com/link",
+ "classes": "menu-action",
+ "instance": "1"
+ }
}}
{{^disabled}}
<a href="{{url}}" class="{{classes}}" {{#attributes}}{{name}}={{#quote}}{{value}}{{/quote}}{{/attributes}} {{#showtext}}aria-labelledby="actionmenuaction-{{instance}}"{{/showtext}}>{{#icon}}{{>core/pix_icon}}{{/icon}}{{#showtext}}<span class="menu-action-text" id="actionmenuaction-{{instance}}">{{{text}}}</span>{{/showtext}}</a>
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
+ @template core/action_menu_trigger
+
Action menu trigger.
+
+ Example context (json):
+ {
+ "instance": "1",
+ "title": "Trigger me!",
+ "menutrigger": true
+ }
}}
<a href="#" class="toggle-display {{#menutrigger}}textmenu{{/menutrigger}}" id="action-menu-toggle-{{instance}}" title="{{title}}" role="menuitem">{{{actiontext}}}{{{menutrigger}}}{{#icon}}{{> core/pix_icon}}{{/icon}}{{#rawicon}}{{{.}}}{{/rawicon}}{{#menutrigger}}<b class="caret"></b>{{/menutrigger}}</a>
\ No newline at end of file
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
+ @template core/actions
+
Actions.
+
+ Example context (json):
+ {
+ "actions": [{"event": "event", "jsfunction": "Y.log", "id": "id"}]
+ }
}}
{{#js}}
require(['core/yui'], function(Y) {
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
+ @template core/chart
+
Chart rendering.
+
+ Example context (json):
+ {
+ "withtable": true,
+ "chartdata": "null"
+ }
}}
<div class="chart-area" id="chart-area-{{uniqid}}">
- <div class="chart-image" role="decoration" aria-describedby="chart-table-data-{{uniqid}}"></div>
+ <div class="chart-image" role="presentation" aria-describedby="chart-table-data-{{uniqid}}"></div>
<div class="chart-table {{^withtable}}accesshide{{/withtable}}">
<p class="chart-table-expand">
<a href="#" aria-controls="chart-table-data-{{uniqid}}" role="button">
{{#str}}showchartdata, moodle{{/str}}
</a>
</p>
- <div class="chart-table-data" id="chart-table-data-{{uniqid}}" {{#withtable}}aria-expanded="false"{{/withtable}}></div>
+ <div class="chart-table-data" id="chart-table-data-{{uniqid}}" {{#withtable}}role="complementary" aria-expanded="false"{{/withtable}}></div>
</div>
</div>
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
+ @template core/chooser
+
Chooser.
+
+ Example context (json):
+ {
+ "title": "Chooser title",
+ "method": "post",
+ "actionurl": "http://example.org/test",
+ "instructions": "Choose one:",
+ "paramname": "param",
+ "sections": [{
+ "id": "section-1",
+ "label": "Section one",
+ "items": [{
+ "label": "item one",
+ "description": "description one"
+ }]
+ }]
+ }
}}
<div class="hd choosertitle">
{{title}}
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
+ @template core/chooser_item
+
Chooser item.
+
+ Example context (json):
+ {
+ "id": "1",
+ "paramname": "param",
+ "value": "1",
+ "label": "item one",
+ "description": "description one"
+ }
}}
<div class="option">
<label for="item_{{id}}">
{{!
@template core/dataformat_selector
- Template for all html emails. Note that it may wrap content formatted
- elsewhere in another a module template.
+ Template for dataformat selection and download form.
Context variables required for this template:
* label
* options
* sesskey
* submit
+
+ Example context (json):
+ {
+ "base": "http://example.org/",
+ "name": "test",
+ "label": "Download table data as",
+ "params": false,
+ "options": [{"label": "CSV", "name": "csv"}, {"label": "Excel", "name": "excel"}],
+ "submit": "Download"
+ }
}}
<form method="get" action="{{base}}" class="dataformatselector">
<div class="mdl-align">
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
- @template core/email_html
+ @template core/email_fromname
Template for all email subjects.
* fromname
* replyto
* replytoname
+
+ Example context (json):
+ {
+ "fromname": "Joe Bloggs"
+ }
}}
{{{fromname}}}
* replyto
* replytoname
* body
+
+ Example context (json):
+ {
+ "body": "Email body"
+ }
}}
{{{body}}}
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
- @template core/email_html
+ @template core/email_subject
Template for all email subjects.
* fromname
* replyto
* replytoname
+
+ Example context (json):
+ {
+ "subject": "Email subject"
+ }
}}
{{{subject}}}
* replyto
* replytoname
* body
+
+ Example context (json):
+ {
+ "body": "Email body"
+ }
}}
{{{body}}}
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
+ @template core/help_icon
+
Help icon.
+
+ Example context (json):
+ {
+ "title": "Help with something",
+ "url": "http://example.org/help",
+ "linktext": "",
+ "icon":{
+ "attributes": [
+ {"name": "class", "value": "iconhelp"},
+ {"name": "src", "value": "../../../pix/help.svg"},
+ {"name": "alt", "value": "Help icon"}
+ ]
+ }
+ }
}}
<span class="helptooltip">
<a href="{{url}}" title={{#quote}}{{title}}{{/quote}} aria-haspopup="true" target="_blank">{{#icon}}{{>core/pix_icon}}{{/icon}}{{#linktext}}{{.}}{{/linktext}}</a>
Context variables required for this template:
* none
+
+ Example context (json):
+ {
+ }
}}
<div class="hover-tooltip-container">
{{$anchor}}{{/anchor}}
Context variables required for this template:
*
+
+ Example context (json):
+ {}
}}
<span class="loading-icon">{{#pix}} y/loading, core, {{#str}} loading {{/str}} {{/pix}}</span>
@template core/login
Moodle template for the login page.
+
+ Example context (json):
+ {
+ "autofocusform": false,
+ "canloginasguest": true,
+ "canloginbyemail": true,
+ "cansignup": true,
+ "error": "testerror",
+ "errorformatted": "Test error formatted",
+ "forgotpasswordurl": "http://example.com/login/forgot_password.php",
+ "hasidentityproviders": false,
+ "hasinstructions": true,
+ "instructions": "For full access to this site, you first need to create an account.",
+ "loginurl": "http://example.com/stable_master/login/index.php",
+ "rememberusername": true,
+ "passwordautocomplete": false,
+ "signupurl": "http://localhost/stable_master/login/signup.php",
+ "cookieshelpiconformatted": "",
+ "username": ""
+ }
}}
{{#hasinstructions}}
<div class="loginbox clearfix twocolumns">
});
{{/autofocusform}}
{{/error}}
- })
+ });
{{/js}}
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
- @template core/modal_save_cancel
+ @template core/modal_cancel
Moodle modal template with one cancel button.
Example context (json):
{
"attributes": [
- { "name": "src", "value": "http://moodle.com/wp-content/themes/moodle/images/logo-hat2.png" },
- { "name": "class", "value": "iconsmall" }
+ { "name": "src", "value": "https://moodle.org/logo/moodle-logo.svg" },
+ { "name": "class", "value": "iconsmall" },
+ {"name": "alt", "value": "Alt text for icon"}
]
}
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
+ @template core/progress_bar
+
Progress bar.
Example context (json):
{
- id: 'progressbar_test',
- width: '500'
+ "id": "progressbar_test",
+ "width": "500"
}
}}
<div class="progressbar_container" style="width: {{width}}px;" id="{{id}}">
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
+ @template core/select_time
+
Select time.
+
+ Example context (json):
+ {
+ "id": "test-id",
+ "name": "test-name",
+ "label": "Test label",
+ "options": [
+ {"name": "Option 1", "value": "1"},
+ {"name": "Option 2", "value": "2"}
+ ]
+ }
}}
<label for="{{id}}" class="accesshide">{{label}}</label>
<select name="{{name}}" id="{{id}}" {{#attributes}} {{name}}="{{value}}"{{/attributes}}>
+{{!
+ @template core/signup_form_layout
+
+ Example context (json):
+ {
+ "formhtml": "<p>(Form html would go here)</p>"
+ }
+}}
<h3>{{#str}}newaccount{{/str}}</h3>
{{{formhtml}}}
+{{!
+ @template core/skip_links
+
+ Example context (json):
+ {
+ "links": [
+ {"url": "http://example.com/link1", "text": "Link 1"},
+ {"url": "http://example.com/link2", "text": "Link 2"}
+ ]
+ }
+}}
<div class="skiplinks">
{{#links}}
<a href="#{{{url}}}" class="skip">{{{text}}}</a>
$this->resize_window('medium');
}
- /**
- * Executed after scenario to go to a page where no JS is executed.
- * This will ensure there are no unwanted ajax calls from browser and
- * site can be reset safely.
- *
- * @param AfterScenarioScope $scope scope passed by event fired after scenario.
- * @AfterScenario
- */
- public function after_scenario(AfterScenarioScope $scope) {
- try {
- $this->wait_for_pending_js();
- $this->getSession()->reset();
- } catch (DriverException $e) {
- // Try restart session, if DriverException caught.
- try {
- $this->getSession()->restart();
- } catch (DriverException $e) {
- // Do nothing, as this will be caught while starting session in before_scenario.
- }
- }
- }
-
/**
* Wait for JS to complete before beginning interacting with the DOM.
*
// The scenario title + the failed step text.
// We want a i-am-the-scenario-title_i-am-the-failed-step.$filetype format.
$filename = $scope->getFeature()->getTitle() . '_' . $scope->getStep()->getText();
- $filename = preg_replace('/([^a-zA-Z0-9\_]+)/', '-', $filename);
- // File name limited to 255 characters. Leaving 5 chars for line number and 4 chars for the file.
+ // As file name is limited to 255 characters. Leaving 5 chars for line number and 4 chars for the file.
// extension as we allow .png for images and .html for DOM contents.
- $filename = substr($filename, 0, 245) . '_' . $scope->getStep()->getLine() . '.' . $filetype;
+ $filenamelen = 245;
+
+ // Suffix suite name to faildump file, if it's not default suite.
+ $suitename = $scope->getSuite()->getName();
+ if ($suitename != 'default') {
+ $suitename = '_' . $suitename;
+ $filenamelen = $filenamelen - strlen($suitename);
+ } else {
+ // No need to append suite name for default.
+ $suitename = '';
+ }
+
+ $filename = preg_replace('/([^a-zA-Z0-9\_]+)/', '-', $filename);
+ $filename = substr($filename, 0, $filenamelen) . $suitename . '_' . $scope->getStep()->getLine() . '.' . $filetype;
return array($dir, $filename);
}
'samecourse' => false, 'result' => false],
];
}
+
+ /**
+ * Test that generate_email_processing_address() returns valid email address.
+ */
+ public function test_generate_email_processing_address() {
+ global $CFG;
+ $this->resetAfterTest();
+
+ $data = (object)[
+ 'id' => 42,
+ 'email' => 'my.email+from_moodle@example.com',
+ ];
+
+ $modargs = 'B'.base64_encode(pack('V', $data->id)).substr(md5($data->email), 0, 16);
+
+ $CFG->maildomain = 'example.com';
+ $CFG->mailprefix = 'mdl+';
+ $this->assertTrue(validate_email(generate_email_processing_address(0, $modargs)));
+
+ $CFG->maildomain = 'mail.example.com';
+ $CFG->mailprefix = 'mdl-';
+ $this->assertTrue(validate_email(generate_email_processing_address(23, $modargs)));
+ }
}
--- /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/>.
+
+/**
+ * This file contains the unittests for core scss.
+ *
+ * @package core
+ * @category phpunit
+ * @copyright 2016 onwards Ankit Agarwal
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * This file contains the unittests for core scss.
+ *
+ * @package core
+ * @category phpunit
+ * @copyright 2016 onwards Ankit Agarwal
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_scss_testcase extends advanced_testcase {
+
+ /**
+ * Data provider for is_valid_file
+ * @return array
+ */
+ public function is_valid_file_provider() {
+ $themedirectory = core_component::get_component_directory('theme_boost');
+ $realroot = realpath($themedirectory);
+ return [
+ "File import 1" => [
+ "path" => "../test.php",
+ "valid" => false
+ ],
+ "File import 2" => [
+ "path" => "../test.py",
+ "valid" => false
+ ],
+ "File import 3" => [
+ "path" => $realroot . "/scss/moodle.scss",
+ "valid" => true
+ ],
+ "File import 4" => [
+ "path" => $realroot . "/scss/../../../config.php",
+ "valid" => false
+ ],
+ "File import 5" => [
+ "path" => "/../../../../etc/passwd",
+ "valid" => false
+ ],
+ "File import 6" => [
+ "path" => "random",
+ "valid" => false
+ ]
+ ];
+ }
+
+ /**
+ * @dataProvider is_valid_file_provider
+ */
+ public function test_is_valid_file($path, $valid) {
+ $scss = new \core_scss();
+ $pathvalid = phpunit_util::call_internal_method($scss, 'is_valid_file', [$path], \core_scss::class);
+ $this->assertSame($valid, $pathvalid);
+ }
+}
\ No newline at end of file
);
}
+ /**
+ * Tests for validate_email() function.
+ */
+ public function test_validate_email() {
+
+ $this->assertTrue(validate_email('moodle@example.com'));
+ $this->assertTrue(validate_email('moodle@localhost.local'));
+ $this->assertTrue(validate_email('verp_email+is=mighty@moodle.org'));
+ $this->assertTrue(validate_email("but_potentially'dangerous'too@example.org"));
+ $this->assertTrue(validate_email('posts+AAAAAAAAAAIAAAAAAAAGQQAAAAABFSXz1eM/P/lR2bYyljM+@posts.moodle.org'));
+
+ $this->assertFalse(validate_email('moodle@localhost'));
+ $this->assertFalse(validate_email('"attacker\\" -oQ/tmp/ -X/var/www/vhost/moodle/backdoor.php some"@email.com'));
+ $this->assertFalse(validate_email("moodle@example.com>\r\nRCPT TO:<victim@example.com"));
+ }
}
information provided here is intended especially for developers.
=== 3.3 ===
+
* YUI module moodle-core-formautosubmit has been removed, use jquery .change() instead (see lib/templates/url_select.mustache for
an example)
+* $mform->init_javascript_enhancement() is deprecated and no longer does anything. Existing uses of smartselect enhancement
+ should be switched to the searchableselector form element or other solutions.
+* Return value of the validate_email() is now proper boolean as documented. Previously the function could return 1, 0 or false.
=== 3.2 ===
}
/**
- * Checks for up-to-date TLS libraries.
+ * Checks for up-to-date TLS libraries. NOTE: this is not currently used, see MDL-57262.
*
* @param environment_results $result object to update, if relevant.
* @return environment_results|null updated results or null if unoconv path is not executable.
*/
function validate_email($address) {
- return (preg_match('#^[-!\#$%&\'*+\\/0-9=?A-Z^_`a-z{|}~]+'.
+ return (bool)preg_match('#^[-!\#$%&\'*+\\/0-9=?A-Z^_`a-z{|}~]+'.
'(\.[-!\#$%&\'*+\\/0-9=?A-Z^_`a-z{|}~]+)*'.
'@'.
'[-!\#$%&\'*+\\/0-9=?A-Z^_`a-z{|}~]+\.'.
'[-!\#$%&\'*+\\./0-9=?A-Z^_`a-z{|}~]+$#',
- $address));
+ $address);
}
/**
/** @type {int} the number of messagess displayed */
Messages.prototype._numMessagesDisplayed = 0;
+ /** @type {array} the messages displayed or about to be displayed on the page */
+ Messages.prototype._messageQueue = [];
+
/** @type {int} the number of messages to retrieve */
Messages.prototype._numMessagesToRetrieve = 20;
}
// Keep track of the number of messages received.
- var numberreceived = 0;
return this._getMessages(this._getUserId(), true).then(function(data) {
- // Filter out any messages already rendered.
- var messagesArea = this.messageArea.find(SELECTORS.MESSAGES);
- data.messages = data.messages.filter(function(message) {
- var id = "" + message.id + message.isread;
- var result = messagesArea.find(SELECTORS.MESSAGE + '[data-id="' + id + '"]');
- return !result.length;
- });
- numberreceived = data.messages.length;
- // We have the data - lets render the template with it.
- return Templates.render('core_message/message_area_messages', data);
- }.bind(this)).then(function(html, js) {
- // Check if we got something to do.
- if (numberreceived > 0) {
- var newHtml = $('<div>' + html + '</div>');
- if (this._hasMatchingBlockTime(this.messageArea.node, newHtml, false)) {
- newHtml.find(SELECTORS.BLOCKTIME + ':first').remove();
- }
- // Show the new content.
- Templates.appendNodeContents(this.messageArea.find(SELECTORS.MESSAGES), newHtml, js);
- // Scroll the new message into view.
- if (shouldScrollBottom) {
- this._scrollBottom();
- }
- // Increment the number of messages displayed.
- this._numMessagesDisplayed += numberreceived;
- // Reset the poll timer because the user may be active.
- this._backoffTimer.restart();
- }
+ return this._addMessagesToDom(data.messages, shouldScrollBottom);
}.bind(this)).always(function() {
// Mark that we are no longer busy loading data.
this._isLoadingMessages = false;
// Fire an event to say the message was sent.
this.messageArea.trigger(Events.MESSAGESENT, [this._getUserId(), text]);
// Update the messaging area.
- return this._addMessageToDom();
+ return this._addLastMessageToDom();
}.bind(this)).then(function() {
// Ok, we are no longer sending a message.
this._isSendingMessage = false;
/**
* Handles adding messages to the DOM.
*
+ * @param {array} messages An array of messages to be added to the DOM.
+ * @param {boolean} shouldScrollBottom True will scroll to the bottom of the message window and show the new messages.
+ * @return {Promise} The promise resolved when the messages have been added to the DOM.
+ * @private
+ */
+ Messages.prototype._addMessagesToDom = function(messages, shouldScrollBottom) {
+ var numberreceived = 0;
+ var messagesArea = this.messageArea.find(SELECTORS.MESSAGES);
+ messages = messages.filter(function(message) {
+ var id = "" + message.id + message.isread;
+ // If the message is already queued to be rendered, remove from the list of messages.
+ if (this._messageQueue[id]) {
+ return false;
+ }
+ // Filter out any messages already rendered.
+ var result = messagesArea.find(SELECTORS.MESSAGE + '[data-id="' + id + '"]');
+ // Any message we are rendering should go in the messageQueue.
+ if (!result.length) {
+ this._messageQueue[id] = true;
+ }
+ return !result.length;
+ }.bind(this));
+ numberreceived = messages.length;
+ // We have the data - lets render the template with it.
+ return Templates.render('core_message/message_area_messages', {messages: messages}).then(function(html, js) {
+ // Check if we got something to do.
+ if (numberreceived > 0) {
+ var newHtml = $('<div>' + html + '</div>');
+ if (this._hasMatchingBlockTime(this.messageArea.node, newHtml, false)) {
+ newHtml.find(SELECTORS.BLOCKTIME + ':first').remove();
+ }
+ // Show the new content.
+ Templates.appendNodeContents(this.messageArea.find(SELECTORS.MESSAGES), newHtml, js);
+ // Scroll the new message into view.
+ if (shouldScrollBottom) {
+ this._scrollBottom();
+ }
+ // Increment the number of messages displayed.
+ this._numMessagesDisplayed += numberreceived;
+ // Reset the poll timer because the user may be active.
+ this._backoffTimer.restart();
+ }
+ }.bind(this));
+ };
+
+ /**
+ * Handles adding the last message to the DOM.
+ *
* @return {Promise} The promise resolved when the message has been added to the DOM.
* @private
*/
- Messages.prototype._addMessageToDom = function() {
+ Messages.prototype._addLastMessageToDom = function() {
// Call the web service to return how the message should look.
var promises = Ajax.call([{
methodname: 'core_message_data_for_messagearea_get_most_recent_message',
// Add the message.
return promises[0].then(function(data) {
- return Templates.render('core_message/message_area_message', data);
- }).then(function(html, js) {
- Templates.appendNodeContents(this.messageArea.find(SELECTORS.MESSAGES), html, js);
- // Empty the response text area.
+ return this._addMessagesToDom([data], true);
+ }.bind(this)).always(function() {
+ // Empty the response text area.text
this.messageArea.find(SELECTORS.SENDMESSAGETEXT).val('').trigger('input');
- // Scroll down.
- this._scrollBottom();
}.bind(this)).fail(Notification.exception);
};
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
- @template core/add_contact_button
+ @template core_message/add_contact_button
Template for the contents of the add contact button on the user's profile page.
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
- @template core/add_contact_button
+ @template core_message/add_contact_button
Template for the contents of the add contact button on the user's profile page.
Context variables required for this template:
- *
+ * none
+
+ Example context (json):
+ {
+ }
}}
<span>
{{#pix}} t/removecontact, core {{/pix}}
'locked' => new external_value(PARAM_INT, 'locked', VALUE_OPTIONAL),
'mailed' => new external_value(PARAM_INT, 'mailed', VALUE_OPTIONAL),
'extensionduedate' => new external_value(PARAM_INT, 'extension due date', VALUE_OPTIONAL),
- 'workflowstate' => new external_value(PARAM_TEXT, 'marking workflow state', VALUE_OPTIONAL),
+ 'workflowstate' => new external_value(PARAM_ALPHA, 'marking workflow state', VALUE_OPTIONAL),
'allocatedmarker' => new external_value(PARAM_INT, 'allocated marker', VALUE_OPTIONAL)
)
)
'locked' => new external_value(PARAM_INT, 'locked'),
'mailed' => new external_value(PARAM_INT, 'mailed'),
'extensionduedate' => new external_value(PARAM_INT, 'extension due date'),
- 'workflowstate' => new external_value(PARAM_TEXT, 'marking workflow state', VALUE_OPTIONAL),
+ 'workflowstate' => new external_value(PARAM_ALPHA, 'marking workflow state', VALUE_OPTIONAL),
'allocatedmarker' => new external_value(PARAM_INT, 'allocated marker')
)
)
$o = '';
$pluginsubtype = required_param('pluginsubtype', PARAM_ALPHA);
- $plugintype = required_param('plugin', PARAM_TEXT);
+ $plugintype = required_param('plugin', PARAM_PLUGIN);
$pluginaction = required_param('pluginaction', PARAM_ALPHA);
$plugin = $this->get_plugin_by_type($pluginsubtype, $plugintype);
$submissionid = optional_param('sid', 0, PARAM_INT);
$gradeid = optional_param('gid', 0, PARAM_INT);
- $plugintype = required_param('plugin', PARAM_TEXT);
+ $plugintype = required_param('plugin', PARAM_PLUGIN);
$item = null;
if ($pluginsubtype == 'assignsubmission') {
$plugin = $this->get_submission_plugin_by_type($plugintype);
$record->userid = $userid;
if ($modified >= 0) {
$record->grade = unformat_float(optional_param('quickgrade_' . $record->userid, -1, PARAM_TEXT));
- $record->workflowstate = optional_param('quickgrade_' . $record->userid.'_workflowstate', false, PARAM_TEXT);
+ $record->workflowstate = optional_param('quickgrade_' . $record->userid.'_workflowstate', false, PARAM_ALPHA);
$record->allocatedmarker = optional_param('quickgrade_' . $record->userid.'_allocatedmarker', false, PARAM_INT);
} else {
// This user was not in the grading table.
$mform->setType('userid', PARAM_INT);
$mform->addElement('hidden', 'action', 'savesubmission');
- $mform->setType('action', PARAM_TEXT);
+ $mform->setType('action', PARAM_ALPHA);
}
/**
$assign = new assign($context, $cm, $course);
$urlparams = array('id' => $id,
- 'action' => optional_param('action', '', PARAM_TEXT),
+ 'action' => optional_param('action', '', PARAM_ALPHA),
'rownum' => optional_param('rownum', 0, PARAM_INT),
'useridlistid' => optional_param('useridlistid', $assign->get_useridlist_key_id(), PARAM_ALPHANUM));
// Get the assign class to
// render the page.
-echo $assign->view(optional_param('action', '', PARAM_TEXT));
+echo $assign->view(optional_param('action', '', PARAM_ALPHA));
$newitemid = $DB->insert_record('forum', $data);
$this->apply_activity_instance($newitemid);
+
+ // Add current enrolled user subscriptions if necessary.
+ $data->id = $newitemid;
+ $ctx = context_module::instance($this->task->get_moduleid());
+ forum_instance_created($ctx, $data);
}
protected function process_forum_discussion($data) {
$mform->addElement('hidden', 'parent');
$mform->setType('parent', PARAM_INT);
- $mform->addElement('hidden', 'userid');
- $mform->setType('userid', PARAM_INT);
-
$mform->addElement('hidden', 'groupid');
$mform->setType('groupid', PARAM_INT);
$string['cannoteditposts'] = 'You can\'t edit other people\'s posts!';
$string['cannotfinddiscussion'] = 'Could not find the discussion in this forum';
$string['cannotfindfirstpost'] = 'Could not find the first post in this forum';
-$string['cannotfindorcreateforum'] = 'Could not find or create a main news forum for the site';
+$string['cannotfindorcreateforum'] = 'Could not find or create a main announcements forum for the site';
$string['cannotfindparentpost'] = 'Could not find top parent of post {$a}';
$string['cannotmovefromsingleforum'] = 'Cannot move discussion from a simple single discussion forum';
$string['cannotmovenotvisible'] = 'Forum not visible';
$string['forcesubscribed'] = 'This forum forces everyone to be subscribed';
$string['forum'] = 'Forum';
$string['forum:addinstance'] = 'Add a new forum';
-$string['forum:addnews'] = 'Add news';
+$string['forum:addnews'] = 'Add announcements';
$string['forum:addquestion'] = 'Add question';
$string['forum:allowforcesubscribe'] = 'Allow force subscribe';
$string['forum:canoverridediscussionlock'] = 'Reply to locked discussions';
$string['forumname'] = 'Forum name';
$string['forumposts'] = 'Forum posts';
$string['forum:rate'] = 'Rate posts';
-$string['forum:replynews'] = 'Reply to news';
+$string['forum:replynews'] = 'Reply to announcements';
$string['forum:replypost'] = 'Reply to posts';
$string['forums'] = 'Forums';
$string['forum:splitdiscussions'] = 'Split discussions';
$string['noguestsubscribe'] = 'Sorry, guests are not allowed to subscribe.';
$string['noguesttracking'] = 'Sorry, guests are not allowed to set tracking options.';
$string['nomorepostscontaining'] = 'No more posts containing \'{$a}\' were found';
-$string['nonews'] = 'No news has been posted yet';
+$string['nonews'] = 'No announcements have been posted yet.';
$string['noonecansubscribenow'] = 'Subscriptions are now disallowed';
$string['nopermissiontosubscribe'] = 'You do not have the permission to view forum subscribers';
$string['nopermissiontoview'] = 'You do not have permissions to view this post';
/**
* Update a post.
*
- * @param stdClass $post The post to update
+ * @param stdClass $newpost The post to update
* @param mixed $mform The submitted form
* @param string $unused
* @return bool
*/
-function forum_update_post($post, $mform, $unused = null) {
- global $DB;
+function forum_update_post($newpost, $mform, $unused = null) {
+ global $DB, $USER;
+ $post = $DB->get_record('forum_posts', array('id' => $newpost->id));
$discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion));
$forum = $DB->get_record('forum', array('id' => $discussion->forum));
$cm = get_coursemodule_from_instance('forum', $forum->id);
$context = context_module::instance($cm->id);
+ // Allowed modifiable fields.
+ $modifiablefields = [
+ 'subject',
+ 'message',
+ 'messageformat',
+ 'messagetrust',
+ 'timestart',
+ 'timeend',
+ 'pinned',
+ 'attachments',
+ ];
+ foreach ($modifiablefields as $field) {
+ if (isset($newpost->{$field})) {
+ $post->{$field} = $newpost->{$field};
+ }
+ }
$post->modified = time();
- $DB->update_record('forum_posts', $post);
-
- $discussion->timemodified = $post->modified; // last modified tracking
- $discussion->usermodified = $post->userid; // last modified tracking
+ // Last post modified tracking.
+ $discussion->timemodified = $post->modified;
+ $discussion->usermodified = $USER->id;
if (!$post->parent) { // Post is a discussion starter - update discussion title and times too
$discussion->name = $post->subject;
$discussion->pinned = $post->pinned;
}
}
- $post->message = file_save_draft_area_files($post->itemid, $context->id, 'mod_forum', 'post', $post->id,
+ $post->message = file_save_draft_area_files($newpost->itemid, $context->id, 'mod_forum', 'post', $post->id,
mod_forum_post_form::editor_options($context, $post->id), $post->message);
- $DB->set_field('forum_posts', 'message', $post->message, array('id'=>$post->id));
-
+ $DB->update_record('forum_posts', $post);
$DB->update_record('forum_discussions', $discussion);
forum_add_attachment($post, $forum, $cm, $mform);
if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) {
- forum_tp_mark_post_read($post->userid, $post, $post->forum);
+ forum_tp_mark_post_read($USER->id, $post, $post->forum);
}
// Let Moodle know that assessable content is uploaded (eg for plagiarism detection)
for ($i = 0; $i < $this->lesson->maxanswers; $i++) {
$answer = clone($newanswer);
- if (!empty($properties->answer_editor[$i])) {
+ if (isset($properties->answer_editor[$i])) {
if (is_array($properties->answer_editor[$i])) {
// Multichoice and true/false pages have an HTML editor.
$answer->answer = $properties->answer_editor[$i]['text'];
public function redirect_to_first_answer($canmanage) {
global $USER, $PAGE;
- $answer = array_shift($this->get_answers());
+ $answers = $this->get_answers();
+ $answer = array_shift($answers);
$jumpto = $answer->jumpto;
if ($jumpto == LESSON_RANDOMBRANCH) {
And the field "Icon URL" matches value "http://download.moodle.org/unittest/test.jpg"
And the field "Secure icon URL" matches value "https://download.moodle.org/unittest/test.jpg"
- @javascript
+ @javascript @_switch_window
Scenario: Add a preconfigured tool from a cartridge
When I log in as "teacher1"
And I follow "Course 1"
And I press "Cancel"
And I switch to the main window
- @javascript
+ @javascript @_switch_window
Scenario: Add and use a preconfigured tool
When I log in as "teacher1"
And I follow "Course 1"
'instance'=>$quiz->id);
if (!empty($override)) {
// Only load events for this override.
- $conds['groupid'] = isset($override->groupid)? $override->groupid : 0;
- $conds['userid'] = isset($override->userid)? $override->userid : 0;
+ if (isset($override->userid)) {
+ $conds['userid'] = $override->userid;
+ } else {
+ $conds['groupid'] = $override->groupid;
+ }
}
$oldevents = $DB->get_records('event', $conds);
$tok = strtok(strtolower($row), "\",\n\r");
$result = new stdClass();
$result->columns = array();
+ $result->mastercol = 0;
$i = 0;
while ($tok) {
if ($tok != '') {
$regexp = scorm_forge_cols_regexp($columns->columns, '(.+),');
for ($i = 1; $i < count($rows); $i++) {
if (preg_match($regexp, $rows[$i], $matches)) {
- $courses[$courseid]->elements[$columns->mastercol + 1]->prerequisites = substr(trim($matches[2 - $columns->mastercol]), 1, -1);
+ $elementid = trim($matches[$columns->mastercol + 1]);
+ $elementid = trim(trim($elementid, '"'), "'"); // Remove any quotes.
+
+ $prereq = trim($matches[2 - $columns->mastercol]);
+ $prereq = trim(trim($prereq, '"'), "'"); // Remove any quotes.
+
+ $courses[$courseid]->elements[$elementid]->prerequisites = $prereq;
}
}
}
var Initialized = false;
function LMSInitialize (param) {
- scoid = scorm_current_node ? scorm_current_node.scoid : scoid;
+ scoid = (scorm_current_node && scorm_current_node.scoid) ? scorm_current_node.scoid : scoid;
initdatamodel(scoid);
errorCode = "0";
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
-/**
- * This is really a little language parser for AICC_SCRIPT
- * evaluates the expression and returns a boolean answer
- * see 2.3.2.5.1. Sequencing/Navigation Today - from the SCORM 1.2 spec (CAM).
- *
- * @param string $prerequisites the aicc_script prerequisites expression
- * @param array $usertracks the tracked user data of each SCO visited
- * @return boolean
- */
-function scorm_eval_prerequisites($prerequisites, $usertracks) {
-
- // This is really a little language parser - AICC_SCRIPT is the reference
- // see 2.3.2.5.1. Sequencing/Navigation Today - from the SCORM 1.2 spec.
- $element = '';
- $stack = array();
- $statuses = array(
- 'passed' => 'passed',
- 'completed' => 'completed',
- 'failed' => 'failed',
- 'incomplete' => 'incomplete',
- 'browsed' => 'browsed',
- 'not attempted' => 'notattempted',
- 'p' => 'passed',
- 'c' => 'completed',
- 'f' => 'failed',
- 'i' => 'incomplete',
- 'b' => 'browsed',
- 'n' => 'notattempted'
- );
- $i = 0;
-
- // Expand the amp entities.
- $prerequisites = preg_replace('/&/', '&', $prerequisites);
- // Find all my parsable tokens.
- $prerequisites = preg_replace('/(&|\||\(|\)|\~)/', '\t$1\t', $prerequisites);
- // Expand operators.
- $prerequisites = preg_replace('/&/', '&&', $prerequisites);
- $prerequisites = preg_replace('/\|/', '||', $prerequisites);
- // Now - grab all the tokens.
- $elements = explode('\t', trim($prerequisites));
-
- // Process each token to build an expression to be evaluated.
- $stack = array();
- foreach ($elements as $element) {
- $element = trim($element);
- if (empty($element)) {
- continue;
- }
- if (!preg_match('/^(&&|\|\||\(|\))$/', $element)) {
- // Create each individual expression.
- // Search for ~ = <> X*{} .
-
- // Sets like 3*{S34, S36, S37, S39}.
- if (preg_match('/^(\d+)\*\{(.+)\}$/', $element, $matches)) {
- $repeat = $matches[1];
- $set = explode(',', $matches[2]);
- $count = 0;
- foreach ($set as $setelement) {
- if (isset($usertracks[$setelement]) &&
- ($usertracks[$setelement]->status == 'completed' || $usertracks[$setelement]->status == 'passed')) {
- $count++;
- }
- }
- if ($count >= $repeat) {
- $element = 'true';
- } else {
- $element = 'false';
- }
- } else if ($element == '~') {
- // Not maps ~.
- $element = '!';
- } else if (preg_match('/^(.+)(\=|\<\>)(.+)$/', $element, $matches)) {
- // Other symbols = | <> .
- $element = trim($matches[1]);
- if (isset($usertracks[$element])) {
- $value = trim(preg_replace('/(\'|\")/', '', $matches[3]));
- if (isset($statuses[$value])) {
- $value = $statuses[$value];
- }
- if ($matches[2] == '<>') {
- $oper = '!=';
- } else {
- $oper = '==';
- }
- $element = '(\''.$usertracks[$element]->status.'\' '.$oper.' \''.$value.'\')';
- } else {
- $element = 'false';
- }
- } else {
- // Everything else must be an element defined like S45 ...
- if (isset($usertracks[$element]) &&
- ($usertracks[$element]->status == 'completed' || $usertracks[$element]->status == 'passed')) {
- $element = 'true';
- } else {
- $element = 'false';
- }
- }
-
- }
- $stack[] = ' '.$element.' ';
- }
- return eval('return '.implode($stack).';');
-}
/**
* Sets up $userdata array and default values for SCORM 1.2 .
$event->add_record_snapshot('scorm_scoes', $sco);
$event->trigger();
}
+
+/**
+ * This is really a little language parser for AICC_SCRIPT
+ * evaluates the expression and returns a boolean answer
+ * see 2.3.2.5.1. Sequencing/Navigation Today - from the SCORM 1.2 spec (CAM).
+ * Also used by AICC packages.
+ *
+ * @param string $prerequisites the aicc_script prerequisites expression
+ * @param array $usertracks the tracked user data of each SCO visited
+ * @return boolean
+ */
+function scorm_eval_prerequisites($prerequisites, $usertracks) {
+
+ // This is really a little language parser - AICC_SCRIPT is the reference
+ // see 2.3.2.5.1. Sequencing/Navigation Today - from the SCORM 1.2 spec.
+ $element = '';
+ $stack = array();
+ $statuses = array(
+ 'passed' => 'passed',
+ 'completed' => 'completed',
+ 'failed' => 'failed',
+ 'incomplete' => 'incomplete',
+ 'browsed' => 'browsed',
+ 'not attempted' => 'notattempted',
+ 'p' => 'passed',
+ 'c' => 'completed',
+ 'f' => 'failed',
+ 'i' => 'incomplete',
+ 'b' => 'browsed',
+ 'n' => 'notattempted'
+ );
+ $i = 0;
+
+ // Expand the amp entities.
+ $prerequisites = preg_replace('/&/', '&', $prerequisites);
+ // Find all my parsable tokens.
+ $prerequisites = preg_replace('/(&|\||\(|\)|\~)/', '\t$1\t', $prerequisites);
+ // Expand operators.
+ $prerequisites = preg_replace('/&/', '&&', $prerequisites);
+ $prerequisites = preg_replace('/\|/', '||', $prerequisites);
+ // Now - grab all the tokens.
+ $elements = explode('\t', trim($prerequisites));
+
+ // Process each token to build an expression to be evaluated.
+ $stack = array();
+ foreach ($elements as $element) {
+ $element = trim($element);
+ if (empty($element)) {
+ continue;
+ }
+ if (!preg_match('/^(&&|\|\||\(|\))$/', $element)) {
+ // Create each individual expression.
+ // Search for ~ = <> X*{} .
+
+ // Sets like 3*{S34, S36, S37, S39}.
+ if (preg_match('/^(\d+)\*\{(.+)\}$/', $element, $matches)) {
+ $repeat = $matches[1];
+ $set = explode(',', $matches[2]);
+ $count = 0;
+ foreach ($set as $setelement) {
+ if (isset($usertracks[$setelement]) &&
+ ($usertracks[$setelement]->status == 'completed' || $usertracks[$setelement]->status == 'passed')) {
+ $count++;
+ }
+ }
+ if ($count >= $repeat) {
+ $element = 'true';
+ } else {
+ $element = 'false';
+ }
+ } else if ($element == '~') {
+ // Not maps ~.
+ $element = '!';
+ } else if (preg_match('/^(.+)(\=|\<\>)(.+)$/', $element, $matches)) {
+ // Other symbols = | <> .
+ $element = trim($matches[1]);
+ if (isset($usertracks[$element])) {
+ $value = trim(preg_replace('/(\'|\")/', '', $matches[3]));
+ if (isset($statuses[$value])) {
+ $value = $statuses[$value];
+ }
+ if ($matches[2] == '<>') {
+ $oper = '!=';
+ } else {
+ $oper = '==';
+ }
+ $element = '(\''.$usertracks[$element]->status.'\' '.$oper.' \''.$value.'\')';
+ } else {
+ $element = 'false';
+ }
+ } else {
+ // Everything else must be an element defined like S45 ...
+ if (isset($usertracks[$element]) &&
+ ($usertracks[$element]->status == 'completed' || $usertracks[$element]->status == 'passed')) {
+ $element = 'true';
+ } else {
+ $element = 'false';
+ }
+ }
+
+ }
+ $stack[] = ' '.$element.' ';
+ }
+ return eval('return '.implode($stack).';');
+}
-@mod @mod_scorm @_file_upload @_switch_frame
+@mod @mod_scorm @_file_upload @_switch_iframe
Feature: Add scorm activity
In order to let students access a scorm package
As a teacher
-@mod @mod_scorm @_file_upload @_switch_frame
+@mod @mod_scorm @_file_upload @_switch_iframe
Feature: Scorm multi-sco completion
In order to let students access a scorm package
As a teacher
--- /dev/null
+@mod @mod_scorm @_file_upload @_switch_iframe
+Feature: Check a SCORM package with missing Organisational structure.
+
+ @javascript
+ Scenario: Add a scorm activity to a course
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ | student1 | Student | 1 | student1@example.com |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ | student1 | C1 | student |
+ When I log in as "teacher1"
+ And I follow "Course 1"
+ And I turn editing mode on
+ And I add a "SCORM package" to section "1"
+ And I set the following fields to these values:
+ | Name | MissingOrg SCORM package |
+ | Description | Description |
+ And I upload "mod/scorm/tests/packages/singlescobasic_missingorg.zip" file to "Package file" filemanager
+ And I click on "Save and display" "button"
+ Then I should see "MissingOrg SCORM package"
+ And I should see "Normal"
+ And I should see "Preview"
+ And I log out
+ And I log in as "student1"
+ And I follow "Course 1"
+ And I follow "MissingOrg SCORM package"
+ And I should see "Normal"
+ And I press "Enter"
+ And I switch to "scorm_object" iframe
+ And I switch to "contentFrame" iframe
+ And I should see "Play of the game"
* invalid.zip - zip file with an single html file, no SCORM config files, used for validation check.
* validscorm.zip - non functional package with an imsmanifest.xml, used for validation check.
* validaicc.zip - non functional package with AICC config files, used for validation check.
-* complexscorm.zip - copied from: https://github.com/jleyva/scorm-debugger
+* complexscorm.zip - copied from: https://github.com/jleyva/scorm-debugger.
+* singlescobasic_missingorg.zip - copy of scorm.com package but with missing org definition.
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2016120500; // The current module version (Date: YYYYMMDDXX).
+$plugin->version = 2016122000; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2016112900; // Requires this Moodle version.
$plugin->component = 'mod_scorm'; // Full name of the plugin (used for diagnostics).
$plugin->cron = 300;
// Onunload is called multiple times in the SCORM window - we only want to handle when it is actually closed.
setTimeout(function() {
if (winobj.closed) {
- // Redirect the parent window to the course homepage.
- parent.window.location = course_url;
+ window.location = course_url;
}
}, 800)
}
* @return array list of origins.
*/
public function get_origin_options() {
- global $DB;
- $origins = $DB->get_records_sql('select distinct origin from {logstore_standard_log} order by origin ASC');
$ret = array();
$ret[''] = get_string('allsources', 'report_log');
- foreach ($origins as $origin) {
- if (!empty($origin->origin)) {
- $ret[$origin->origin] = get_string($origin->origin, 'report_log');
- }
- }
+ $ret['cli'] = get_string('cli', 'report_log');
+ $ret['restore'] = get_string('restore', 'report_log');
+ $ret['web'] = get_string('web', 'report_log');
+ $ret['ws'] = get_string('ws', 'report_log');
+ $ret['---'] = get_string('other', 'report_log');
return $ret;
}
$joins[] = "edulevel ".$edulevelsql;
$params = array_merge($params, $edulevelparams);
}
+
// Origin.
if (isset($this->filterparams->origin) && ($this->filterparams->origin != '')) {
- $joins[] = "origin = :origin";
- $params['origin'] = $this->filterparams->origin;
+ if ($this->filterparams->origin !== '---') {
+ // Filter by a single origin.
+ $joins[] = "origin = :origin";
+ $params['origin'] = $this->filterparams->origin;
+ } else {
+ // Filter by everything else.
+ list($originsql, $originparams) = $DB->get_in_or_equal(array('cli', 'restore', 'ws', 'web'),
+ SQL_PARAMS_NAMED, 'origin', false);
+ $joins[] = "origin " . $originsql;
+ $params = array_merge($params, $originparams);
+ }
}
if (!($this->filterparams->logreader instanceof logstore_legacy\log\store)) {
$string['logsformat'] = 'Logs format';
$string['nologreaderenabled'] = 'No log reader enabled';
$string['origin'] = 'Source';
+$string['other'] = 'Other';
$string['page-report-log-x'] = 'Any log report';
$string['page-report-log-index'] = 'Course log report';
$string['page-report-log-user'] = 'User course log report';
print_error('notyourinstances', 'repository');
}
$user = $USER;
- $PAGE->set_pagelayout('mydashboard');
} else {
print_error('invalidcontext');
}
text-align: left;
border: 0 solid black;
}
-/**
-* Smart Select Element
-*/
-.smartselect {
- position: absolute;
-}
-
-.smartselect .smartselect_mask {
- background-color: #fff;
-}
-
-.smartselect ul {
- padding: 0;
- margin: 0;
-}
-
-.smartselect ul li {
- list-style: none;
-}
-
-.smartselect .smartselect_menu {
- margin-right: 5px;
-}
-
-.safari .smartselect .smartselect_menu {
- margin-left: 2px;
-}
-
-.smartselect .smartselect_menu,
-.smartselect .smartselect_submenu {
- border: 1px solid #000;
- background-color: #fff;
- display: none;
-}
-
-.smartselect .smartselect_menu.visible,
-.smartselect .smartselect_submenu.visible {
- display: block;
-}
-
-.smartselect .smartselect_menu_content ul li {
- position: relative;
- padding: 2px 5px;
-}
-
-.smartselect .smartselect_menu_content ul li a {
- color: #333;
- text-decoration: none;
-}
-
-.smartselect .smartselect_menu_content ul li a.selectable {
- color: inherit;
-}
-
-.smartselect .smartselect_submenuitem {
- background-image: url([[pix:moodle|t/collapsed]]);
- background-repeat: no-repeat;
- background-position: 100%;
-}
-/** Spanning mode */
-.smartselect.spanningmenu .smartselect_submenu {
- position: absolute;
- top: -1px;
- left: 100%;
-}
-
-.smartselect.spanningmenu .smartselect_submenu a {
- white-space: nowrap;
- padding-right: 16px;
-}
-
-.smartselect.spanningmenu .smartselect_menu_content ul li a.selectable:hover {
- text-decoration: underline;
-}
-/** Compact mode */
-.smartselect.compactmenu .smartselect_submenu {
- position: relative;
- margin: 2px -3px;
- margin-left: 10px;
- display: none;
- border-width: 0;
- z-index: 1010;
-}
-
-.smartselect.compactmenu .smartselect_submenu.visible {
- display: block;
-}
-
-.smartselect.compactmenu .smartselect_menu {
- z-index: 1000;
- overflow: hidden;
-}
-
-.smartselect.compactmenu .smartselect_submenu .smartselect_submenu {
- z-index: 1020;
-}
-
-.smartselect.compactmenu .smartselect_submenuitem:hover > .smartselect_menuitem_label {
- font-weight: bold;
-}
/**
* Enrol
.file-picker .ygtvtn,
.filemanager .ygtvtn {
/*rtl:remove*/
- background: url('[[pix:moodle|y/tn]]') 0 10px no-repeat;
+ background: url('[[pix:moodle|y/tn]]') 0 0 no-repeat;
/*rtl:raw:
- background: url('[[pix:moodle|y/tn_rtl]]') 2px 10px no-repeat;
+ background: url('[[pix:moodle|y/tn_rtl]]') 0 0 no-repeat;
*/
- width: 17px;
- height: 22px;
+ width: 19px;
+ height: 32px;
}
// first or middle sibling, collapsable
.file-picker .ygtvtm,
.file-picker .ygtvln,
.filemanager .ygtvln {
/*rtl:remove*/
- background: url('[[pix:moodle|y/ln]]') 0 10px no-repeat;
+ background: url('[[pix:moodle|y/ln]]') 0 0 no-repeat;
/*rtl:raw:
- background: url('[[pix:moodle|y/ln_rtl]]') 2px 10px no-repeat;
+ background: url('[[pix:moodle|y/ln_rtl]]') 0 0 no-repeat;
*/
- width: 17px;
- height: 22px;
+ width: 19px;
+ height: 32px;
}
// Last sibling, collapsable
.file-picker .ygtvlm,
// the style for the empty cells that are used for rendering the depth of the node
.file-picker .ygtvdepthcell,
.filemanager .ygtvdepthcell {
- background: url('[[pix:moodle|y/vline]]') 0 10px no-repeat;
+ background: url('[[pix:moodle|y/vline]]') 0 0 no-repeat;
/*rtl:raw:
- background-position: 2px 10px;
+ background-position: 0 0;
*/
width: 17px;
height: 32px;
$page = new admin_settingpage('theme_boost_advanced', get_string('advancedsettings', 'theme_boost'));
// Raw SCSS to include before the content.
- $setting = new admin_setting_configtextarea('theme_boost/scsspre',
+ $setting = new admin_setting_scsscode('theme_boost/scsspre',
get_string('rawscsspre', 'theme_boost'), get_string('rawscsspre_desc', 'theme_boost'), '', PARAM_RAW);
$setting->set_updatedcallback('theme_reset_all_caches');
$page->add($setting);
// Raw SCSS to include after the content.
- $setting = new admin_setting_configtextarea('theme_boost/scss', get_string('rawscss', 'theme_boost'),
+ $setting = new admin_setting_scsscode('theme_boost/scss', get_string('rawscss', 'theme_boost'),
get_string('rawscss_desc', 'theme_boost'), '', PARAM_RAW);
$setting->set_updatedcallback('theme_reset_all_caches');
$page->add($setting);
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
-<ul class="nav nav-tabs">
+{{!
+ @template theme_boost/admin_setting_tabs
+
+
+ Example context (json):
+ {
+ "tabs": [
+ {
+ "name": "tab1",
+ "active": 0,
+ "displayname": "Inactive tab1",
+ "html": "<p>Tab 1 content</p>"
+ },
+ {
+ "name": "tab2",
+ "active": 1,
+ "displayname": "Active tab2",
+ "html": "<p>Tab 2 content</p>"
+ }
+ ]
+ }
+}}
+<ul class="nav nav-tabs" role="tablist">
{{#tabs}}
<li class="nav-item">
<a href="#{{name}}" class="nav-link {{#active}}active{{/active}}" data-toggle="tab" role="tab">{{displayname}}</a>
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
+ @template core/action_menu
+
Action menu.
+
+ Example context (json):
+ {
+ "classes": "",
+ "primary": {
+ "items": [{"rawhtml": "<p>Item in primary menu</p>"}]
+ },
+ "secondary": {
+ "items": [{"rawhtml": "<p>Item in secondary menu</p>"}]
+ }
+ }
}}
<div class="action-menu {{classes}}" {{#attributes}}{{name}}="{{value}}"{{/attributes}}>
{{#primary}}
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
+ @template core/action_menu_item
+
Action menu item.
+
+ Example context (json):
+ {
+ "rawhtml": "<p>[rawhtml]</p>"
+ }
}}
{{#actionmenulink}}{{> core/action_menu_link }}{{/actionmenulink}}
{{#actionmenufiller}}<span class="filler"> </span>{{/actionmenufiller}}
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
+ @template core/action_menu_link
+
Action menu link.
+
+ Example context (json):
+ {
+ "text": "Example link text",
+ "showtext": true,
+ "url": "http://example.com/link"
+ }
}}
{{^disabled}}
<a href="{{url}}" class="{{classes}}" {{#attributes}}{{name}}={{#quote}}{{value}}{{/quote}}{{/attributes}} {{#showtext}}aria-labelledby="actionmenuaction-{{instance}}"{{/showtext}}>{{#icon}}{{>core/pix_icon}}{{/icon}}{{#showtext}}<span class="menu-action-text" id="actionmenuaction-{{instance}}">{{{text}}}</span>{{/showtext}}</a>
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
+ @template core/chooser
+
Chooser.
+
+ Example context (json):
+ {
+ "title": "Chooser title",
+ "method": "post",
+ "actionurl": "http://example.org/test",
+ "instructions": "Choose one:",
+ "paramname": "param",
+ "sections": [{
+ "id": "section-1",
+ "label": "Section one",
+ "items": [{
+ "label": "item one",
+ "description": "description one"
+ }]
+ }]
+ }
}}
<div class="hd choosertitle">
{{title}}
* options
* sesskey
* submit
+
+ Example context (json):
+ {
+ "base": "http://example.org/",
+ "name": "test",
+ "value": "test",
+ "label": "Download table data as",
+ "params": false,
+ "options": [{"label": "CSV", "name": "csv"}, {"label": "Excel", "name": "excel"}],
+ "submit": "Download",
+ "sesskey": ""
+ }
}}
<form method="get" action="{{base}}" class="dataformatselector m-a-1">
<div class="form-inline text-xs-right">
+{{!
+ @template core/help_icon
+
+ Help icon.
+
+ Example context (json):
+ {
+ "title": "Help with something",
+ "url": "http://example.org/help",
+ "linktext": "",
+ "icon":{
+ "attributes": [
+ {"name": "class", "value": "iconhelp"},
+ {"name": "src", "value": "../../../pix/help.svg"},
+ {"name": "alt", "value": "Help icon"}
+ ]
+ }
+ }
+}}
<a class="btn btn-link p-a-0" role="button"
data-container="body" data-toggle="popover"
data-placement="{{#ltr}}right{{/ltr}}{{^ltr}}left{{/ltr}}" data-content="{{text}} {{completedoclink}}"
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
+ @template core/progress_bar
+
Progress bar.
Example context (json):
{
- id: 'progressbar_test',
- width: '500'
+ "id": "progressbar_test",
+ "width": "500"
}
}}
-<div class="row" id="{{id}}" class="progressbar_container">
+<div id="{{id}}" class="row progressbar_container">
<div class="col-md-6 push-md-3">
<p id="{{id}}_status" class="text-xs-center"></p>
<progress id="{{id}}_bar" class="progress progress-striped progress-animated" value="0" max="100"></progress>
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
+ @template core/select_time
+
Select time.
+
+ Example context (json):
+ {
+ "id": "test-id",
+ "name": "test-name",
+ "label": "Test label",
+ "options": [
+ {"name": "Option 1", "value": "1"},
+ {"name": "Option 2", "value": "2"}
+ ]
+ }
}}
<label for="{{id}}" class="sr-only">{{label}}</label>
<select name="{{name}}" id="{{id}}" {{#attributes}} {{name}}="{{value}}"{{/attributes}} class="form-control">
+{{!
+ @template core/signup_form_layout
+
+ Example context (json):
+ {
+ "logourl": "https://moodle.org/logo/moodle-logo.svg",
+ "sitename": "Site name",
+ "formhtml": "<p>(Form html would go here)</p>"
+ }
+}}
<div class="container-fluid">
<div class="row">
<div class="col-md-8 push-md-2 col-xl-6 push-xl-3">
+{{!
+ @template core/skip_links
+
+ Example context (json):
+ {
+ "links": [
+ {"url": "http://example.com/link1", "text": "Link 1"},
+ {"url": "http://example.com/link2", "text": "Link 2"}
+ ]
+ }
+}}
<div>
{{#links}}
<a class="sr-only sr-only-focusable" href="#{{{url}}}">{{{text}}}</a>
<input type="hidden" name="{{element.name}}" value="{{element.frozenvalue}}">
{{/element.frozen}}
{{/element.hardfrozen}}
-<input type="checkbox" name="{{element.name}}"
+<input type="checkbox" name="{{element.name}}" class="{{element.extraclasses}}"
id="{{element.id}}"
{{#element.selectedvalue}}
value="{{element.selectedvalue}}"
{{/element.hardfrozen}}
<input type="checkbox"
name="{{element.name}}"
+ class="{{element.extraclasses}}"
{{#element.selectedvalue}}
value="{{element.selectedvalue}}"
{{/element.selectedvalue}}
<input type="hidden" name="{{element.name}}" value="{{element.frozenvalue}}">
{{/element.frozen}}
{{/element.hardfrozen}}
-<input type="checkbox" name="{{element.name}}"
+<input type="checkbox" name="{{element.name}}" class="{{element.extraclasses}}"
id="{{element.id}}"
{{#element.value}}
value="{{element.value}}"
{{/element.hardfrozen}}
<input type="checkbox"
name="{{element.name}}"
+ class="{{element.extraclasses}}"
{{#element.value}}
value="{{element.value}}"
{{/element.value}}
+{{!
+ @template theme_boost/nav-drawer
+
+
+ Example context (json): {}
+}}
<div id="nav-drawer" data-region="drawer" class="hidden-print moodle-has-zindex {{^navdraweropen}}closed{{/navdraweropen}}" aria-hidden="{{#navdraweropen}}false{{/navdraweropen}}{{^navdraweropen}}true{{/navdraweropen}}" tabindex="-1">
{{> theme_boost/flat_navigation }}
</div>
text-align: left;
border: 0 solid black;
}
-/**
-* Smart Select Element
-*/
-.smartselect {
- position: absolute;
-}
-.smartselect .smartselect_mask {
- background-color: #fff;
-}
-.smartselect ul {
- padding: 0;
- margin: 0;
-}
-.smartselect ul li {
- list-style: none;
-}
-.smartselect .smartselect_menu {
- margin-right: 5px;
-}
-.safari .smartselect .smartselect_menu {
- margin-left: 2px;
-}
-.smartselect .smartselect_menu,
-.smartselect .smartselect_submenu {
- border: 1px solid #000;
- background-color: #fff;
- display: none;
-}
-.smartselect .smartselect_menu.visible,
-.smartselect .smartselect_submenu.visible {
- display: block;
-}
-.smartselect .smartselect_menu_content ul li {
- position: relative;
- padding: 2px 5px;
-}
-.smartselect .smartselect_menu_content ul li a {
- color: #333;
- text-decoration: none;
-}
-.smartselect .smartselect_menu_content ul li a.selectable {
- color: inherit;
-}
-.smartselect .smartselect_submenuitem {
- background-image: url([[pix:moodle|t/collapsed]]);
- background-repeat: no-repeat;
- background-position: 100%;
-}
-/** Spanning mode */
-.smartselect.spanningmenu .smartselect_submenu {
- position: absolute;
- top: -1px;
- left: 100%;
-}
-.smartselect.spanningmenu .smartselect_submenu a {
- white-space: nowrap;
- padding-right: 16px;
-}
-.smartselect.spanningmenu .smartselect_menu_content ul li a.selectable:hover {
- text-decoration: underline;
-}
-/** Compact mode */
-.smartselect.compactmenu .smartselect_submenu {
- position: relative;
- margin: 2px -3px;
- margin-left: 10px;
- display: none;
- border-width: 0;
- z-index: 1010;
-}
-.smartselect.compactmenu .smartselect_submenu.visible {
- display: block;
-}
-.smartselect.compactmenu .smartselect_menu {
- z-index: 1000;
- overflow: hidden;
-}
-.smartselect.compactmenu .smartselect_submenu .smartselect_submenu {
- z-index: 1020;
-}
-.smartselect.compactmenu .smartselect_submenuitem:hover > .smartselect_menuitem_label {
- font-weight: bold;
-}
+
/**
* Registration
*/
/* Section and module editing forms contain special JS components for the
availability system (if enabled). */
-#fitem_id_availabilityconditionsjson {
- *[aria-hidden=true] {
- display: none;
- }
- select,
- input[type=text] {
- position: relative;
- top: 4px;
- }
- label {
- display: inline;
- }
- br + .availability-group {
- display: inline-block;
- margin-top: 8px;
- margin-bottom: 8px;
- }
- .availability-group {
- margin-right: 8px;
- }
- .availability-item {
- margin-bottom: 6px;
- }
- .availability-none {
- margin-left: 20px;
- margin-bottom: 4px;
- }
- .availability-plugincontrols {
- min-height: 40px;
- padding: 2px 0 0 4px;
- background: none repeat scroll 0% 0% @wellBackground;
- border: 1px solid @grayLighter;
- border-radius: 4px;
- display: inline-block;
- margin-right: 8px;
- select {
- width: auto;
- max-width: 200px;
- }
- }
- /* Eye icon in front of an item and delete icon after it. */
- .availability-eye,
- .availability-delete {
- margin-right: 8px;
- }
- /* Hidden eye icon still takes up space. */
- .availability-eye[aria-hidden=true] {
- display: inline;
- visibility: hidden;
- }
- /* Eye icons in front of child lists are aligned specially. */
- .availability-list > .availability-eye img {
- vertical-align: top;
- margin-top: 12px;
- }
- /* Add button lines up with child elements. */
- .availability-button {
- margin-left: 15px;
- }
- /* Nested section is grey. */
- .availability-childlist > .availability-inner {
- display: inline-block;
- background: @wellBackground;
- border: 1px solid @grayLighter;
- border-radius: 4px;
- padding: 6px;
- margin-bottom: 6px;
- }
- /* Second (and more) levels of nested sections are white. */
- .availability-childlist .availability-childlist > .availability-inner {
- background: white;
- }
- /* Connecting text needs to be indented. */
- .availability-connector {
- margin-left: 20px;
- margin-bottom: 6px;
+#id_availabilityconditionsjson[aria-hidden=true],
+.availability-field [aria-hidden=true] {
+ display: none;
+}
+.availability-eye,
+.availability-delete {
+ margin-right: 8px;
+}
+/* Eye icons in front of child lists are aligned specially. */
+.availability-list > .availability-eye img {
+ vertical-align: top;
+ margin-top: 12px;
+}
+.availability-plugincontrols {
+ min-height: 40px;
+ padding: 2px 0 0 4px;
+ background: none repeat scroll 0% 0% @wellBackground;
+ border: 1px solid @grayLighter;
+ border-radius: 4px;
+ display: inline-block;
+ margin-right: 8px;
+ select {
+ width: auto;
+ max-width: 200px;
}
}
-
-
+.availability-field .availability-plugincontrols .availability-group select {
+ max-width: 12rem;
+}
+/* Nested section is grey. */
+.availability-childlist > .availability-inner {
+ display: inline-block;
+ background: @wellBackground;
+ border: 1px solid @grayLighter;
+ border-radius: 4px;
+ padding: 6px;
+ margin-bottom: 6px;
+}
+/* Second (and more) levels of nested sections are white. */
+.availability-childlist .availability-childlist > .availability-inner {
+ background: white;
+}
/* Default form styling colours all text red. With availability conditions
this looks excessive as we show 'Invalid' markers in specific places. */
.mform .error .availability-field {
border: 0 solid black;
}
/**
-* Smart Select Element
-*/
-.smartselect {
- position: absolute;
-}
-.smartselect .smartselect_mask {
- background-color: #fff;
-}
-.smartselect ul {
- padding: 0;
- margin: 0;
-}
-.smartselect ul li {
- list-style: none;
-}
-.smartselect .smartselect_menu {
- margin-right: 5px;
-}
-.safari .smartselect .smartselect_menu {
- margin-left: 2px;
-}
-.smartselect .smartselect_menu,
-.smartselect .smartselect_submenu {
- border: 1px solid #000;
- background-color: #fff;
- display: none;
-}
-.smartselect .smartselect_menu.visible,
-.smartselect .smartselect_submenu.visible {
- display: block;
-}
-.smartselect .smartselect_menu_content ul li {
- position: relative;
- padding: 2px 5px;
-}
-.smartselect .smartselect_menu_content ul li a {
- color: #333;
- text-decoration: none;
-}
-.smartselect .smartselect_menu_content ul li a.selectable {
- color: inherit;
-}
-.smartselect .smartselect_submenuitem {
- background-image: url([[pix:moodle|t/collapsed]]);
- background-repeat: no-repeat;
- background-position: 100%;
-}
-/** Spanning mode */
-.smartselect.spanningmenu .smartselect_submenu {
- position: absolute;
- top: -1px;
- left: 100%;
-}
-.smartselect.spanningmenu .smartselect_submenu a {
- white-space: nowrap;
- padding-right: 16px;
-}
-.smartselect.spanningmenu .smartselect_menu_content ul li a.selectable:hover {
- text-decoration: underline;
-}
-/** Compact mode */
-.smartselect.compactmenu .smartselect_submenu {
- position: relative;
- margin: 2px -3px;
- margin-left: 10px;
- display: none;
- border-width: 0;
- z-index: 1010;
-}
-.smartselect.compactmenu .smartselect_submenu.visible {
- display: block;
-}
-.smartselect.compactmenu .smartselect_menu {
- z-index: 1000;
- overflow: hidden;
-}
-.smartselect.compactmenu .smartselect_submenu .smartselect_submenu {
- z-index: 1020;
-}
-.smartselect.compactmenu .smartselect_submenuitem:hover > .smartselect_menuitem_label {
- font-weight: bold;
-}
-/**
* Registration
*/
#page-admin-registration-register .registration_textfield {
}
/* Section and module editing forms contain special JS components for the
availability system (if enabled). */
-#fitem_id_availabilityconditionsjson {
- /* Eye icon in front of an item and delete icon after it. */
- /* Hidden eye icon still takes up space. */
- /* Eye icons in front of child lists are aligned specially. */
- /* Add button lines up with child elements. */
- /* Nested section is grey. */
- /* Second (and more) levels of nested sections are white. */
- /* Connecting text needs to be indented. */
-}
-#fitem_id_availabilityconditionsjson *[aria-hidden=true] {
+#id_availabilityconditionsjson[aria-hidden=true],
+.availability-field [aria-hidden=true] {
display: none;
}
-#fitem_id_availabilityconditionsjson select,
-#fitem_id_availabilityconditionsjson input[type=text] {
- position: relative;
- top: 4px;
-}
-#fitem_id_availabilityconditionsjson label {
- display: inline;
-}
-#fitem_id_availabilityconditionsjson br + .availability-group {
- display: inline-block;
- margin-top: 8px;
- margin-bottom: 8px;
-}
-#fitem_id_availabilityconditionsjson .availability-group {
+.availability-eye,
+.availability-delete {
margin-right: 8px;
}
-#fitem_id_availabilityconditionsjson .availability-item {
- margin-bottom: 6px;
-}
-#fitem_id_availabilityconditionsjson .availability-none {
- margin-left: 20px;
- margin-bottom: 4px;
+/* Eye icons in front of child lists are aligned specially. */
+.availability-list > .availability-eye img {
+ vertical-align: top;
+ margin-top: 12px;
}
-#fitem_id_availabilityconditionsjson .availability-plugincontrols {
+.availability-plugincontrols {
min-height: 40px;
padding: 2px 0 0 4px;
background: none repeat scroll 0% 0% #f5f5f5;
display: inline-block;
margin-right: 8px;
}
-#fitem_id_availabilityconditionsjson .availability-plugincontrols select {
+.availability-plugincontrols select {
width: auto;
max-width: 200px;
}
-#fitem_id_availabilityconditionsjson .availability-eye,
-#fitem_id_availabilityconditionsjson .availability-delete {
- margin-right: 8px;
+.availability-field .availability-plugincontrols .availability-group select {
+ max-width: 12rem;
}
-#fitem_id_availabilityconditionsjson .availability-eye[aria-hidden=true] {
- display: inline;
- visibility: hidden;
-}
-#fitem_id_availabilityconditionsjson .availability-list > .availability-eye img {
- vertical-align: top;
- margin-top: 12px;
-}
-#fitem_id_availabilityconditionsjson .availability-button {
- margin-left: 15px;
-}
-#fitem_id_availabilityconditionsjson .availability-childlist > .availability-inner {
+/* Nested section is grey. */
+.availability-childlist > .availability-inner {
display: inline-block;
background: #f5f5f5;
border: 1px solid #eee;
padding: 6px;
margin-bottom: 6px;
}
-#fitem_id_availabilityconditionsjson .availability-childlist .availability-childlist > .availability-inner {
+/* Second (and more) levels of nested sections are white. */
+.availability-childlist .availability-childlist > .availability-inner {
background: white;
}
-#fitem_id_availabilityconditionsjson .availability-connector {
- margin-left: 20px;
- margin-bottom: 6px;
-}
/* Default form styling colours all text red. With availability conditions
this looks excessive as we show 'Invalid' markers in specific places. */
.mform .error .availability-field {
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
-<ul class="nav nav-tabs">
+{{!
+ @template theme_boost/admin_setting_tabs
+
+
+ Example context (json):
+ {
+ "tabs": [
+ {
+ "name": "tab1",
+ "active": 0,
+ "displayname": "Inactive tab1",
+ "html": "<p>Tab 1 content</p>"
+ },
+ {
+ "name": "tab2",
+ "active": 1,
+ "displayname": "Active tab2",
+ "html": "<p>Tab 2 content</p>"
+ }
+ ]
+ }
+}}
+<ul class="nav nav-tabs" role="tablist">
{{#tabs}}
<li class="{{#active}}active{{/active}}">
<a href="#{{name}}" data-toggle="tab" role="tab">{{displayname}}</a>
$PAGE->set_context($context);
$PAGE->set_title($title);
$PAGE->set_heading(fullname($USER));
-$PAGE->set_pagelayout('mydashboard');
+$PAGE->set_pagelayout('standard');
$PAGE->set_pagetype('user-files');
$maxbytes = $CFG->userquota;
defined('MOODLE_INTERNAL') || die();
-$version = 2016122200.00; // YYYYMMDD = weekly release date of this DEV branch.
+$version = 2017010600.00; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
-$release = '3.3dev (Build: 20161222)'; // Human-friendly version name
+$release = '3.3dev (Build: 20170106)'; // Human-friendly version name
$branch = '33'; // This version's branch.
$maturity = MATURITY_ALPHA; // This version's maturity level.